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}