atomicswap.gno

5.50 Kb ยท 191 lines
  1// Package atomicswap implements a hash time-locked contract (HTLC) for atomic swaps
  2// between native coins (ugnot) or GRC20 tokens.
  3//
  4// An atomic swap allows two parties to exchange assets in a trustless way, where
  5// either both transfers happen or neither does. The process works as follows:
  6//
  7//  1. Alice wants to swap with Bob. She generates a secret and creates a swap with
  8//     Bob's address and the hash of the secret (hashlock).
  9//
 10//  2. Bob can claim the assets by providing the correct secret before the timelock expires.
 11//     The secret proves Bob knows the preimage of the hashlock.
 12//
 13// 3. If Bob doesn't claim in time, Alice can refund the assets back to herself.
 14//
 15// Example usage for native coins:
 16//
 17//	// Alice creates a swap with 1000ugnot for Bob
 18//	secret := "mysecret"
 19//	hashlock := hex.EncodeToString(sha256.Sum256([]byte(secret)))
 20//	id, _ := atomicswap.NewCoinSwap(bobAddr, hashlock) // -send 1000ugnot
 21//
 22//	// Bob claims the swap by providing the secret
 23//	atomicswap.Claim(id, "mysecret")
 24//
 25// Example usage for GRC20 tokens:
 26//
 27//	// Alice approves the swap contract to spend her tokens
 28//	token.Approve(swapAddr, 1000)
 29//
 30//	// Alice creates a swap with 1000 tokens for Bob
 31//	id, _ := atomicswap.NewGRC20Swap(bobAddr, hashlock, "gno.land/r/demo/token")
 32//
 33//	// Bob claims the swap by providing the secret
 34//	atomicswap.Claim(id, "mysecret")
 35//
 36// If Bob doesn't claim in time (default 1 week), Alice can refund:
 37//
 38//	atomicswap.Refund(id)
 39package atomicswap
 40
 41import (
 42	"std"
 43	"strconv"
 44	"time"
 45
 46	"gno.land/p/demo/avl"
 47	"gno.land/p/demo/grc/grc20"
 48	"gno.land/p/demo/ufmt"
 49	"gno.land/r/demo/grc20reg"
 50)
 51
 52const defaultTimelockDuration = 7 * 24 * time.Hour // 1w
 53
 54var (
 55	swaps   avl.Tree // id -> *Swap
 56	counter int
 57)
 58
 59// NewCoinSwap creates a new atomic swap contract for native coins.
 60// It uses a default timelock duration.
 61func NewCoinSwap(recipient std.Address, hashlock string) (int, *Swap) {
 62	crossing()
 63
 64	timelock := time.Now().Add(defaultTimelockDuration)
 65	return NewCustomCoinSwap(recipient, hashlock, timelock)
 66}
 67
 68// NewGRC20Swap creates a new atomic swap contract for grc20 tokens.
 69// It uses gno.land/r/demo/grc20reg to lookup for a registered token.
 70func NewGRC20Swap(recipient std.Address, hashlock string, tokenRegistryKey string) (int, *Swap) {
 71	crossing()
 72
 73	timelock := time.Now().Add(defaultTimelockDuration)
 74	token := grc20reg.MustGet(tokenRegistryKey)
 75	return NewCustomGRC20Swap(recipient, hashlock, timelock, token)
 76}
 77
 78// NewCoinSwapWithTimelock creates a new atomic swap contract for native coin.
 79// It allows specifying a custom timelock duration.
 80// It is not callable with `gnokey maketx call`, but can be imported by another contract or `gnokey maketx run`.
 81func NewCustomCoinSwap(recipient std.Address, hashlock string, timelock time.Time) (int, *Swap) {
 82	crossing()
 83
 84	sender := std.PreviousRealm().Address()
 85	sent := std.OriginSend()
 86	require(len(sent) != 0, "at least one coin needs to be sent")
 87
 88	// Create the swap
 89	sendFn := func(to std.Address) {
 90		crossing()
 91
 92		banker := std.NewBanker(std.BankerTypeRealmSend)
 93		pkgAddr := std.CurrentRealm().Address()
 94		banker.SendCoins(pkgAddr, to, sent)
 95	}
 96	amountStr := sent.String()
 97	swap := newSwap(sender, recipient, hashlock, timelock, amountStr, sendFn)
 98
 99	counter++
100	id := strconv.Itoa(counter)
101	swaps.Set(id, swap)
102	return counter, swap
103}
104
105// NewCustomGRC20Swap creates a new atomic swap contract for grc20 tokens.
106// It is not callable with `gnokey maketx call`, but can be imported by another contract or `gnokey maketx run`.
107func NewCustomGRC20Swap(recipient std.Address, hashlock string, timelock time.Time, token *grc20.Token) (int, *Swap) {
108	crossing()
109
110	sender := std.PreviousRealm().Address()
111	curAddr := std.CurrentRealm().Address()
112
113	allowance := token.Allowance(sender, curAddr)
114	require(allowance > 0, "no allowance")
115
116	userTeller := token.RealmTeller()
117	err := userTeller.TransferFrom(sender, curAddr, allowance)
118	require(err == nil, "cannot retrieve tokens from allowance")
119
120	amountStr := ufmt.Sprintf("%d%s", allowance, token.GetSymbol())
121	sendFn := func(to std.Address) {
122		crossing()
123
124		err := userTeller.Transfer(to, allowance)
125		require(err == nil, "cannot transfer tokens")
126	}
127
128	swap := newSwap(sender, recipient, hashlock, timelock, amountStr, sendFn)
129
130	counter++
131	id := strconv.Itoa(counter)
132	swaps.Set(id, swap)
133
134	return counter, swap
135}
136
137// Claim loads a registered swap and tries to claim it.
138func Claim(id int, secret string) {
139	crossing()
140
141	swap := mustGet(id)
142	swap.Claim(secret)
143}
144
145// Refund loads a registered swap and tries to refund it.
146func Refund(id int) {
147	crossing()
148
149	swap := mustGet(id)
150	swap.Refund()
151}
152
153// Render returns a list of swaps (simplified) for the homepage, and swap details when specifying a swap ID.
154func Render(path string) string {
155	crossing()
156	if path == "" { // home
157		output := ""
158		size := swaps.Size()
159		max := 10
160		swaps.ReverseIterateByOffset(size-max, max, func(key string, value any) bool {
161			swap := value.(*Swap)
162			output += ufmt.Sprintf("- %s: %s -(%s)> %s - %s\n",
163				key, swap.sender, swap.amountStr, swap.recipient, swap.Status())
164			return false
165		})
166		return output
167	} else { // by id
168		swap, ok := swaps.Get(path)
169		if !ok {
170			return "404"
171		}
172		return swap.(*Swap).String()
173	}
174}
175
176// require checks a condition and panics with a message if the condition is false.
177func require(check bool, msg string) {
178	if !check {
179		panic(msg)
180	}
181}
182
183// mustGet retrieves a swap by its id or panics.
184func mustGet(id int) *Swap {
185	key := strconv.Itoa(id)
186	swap, ok := swaps.Get(key)
187	if !ok {
188		panic("unknown swap ID")
189	}
190	return swap.(*Swap)
191}