Skip to content

feat: r/coins balance checker #3899

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

Merged
merged 46 commits into from
May 9, 2025
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
c59d58d
feat(examples): add r/gnoland/coins
moul Feb 25, 2025
a654e50
feat: TotalCoin
notJoon Mar 10, 2025
df1bf50
Merge branch 'master' into airdrop-checker
notJoon Mar 10, 2025
c0b825a
fix
notJoon Mar 10, 2025
b12bbe7
test: mock TotalCoin
notJoon Mar 10, 2025
ee072ef
fmt
notJoon Mar 10, 2025
9cb5c81
fix
notJoon Mar 10, 2025
dc636d3
fix: create proper denom
notJoon Mar 10, 2025
ef43e83
doc
notJoon Mar 10, 2025
695412e
fix
notJoon Mar 11, 2025
0e325f3
remove nav
notJoon Mar 11, 2025
0d751b6
use IterateAccounts
notJoon Mar 11, 2025
15296c5
airdrop things
notJoon Mar 11, 2025
f150093
Revert "airdrop things"
notJoon Mar 11, 2025
bd3eb64
render funcs
notJoon Mar 11, 2025
99fb5b7
Merge branch 'master' into airdrop-checker
notJoon Mar 11, 2025
0c8d344
use md package
notJoon Mar 11, 2025
6f00822
supply keeper
notJoon Mar 12, 2025
8b09a6a
Merge branch 'master' into airdrop-checker
notJoon Mar 12, 2025
5c175b2
docs
notJoon Mar 12, 2025
b535661
test: SDKBanker
notJoon Mar 12, 2025
bd710bd
remove unused
notJoon Mar 12, 2025
07584c3
Merge branch 'master' into airdrop-checker
notJoon Mar 13, 2025
aa509df
Merge branch 'master' into airdrop-checker
notJoon Mar 25, 2025
4bddb4a
fix
notJoon Mar 25, 2025
bfee6be
Revert "supply keeper"
notJoon Apr 3, 2025
c5a012b
Merge branch 'master' into airdrop-checker
notJoon Apr 3, 2025
4cbd61c
fix
notJoon Apr 3, 2025
cb9a437
fix
notJoon Apr 3, 2025
8efad65
use mux
notJoon Apr 3, 2025
468a717
fix
notJoon Apr 3, 2025
7b5d23e
Merge branch 'master' into airdrop-checker
notJoon Apr 14, 2025
41ecdaf
start from the beginning
notJoon Apr 16, 2025
1978a90
simple r/coins
notJoon Apr 16, 2025
b5893d6
disable totalsupply
notJoon Apr 16, 2025
f20a4d6
Merge branch 'master' into airdrop-checker
notJoon Apr 16, 2025
dc45336
fmt
notJoon Apr 16, 2025
b451b9a
Merge branch 'master' into airdrop-checker
moul Apr 22, 2025
a357a08
Update examples/gno.land/r/gnoland/coins/coins.gno
notJoon Apr 23, 2025
c1b246d
Apply suggestions from code review
notJoon Apr 23, 2025
16ed599
fix
notJoon Apr 23, 2025
b713afd
remove unnecessary package
notJoon Apr 23, 2025
1398f4f
Merge branch 'master' into airdrop-checker
notJoon Apr 23, 2025
28c00d1
fix
notJoon Apr 23, 2025
55d979d
remove redundant return
notJoon Apr 23, 2025
6b5d146
Merge branch 'master' into airdrop-checker
thehowl May 9, 2025
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
112 changes: 112 additions & 0 deletions examples/gno.land/r/gnoland/coins/coins.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Package coins provides simple helpers to retrieve information about coins
// on the Gno.land blockchain.
//
// The primary goal of this realm is to allow users to check their token balances without
// relying on external tools or services. This is particularly valuable for new networks
// that aren't yet widely supported by public explorers or wallets. By using this realm,
// users can always access their balance information directly through the gnodev.
//
// While currently focused on basic balance checking functionality, this realm could
// potentially be extended to support other banker-related workflows in the future.
// However, we aim to keep it minimal and focused on its core purpose.
//
// This is a "Render-only realm" - it exposes only a Render function as its public
// interface and doesn't maintain any state of its own. This pattern allows for
// simple, stateless information retrieval directly through the blockchain's
// rendering capabilities.
//
// Example usage:
//
// /r/gnoland/coins:ugnot - shows the total supply of ugnot
// /r/gnoland/coins:ugnot/g1... - shows the ugnot balance of a specific address
package coins

