-
Notifications
You must be signed in to change notification settings - Fork 405
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
base: master
Are you sure you want to change the base?
Changes from 9 commits
c59d58d
a654e50
df1bf50
c0b825a
b12bbe7
ee072ef
9cb5c81
dc636d3
ef43e83
695412e
0e325f3
0d751b6
15296c5
f150093
bd3eb64
99fb5b7
0c8d344
6f00822
8b09a6a
5c175b2
b535661
bd710bd
07584c3
aa509df
4bddb4a
bfee6be
c5a012b
4cbd61c
cb9a437
8efad65
468a717
7b5d23e
41ecdaf
1978a90
b5893d6
f20a4d6
dc45336
b451b9a
a357a08
c1b246d
16ed599
b713afd
1398f4f
28c00d1
55d979d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
leohhhn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
func renderHomepage() string { | ||
return `# Gno.land Coins | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. homepage should give some information, and provide default links so that we can try the features with just a few clicks. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is currently a placeholder. It is must be updated later. |
||
` | ||
} | ||
|
||
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:) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. back is already available in the navigation breadcrumb. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. removed 695412e |
||
` | ||
} | ||
|
||
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:) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. remove this, we should use the navigation breadcrumb for this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
` | ||
} |
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do we need to initialize a banker from this realm? (if there is a good reason, why not a readonly one?) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In I think there won't be any issues because the inside of the |
||
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I highly recommend even asserting on the panic by making it a string and checking if != "" |
||
if !strings.Contains(result, tt.expected) { | ||
t.Errorf("expected %s to contain %s", result, tt.expected) | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
module gno.land/r/gnoland/coins |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not scalable. We need to find a more efficient solution, or we should refrain from offering this feature for now. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I modified this function to process in a single loop. This is what I could think of for now. What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This needs a change in the underlying banker module, so that it keeps track of the available tokens, rather than iterating on the accounts here. |
||
} | ||
totalAmount += acc.GetCoins().AmountOf(denom) | ||
} | ||
return totalAmount | ||
} | ||
|
||
func (bnk *SDKBanker) IssueCoin(b32addr crypto.Bech32Address, denom string, amount int64) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. still not good; we should not iterate through the whole table at each read, we should do the opposite and maintain a totalsupply value somewhere else; or just remove this feature. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of iterating, I added a So, it only needs to retrieve values from a kv store, the operation of |
||
} | ||
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 | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we have this API instead, and always use a readonly banker?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Modified it so that the banker object is created inside the function. I also considered placing the banker globally, but it seemed better to restrict the object's lifetime to within the function (It's also purer than global).
695412e