-
Notifications
You must be signed in to change notification settings - Fork 393
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- [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
Showing
5 changed files
with
749 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
Oops, something went wrong.