import (
"std"
"strings"
)

// TotalSupply returns the total supply of the specified denomination
func TotalSupply(banker std.Banker, denom string) int64 {
if denom == "" {
panic("empty denom")
}
// make the coin's denom to follow the correct format
qualifiedDenom := std.CurrentRealm().CoinDenom(denom)
return banker.TotalCoin(qualifiedDenom)
}

// AddressBalance returns the balance of the specified token for the given address
func AddressBalance(banker std.Banker, denom string, address std.Address) int64 {
if denom == "" {
panic("empty denom")
}
if !address.IsValid() {
panic("invalid address")
}
return banker.GetCoins(address).AmountOf(denom)
}

// Path parsing logic:
// - Empty path: Show homepage with general info and usage instructions
// - "<denom>": Show total supply and info about the specified coin
// - "<denom>/<address>": Show specific address balance for the given coin
func Render(path string) string {
banker := std.NewBanker(std.BankerTypeReadonly)

if path == "" {
return renderHomepage()
}

if !strings.Contains(path, "/") {
denom := path
totalSupply := TotalSupply(banker, denom)
return renderCoinInfo(denom, totalSupply)
}

parts := strings.Split(path, "/")
if len(parts) != 2 {
panic("invalid path")
}

denom, addr := parts[0], parts[1]
return renderAddressBalance(banker, denom, std.Address(addr))

// TODO: implement an explanatory homepage that shows usage instructions
// TODO: implement a coin info page showing total supply, and other metrics if we have them
// TODO: implement an address page showing balance for specified coin
// TODO: create navigation links between pages to enable intuitive exploration without typing URLs
}

func renderHomepage() string {
return `# Gno.land Coins
`
}

func renderCoinInfo(denom string, totalSupply int64) string {
return `# Coin Info

## Denom
` + denom + `

## Total Supply
` + std.NewCoin(denom, totalSupply).String() + `

[Back to Home](/r/gnoland/coins:)
`
}

func renderAddressBalance(banker std.Banker, denom string, address std.Address) string {
balance := AddressBalance(banker, denom, address)
return `# Address Balance

## Address
` + address.String() + `

## Balance
` + std.NewCoin(denom, balance).String() + `

[Back to Coin Info](/r/gnoland/coins:` + denom + `)
[Back to Home](/r/gnoland/coins:)
`
}
67 changes: 67 additions & 0 deletions examples/gno.land/r/gnoland/coins/coins_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package coins

import (
"std"
"strings"
"testing"

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

func TestAirdropChecker(t *testing.T) {
addr1 := testutils.TestAddress("addr1")
addr2 := testutils.TestAddress("addr2")

realm := std.CurrentRealm()
testDenom := realm.CoinDenom("test")
test2Denom := realm.CoinDenom("test2")

tests := []struct {
name string
path string
setupFunc func(std.Banker)
expected string
wantPanic bool
}{
{
name: "homepage shows supported tokens",
path: "",
setupFunc: func(banker std.Banker) {
banker.IssueCoin(addr1, test2Denom, 1000)
banker.IssueCoin(addr2, test2Denom, 1000)
},
expected: `# Gno.land Coins`,
},
{
name: "show total supply for test token",
path: "test",
setupFunc: func(banker std.Banker) {
banker.IssueCoin(addr1, testDenom, 1000000)
banker.IssueCoin(addr2, testDenom, 500000)

println("setup amount", banker.TotalCoin(testDenom))
},
expected: `1500000test`,
},
}

for _, tt := range tests {
banker := std.NewBanker(std.BankerTypeRealmIssue)
tt.setupFunc(banker)

if tt.wantPanic {
defer func() {
if r := recover(); r == nil {
t.Errorf("expected panic for %s", tt.name)
}
}()
}

result := Render(tt.path)
if !tt.wantPanic {
if !strings.Contains(result, tt.expected) {
t.Errorf("expected %s to contain %s", result, tt.expected)
}
}
}
}
1 change: 1 addition & 0 deletions examples/gno.land/r/gnoland/coins/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module gno.land/r/gnoland/coins
14 changes: 13 additions & 1 deletion gno.land/pkg/sdk/vm/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,19 @@
}

