Skip to content

Commit

Permalink
feat(examples): add disperse (v2) (#2613)
Browse files Browse the repository at this point in the history
This PR adds a gno version of the [disperse ethereum
app](https://disperse.app/) to gno.land !
Another attempt was made in #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 #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/)

<img width="649" alt="image"
src="https://github.com/user-attachments/assets/ad26a1a9-0447-4333-858a-253441c457ba">

<details><summary>Contributors' checklist...</summary>

- [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).
</details>

---------

Co-authored-by: leohhhn <hudakleon@gmail.com>
Co-authored-by: Leon Hudak <33522493+leohhhn@users.noreply.github.com>
Co-authored-by: lennyvongphouthone <lenny.vongphouthone@epitech.eu>
Co-authored-by: Guilhem Fanton <8671905+gfanton@users.noreply.github.com>
  • Loading branch information
5 people authored Aug 21, 2024
1 parent 04d5239 commit aae5d49
Show file tree
Hide file tree
Showing 10 changed files with 382 additions and 0 deletions.
99 changes: 99 additions & 0 deletions examples/gno.land/r/demo/disperse/disperse.gno
Original file line number Diff line number Diff line change
@@ -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)
}
19 changes: 19 additions & 0 deletions examples/gno.land/r/demo/disperse/doc.gno
Original file line number Diff line number Diff line change
@@ -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"
12 changes: 12 additions & 0 deletions examples/gno.land/r/demo/disperse/errors.gno
Original file line number Diff line number Diff line change
@@ -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")
)
3 changes: 3 additions & 0 deletions examples/gno.land/r/demo/disperse/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module gno.land/r/demo/disperse

require gno.land/r/demo/grc20factory v0.0.0-latest
67 changes: 67 additions & 0 deletions examples/gno.land/r/demo/disperse/util.gno
Original file line number Diff line number Diff line change
@@ -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
}
32 changes: 32 additions & 0 deletions examples/gno.land/r/demo/disperse/z_0_filetest.gno
Original file line number Diff line number Diff line change
@@ -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
32 changes: 32 additions & 0 deletions examples/gno.land/r/demo/disperse/z_1_filetest.gno
Original file line number Diff line number Diff line change
@@ -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
25 changes: 25 additions & 0 deletions examples/gno.land/r/demo/disperse/z_2_filetest.gno
Original file line number Diff line number Diff line change
@@ -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
45 changes: 45 additions & 0 deletions examples/gno.land/r/demo/disperse/z_3_filetest.gno
Original file line number Diff line number Diff line change
@@ -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
48 changes: 48 additions & 0 deletions examples/gno.land/r/demo/disperse/z_4_filetest.gno
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit aae5d49

Please sign in to comment.