Skip to content
Open
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
145 changes: 145 additions & 0 deletions examples/gno.land/r/samcrew/subscriptions/coins.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package subscriptions

import (
"chain"
"chain/banker"
"chain/runtime"
"errors"
"strings"

"gno.land/r/demo/defi/grc20reg"
)

// XXX: this part is 100% from payrolls realm from N0izN0iz <3
// Payrolls realm: https://github.com/gnolang/gno/pull/3432

// Coins transforms coins into subscriptions coins, prefixing their denom them with their type. Examples: "ugnot" -> "/native/ugnot", "gno.land/r/demo/foo20" -> "/grc20/gno.land/r/demo/foo20"
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this should be move to its own standalone package
/p/samcrew/coins/normalizedCoins.gno
Or something like that

func Coins(cur realm, native chain.Coins, grc20 chain.Coins) (chain.Coins, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
func Coins(cur realm, native chain.Coins, grc20 chain.Coins) (chain.Coins, error) {
func PrefixCoins(cur realm, native chain.Coins, grc20 chain.Coins) (chain.Coins, error) {

Or NormalizeCoins, FormatCoins

out := make(chain.Coins, len(native)+len(grc20))
for i, coin := range native {
out[i].Amount = coin.Amount
out[i].Denom = "/native/" + coin.Denom
}
offset := len(native)
for i, coin := range grc20 {
j := offset + i
out[j].Amount = coin.Amount
out[j].Denom = "/grc20/" + coin.Denom
}
return out, nil
Copy link
Contributor

Choose a reason for hiding this comment

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

error is always == to nil
If both len == 0, we should return an error

}

func coinsHasPositive(coins chain.Coins) bool {
for _, coin := range coins {
if coin.Amount > 0 {
return true
}
}
return false
}

func addCoins(a chain.Coins, b chain.Coins) chain.Coins {
out := make(chain.Coins, len(a))
copy(out, a)
for _, coin := range b {
out = addCoinAmount(out, coin)
}
return out
}

func subCoins(a chain.Coins, b chain.Coins) chain.Coins {
out := make(chain.Coins, len(a))
copy(out, a)
for _, coin := range b {
out = addCoinAmount(out, chain.NewCoin(coin.Denom, -coin.Amount))
}
return out
}

func multiplyCoins(coins chain.Coins, factor int64) chain.Coins {
out := make(chain.Coins, len(coins))
for i, coin := range coins {
out[i] = chain.Coin{
Denom: coin.Denom,
Amount: coin.Amount * factor,
}
}
return out
}

func lessCoinsThan(a chain.Coins, b chain.Coins) bool {
for _, coinA := range a {
found := false
for _, coinB := range b {
if coinA.Denom == coinB.Denom {
if coinA.Amount < coinB.Amount {
return true
}
found = true
Copy link
Contributor

Choose a reason for hiding this comment

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

You can remove the found variable and do the next condition in that line

break
}
}
if !found && coinA.Amount > 0 {
return false
}
}
return false
}

func addCoinAmount(coins chain.Coins, value chain.Coin) chain.Coins {
for i, coin := range coins {
if coin.Denom != value.Denom {
continue
}

out := make(chain.Coins, len(coins))
copy(out, coins)
out[i].Amount += value.Amount
return out
}
return append(coins, value)
}

func sendCoins(dst address, coins chain.Coins) {
if len(coins) == 0 {
return
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of returning silently, we should return an error, a status or a panic

}

natives := chain.Coins{}
grc20s := chain.Coins{}

for _, coin := range coins {
if coin.Amount == 0 {
continue
}
if coin.Amount < 0 {
panic(errors.New("negative send amount"))
}

var (
target *chain.Coins
Copy link
Contributor

Choose a reason for hiding this comment

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

The value is always de-referenced, so the pointer is not necessary

denom string
)

if strings.HasPrefix(coin.Denom, "/native/") {
target = &natives
Copy link
Contributor

Choose a reason for hiding this comment

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

Why extracting target + denom, if it's merged in the addCoinAmount call

denom = strings.TrimPrefix(coin.Denom, "/native/")
} else if strings.HasPrefix(coin.Denom, "/grc20/") {
target = &grc20s
denom = strings.TrimPrefix(coin.Denom, "/grc20/")
} else {
panic(errors.New("invalid coin denom prefix: " + coin.Denom))
}
*target = addCoinAmount(*target, chain.NewCoin(denom, coin.Amount))
}

banker_ := banker.NewBanker(banker.BankerTypeRealmIssue)
from := runtime.CurrentRealm().Address()
if len(natives) != 0 {
banker_.SendCoins(from, dst, natives)
}

for _, coin := range grc20s {
grc20reg.MustGet(coin.Denom).RealmTeller().Transfer(dst, coin.Amount)
}
}
243 changes: 243 additions & 0 deletions examples/gno.land/r/samcrew/subscriptions/coins_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
package subscriptions

import (
"chain"
"chain/banker"
"testing"

"gno.land/p/nt/testutils"
)

func TestCoins(t *testing.T) {
native := chain.NewCoins(
chain.NewCoin("ugnot", 1000),
)
grc20 := chain.NewCoins(
chain.NewCoin("gno.land/r/demo/foo20", 500),
)
expected := chain.NewCoins(
chain.NewCoin("/native/ugnot", 1000),
chain.NewCoin("/grc20/gno.land/r/demo/foo20", 500),
)

result, err := Coins(cross, native, grc20)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
for i, coin := range expected {
if result[i].Amount != coin.Amount {
t.Fatalf("invalid amount of %s got %d, want %d", coin.Denom, result[i].Amount, coin.Amount)
}
if result[i].Denom != coin.Denom {
t.Fatalf("invalid denom: got %s, want %s", result[i].Denom, coin.Denom)
}
}
}

func TestCoinsHasPositive(t *testing.T) {
tests := []struct {
coins chain.Coins
expected bool
}{
{chain.NewCoins(chain.NewCoin("coinA", 0)), false},
{chain.NewCoins(chain.NewCoin("coinA", -100)), false},
{chain.NewCoins(chain.NewCoin("coinA", 50)), true},
{chain.NewCoins(chain.NewCoin("coinA", 0), chain.NewCoin("coinB", 0)), false},
{chain.NewCoins(chain.NewCoin("coinA", -10), chain.NewCoin("coinB", 20)), true},
}

for _, test := range tests {
result := coinsHasPositive(test.coins)
if result != test.expected {
t.Fatalf("coinsHasPositive(%v) = %v; want %v", test.coins, result, test.expected)
}
}
}

func TestAddCoins(t *testing.T) {
a := chain.NewCoins(
chain.NewCoin("coinA", 100),
chain.NewCoin("coinB", 200),
)
b := chain.NewCoins(
chain.NewCoin("coinB", 150),
chain.NewCoin("coinC", 300),
)
expected := chain.NewCoins(
chain.NewCoin("coinA", 100),
chain.NewCoin("coinB", 350),
chain.NewCoin("coinC", 300),
)

result := addCoins(a, b)
if len(result) != len(expected) {
t.Fatalf("invalid result length: got %d, want %d", len(result), len(expected))
}
for _, coin := range expected {
found := false
for _, resCoin := range result {
if resCoin.Denom == coin.Denom {
found = true
if resCoin.Amount != coin.Amount {
t.Fatalf("invalid amount for %s: got %d, want %d", coin.Denom, resCoin.Amount, coin.Amount)
}
break
}
}
if !found {
t.Fatalf("missing coin in result: %s", coin.Denom)
}
}
}

func TestSubCoins(t *testing.T) {
a := chain.NewCoins(
chain.NewCoin("coinA", 300),
chain.NewCoin("coinB", 400),
)
b := chain.NewCoins(
chain.NewCoin("coinB", 150),
chain.NewCoin("coinC", 100),
)
expected := chain.NewCoins(
chain.NewCoin("coinA", 300),
chain.NewCoin("coinB", 250),
chain.NewCoin("coinC", -100),
)

result := subCoins(a, b)
if len(result) != len(expected) {
t.Fatalf("invalid result length: got %d, want %d", len(result), len(expected))
}
for _, coin := range expected {
found := false
for _, resCoin := range result {
if resCoin.Denom == coin.Denom {
found = true
if resCoin.Amount != coin.Amount {
t.Fatalf("invalid amount for %s: got %d, want %d", coin.Denom, resCoin.Amount, coin.Amount)
}
break
}
}
if !found {
t.Fatalf("missing coin in result: %s", coin.Denom)
}
}
}

func TestMultiplyCoins(t *testing.T) {
coins := chain.NewCoins(
chain.NewCoin("coinA", 100),
chain.NewCoin("coinB", 200),
)
factor := int64(3)
expected := chain.NewCoins(
chain.NewCoin("coinA", 300),
chain.NewCoin("coinB", 600),
)

result := multiplyCoins(coins, factor)
if len(result) != len(expected) {
t.Fatalf("invalid result length: got %d, want %d", len(result), len(expected))
}
for _, coin := range expected {
found := false
for _, resCoin := range result {
if resCoin.Denom == coin.Denom {
found = true
if resCoin.Amount != coin.Amount {
t.Fatalf("invalid amount for %s: got %d, want %d", coin.Denom, resCoin.Amount, coin.Amount)
}
break
}
}
if !found {
t.Fatalf("missing coin in result: %s", coin.Denom)
}
}
}

func TestAddCoinAmount(t *testing.T) {
coins := chain.NewCoins(
chain.NewCoin("coinA", 100),
chain.NewCoin("coinB", 200),
)
value := chain.NewCoin("coinB", 150)
expected := chain.NewCoins(
chain.NewCoin("coinA", 100),
chain.NewCoin("coinB", 350),
)

result := addCoinAmount(coins, value)
if len(result) != len(expected) {
t.Fatalf("invalid result length: got %d, want %d", len(result), len(expected))
}
for _, coin := range expected {
found := false
for _, resCoin := range result {
if resCoin.Denom == coin.Denom {
found = true
if resCoin.Amount != coin.Amount {
t.Fatalf("invalid amount for %s: got %d, want %d", coin.Denom, resCoin.Amount, coin.Amount)
}
break
}
}
if !found {
t.Fatalf("missing coin in result: %s", coin.Denom)
}
}
}

func TestSendCoins(t *testing.T) {
alice := testutils.TestAddress("alice")
testing.SetOriginCaller(alice)
testing.SetRealm(testing.NewUserRealm(alice))
coinsToSend := chain.NewCoins(
chain.NewCoin("/native/gno.land/r/samcrew/subscriptions:a", 100),
chain.NewCoin("/native/gno.land/r/samcrew/subscriptions:b", 200),
)

// without native prefix
coinsToIssue := chain.NewCoins(
chain.NewCoin("gno.land/r/samcrew/subscriptions:a", 100),
chain.NewCoin("gno.land/r/samcrew/subscriptions:b", 200),
)

testing.IssueCoins(alice, coinsToIssue)

bob := testutils.TestAddress("bob")
testing.SetOriginSend(coinsToSend)

sendCoins(bob, coinsToSend)

banker := banker.NewBanker(banker.BankerTypeRealmIssue)
aliceCoins := banker.GetCoins(alice)
bobCoins := banker.GetCoins(bob)
for _, coin := range coinsToIssue {
aliceAmount := int64(0)
bobAmount := int64(0)
for _, aCoin := range aliceCoins {
if aCoin.Denom == coin.Denom {
aliceAmount = aCoin.Amount
break
}
}
for _, bCoin := range bobCoins {
if bCoin.Denom == coin.Denom {
bobAmount = bCoin.Amount
break
}
}

expectedAliceAmount := int64(0)
expectedBobAmount := coin.Amount
if aliceAmount != expectedAliceAmount {
t.Fatalf("invalid alice amount for %s: got %d, want %d", coin.Denom, aliceAmount, expectedAliceAmount)
}
if bobAmount != expectedBobAmount {
t.Fatalf("invalid bob amount for %s: got %d, want %d", coin.Denom, bobAmount, expectedBobAmount)
}
}
}
3 changes: 3 additions & 0 deletions examples/gno.land/r/samcrew/subscriptions/gnomod.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module = "gno.land/r/samcrew/subscriptions"
gno = "0.9"
private = true
Loading
Loading