func (bnk *SDKBanker) TotalCoin(denom string) int64 {
panic("not yet implemented")
if denom == "" {
panic("empty denom")
}
accounts := bnk.vmk.acck.GetAllAccounts(bnk.ctx)
var totalAmount int64

for _, acc := range accounts {
if acc == nil {
continue

Check warning on line 52 in gno.land/pkg/sdk/vm/builtins.go

View check run for this annotation

Codecov / codecov/patch

gno.land/pkg/sdk/vm/builtins.go#L52

Added line #L52 was not covered by tests
}
totalAmount += acc.GetCoins().AmountOf(denom)
}
return totalAmount
}

func (bnk *SDKBanker) IssueCoin(b32addr crypto.Bech32Address, denom string, amount int64) {
Expand Down
50 changes: 49 additions & 1 deletion gno.land/pkg/sdk/vm/builtins_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ package vm
import (
"testing"

"github.com/gnolang/gno/tm2/pkg/crypto"
"github.com/gnolang/gno/tm2/pkg/std"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestParamsKeeper(t *testing.T) {
env := setupTestEnv()
params := NewSDKParams(env.vmk.prmk, env.ctx)
params := NewSDKParams(env.prmk, env.ctx)

testCases := []struct {
name string
Expand Down Expand Up @@ -65,3 +69,47 @@ func TestParamsKeeper(t *testing.T) {
})
}
}

func TestSDKBankerTotalCoin(t *testing.T) {
env := setupTestEnv()
ctx := env.vmk.MakeGnoTransactionStore(env.ctx)
banker := NewSDKBanker(env.vmk, ctx)

// create test accounts and set coins
addr1 := crypto.AddressFromPreimage([]byte("addr1"))
addr2 := crypto.AddressFromPreimage([]byte("addr2"))

acc1 := env.acck.NewAccountWithAddress(ctx, addr1)
env.acck.SetAccount(ctx, acc1)
env.bankk.SetCoins(ctx, addr1, std.MustParseCoins("1000ugnot,500atom"))

acc2 := env.acck.NewAccountWithAddress(ctx, addr2)
env.acck.SetAccount(ctx, acc2)
env.bankk.SetCoins(ctx, addr2, std.MustParseCoins("2000ugnot,1500atom"))

tests := []struct {
name string
denom string
expected int64
mustPanic bool
}{
{"ugnot total", "ugnot", 3000, false},
{"atom total", "atom", 2000, false},
{"non-existent denom", "foo", 0, false},
{"zero balance accounts included", "ugnot", 3000, false},
{"empty string denom", "", 0, true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.mustPanic {
assert.Panics(t, func() {
banker.TotalCoin(tt.denom)
})
} else {
total := banker.TotalCoin(tt.denom)
assert.Equal(t, tt.expected, total)
}
})
}
}
1 change: 1 addition & 0 deletions gnovm/stdlibs/std/banker.gno
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ func (b banker) SendCoins(from, to Address, amt Coins) {
}

func (b banker) TotalCoin(denom string) int64 {
assertCoinDenom(denom)
return bankerTotalCoin(uint8(b.bt), denom)
}

Expand Down
15 changes: 11 additions & 4 deletions gnovm/tests/stdlibs/std/std.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,26 +209,33 @@ func (tb *TestBanker) SendCoins(from, to crypto.Bech32Address, amt tm2std.Coins)
tb.CoinTable[from] = frest
// Second, add to 'to'.
// NOTE: even works when from==to, due to 2-step isolation.
tcoins, _ := tb.CoinTable[to]
tcoins := tb.CoinTable[to]
tsum := tcoins.Add(amt)
tb.CoinTable[to] = tsum
}

// TotalCoin implements the Banker interface.
func (tb *TestBanker) TotalCoin(denom string) int64 {
panic("not yet implemented")
if denom == "" {
panic("empty denom")
}
var total int64
for _, coins := range tb.CoinTable {
total += coins.AmountOf(denom)
}
return total
}

// IssueCoin implements the Banker interface.
func (tb *TestBanker) IssueCoin(addr crypto.Bech32Address, denom string, amt int64) {
coins, _ := tb.CoinTable[addr]
coins := tb.CoinTable[addr]
sum := coins.Add(tm2std.Coins{{Denom: denom, Amount: amt}})
tb.CoinTable[addr] = sum
}

// RemoveCoin implements the Banker interface.
func (tb *TestBanker) RemoveCoin(addr crypto.Bech32Address, denom string, amt int64) {
coins, _ := tb.CoinTable[addr]
coins := tb.CoinTable[addr]
rest := coins.Sub(tm2std.Coins{{Denom: denom, Amount: amt}})
tb.CoinTable[addr] = rest
}
Expand Down
Loading