From ef3b26432523c331634f508bc33ba508b27b4609 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Mon, 24 Jun 2024 16:37:31 +0200 Subject: [PATCH] feat: re-organize std --- gnovm/stdlibs/chain/chain.gno | 80 +++++++++ gnovm/stdlibs/chain/coins.gno | 183 +++++++++++++++++++++ gnovm/stdlibs/crypto/bech32/bech32.gno | 97 +++++++++++ gnovm/stdlibs/internal/runtime/runtime.gno | 10 ++ gnovm/stdlibs/runtime/runtime.gno | 35 ++++ gnovm/stdlibs/testing/context.gno | 21 +++ 6 files changed, 426 insertions(+) create mode 100644 gnovm/stdlibs/chain/chain.gno create mode 100644 gnovm/stdlibs/chain/coins.gno create mode 100644 gnovm/stdlibs/crypto/bech32/bech32.gno create mode 100644 gnovm/stdlibs/internal/runtime/runtime.gno create mode 100644 gnovm/stdlibs/runtime/runtime.gno create mode 100644 gnovm/stdlibs/testing/context.gno diff --git a/gnovm/stdlibs/chain/chain.gno b/gnovm/stdlibs/chain/chain.gno new file mode 100644 index 00000000000..b774d423506 --- /dev/null +++ b/gnovm/stdlibs/chain/chain.gno @@ -0,0 +1,80 @@ +// Package chain contains core types and their helper functions to deal with the +// blockchain. +package chain + +import ( + iruntime "internal/runtime" +) + +// Address represents a bech32-encoded blockchain address. +type Address string + +func (a Address) IsValid() bool { + // TODO: make stdlib crypto/bech32 + _, _, ok := DecodeBech32(string(a)) + return ok +} + +func NewAddressFromImportPath(importPath string) { + // TODO: find better name + panic("not implemented") +} + +// A Realm represents an on-chain entity which has an address. This is generally +// an Externally Owned Account (ie. a "user") or a code realm, which also has an +// associated import path. +type Realm struct { + address Address + importPath string +} + +func (r Realm) Address() Address { + return r.addr +} + +func (r Realm) ImportPath() string { + return r.importPath +} + +func (r Realm) IsUser() bool { + return r.importPath == "" +} + +// NewCodeRealm creates a new realm, representing a code realm with a published +// import path, existing on-chain. +func NewCodeRealm(importPath string) Realm { + if !iruntime.IsTesting() && !iruntime.IsRuntime() { + panic("realms may only be created in testing or by the runtime package") + } + return Realm{ + address: NewAddressFromImportPath(importPath), + importPath: importPath, + } +} + +// NewUserRealm creates a new realm, representing a regular user. +func NewUserRealm(address Address) Realm { + if !iruntime.IsTesting() && !iruntime.IsRuntime() { + panic("realms may only be created in testing or by the runtime package") + } + return Realm{ + address: address, + } +} + +// Banker is an interface to access the blockchain's native banker. +type Banker interface { + // Balance returns the balance in the bank for the given address, as a list + // of coins. + Balance(addr Address) (dst Coins) + + // Supply returns the circulating supply of the given denomination. + Supply(denom string) int64 + + // Send sends the given Coins between the from and to addresses. + Send(from, to Address, amt Coins) + + // Issue issues the coin with the given denomination + Issue(addr Address, denom string, amount int64) + Burn(addr Address, denom string, amount int64) +} diff --git a/gnovm/stdlibs/chain/coins.gno b/gnovm/stdlibs/chain/coins.gno new file mode 100644 index 00000000000..e55f28c7d48 --- /dev/null +++ b/gnovm/stdlibs/chain/coins.gno @@ -0,0 +1,183 @@ +package chain + +// Coin holds some amount of one currency. +// A negative amount is invalid. +type Coin struct { + Denom string `json:"denom"` + Amount int64 `json:"amount"` +} + +// NewCoin returns a new coin with a denomination and amount +func NewCoin(denom string, amount int64) Coin { + return Coin{ + Denom: denom, + Amount: amount, + } +} + +// String provides a human-readable representation of a coin +func (c Coin) String() string { + return strconv.Itoa(int(c.Amount)) + c.Denom +} + +// IsGTE returns true if they are the same type and the receiver is +// an equal or greater value +func (c Coin) IsGTE(other Coin) bool { + mustMatchDenominations(c.Denom, other.Denom) + + return c.Amount >= other.Amount +} + +// IsLT returns true if they are the same type and the receiver is +// a smaller value +func (c Coin) IsLT(other Coin) bool { + mustMatchDenominations(c.Denom, other.Denom) + + return c.Amount < other.Amount +} + +// IsEqual returns true if the two sets of Coins have the same value +func (c Coin) IsEqual(other Coin) bool { + mustMatchDenominations(c.Denom, other.Denom) + + return c.Amount == other.Amount +} + +// Add adds amounts of two coins with same denom. +// If the coins differ in denom then it panics. +// An overflow or underflow panics. +// An invalid result panics. +func (c Coin) Add(other Coin) Coin { + mustMatchDenominations(c.Denom, other.Denom) + + sum, ok := overflow.Add64(c.Amount, other.Amount) + if !ok { + panic("coin add overflow/underflow: " + strconv.Itoa(int(c.Amount)) + " +/- " + strconv.Itoa(int(other.Amount))) + } + + c.Amount = sum + return c +} + +// Sub subtracts amounts of two coins with same denom. +// If the coins differ in denom then it panics. +// An overflow or underflow panics. +// An invalid result panics. +func (c Coin) Sub(other Coin) Coin { + mustMatchDenominations(c.Denom, other.Denom) + + dff, ok := overflow.Sub64(c.Amount, other.Amount) + if !ok { + panic("coin sub overflow/underflow: " + strconv.Itoa(int(c.Amount)) + " +/- " + strconv.Itoa(int(other.Amount))) + } + c.Amount = dff + + return c +} + +// IsPositive returns true if coin amount is positive. +func (c Coin) IsPositive() bool { + return c.Amount > 0 +} + +// IsNegative returns true if the coin amount is negative and false otherwise. +func (c Coin) IsNegative() bool { + return c.Amount < 0 +} + +// IsZero returns true if the amount of given coin is zero +func (c Coin) IsZero() bool { + return c.Amount == 0 +} + +func mustMatchDenominations(denomA, denomB string) { + if denomA != denomB { + panic("incompatible coin denominations: " + denomA + ", " + denomB) + } +} + +// Coins is a set of Coin, one per currency +type Coins []Coin + +// NewCoins returns a new set of Coins given one or more Coins +// Consolidates any denom duplicates into one, keeping the properties of a mathematical set +func NewCoins(coins ...Coin) Coins { + coinMap := make(map[string]int64) + + for _, coin := range coins { + if currentAmount, exists := coinMap[coin.Denom]; exists { + var ok bool + if coinMap[coin.Denom], ok = overflow.Add64(currentAmount, coin.Amount); !ok { + panic("coin sub overflow/underflow: " + strconv.Itoa(int(currentAmount)) + " +/- " + strconv.Itoa(int(coin.Amount))) + } + } else { + coinMap[coin.Denom] = coin.Amount + } + } + + var setCoins Coins + for denom, amount := range coinMap { + setCoins = append(setCoins, NewCoin(denom, amount)) + } + + return setCoins +} + +// String returns the string representation of Coins +func (cz Coins) String() string { + if len(cz) == 0 { + return "" + } + + res := "" + for i, c := range cz { + if i > 0 { + res += "," + } + res += c.String() + } + + return res +} + +// AmountOf returns the amount of a specific coin from the Coins set +func (cz Coins) AmountOf(denom string) int64 { + for _, c := range cz { + if c.Denom == denom { + return c.Amount + } + } + + return 0 +} + +// Add adds a Coin to the Coins set +func (cz Coins) Add(b Coins) Coins { + c := Coins{} + for _, ac := range cz { + bc := b.AmountOf(ac.Denom) + ac.Amount += bc + c = append(c, ac) + } + + for _, bc := range b { + cc := c.AmountOf(bc.Denom) + if cc == 0 { + c = append(c, bc) + } + } + + return c +} + +// expandNative expands for usage within natively bound functions. +func (cz Coins) expandNative() (denoms []string, amounts []int64) { + denoms = make([]string, len(cz)) + amounts = make([]int64, len(cz)) + for i, coin := range cz { + denoms[i] = coin.Denom + amounts[i] = coin.Amount + } + + return denoms, amounts +} diff --git a/gnovm/stdlibs/crypto/bech32/bech32.gno b/gnovm/stdlibs/crypto/bech32/bech32.gno new file mode 100644 index 00000000000..98c3d274ba0 --- /dev/null +++ b/gnovm/stdlibs/crypto/bech32/bech32.gno @@ -0,0 +1,97 @@ +package bech32 + +// ConvertBits converts a byte slice where each byte is encoding fromBits bits, +// to a byte slice where each byte is encoding toBits bits. +func ConvertBits(data []byte, fromBits, toBits uint8, pad bool) ([]byte, error) { + if fromBits < 1 || fromBits > 8 || toBits < 1 || toBits > 8 { + return nil, ErrInvalidBitGroups{} + } + + // Determine the maximum size the resulting array can have after base + // conversion, so that we can size it a single time. This might be off + // by a byte depending on whether padding is used or not and if the input + // data is a multiple of both fromBits and toBits, but we ignore that and + // just size it to the maximum possible. + maxSize := len(data)*int(fromBits)/int(toBits) + 1 + + // The final bytes, each byte encoding toBits bits. + regrouped := make([]byte, 0, maxSize) + + // Keep track of the next byte we create and how many bits we have + // added to it out of the toBits goal. + nextByte := byte(0) + filledBits := uint8(0) + + for _, b := range data { + + // Discard unused bits. + b <<= 8 - fromBits + + // How many bits remaining to extract from the input data. + remFromBits := fromBits + for remFromBits > 0 { + // How many bits remaining to be added to the next byte. + remToBits := toBits - filledBits + + // The number of bytes to next extract is the minimum of + // remFromBits and remToBits. + toExtract := remFromBits + if remToBits < toExtract { + toExtract = remToBits + } + + // Add the next bits to nextByte, shifting the already + // added bits to the left. + nextByte = (nextByte << toExtract) | (b >> (8 - toExtract)) + + // Discard the bits we just extracted and get ready for + // next iteration. + b <<= toExtract + remFromBits -= toExtract + filledBits += toExtract + + // If the nextByte is completely filled, we add it to + // our regrouped bytes and start on the next byte. + if filledBits == toBits { + regrouped = append(regrouped, nextByte) + filledBits = 0 + nextByte = 0 + } + } + } + + // We pad any unfinished group if specified. + if pad && filledBits > 0 { + nextByte <<= toBits - filledBits + regrouped = append(regrouped, nextByte) + filledBits = 0 + nextByte = 0 + } + + // Any incomplete group must be <= 4 bits, and all zeroes. + if filledBits > 0 && (filledBits > 4 || nextByte != 0) { + return nil, ErrInvalidIncompleteGroup{} + } + + return regrouped, nil +} + +func Encode(prefix string, data []byte) (string, error) { + converted, err := ConvertBits(data, 8, 5, true) + if err != nil { + return "", errors.Wrap(err, "encoding bech32 failed") + } + return bech32.Encode(hrp, converted) +} + +func Decode(s string) (prefix string, data []byte, err error) { + hrp, data, err := bech32.DecodeNoLimit(bech) + if err != nil { + return "", nil, errors.Wrap(err, "decoding bech32 failed") + } + converted, err := ConvertBits(data, 5, 8, false) + if err != nil { + return "", nil, errors.Wrap(err, "decoding bech32 failed") + } + return hrp, converted, nil +} diff --git a/gnovm/stdlibs/internal/runtime/runtime.gno b/gnovm/stdlibs/internal/runtime/runtime.gno new file mode 100644 index 00000000000..5c0ed65ddde --- /dev/null +++ b/gnovm/stdlibs/internal/runtime/runtime.gno @@ -0,0 +1,10 @@ +// Package runtime contains some runtime information, which is used by packages +// like "chain" which cannot import "runtime" to avoid circular dependencies. +package runtime + +// IsTesting returns whether the caller's caller is a function within a +// _test.gno or a _filetest.gno file. +func IsTesting() bool // injected + +// IsRuntime returns whether the caller's caller is the "runtime" package. +func IsRuntime() bool // injected diff --git a/gnovm/stdlibs/runtime/runtime.gno b/gnovm/stdlibs/runtime/runtime.gno new file mode 100644 index 00000000000..0cff2d36cd5 --- /dev/null +++ b/gnovm/stdlibs/runtime/runtime.gno @@ -0,0 +1,35 @@ +// Package runtime allows access to the GnoVM's runtime information. +package runtime + +func NewReadOnlyBanker() chain.Banker { +} + +func NewLimitedSendBanker() chain.Banker { +} + +func NewSendBanker() chain.Banker { +} + +func NewIssueBanker() chain.Banker { +} + +func AssertOriginCall() { +} + +func CallerRealm() chain.Realm { +} + +func CurrentRealm() chain.Realm { +} + +func Deposits() chain.Coins { +} + +func ChainID() string { +} + +func Height() uint64 { +} + +func Emit(eventType string, attrKeyValues ...string) { +} diff --git a/gnovm/stdlibs/testing/context.gno b/gnovm/stdlibs/testing/context.gno new file mode 100644 index 00000000000..978fe6fa3e5 --- /dev/null +++ b/gnovm/stdlibs/testing/context.gno @@ -0,0 +1,21 @@ +package testing + +import ( + "chain" +) + +// Context contains execution context variables which are used primarily by the +// [runtime] package to give information about the current GnoVM execution. +type Context struct { + IsOrigin bool + CurrentRealm chain.Realm + Deposit chain.Coins + ChainID string + Height uint64 + Time time.Time + Banker chain.Banker + Logger bool // TODO +} + +func (c Context) Run(f func()) { +}