Skip to content

Commit

Permalink
feat: add r/demo/atomicswap (#2510)
Browse files Browse the repository at this point in the history
- [x] implement
- [x] unit test / txtar
- [x] question: ugnot or grc20 or both? Maybe it’s time to encourage
using `wugnot`.
	**-> both, with a callback mechanism.**
- [x] question: p+r or just r?
	**-> just `r`, and a single file. let's do it more!**
- [x] make the API gnokey compatible + add Render.

Depends on #3397 (cherry-picked)
Depends on #2529 (cherry-picked)
Depends on #2549 (cherry-picked)
Depends on #2551
Closes #2549

---------

Signed-off-by: moul <94029+moul@users.noreply.github.com>
Co-authored-by: Leon Hudak <33522493+leohhhn@users.noreply.github.com>
  • Loading branch information
moul and leohhhn authored Jan 20, 2025
1 parent 7e21e23 commit 5448754
Show file tree
Hide file tree
Showing 5 changed files with 749 additions and 0 deletions.
175 changes: 175 additions & 0 deletions examples/gno.land/r/demo/atomicswap/atomicswap.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// Package atomicswap implements a hash time-locked contract (HTLC) for atomic swaps
// between native coins (ugnot) or GRC20 tokens.
//
// An atomic swap allows two parties to exchange assets in a trustless way, where
// either both transfers happen or neither does. The process works as follows:
//
// 1. Alice wants to swap with Bob. She generates a secret and creates a swap with
// Bob's address and the hash of the secret (hashlock).
//
// 2. Bob can claim the assets by providing the correct secret before the timelock expires.
// The secret proves Bob knows the preimage of the hashlock.
//
// 3. If Bob doesn't claim in time, Alice can refund the assets back to herself.
//
// Example usage for native coins:
//
// // Alice creates a swap with 1000ugnot for Bob
// secret := "mysecret"
// hashlock := hex.EncodeToString(sha256.Sum256([]byte(secret)))
// id, _ := atomicswap.NewCoinSwap(bobAddr, hashlock) // -send 1000ugnot
//
// // Bob claims the swap by providing the secret
// atomicswap.Claim(id, "mysecret")
//
// Example usage for GRC20 tokens:
//
// // Alice approves the swap contract to spend her tokens
// token.Approve(swapAddr, 1000)
//
// // Alice creates a swap with 1000 tokens for Bob
// id, _ := atomicswap.NewGRC20Swap(bobAddr, hashlock, "gno.land/r/demo/token")
//
// // Bob claims the swap by providing the secret
// atomicswap.Claim(id, "mysecret")
//
// If Bob doesn't claim in time (default 1 week), Alice can refund:
//
// atomicswap.Refund(id)
package atomicswap

import (
"std"
"strconv"
"time"

"gno.land/p/demo/avl"
"gno.land/p/demo/grc/grc20"
"gno.land/p/demo/ufmt"
"gno.land/r/demo/grc20reg"
)

const defaultTimelockDuration = 7 * 24 * time.Hour // 1w

var (
swaps avl.Tree // id -> *Swap
counter int
)

// NewCoinSwap creates a new atomic swap contract for native coins.
// It uses a default timelock duration.
func NewCoinSwap(recipient std.Address, hashlock string) (int, *Swap) {
timelock := time.Now().Add(defaultTimelockDuration)
return NewCustomCoinSwap(recipient, hashlock, timelock)
}

// NewGRC20Swap creates a new atomic swap contract for grc20 tokens.
// It uses gno.land/r/demo/grc20reg to lookup for a registered token.
func NewGRC20Swap(recipient std.Address, hashlock string, tokenRegistryKey string) (int, *Swap) {
timelock := time.Now().Add(defaultTimelockDuration)
tokenGetter := grc20reg.MustGet(tokenRegistryKey)
token := tokenGetter()
return NewCustomGRC20Swap(recipient, hashlock, timelock, token)
}

// NewCoinSwapWithTimelock creates a new atomic swap contract for native coin.
// It allows specifying a custom timelock duration.
// It is not callable with `gnokey maketx call`, but can be imported by another contract or `gnokey maketx run`.
func NewCustomCoinSwap(recipient std.Address, hashlock string, timelock time.Time) (int, *Swap) {
sender := std.PrevRealm().Addr()
sent := std.GetOrigSend()
require(len(sent) != 0, "at least one coin needs to be sent")

// Create the swap
sendFn := func(to std.Address) {
banker := std.GetBanker(std.BankerTypeRealmSend)
pkgAddr := std.GetOrigPkgAddr()
banker.SendCoins(pkgAddr, to, sent)
}
amountStr := sent.String()
swap := newSwap(sender, recipient, hashlock, timelock, amountStr, sendFn)

counter++
id := strconv.Itoa(counter)
swaps.Set(id, swap)
return counter, swap
}

// NewCustomGRC20Swap creates a new atomic swap contract for grc20 tokens.
// It is not callable with `gnokey maketx call`, but can be imported by another contract or `gnokey maketx run`.
func NewCustomGRC20Swap(recipient std.Address, hashlock string, timelock time.Time, token *grc20.Token) (int, *Swap) {
sender := std.PrevRealm().Addr()
curAddr := std.CurrentRealm().Addr()

allowance := token.Allowance(sender, curAddr)
require(allowance > 0, "no allowance")

userTeller := token.CallerTeller()
err := userTeller.TransferFrom(sender, curAddr, allowance)
require(err == nil, "cannot retrieve tokens from allowance")

amountStr := ufmt.Sprintf("%d%s", allowance, token.GetSymbol())
sendFn := func(to std.Address) {
err := userTeller.Transfer(to, allowance)
require(err == nil, "cannot transfer tokens")
}

swap := newSwap(sender, recipient, hashlock, timelock, amountStr, sendFn)

counter++
id := strconv.Itoa(counter)
swaps.Set(id, swap)

return counter, swap
}

// Claim loads a registered swap and tries to claim it.
func Claim(id int, secret string) {
swap := mustGet(id)
swap.Claim(secret)
}

// Refund loads a registered swap and tries to refund it.
func Refund(id int) {
swap := mustGet(id)
swap.Refund()
}

// Render returns a list of swaps (simplified) for the homepage, and swap details when specifying a swap ID.
func Render(path string) string {
if path == "" { // home
output := ""
size := swaps.Size()
max := 10
swaps.ReverseIterateByOffset(size-max, max, func(key string, value interface{}) bool {
swap := value.(*Swap)
output += ufmt.Sprintf("- %s: %s -(%s)> %s - %s\n",
key, swap.sender, swap.amountStr, swap.recipient, swap.Status())
return false
})
return output
} else { // by id
swap, ok := swaps.Get(path)
if !ok {
return "404"
}
return swap.(*Swap).String()
}
}

// require checks a condition and panics with a message if the condition is false.
func require(check bool, msg string) {
if !check {
panic(msg)
}
}

// mustGet retrieves a swap by its id or panics.
func mustGet(id int) *Swap {
key := strconv.Itoa(id)
swap, ok := swaps.Get(key)
if !ok {
panic("unknown swap ID")
}
return swap.(*Swap)
}
Loading

0 comments on commit 5448754

Please sign in to comment.