From aae5d49f9849c3e30f36cbbc60f162d8e3b558d6 Mon Sep 17 00:00:00 2001
From: Alex Gherasie <68433935+agherasie@users.noreply.github.com>
Date: Wed, 21 Aug 2024 23:04:30 +0200
Subject: [PATCH] feat(examples): add disperse (v2) (#2613)
This PR adds a gno version of the [disperse ethereum
app](https://disperse.app/) to gno.land !
Another attempt was made in https://github.com/gnolang/gno/pull/1414 but
we have decided to pick up from @leohhhn 's work with @lennyvong and
made this PR, tested using txtar tests to avoid the situation described
in https://github.com/gnolang/gno/issues/2595
There is also an older (but functional for the most part) version
[deployed in
test4](https://test4.gno.land/r/g1w62226g8hykfmtuasvz80rdf0jl6phgxsphh5v/testing/disperse2?help)
with a linked [webapp](https://gno-disperse.netlify.app/)
Contributors' checklist...
- [X] Added new tests, or not needed, or not feasible
- [X] Provided an example (e.g. screenshot) to aid review or the PR is
self-explanatory
- [X] Updated the official documentation or not needed
- [X] No breaking changes were made, or a `BREAKING CHANGE: xxx` message
was included in the description
- [X] Added references to related issues and PRs
- [X] Provided any useful hints for running manual tests
- [ ] Added new benchmarks to [generated
graphs](https://gnoland.github.io/benchmarks), if any. More info
[here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
---------
Co-authored-by: leohhhn
Co-authored-by: Leon Hudak <33522493+leohhhn@users.noreply.github.com>
Co-authored-by: lennyvongphouthone
Co-authored-by: Guilhem Fanton <8671905+gfanton@users.noreply.github.com>
---
.../gno.land/r/demo/disperse/disperse.gno | 99 +++++++++++++++++++
examples/gno.land/r/demo/disperse/doc.gno | 19 ++++
examples/gno.land/r/demo/disperse/errors.gno | 12 +++
examples/gno.land/r/demo/disperse/gno.mod | 3 +
examples/gno.land/r/demo/disperse/util.gno | 67 +++++++++++++
.../gno.land/r/demo/disperse/z_0_filetest.gno | 32 ++++++
.../gno.land/r/demo/disperse/z_1_filetest.gno | 32 ++++++
.../gno.land/r/demo/disperse/z_2_filetest.gno | 25 +++++
.../gno.land/r/demo/disperse/z_3_filetest.gno | 45 +++++++++
.../gno.land/r/demo/disperse/z_4_filetest.gno | 48 +++++++++
10 files changed, 382 insertions(+)
create mode 100644 examples/gno.land/r/demo/disperse/disperse.gno
create mode 100644 examples/gno.land/r/demo/disperse/doc.gno
create mode 100644 examples/gno.land/r/demo/disperse/errors.gno
create mode 100644 examples/gno.land/r/demo/disperse/gno.mod
create mode 100644 examples/gno.land/r/demo/disperse/util.gno
create mode 100644 examples/gno.land/r/demo/disperse/z_0_filetest.gno
create mode 100644 examples/gno.land/r/demo/disperse/z_1_filetest.gno
create mode 100644 examples/gno.land/r/demo/disperse/z_2_filetest.gno
create mode 100644 examples/gno.land/r/demo/disperse/z_3_filetest.gno
create mode 100644 examples/gno.land/r/demo/disperse/z_4_filetest.gno
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