Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: std re-organization #2425

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions gnovm/stdlibs/chain/chain.gno
Original file line number Diff line number Diff line change
@@ -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
Copy link
Member

@moul moul Jul 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the problem with PkgPath?

Edit: I think we should keep pkgPath over importPath. Because ultimately, the package path is an identifier which, yes, can be imported, but it's an identifier first, just as a std.Address is an identifier for an account. This one is the identifier for the realm and its assets, and the cool thing is that it's importable. It's like we would say that Go's package path can be imported, so let's not name them ImportPath. But we can also go mod download them, so let's not rename them goModDownloadPath either. I think we should be clear that the identifier is the unique identifier of a package, and then importPath is currently the same 1-1, but maybe in the future, we'll support import aliases for versioning or to support, for instance, r/morgan/foo and r/g1morganxxx/foo to be the same pkgPath but different importPath.

Copy link
Contributor

@leohhhn leohhhn Sep 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am for a different name that pkgpath, because it conflicts most of the time when explaining the topics, making it very confusing. possibly URL, URI, ID, just path, etc. in the end its a unique identifier for code, and it can be used to look up code or import code.

  • realm path - better than realm package path
  • pure package path - better than pure package package path.

}

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
}
183 changes: 183 additions & 0 deletions gnovm/stdlibs/chain/coins.gno
Original file line number Diff line number Diff line change
@@ -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
}
97 changes: 97 additions & 0 deletions gnovm/stdlibs/crypto/bech32/bech32.gno
Original file line number Diff line number Diff line change
@@ -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
}
10 changes: 10 additions & 0 deletions gnovm/stdlibs/internal/runtime/runtime.gno
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading