diff --git a/examples/gno.land/r/demo/disperse/disperse.gno b/examples/gno.land/r/demo/disperse/disperse.gno new file mode 100644 index 00000000000..0dc833dda95 --- /dev/null +++ b/examples/gno.land/r/demo/disperse/disperse.gno @@ -0,0 +1,99 @@ +package disperse + +import ( + "std" + + tokens "gno.land/r/demo/grc20factory" +) + +// Get address of Disperse realm +var realmAddr = std.CurrentRealm().Addr() + +// DisperseUgnot parses receivers and amounts and sends out ugnot +// The function will send out the coins to the addresses and return the leftover coins to the caller +// if there are any to return +func DisperseUgnot(addresses []std.Address, coins std.Coins) { + coinSent := std.GetOrigSend() + caller := std.PrevRealm().Addr() + banker := std.GetBanker(std.BankerTypeOrigSend) + + if len(addresses) != len(coins) { + panic(ErrNumAddrValMismatch) + } + + for _, coin := range coins { + if coin.Amount <= 0 { + panic(ErrNegativeCoinAmount) + } + + if banker.GetCoins(realmAddr).AmountOf(coin.Denom) < coin.Amount { + panic(ErrMismatchBetweenSentAndParams) + } + } + + // Send coins + for i, _ := range addresses { + banker.SendCoins(realmAddr, addresses[i], std.NewCoins(coins[i])) + } + + // Return possible leftover coins + for _, coin := range coinSent { + leftoverAmt := banker.GetCoins(realmAddr).AmountOf(coin.Denom) + if leftoverAmt > 0 { + send := std.Coins{std.NewCoin(coin.Denom, leftoverAmt)} + banker.SendCoins(realmAddr, caller, send) + } + } +} + +// DisperseGRC20 disperses tokens to multiple addresses +// Note that it is necessary to approve the realm to spend the tokens before calling this function +// see the corresponding filetests for examples +func DisperseGRC20(addresses []std.Address, amounts []uint64, symbols []string) { + caller := std.PrevRealm().Addr() + + if (len(addresses) != len(amounts)) || (len(amounts) != len(symbols)) { + panic(ErrArgLenAndSentLenMismatch) + } + + for i := 0; i < len(addresses); i++ { + tokens.TransferFrom(symbols[i], caller, addresses[i], amounts[i]) + } +} + +// DisperseGRC20String receives a string of addresses and a string of tokens +// and parses them to be used in DisperseGRC20 +func DisperseGRC20String(addresses string, tokens string) { + parsedAddresses, err := parseAddresses(addresses) + if err != nil { + panic(err) + } + + parsedAmounts, parsedSymbols, err := parseTokens(tokens) + if err != nil { + panic(err) + } + + DisperseGRC20(parsedAddresses, parsedAmounts, parsedSymbols) +} + +// DisperseUgnotString receives a string of addresses and a string of amounts +// and parses them to be used in DisperseUgnot +func DisperseUgnotString(addresses string, amounts string) { + parsedAddresses, err := parseAddresses(addresses) + if err != nil { + panic(err) + } + + parsedAmounts, err := parseAmounts(amounts) + if err != nil { + panic(err) + } + + coins := make(std.Coins, len(parsedAmounts)) + for i, amount := range parsedAmounts { + coins[i] = std.NewCoin("ugnot", amount) + } + + DisperseUgnot(parsedAddresses, coins) +} diff --git a/examples/gno.land/r/demo/disperse/doc.gno b/examples/gno.land/r/demo/disperse/doc.gno new file mode 100644 index 00000000000..100aa92cb3d --- /dev/null +++ b/examples/gno.land/r/demo/disperse/doc.gno @@ -0,0 +1,19 @@ +// Package disperse provides methods to disperse coins or GRC20 tokens among multiple addresses. +// +// The disperse package is an implementation of an existing service that allows users to send coins or GRC20 tokens to multiple addresses +// on the Ethereum blockchain. +// +// Usage: +// To use disperse, you can either use `DisperseUgnot` to send coins or `DisperseGRC20` to send GRC20 tokens to multiple addresses. +// +// Example: +// Dispersing 200 coins to two addresses: +// - DisperseUgnotString("g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c", "150,50") +// Dispersing 200 worth of a GRC20 token "TEST" to two addresses: +// - DisperseGRC20String("g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c", "150TEST,50TEST") +// +// Reference: +// - [the original dispere app](https://disperse.app/) +// - [the original disperse app on etherscan](https://etherscan.io/address/0xd152f549545093347a162dce210e7293f1452150#code) +// - [the gno disperse web app](https://gno-disperse.netlify.app/) +package disperse // import "gno.land/r/demo/disperse" diff --git a/examples/gno.land/r/demo/disperse/errors.gno b/examples/gno.land/r/demo/disperse/errors.gno new file mode 100644 index 00000000000..c054e658651 --- /dev/null +++ b/examples/gno.land/r/demo/disperse/errors.gno @@ -0,0 +1,12 @@ +package disperse + +import "errors" + +var ( + ErrNotEnoughCoin = errors.New("disperse: not enough coin sent in") + ErrNumAddrValMismatch = errors.New("disperse: number of addresses and values to send doesn't match") + ErrInvalidAddress = errors.New("disperse: invalid address") + ErrNegativeCoinAmount = errors.New("disperse: coin amount cannot be negative") + ErrMismatchBetweenSentAndParams = errors.New("disperse: mismatch between coins sent and params called") + ErrArgLenAndSentLenMismatch = errors.New("disperse: mismatch between coins sent and args called") +) diff --git a/examples/gno.land/r/demo/disperse/gno.mod b/examples/gno.land/r/demo/disperse/gno.mod new file mode 100644 index 00000000000..0ba9c88810a --- /dev/null +++ b/examples/gno.land/r/demo/disperse/gno.mod @@ -0,0 +1,3 @@ +module gno.land/r/demo/disperse + +require gno.land/r/demo/grc20factory v0.0.0-latest diff --git a/examples/gno.land/r/demo/disperse/util.gno b/examples/gno.land/r/demo/disperse/util.gno new file mode 100644 index 00000000000..7101522572d --- /dev/null +++ b/examples/gno.land/r/demo/disperse/util.gno @@ -0,0 +1,67 @@ +package disperse + +import ( + "std" + "strconv" + "strings" + "unicode" +) + +func parseAddresses(addresses string) ([]std.Address, error) { + var ret []std.Address + + for _, str := range strings.Split(addresses, ",") { + addr := std.Address(str) + if !addr.IsValid() { + return nil, ErrInvalidAddress + } + + ret = append(ret, addr) + } + + return ret, nil +} + +func splitString(input string) (string, string) { + var pos int + for i, char := range input { + if !unicode.IsDigit(char) { + pos = i + break + } + } + return input[:pos], input[pos:] +} + +func parseTokens(tokens string) ([]uint64, []string, error) { + var amounts []uint64 + var symbols []string + + for _, token := range strings.Split(tokens, ",") { + amountStr, symbol := splitString(token) + amount, _ := strconv.Atoi(amountStr) + if amount < 0 { + return nil, nil, ErrNegativeCoinAmount + } + + amounts = append(amounts, uint64(amount)) + symbols = append(symbols, symbol) + } + + return amounts, symbols, nil +} + +func parseAmounts(amounts string) ([]int64, error) { + var ret []int64 + + for _, amt := range strings.Split(amounts, ",") { + amount, _ := strconv.Atoi(amt) + if amount < 0 { + return nil, ErrNegativeCoinAmount + } + + ret = append(ret, int64(amount)) + } + + return ret, nil +} diff --git a/examples/gno.land/r/demo/disperse/z_0_filetest.gno b/examples/gno.land/r/demo/disperse/z_0_filetest.gno new file mode 100644 index 00000000000..62a34cfdf26 --- /dev/null +++ b/examples/gno.land/r/demo/disperse/z_0_filetest.gno @@ -0,0 +1,32 @@ +// SEND: 200ugnot + +package main + +import ( + "std" + + "gno.land/r/demo/disperse" +) + +func main() { + disperseAddr := std.DerivePkgAddr("gno.land/r/demo/disperse") + mainaddr := std.DerivePkgAddr("main") + + std.TestSetOrigPkgAddr(disperseAddr) + std.TestSetOrigCaller(mainaddr) + + banker := std.GetBanker(std.BankerTypeRealmSend) + + mainbal := banker.GetCoins(mainaddr) + println("main before:", mainbal) + + banker.SendCoins(mainaddr, disperseAddr, std.Coins{{"ugnot", 200}}) + disperse.DisperseUgnotString("g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c", "150,50") + + mainbal = banker.GetCoins(mainaddr) + println("main after:", mainbal) +} + +// Output: +// main before: 200000200ugnot +// main after: 200000000ugnot diff --git a/examples/gno.land/r/demo/disperse/z_1_filetest.gno b/examples/gno.land/r/demo/disperse/z_1_filetest.gno new file mode 100644 index 00000000000..1e042d320f6 --- /dev/null +++ b/examples/gno.land/r/demo/disperse/z_1_filetest.gno @@ -0,0 +1,32 @@ +// SEND: 300ugnot + +package main + +import ( + "std" + + "gno.land/r/demo/disperse" +) + +func main() { + disperseAddr := std.DerivePkgAddr("gno.land/r/demo/disperse") + mainaddr := std.DerivePkgAddr("main") + + std.TestSetOrigPkgAddr(disperseAddr) + std.TestSetOrigCaller(mainaddr) + + banker := std.GetBanker(std.BankerTypeRealmSend) + + mainbal := banker.GetCoins(mainaddr) + println("main before:", mainbal) + + banker.SendCoins(mainaddr, disperseAddr, std.Coins{{"ugnot", 300}}) + disperse.DisperseUgnotString("g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c", "150,50") + + mainbal = banker.GetCoins(mainaddr) + println("main after:", mainbal) +} + +// Output: +// main before: 200000300ugnot +// main after: 200000100ugnot diff --git a/examples/gno.land/r/demo/disperse/z_2_filetest.gno b/examples/gno.land/r/demo/disperse/z_2_filetest.gno new file mode 100644 index 00000000000..163bb2fc1ab --- /dev/null +++ b/examples/gno.land/r/demo/disperse/z_2_filetest.gno @@ -0,0 +1,25 @@ +// SEND: 300ugnot + +package main + +import ( + "std" + + "gno.land/r/demo/disperse" +) + +func main() { + disperseAddr := std.DerivePkgAddr("gno.land/r/demo/disperse") + mainaddr := std.DerivePkgAddr("main") + + std.TestSetOrigPkgAddr(disperseAddr) + std.TestSetOrigCaller(mainaddr) + + banker := std.GetBanker(std.BankerTypeRealmSend) + + banker.SendCoins(mainaddr, disperseAddr, std.Coins{{"ugnot", 100}}) + disperse.DisperseUgnotString("g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c", "150,50") +} + +// Error: +// disperse: mismatch between coins sent and params called diff --git a/examples/gno.land/r/demo/disperse/z_3_filetest.gno b/examples/gno.land/r/demo/disperse/z_3_filetest.gno new file mode 100644 index 00000000000..eabed52fb38 --- /dev/null +++ b/examples/gno.land/r/demo/disperse/z_3_filetest.gno @@ -0,0 +1,45 @@ +// SEND: 300ugnot + +package main + +import ( + "std" + + "gno.land/r/demo/disperse" + tokens "gno.land/r/demo/grc20factory" +) + +func main() { + disperseAddr := std.DerivePkgAddr("gno.land/r/demo/disperse") + mainaddr := std.DerivePkgAddr("main") + beneficiary1 := std.Address("g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0") + beneficiary2 := std.Address("g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c") + + std.TestSetOrigPkgAddr(disperseAddr) + std.TestSetOrigCaller(mainaddr) + + banker := std.GetBanker(std.BankerTypeRealmSend) + + tokens.New("test", "TEST", 4, 0, 0) + tokens.Mint("TEST", mainaddr, 200) + + mainbal := tokens.BalanceOf("TEST", mainaddr) + println("main before:", mainbal) + + tokens.Approve("TEST", disperseAddr, 200) + + disperse.DisperseGRC20String("g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c", "150TEST,50TEST") + + mainbal = tokens.BalanceOf("TEST", mainaddr) + println("main after:", mainbal) + ben1bal := tokens.BalanceOf("TEST", beneficiary1) + println("beneficiary1:", ben1bal) + ben2bal := tokens.BalanceOf("TEST", beneficiary2) + println("beneficiary2:", ben2bal) +} + +// Output: +// main before: 200 +// main after: 0 +// beneficiary1: 150 +// beneficiary2: 50 diff --git a/examples/gno.land/r/demo/disperse/z_4_filetest.gno b/examples/gno.land/r/demo/disperse/z_4_filetest.gno new file mode 100644 index 00000000000..ebf4bed4473 --- /dev/null +++ b/examples/gno.land/r/demo/disperse/z_4_filetest.gno @@ -0,0 +1,48 @@ +// SEND: 300ugnot + +package main + +import ( + "std" + + "gno.land/r/demo/disperse" + tokens "gno.land/r/demo/grc20factory" +) + +func main() { + disperseAddr := std.DerivePkgAddr("gno.land/r/demo/disperse") + mainaddr := std.DerivePkgAddr("main") + beneficiary1 := std.Address("g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0") + beneficiary2 := std.Address("g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c") + + std.TestSetOrigPkgAddr(disperseAddr) + std.TestSetOrigCaller(mainaddr) + + banker := std.GetBanker(std.BankerTypeRealmSend) + + tokens.New("test1", "TEST1", 4, 0, 0) + tokens.Mint("TEST1", mainaddr, 200) + tokens.New("test2", "TEST2", 4, 0, 0) + tokens.Mint("TEST2", mainaddr, 200) + + mainbal := tokens.BalanceOf("TEST1", mainaddr) + tokens.BalanceOf("TEST2", mainaddr) + println("main before:", mainbal) + + tokens.Approve("TEST1", disperseAddr, 200) + tokens.Approve("TEST2", disperseAddr, 200) + + disperse.DisperseGRC20String("g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c", "200TEST1,200TEST2") + + mainbal = tokens.BalanceOf("TEST1", mainaddr) + tokens.BalanceOf("TEST2", mainaddr) + println("main after:", mainbal) + ben1bal := tokens.BalanceOf("TEST1", beneficiary1) + tokens.BalanceOf("TEST2", beneficiary1) + println("beneficiary1:", ben1bal) + ben2bal := tokens.BalanceOf("TEST1", beneficiary2) + tokens.BalanceOf("TEST2", beneficiary2) + println("beneficiary2:", ben2bal) +} + +// Output: +// main before: 400 +// main after: 0 +// beneficiary1: 200 +// beneficiary2: 200