tempo.ts 0.0.5 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/dist/chains.d.ts +244 -541
  2. package/dist/chains.d.ts.map +1 -1
  3. package/dist/chains.js +10 -23
  4. package/dist/chains.js.map +1 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/ox/SignatureEnvelope.d.ts +245 -0
  7. package/dist/ox/SignatureEnvelope.d.ts.map +1 -0
  8. package/dist/ox/SignatureEnvelope.js +437 -0
  9. package/dist/ox/SignatureEnvelope.js.map +1 -0
  10. package/dist/ox/Transaction.d.ts +61 -24
  11. package/dist/ox/Transaction.d.ts.map +1 -1
  12. package/dist/ox/Transaction.js +63 -18
  13. package/dist/ox/Transaction.js.map +1 -1
  14. package/dist/ox/TransactionEnvelopeAA.d.ts +461 -0
  15. package/dist/ox/TransactionEnvelopeAA.d.ts.map +1 -0
  16. package/dist/ox/TransactionEnvelopeAA.js +528 -0
  17. package/dist/ox/TransactionEnvelopeAA.js.map +1 -0
  18. package/dist/ox/TransactionRequest.d.ts +7 -5
  19. package/dist/ox/TransactionRequest.d.ts.map +1 -1
  20. package/dist/ox/TransactionRequest.js +21 -12
  21. package/dist/ox/TransactionRequest.js.map +1 -1
  22. package/dist/ox/index.d.ts +5 -4
  23. package/dist/ox/index.d.ts.map +1 -1
  24. package/dist/ox/index.js +5 -4
  25. package/dist/ox/index.js.map +1 -1
  26. package/dist/prool/Instance.d.ts +0 -4
  27. package/dist/prool/Instance.d.ts.map +1 -1
  28. package/dist/prool/Instance.js +7 -7
  29. package/dist/prool/Instance.js.map +1 -1
  30. package/dist/prool/index.d.ts +1 -1
  31. package/dist/prool/index.d.ts.map +1 -1
  32. package/dist/prool/index.js +1 -1
  33. package/dist/prool/index.js.map +1 -1
  34. package/dist/viem/{abis.d.ts → Abis.d.ts} +523 -9
  35. package/dist/viem/Abis.d.ts.map +1 -0
  36. package/dist/viem/{abis.js → Abis.js} +321 -9
  37. package/dist/viem/Abis.js.map +1 -0
  38. package/dist/viem/{actions → Actions}/amm.d.ts +21 -21
  39. package/dist/viem/Actions/amm.d.ts.map +1 -0
  40. package/dist/viem/{actions → Actions}/amm.js +55 -43
  41. package/dist/viem/Actions/amm.js.map +1 -0
  42. package/dist/viem/Actions/dex.d.ts +3263 -0
  43. package/dist/viem/Actions/dex.d.ts.map +1 -0
  44. package/dist/viem/Actions/dex.js +1357 -0
  45. package/dist/viem/Actions/dex.js.map +1 -0
  46. package/dist/viem/{actions → Actions}/fee.d.ts +8 -8
  47. package/dist/viem/Actions/fee.d.ts.map +1 -0
  48. package/dist/viem/{actions → Actions}/fee.js +14 -13
  49. package/dist/viem/Actions/fee.js.map +1 -0
  50. package/dist/viem/Actions/index.d.ts +6 -0
  51. package/dist/viem/Actions/index.d.ts.map +1 -0
  52. package/dist/viem/Actions/index.js +6 -0
  53. package/dist/viem/Actions/index.js.map +1 -0
  54. package/dist/viem/{actions → Actions}/policy.d.ts +19 -19
  55. package/dist/viem/Actions/policy.d.ts.map +1 -0
  56. package/dist/viem/{actions → Actions}/policy.js +59 -46
  57. package/dist/viem/Actions/policy.js.map +1 -0
  58. package/dist/viem/{actions → Actions}/token.d.ts +3251 -707
  59. package/dist/viem/Actions/token.d.ts.map +1 -0
  60. package/dist/viem/{actions → Actions}/token.js +420 -88
  61. package/dist/viem/Actions/token.js.map +1 -0
  62. package/dist/viem/Addresses.d.ts +9 -0
  63. package/dist/viem/Addresses.d.ts.map +1 -0
  64. package/dist/viem/Addresses.js +9 -0
  65. package/dist/viem/Addresses.js.map +1 -0
  66. package/dist/viem/{chain.d.ts → Chain.d.ts} +81 -57
  67. package/dist/viem/Chain.d.ts.map +1 -0
  68. package/dist/viem/{chain.js → Chain.js} +7 -7
  69. package/dist/viem/Chain.js.map +1 -0
  70. package/dist/viem/{client.d.ts → Client.d.ts} +4 -4
  71. package/dist/viem/Client.d.ts.map +1 -0
  72. package/dist/viem/{client.js → Client.js} +3 -3
  73. package/dist/viem/Client.js.map +1 -0
  74. package/dist/viem/{decorator.d.ts → Decorator.d.ts} +507 -5
  75. package/dist/viem/Decorator.d.ts.map +1 -0
  76. package/dist/viem/{decorator.js → Decorator.js} +31 -5
  77. package/dist/viem/Decorator.js.map +1 -0
  78. package/dist/viem/{formatters.d.ts → Formatters.d.ts} +2 -2
  79. package/dist/viem/Formatters.d.ts.map +1 -0
  80. package/dist/viem/{formatters.js → Formatters.js} +24 -17
  81. package/dist/viem/Formatters.js.map +1 -0
  82. package/dist/viem/Tick.d.ts +111 -0
  83. package/dist/viem/Tick.d.ts.map +1 -0
  84. package/dist/viem/Tick.js +127 -0
  85. package/dist/viem/Tick.js.map +1 -0
  86. package/dist/viem/TokenIds.d.ts +3 -0
  87. package/dist/viem/TokenIds.d.ts.map +1 -0
  88. package/dist/viem/TokenIds.js +3 -0
  89. package/dist/viem/TokenIds.js.map +1 -0
  90. package/dist/viem/Transaction.d.ts +57 -0
  91. package/dist/viem/Transaction.d.ts.map +1 -0
  92. package/dist/viem/Transaction.js +137 -0
  93. package/dist/viem/Transaction.js.map +1 -0
  94. package/dist/viem/{transport.d.ts → Transport.d.ts} +3 -3
  95. package/dist/viem/Transport.d.ts.map +1 -0
  96. package/dist/viem/{transport.js → Transport.js} +3 -3
  97. package/dist/viem/Transport.js.map +1 -0
  98. package/dist/viem/index.d.ts +13 -9
  99. package/dist/viem/index.d.ts.map +1 -1
  100. package/dist/viem/index.js +13 -9
  101. package/dist/viem/index.js.map +1 -1
  102. package/dist/viem/{types.d.ts → internal/types.d.ts} +3 -3
  103. package/dist/viem/internal/types.d.ts.map +1 -0
  104. package/dist/viem/{types.js.map → internal/types.js.map} +1 -1
  105. package/dist/viem/internal/utils.d.ts.map +1 -0
  106. package/dist/viem/internal/utils.js.map +1 -0
  107. package/package.json +87 -101
  108. package/src/chains.ts +10 -24
  109. package/src/ox/SignatureEnvelope.test.ts +1252 -0
  110. package/src/ox/SignatureEnvelope.ts +709 -0
  111. package/src/ox/Transaction.test.ts +144 -89
  112. package/src/ox/Transaction.ts +104 -29
  113. package/src/ox/TransactionEnvelopeAA.test.ts +1533 -0
  114. package/src/ox/TransactionEnvelopeAA.ts +858 -0
  115. package/src/ox/TransactionRequest.ts +25 -17
  116. package/src/ox/index.ts +2 -1
  117. package/src/prool/Instance.ts +6 -14
  118. package/src/prool/internal/chain.json +101 -27
  119. package/src/viem/{abis.ts → Abis.ts} +322 -8
  120. package/src/viem/{actions → Actions}/amm.test.ts +65 -68
  121. package/src/viem/{actions → Actions}/amm.ts +72 -60
  122. package/src/viem/Actions/dex.test.ts +1608 -0
  123. package/src/viem/Actions/dex.ts +2026 -0
  124. package/src/viem/{actions → Actions}/fee.test.ts +34 -36
  125. package/src/viem/{actions → Actions}/fee.ts +18 -17
  126. package/src/viem/{actions → Actions}/index.ts +1 -0
  127. package/src/viem/{actions → Actions}/policy.test.ts +2 -2
  128. package/src/viem/{actions → Actions}/policy.ts +77 -64
  129. package/src/viem/{actions → Actions}/token.test.ts +406 -67
  130. package/src/viem/{actions → Actions}/token.ts +675 -144
  131. package/src/viem/Addresses.ts +9 -0
  132. package/src/viem/{chain.ts → Chain.ts} +6 -6
  133. package/src/viem/{client.bench-d.ts → Client.bench-d.ts} +2 -2
  134. package/src/viem/{client.test.ts → Client.test.ts} +31 -6
  135. package/src/viem/{client.ts → Client.ts} +1 -1
  136. package/src/viem/{decorator.bench-d.ts → Decorator.bench-d.ts} +2 -2
  137. package/src/viem/{decorator.test.ts → Decorator.test.ts} +1 -0
  138. package/src/viem/{decorator.ts → Decorator.ts} +586 -4
  139. package/src/viem/{formatters.ts → Formatters.ts} +31 -20
  140. package/src/viem/Tick.test.ts +281 -0
  141. package/src/viem/Tick.ts +176 -0
  142. package/src/viem/TokenIds.ts +2 -0
  143. package/src/viem/Transaction.ts +303 -0
  144. package/src/viem/{transport.ts → Transport.ts} +5 -5
  145. package/src/viem/e2e.test.ts +153 -78
  146. package/src/viem/index.ts +13 -9
  147. package/src/viem/{types.ts → internal/types.ts} +3 -3
  148. package/dist/ox/TransactionEnvelopeFeeToken.d.ts +0 -393
  149. package/dist/ox/TransactionEnvelopeFeeToken.d.ts.map +0 -1
  150. package/dist/ox/TransactionEnvelopeFeeToken.js +0 -452
  151. package/dist/ox/TransactionEnvelopeFeeToken.js.map +0 -1
  152. package/dist/viem/abis.d.ts.map +0 -1
  153. package/dist/viem/abis.js.map +0 -1
  154. package/dist/viem/actions/amm.d.ts.map +0 -1
  155. package/dist/viem/actions/amm.js.map +0 -1
  156. package/dist/viem/actions/fee.d.ts.map +0 -1
  157. package/dist/viem/actions/fee.js.map +0 -1
  158. package/dist/viem/actions/index.d.ts +0 -5
  159. package/dist/viem/actions/index.d.ts.map +0 -1
  160. package/dist/viem/actions/index.js +0 -5
  161. package/dist/viem/actions/index.js.map +0 -1
  162. package/dist/viem/actions/policy.d.ts.map +0 -1
  163. package/dist/viem/actions/policy.js.map +0 -1
  164. package/dist/viem/actions/token.d.ts.map +0 -1
  165. package/dist/viem/actions/token.js.map +0 -1
  166. package/dist/viem/addresses.d.ts +0 -8
  167. package/dist/viem/addresses.d.ts.map +0 -1
  168. package/dist/viem/addresses.js +0 -8
  169. package/dist/viem/addresses.js.map +0 -1
  170. package/dist/viem/chain.d.ts.map +0 -1
  171. package/dist/viem/chain.js.map +0 -1
  172. package/dist/viem/client.d.ts.map +0 -1
  173. package/dist/viem/client.js.map +0 -1
  174. package/dist/viem/decorator.d.ts.map +0 -1
  175. package/dist/viem/decorator.js.map +0 -1
  176. package/dist/viem/formatters.d.ts.map +0 -1
  177. package/dist/viem/formatters.js.map +0 -1
  178. package/dist/viem/transaction.d.ts +0 -54
  179. package/dist/viem/transaction.d.ts.map +0 -1
  180. package/dist/viem/transaction.js +0 -108
  181. package/dist/viem/transaction.js.map +0 -1
  182. package/dist/viem/transport.d.ts.map +0 -1
  183. package/dist/viem/transport.js.map +0 -1
  184. package/dist/viem/types.d.ts.map +0 -1
  185. package/dist/viem/utils.d.ts.map +0 -1
  186. package/dist/viem/utils.js.map +0 -1
  187. package/src/ox/TransactionEnvelopeFeeToken.test.ts +0 -1119
  188. package/src/ox/TransactionEnvelopeFeeToken.ts +0 -717
  189. package/src/prool/internal/consensus.toml +0 -32
  190. package/src/viem/addresses.ts +0 -10
  191. package/src/viem/transaction.ts +0 -253
  192. /package/dist/viem/{types.js → internal/types.js} +0 -0
  193. /package/dist/viem/{utils.d.ts → internal/utils.d.ts} +0 -0
  194. /package/dist/viem/{utils.js → internal/utils.js} +0 -0
  195. /package/src/viem/{utils.ts → internal/utils.ts} +0 -0
@@ -0,0 +1,1608 @@
1
+ import { Actions, Addresses, createTempoClient, Tick } from 'tempo.ts/viem'
2
+ import { parseEther, publicActions } from 'viem'
3
+ import { mnemonicToAccount } from 'viem/accounts'
4
+ import { describe, expect, test } from 'vitest'
5
+ import { tempoTest } from '../../../test/viem/config.js'
6
+
7
+ const account = mnemonicToAccount(
8
+ 'test test test test test test test test test test test junk',
9
+ )
10
+
11
+ const client = createTempoClient({
12
+ account,
13
+ chain: tempoTest,
14
+ pollingInterval: 100,
15
+ }).extend(publicActions)
16
+
17
+ async function setupTokenPair() {
18
+ // Create quote token
19
+ const { token: quoteToken } = await Actions.token.createSync(client, {
20
+ name: 'Test Quote Token',
21
+ symbol: 'QUOTE',
22
+ currency: 'USD',
23
+ })
24
+
25
+ // Create base token
26
+ const { token: baseToken } = await Actions.token.createSync(client, {
27
+ name: 'Test Base Token',
28
+ symbol: 'BASE',
29
+ currency: 'USD',
30
+ quoteToken,
31
+ })
32
+
33
+ // Grant issuer role to mint base tokens
34
+ await Actions.token.grantRolesSync(client, {
35
+ token: baseToken,
36
+ roles: ['issuer'],
37
+ to: client.account.address,
38
+ })
39
+
40
+ // Grant issuer role to mint quote tokens
41
+ await Actions.token.grantRolesSync(client, {
42
+ token: quoteToken,
43
+ roles: ['issuer'],
44
+ to: client.account.address,
45
+ })
46
+
47
+ // Mint base tokens
48
+ await Actions.token.mintSync(client, {
49
+ token: baseToken,
50
+ to: account.address,
51
+ amount: parseEther('10000'),
52
+ })
53
+
54
+ // Mint quote tokens
55
+ await Actions.token.mintSync(client, {
56
+ token: quoteToken,
57
+ to: account.address,
58
+ amount: parseEther('10000'),
59
+ })
60
+
61
+ // Approve DEX to spend base tokens
62
+ await Actions.token.approveSync(client, {
63
+ token: baseToken,
64
+ spender: Addresses.stablecoinExchange,
65
+ amount: parseEther('10000'),
66
+ })
67
+
68
+ // Approve DEX to spend quote tokens
69
+ await Actions.token.approveSync(client, {
70
+ token: quoteToken,
71
+ spender: Addresses.stablecoinExchange,
72
+ amount: parseEther('10000'),
73
+ })
74
+
75
+ // Create the pair on the DEX
76
+ return await Actions.dex.createPairSync(client, {
77
+ base: baseToken,
78
+ })
79
+ }
80
+
81
+ describe('buy', () => {
82
+ test('default', async () => {
83
+ const { base, quote } = await setupTokenPair()
84
+
85
+ // Place ask order to create liquidity
86
+ await Actions.dex.placeSync(client, {
87
+ token: base,
88
+ amount: parseEther('500'),
89
+ type: 'sell',
90
+ tick: Tick.fromPrice('1.001'),
91
+ })
92
+
93
+ // Get initial balances
94
+ const baseBalanceBefore = await Actions.token.getBalance(client, {
95
+ token: base,
96
+ })
97
+
98
+ // Buy base tokens with quote tokens
99
+ const { receipt } = await Actions.dex.buySync(client, {
100
+ tokenIn: quote,
101
+ tokenOut: base,
102
+ amountOut: parseEther('100'),
103
+ maxAmountIn: parseEther('150'),
104
+ })
105
+
106
+ expect(receipt).toBeDefined()
107
+ expect(receipt.status).toBe('success')
108
+
109
+ // Verify balances changed
110
+ const baseBalanceAfter = await Actions.token.getBalance(client, {
111
+ token: base,
112
+ })
113
+
114
+ // Should have received base tokens
115
+ expect(baseBalanceAfter).toBeGreaterThan(baseBalanceBefore)
116
+ })
117
+
118
+ test('behavior: respects maxAmountIn', async () => {
119
+ const { base, quote } = await setupTokenPair()
120
+
121
+ // Place ask order at high price
122
+ await Actions.dex.placeSync(client, {
123
+ token: base,
124
+ amount: parseEther('500'),
125
+ type: 'sell',
126
+ tick: Tick.fromPrice('1.01'), // 1% above peg
127
+ })
128
+
129
+ // Try to buy with insufficient maxAmountIn - should fail
130
+ await expect(
131
+ Actions.dex.buySync(client, {
132
+ tokenIn: quote,
133
+ tokenOut: base,
134
+ amountOut: parseEther('100'),
135
+ maxAmountIn: parseEther('50'), // Way too low for 1% premium
136
+ }),
137
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
138
+ [ContractFunctionExecutionError: The contract function "swapExactAmountOut" reverted.
139
+
140
+ Error: MaxInputExceeded()
141
+
142
+ Contract Call:
143
+ address: 0xdec0000000000000000000000000000000000000
144
+ function: swapExactAmountOut(address tokenIn, address tokenOut, uint128 amountOut, uint128 maxAmountIn)
145
+ args: (0x20C0000000000000000000000000000000000004, 0x20c0000000000000000000000000000000000005, 100000000000000000000, 50000000000000000000)
146
+ sender: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
147
+
148
+ Docs: https://viem.sh/docs/contract/writeContract
149
+ Version: viem@2.38.2]
150
+ `)
151
+ })
152
+
153
+ test('behavior: fails with insufficient liquidity', async () => {
154
+ const { base, quote } = await setupTokenPair()
155
+
156
+ // Don't place any orders - no liquidity
157
+
158
+ // Try to buy - should fail due to no liquidity
159
+ await expect(
160
+ Actions.dex.buySync(client, {
161
+ tokenIn: quote,
162
+ tokenOut: base,
163
+ amountOut: parseEther('100'),
164
+ maxAmountIn: parseEther('150'),
165
+ }),
166
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
167
+ [ContractFunctionExecutionError: The contract function "swapExactAmountOut" reverted.
168
+
169
+ Error: InsufficientLiquidity()
170
+
171
+ Contract Call:
172
+ address: 0xdec0000000000000000000000000000000000000
173
+ function: swapExactAmountOut(address tokenIn, address tokenOut, uint128 amountOut, uint128 maxAmountIn)
174
+ args: (0x20C0000000000000000000000000000000000004, 0x20c0000000000000000000000000000000000005, 100000000000000000000, 150000000000000000000)
175
+ sender: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
176
+
177
+ Docs: https://viem.sh/docs/contract/writeContract
178
+ Version: viem@2.38.2]
179
+ `)
180
+ })
181
+ })
182
+
183
+ describe('cancel', () => {
184
+ test('default', async () => {
185
+ const { base, quote } = await setupTokenPair()
186
+
187
+ // Place a bid order
188
+ const { orderId } = await Actions.dex.placeSync(client, {
189
+ token: base,
190
+ amount: parseEther('100'),
191
+ type: 'buy',
192
+ tick: Tick.fromPrice('1.001'),
193
+ })
194
+
195
+ // Check initial DEX balance (should be 0)
196
+ const dexBalanceBefore = await Actions.dex.getBalance(client, {
197
+ account: account.address,
198
+ token: quote,
199
+ })
200
+ expect(dexBalanceBefore).toBe(0n)
201
+
202
+ // Cancel the order
203
+ const { receipt, ...result } = await Actions.dex.cancelSync(client, {
204
+ orderId,
205
+ })
206
+
207
+ expect(receipt).toBeDefined()
208
+ expect(receipt.status).toBe('success')
209
+ expect(result.orderId).toBe(orderId)
210
+ expect(result).toMatchInlineSnapshot(`
211
+ {
212
+ "orderId": 1n,
213
+ }
214
+ `)
215
+
216
+ // Check DEX balance after cancel - tokens should be refunded to internal balance
217
+ const dexBalanceAfter = await Actions.dex.getBalance(client, {
218
+ account: account.address,
219
+ token: quote,
220
+ })
221
+ expect(dexBalanceAfter).toBeGreaterThan(0n)
222
+ })
223
+
224
+ test('behavior: only maker can cancel', async () => {
225
+ const { base } = await setupTokenPair()
226
+
227
+ // Account places order
228
+ const { orderId } = await Actions.dex.placeSync(client, {
229
+ token: base,
230
+ amount: parseEther('100'),
231
+ type: 'buy',
232
+ tick: Tick.fromPrice('1.001'),
233
+ })
234
+
235
+ // Create another account
236
+ const account2 = mnemonicToAccount(
237
+ 'test test test test test test test test test test test junk',
238
+ { accountIndex: 1 },
239
+ )
240
+
241
+ // Transfer gas to account2
242
+ await Actions.token.transferSync(client, {
243
+ to: account2.address,
244
+ amount: parseEther('1'),
245
+ token: Addresses.defaultFeeToken,
246
+ })
247
+
248
+ // Account2 tries to cancel - should fail
249
+ await expect(
250
+ Actions.dex.cancelSync(client, {
251
+ account: account2,
252
+ orderId,
253
+ }),
254
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
255
+ [ContractFunctionExecutionError: The contract function "cancel" reverted.
256
+
257
+ Error: Unauthorized()
258
+
259
+ Contract Call:
260
+ address: 0xdec0000000000000000000000000000000000000
261
+ function: cancel(uint128 orderId)
262
+ args: (1)
263
+ sender: 0x8C8d35429F74ec245F8Ef2f4Fd1e551cFF97d650
264
+
265
+ Docs: https://viem.sh/docs/contract/writeContract
266
+ Version: viem@2.38.2]
267
+ `)
268
+ })
269
+
270
+ test('behavior: cannot cancel non-existent order', async () => {
271
+ await setupTokenPair()
272
+
273
+ // Try to cancel an order that doesn't exist
274
+ await expect(
275
+ Actions.dex.cancelSync(client, {
276
+ orderId: 999n,
277
+ }),
278
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
279
+ [ContractFunctionExecutionError: The contract function "cancel" reverted.
280
+
281
+ Error: OrderDoesNotExist()
282
+
283
+ Contract Call:
284
+ address: 0xdec0000000000000000000000000000000000000
285
+ function: cancel(uint128 orderId)
286
+ args: (999)
287
+ sender: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
288
+
289
+ Docs: https://viem.sh/docs/contract/writeContract
290
+ Version: viem@2.38.2]
291
+ `)
292
+ })
293
+ })
294
+
295
+ describe('createPair', () => {
296
+ test('default', async () => {
297
+ const { token: baseToken } = await Actions.token.createSync(client, {
298
+ name: 'Test Base Token',
299
+ symbol: 'BASE',
300
+ currency: 'USD',
301
+ })
302
+
303
+ const { receipt, ...result } = await Actions.dex.createPairSync(client, {
304
+ base: baseToken,
305
+ })
306
+
307
+ expect(receipt).toBeDefined()
308
+ expect(receipt.status).toBe('success')
309
+
310
+ const { key, ...rest } = result
311
+ expect(key).toBeDefined()
312
+ expect(rest).toMatchInlineSnapshot(`
313
+ {
314
+ "base": "0x20C0000000000000000000000000000000000004",
315
+ "quote": "0x20C0000000000000000000000000000000000000",
316
+ }
317
+ `)
318
+ })
319
+ })
320
+
321
+ describe('getBalance', () => {
322
+ test('default', async () => {
323
+ const { base, quote } = await setupTokenPair()
324
+
325
+ // Initial balance should be 0
326
+ const initialBalance = await Actions.dex.getBalance(client, {
327
+ account: account.address,
328
+ token: quote,
329
+ })
330
+ expect(initialBalance).toBe(0n)
331
+
332
+ // Place and cancel order to create internal balance
333
+ const { orderId } = await Actions.dex.placeSync(client, {
334
+ token: base,
335
+ amount: parseEther('50'),
336
+ type: 'buy',
337
+ tick: Tick.fromPrice('1.0005'),
338
+ })
339
+
340
+ await Actions.dex.cancelSync(client, {
341
+ orderId,
342
+ })
343
+
344
+ // Now balance should be > 0 (refunded quote tokens)
345
+ const balance = await Actions.dex.getBalance(client, {
346
+ account: account.address,
347
+ token: quote,
348
+ })
349
+ expect(balance).toBeGreaterThan(0n)
350
+ })
351
+
352
+ test('behavior: check different account', async () => {
353
+ const { quote } = await setupTokenPair()
354
+
355
+ const account2 = mnemonicToAccount(
356
+ 'test test test test test test test test test test test junk',
357
+ { accountIndex: 1 },
358
+ )
359
+
360
+ // Check account2's balance (should be 0)
361
+ const balance = await Actions.dex.getBalance(client, {
362
+ account: account2.address,
363
+ token: quote,
364
+ })
365
+ expect(balance).toBe(0n)
366
+ })
367
+
368
+ test('behavior: balances are per-token', async () => {
369
+ const { base, quote } = await setupTokenPair()
370
+
371
+ // Create balance in quote token
372
+ const { orderId } = await Actions.dex.placeSync(client, {
373
+ token: base,
374
+ amount: parseEther('100'),
375
+ type: 'buy',
376
+ tick: Tick.fromPrice('1.001'),
377
+ })
378
+ await Actions.dex.cancelSync(client, { orderId })
379
+
380
+ // Check quote balance (should have refunded tokens)
381
+ const quoteBalance = await Actions.dex.getBalance(client, {
382
+ account: account.address,
383
+ token: quote,
384
+ })
385
+ expect(quoteBalance).toBeGreaterThan(0n)
386
+
387
+ // Check base balance (should still be 0)
388
+ const baseBalance = await Actions.dex.getBalance(client, {
389
+ account: account.address,
390
+ token: base,
391
+ })
392
+ expect(baseBalance).toBe(0n)
393
+ })
394
+ })
395
+
396
+ describe('getBuyQuote', () => {
397
+ test('default', async () => {
398
+ const { base, quote } = await setupTokenPair()
399
+
400
+ // Place ask orders to create liquidity
401
+ await Actions.dex.placeSync(client, {
402
+ token: base,
403
+ amount: parseEther('500'),
404
+ type: 'sell',
405
+ tick: Tick.fromPrice('1.001'),
406
+ })
407
+
408
+ // Get quote for buying base tokens
409
+ const amountIn = await Actions.dex.getBuyQuote(client, {
410
+ tokenIn: quote,
411
+ tokenOut: base,
412
+ amountOut: parseEther('100'),
413
+ })
414
+
415
+ expect(amountIn).toBeGreaterThan(0n)
416
+ // Should be approximately 100 * 1.001 = 100.1
417
+ expect(amountIn).toBeGreaterThan(parseEther('100'))
418
+ })
419
+
420
+ test('behavior: fails with no liquidity', async () => {
421
+ const { base, quote } = await setupTokenPair()
422
+
423
+ // No orders placed - no liquidity
424
+
425
+ // Quote should fail
426
+ await expect(
427
+ Actions.dex.getBuyQuote(client, {
428
+ tokenIn: quote,
429
+ tokenOut: base,
430
+ amountOut: parseEther('100'),
431
+ }),
432
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
433
+ [ContractFunctionExecutionError: The contract function "quoteSwapExactAmountOut" reverted.
434
+
435
+ Error: InsufficientLiquidity()
436
+
437
+ Contract Call:
438
+ address: 0xdec0000000000000000000000000000000000000
439
+ function: quoteSwapExactAmountOut(address tokenIn, address tokenOut, uint128 amountOut)
440
+ args: (0x20C0000000000000000000000000000000000004, 0x20c0000000000000000000000000000000000005, 100000000000000000000)
441
+
442
+ Docs: https://viem.sh/docs/contract/readContract
443
+ Version: viem@2.38.2]
444
+ `)
445
+ })
446
+ })
447
+
448
+ describe('getOrder', () => {
449
+ test('default', async () => {
450
+ const { base } = await setupTokenPair()
451
+
452
+ // Place an order to get an order ID
453
+ const { orderId } = await Actions.dex.placeSync(client, {
454
+ token: base,
455
+ amount: parseEther('100'),
456
+ type: 'buy',
457
+ tick: Tick.fromPrice('1.001'),
458
+ })
459
+
460
+ // Get the order details
461
+ const order = await Actions.dex.getOrder(client, {
462
+ orderId,
463
+ })
464
+
465
+ expect(order).toBeDefined()
466
+ expect(order.maker).toBe(client.account.address)
467
+ expect(order.isBid).toBe(true)
468
+ expect(order.tick).toBe(Tick.fromPrice('1.001'))
469
+ expect(order.amount).toBe(parseEther('100'))
470
+ expect(order.remaining).toBe(parseEther('100'))
471
+ expect(order.isFlip).toBe(false)
472
+ })
473
+
474
+ test('behavior: returns flip order details', async () => {
475
+ const { base } = await setupTokenPair()
476
+
477
+ // Place a flip order
478
+ const { orderId } = await Actions.dex.placeFlipSync(client, {
479
+ token: base,
480
+ amount: parseEther('50'),
481
+ type: 'buy',
482
+ tick: Tick.fromPrice('1.001'),
483
+ flipTick: Tick.fromPrice('1.002'),
484
+ })
485
+
486
+ // Get the order details
487
+ const order = await Actions.dex.getOrder(client, {
488
+ orderId,
489
+ })
490
+
491
+ expect(order).toBeDefined()
492
+ expect(order.maker).toBe(client.account.address)
493
+ expect(order.isBid).toBe(true)
494
+ expect(order.tick).toBe(Tick.fromPrice('1.001'))
495
+ expect(order.amount).toBe(parseEther('50'))
496
+ expect(order.isFlip).toBe(true)
497
+ expect(order.flipTick).toBe(Tick.fromPrice('1.002'))
498
+ })
499
+
500
+ test('behavior: fails for non-existent order', async () => {
501
+ await setupTokenPair()
502
+
503
+ // Try to get an order that doesn't exist
504
+ await expect(
505
+ Actions.dex.getOrder(client, {
506
+ orderId: 999n,
507
+ }),
508
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
509
+ [ContractFunctionExecutionError: The contract function "getOrder" reverted.
510
+
511
+ Error: OrderDoesNotExist()
512
+
513
+ Contract Call:
514
+ address: 0xdec0000000000000000000000000000000000000
515
+ function: getOrder(uint128 orderId)
516
+ args: (999)
517
+
518
+ Docs: https://viem.sh/docs/contract/readContract
519
+ Version: viem@2.38.2]
520
+ `)
521
+ })
522
+
523
+ test('behavior: reflects order state after partial fill', async () => {
524
+ const { base, quote } = await setupTokenPair()
525
+
526
+ // Place a large sell order
527
+ const { orderId } = await Actions.dex.placeSync(client, {
528
+ token: base,
529
+ amount: parseEther('500'),
530
+ type: 'sell',
531
+ tick: Tick.fromPrice('1.001'),
532
+ })
533
+
534
+ // Get initial order state
535
+ const orderBefore = await Actions.dex.getOrder(client, {
536
+ orderId,
537
+ })
538
+ expect(orderBefore.amount).toBe(parseEther('500'))
539
+ expect(orderBefore.remaining).toBe(parseEther('500'))
540
+
541
+ // Partially fill the order with a buy
542
+ await Actions.dex.buySync(client, {
543
+ tokenIn: quote,
544
+ tokenOut: base,
545
+ amountOut: parseEther('100'),
546
+ maxAmountIn: parseEther('150'),
547
+ })
548
+
549
+ // Get order state after partial fill
550
+ const orderAfter = await Actions.dex.getOrder(client, {
551
+ orderId,
552
+ })
553
+ expect(orderAfter.amount).toBe(parseEther('500')) // amount unchanged
554
+ expect(orderAfter.remaining).toBeLessThan(parseEther('500')) // remaining decreased
555
+ })
556
+
557
+ test('behavior: linked list pointers for multiple orders at same tick', async () => {
558
+ const { base } = await setupTokenPair()
559
+
560
+ const tick = Tick.fromPrice('1.001')
561
+
562
+ // Place first order
563
+ const { orderId: orderId1 } = await Actions.dex.placeSync(client, {
564
+ token: base,
565
+ amount: parseEther('100'),
566
+ type: 'buy',
567
+ tick,
568
+ })
569
+
570
+ // Place second order at same tick
571
+ const { orderId: orderId2 } = await Actions.dex.placeSync(client, {
572
+ token: base,
573
+ amount: parseEther('50'),
574
+ type: 'buy',
575
+ tick,
576
+ })
577
+
578
+ // Get first order
579
+ const order1 = await Actions.dex.getOrder(client, {
580
+ orderId: orderId1,
581
+ })
582
+ expect(order1.prev).toBe(0n) // should be 0 as it's first
583
+ expect(order1.next).toBe(orderId2) // should point to second order
584
+
585
+ // Get second order
586
+ const order2 = await Actions.dex.getOrder(client, {
587
+ orderId: orderId2,
588
+ })
589
+ expect(order2.prev).toBe(orderId1) // should point to first order
590
+ expect(order2.next).toBe(0n) // should be 0 as it's last
591
+ })
592
+ })
593
+
594
+ describe('getPriceLevel', () => {
595
+ test('default', async () => {
596
+ const { base } = await setupTokenPair()
597
+
598
+ const tick = Tick.fromPrice('1.001')
599
+
600
+ // Place an order to create liquidity at this tick
601
+ const { orderId } = await Actions.dex.placeSync(client, {
602
+ token: base,
603
+ amount: parseEther('100'),
604
+ type: 'buy',
605
+ tick,
606
+ })
607
+
608
+ // Get the price level
609
+ const level = await Actions.dex.getPriceLevel(client, {
610
+ base,
611
+ tick,
612
+ isBid: true,
613
+ })
614
+
615
+ expect(level).toBeDefined()
616
+ expect(level.head).toBe(orderId) // head should be our order
617
+ expect(level.tail).toBe(orderId) // tail should also be our order (only one)
618
+ expect(level.totalLiquidity).toBeGreaterThan(0n)
619
+ })
620
+
621
+ test('behavior: empty price level', async () => {
622
+ const { base } = await setupTokenPair()
623
+
624
+ const tick = Tick.fromPrice('1.001')
625
+
626
+ // Query a tick with no orders
627
+ const level = await Actions.dex.getPriceLevel(client, {
628
+ base,
629
+ tick,
630
+ isBid: true,
631
+ })
632
+
633
+ expect(level).toBeDefined()
634
+ expect(level.head).toBe(0n)
635
+ expect(level.tail).toBe(0n)
636
+ expect(level.totalLiquidity).toBe(0n)
637
+ })
638
+
639
+ test('behavior: multiple orders at same tick', async () => {
640
+ const { base } = await setupTokenPair()
641
+
642
+ const tick = Tick.fromPrice('1.001')
643
+
644
+ // Place first order
645
+ const { orderId: orderId1 } = await Actions.dex.placeSync(client, {
646
+ token: base,
647
+ amount: parseEther('100'),
648
+ type: 'buy',
649
+ tick,
650
+ })
651
+
652
+ // Place second order at same tick
653
+ const { orderId: orderId2 } = await Actions.dex.placeSync(client, {
654
+ token: base,
655
+ amount: parseEther('50'),
656
+ type: 'buy',
657
+ tick,
658
+ })
659
+
660
+ // Get the price level
661
+ const level = await Actions.dex.getPriceLevel(client, {
662
+ base,
663
+ tick,
664
+ isBid: true,
665
+ })
666
+
667
+ expect(level.head).toBe(orderId1) // head should be first order
668
+ expect(level.tail).toBe(orderId2) // tail should be last order
669
+ // Total liquidity should be sum of both orders (approximately)
670
+ expect(level.totalLiquidity).toBeGreaterThan(parseEther('145'))
671
+ })
672
+
673
+ test('behavior: bid vs ask sides', async () => {
674
+ const { base } = await setupTokenPair()
675
+
676
+ const tick = Tick.fromPrice('1.001')
677
+
678
+ // Place a buy order (bid)
679
+ await Actions.dex.placeSync(client, {
680
+ token: base,
681
+ amount: parseEther('100'),
682
+ type: 'buy',
683
+ tick,
684
+ })
685
+
686
+ // Place a sell order (ask) at same tick
687
+ await Actions.dex.placeSync(client, {
688
+ token: base,
689
+ amount: parseEther('50'),
690
+ type: 'sell',
691
+ tick,
692
+ })
693
+
694
+ // Get bid side
695
+ const bidLevel = await Actions.dex.getPriceLevel(client, {
696
+ base,
697
+ tick,
698
+ isBid: true,
699
+ })
700
+
701
+ // Get ask side
702
+ const askLevel = await Actions.dex.getPriceLevel(client, {
703
+ base,
704
+ tick,
705
+ isBid: false,
706
+ })
707
+
708
+ // Both should have liquidity but different amounts
709
+ expect(bidLevel.totalLiquidity).toBeGreaterThan(0n)
710
+ expect(askLevel.totalLiquidity).toBeGreaterThan(0n)
711
+ expect(bidLevel.head).not.toBe(askLevel.head)
712
+ })
713
+
714
+ test('behavior: liquidity changes after order cancellation', async () => {
715
+ const { base } = await setupTokenPair()
716
+
717
+ const tick = Tick.fromPrice('1.001')
718
+
719
+ // Place orders
720
+ const { orderId: orderId1 } = await Actions.dex.placeSync(client, {
721
+ token: base,
722
+ amount: parseEther('100'),
723
+ type: 'buy',
724
+ tick,
725
+ })
726
+
727
+ await Actions.dex.placeSync(client, {
728
+ token: base,
729
+ amount: parseEther('50'),
730
+ type: 'buy',
731
+ tick,
732
+ })
733
+
734
+ // Get level before cancellation
735
+ const levelBefore = await Actions.dex.getPriceLevel(client, {
736
+ base,
737
+ tick,
738
+ isBid: true,
739
+ })
740
+
741
+ // Cancel first order
742
+ await Actions.dex.cancelSync(client, {
743
+ orderId: orderId1,
744
+ })
745
+
746
+ // Get level after cancellation
747
+ const levelAfter = await Actions.dex.getPriceLevel(client, {
748
+ base,
749
+ tick,
750
+ isBid: true,
751
+ })
752
+
753
+ // Total liquidity should decrease
754
+ expect(levelAfter.totalLiquidity).toBeLessThan(levelBefore.totalLiquidity)
755
+ })
756
+
757
+ test('behavior: liquidity changes after partial fill', async () => {
758
+ const { base, quote } = await setupTokenPair()
759
+
760
+ const tick = Tick.fromPrice('1.001')
761
+
762
+ // Place sell order
763
+ await Actions.dex.placeSync(client, {
764
+ token: base,
765
+ amount: parseEther('500'),
766
+ type: 'sell',
767
+ tick,
768
+ })
769
+
770
+ // Get level before fill
771
+ const levelBefore = await Actions.dex.getPriceLevel(client, {
772
+ base,
773
+ tick,
774
+ isBid: false,
775
+ })
776
+
777
+ // Partially fill the order
778
+ await Actions.dex.buySync(client, {
779
+ tokenIn: quote,
780
+ tokenOut: base,
781
+ amountOut: parseEther('100'),
782
+ maxAmountIn: parseEther('150'),
783
+ })
784
+
785
+ // Get level after fill
786
+ const levelAfter = await Actions.dex.getPriceLevel(client, {
787
+ base,
788
+ tick,
789
+ isBid: false,
790
+ })
791
+
792
+ // Total liquidity should decrease
793
+ expect(levelAfter.totalLiquidity).toBeLessThan(levelBefore.totalLiquidity)
794
+ })
795
+
796
+ test('behavior: tick at boundaries', async () => {
797
+ const { base } = await setupTokenPair()
798
+
799
+ // Place order at min tick
800
+ await Actions.dex.placeSync(client, {
801
+ token: base,
802
+ amount: parseEther('10'),
803
+ type: 'sell',
804
+ tick: Tick.minTick,
805
+ })
806
+
807
+ // Query min tick
808
+ const minLevel = await Actions.dex.getPriceLevel(client, {
809
+ base,
810
+ tick: Tick.minTick,
811
+ isBid: false,
812
+ })
813
+ expect(minLevel.totalLiquidity).toBeGreaterThan(0n)
814
+
815
+ // Place order at max tick
816
+ await Actions.dex.placeSync(client, {
817
+ token: base,
818
+ amount: parseEther('10'),
819
+ type: 'buy',
820
+ tick: Tick.maxTick,
821
+ })
822
+
823
+ // Query max tick
824
+ const maxLevel = await Actions.dex.getPriceLevel(client, {
825
+ base,
826
+ tick: Tick.maxTick,
827
+ isBid: true,
828
+ })
829
+ expect(maxLevel.totalLiquidity).toBeGreaterThan(0n)
830
+ })
831
+ })
832
+
833
+ describe('getSellQuote', () => {
834
+ test('default', async () => {
835
+ const { base, quote } = await setupTokenPair()
836
+
837
+ // Place bid orders to create liquidity
838
+ await Actions.dex.placeSync(client, {
839
+ token: base,
840
+ amount: parseEther('500'),
841
+ type: 'buy',
842
+ tick: Tick.fromPrice('0.999'),
843
+ })
844
+
845
+ // Get quote for selling base tokens
846
+ const amountOut = await Actions.dex.getSellQuote(client, {
847
+ tokenIn: base,
848
+ tokenOut: quote,
849
+ amountIn: parseEther('100'),
850
+ })
851
+
852
+ expect(amountOut).toBeGreaterThan(0n)
853
+ // Should be approximately 100 * 0.999 = 99.9
854
+ expect(amountOut).toBeLessThan(parseEther('100'))
855
+ })
856
+
857
+ test('behavior: fails with no liquidity', async () => {
858
+ const { base, quote } = await setupTokenPair()
859
+
860
+ // Quote should fail with no liquidity
861
+ await expect(
862
+ Actions.dex.getSellQuote(client, {
863
+ tokenIn: base,
864
+ tokenOut: quote,
865
+ amountIn: parseEther('100'),
866
+ }),
867
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
868
+ [ContractFunctionExecutionError: The contract function "quoteSwapExactAmountIn" reverted.
869
+
870
+ Error: InsufficientLiquidity()
871
+
872
+ Contract Call:
873
+ address: 0xdec0000000000000000000000000000000000000
874
+ function: quoteSwapExactAmountIn(address tokenIn, address tokenOut, uint128 amountIn)
875
+ args: (0x20c0000000000000000000000000000000000005, 0x20C0000000000000000000000000000000000004, 100000000000000000000)
876
+
877
+ Docs: https://viem.sh/docs/contract/readContract
878
+ Version: viem@2.38.2]
879
+ `)
880
+ })
881
+ })
882
+
883
+ describe('place', () => {
884
+ test('default', async () => {
885
+ // Setup token pair
886
+ const { base } = await setupTokenPair()
887
+
888
+ // Place a sell order
889
+ const { receipt, ...result } = await Actions.dex.placeSync(client, {
890
+ token: base,
891
+ amount: parseEther('100'),
892
+ type: 'sell',
893
+ tick: Tick.fromPrice('1.001'),
894
+ })
895
+
896
+ expect(receipt).toBeDefined()
897
+ expect(receipt.status).toBe('success')
898
+ expect(result.orderId).toBeGreaterThan(0n)
899
+ expect(result).toMatchInlineSnapshot(`
900
+ {
901
+ "amount": 100000000000000000000n,
902
+ "isBid": false,
903
+ "maker": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
904
+ "orderId": 1n,
905
+ "tick": 100,
906
+ "token": "0x20c0000000000000000000000000000000000005",
907
+ }
908
+ `)
909
+
910
+ // Place a buy order
911
+ const { receipt: receipt2, ...result2 } = await Actions.dex.placeSync(
912
+ client,
913
+ {
914
+ token: base,
915
+ amount: parseEther('100'),
916
+ type: 'buy',
917
+ tick: Tick.fromPrice('1.001'),
918
+ },
919
+ )
920
+ expect(receipt2.status).toBe('success')
921
+ expect(result2.orderId).toBeGreaterThan(0n)
922
+ expect(result2).toMatchInlineSnapshot(`
923
+ {
924
+ "amount": 100000000000000000000n,
925
+ "isBid": true,
926
+ "maker": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
927
+ "orderId": 2n,
928
+ "tick": 100,
929
+ "token": "0x20c0000000000000000000000000000000000005",
930
+ }
931
+ `)
932
+ })
933
+
934
+ test('behavior: tick at boundaries', async () => {
935
+ const { base } = await setupTokenPair()
936
+
937
+ // Test at min tick (-2000)
938
+ const { receipt: receipt1, ...result1 } = await Actions.dex.placeSync(
939
+ client,
940
+ {
941
+ token: base,
942
+ amount: parseEther('10'),
943
+ type: 'sell',
944
+ tick: Tick.minTick,
945
+ },
946
+ )
947
+ expect(receipt1.status).toBe('success')
948
+ expect(result1.tick).toBe(-2000)
949
+
950
+ // Test at max tick (2000)
951
+ const { receipt: receipt2, ...result2 } = await Actions.dex.placeSync(
952
+ client,
953
+ {
954
+ token: base,
955
+ amount: parseEther('10'),
956
+ type: 'buy',
957
+ tick: Tick.maxTick,
958
+ },
959
+ )
960
+ expect(receipt2.status).toBe('success')
961
+ expect(result2.tick).toBe(2000)
962
+ })
963
+
964
+ test('behavior: tick validation fails outside bounds', async () => {
965
+ const { base } = await setupTokenPair()
966
+
967
+ // Test tick above max tix should fail
968
+ await expect(
969
+ Actions.dex.placeSync(client, {
970
+ token: base,
971
+ amount: parseEther('10'),
972
+ type: 'buy',
973
+ tick: Tick.maxTick + 1,
974
+ }),
975
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
976
+ [ContractFunctionExecutionError: The contract function "place" reverted.
977
+
978
+ Error: TickOutOfBounds(int16 tick)
979
+ (2001)
980
+
981
+ Contract Call:
982
+ address: 0xdec0000000000000000000000000000000000000
983
+ function: place(address token, uint128 amount, bool isBid, int16 tick)
984
+ args: (0x20c0000000000000000000000000000000000005, 10000000000000000000, true, 2001)
985
+ sender: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
986
+
987
+ Docs: https://viem.sh/docs/contract/writeContract
988
+ Version: viem@2.38.2]
989
+ `)
990
+
991
+ // Test tick below min tick should fail
992
+ await expect(
993
+ Actions.dex.placeSync(client, {
994
+ token: base,
995
+ amount: parseEther('10'),
996
+ type: 'sell',
997
+ tick: Tick.minTick - 1,
998
+ }),
999
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
1000
+ [ContractFunctionExecutionError: The contract function "place" reverted.
1001
+
1002
+ Error: TickOutOfBounds(int16 tick)
1003
+ (-2001)
1004
+
1005
+ Contract Call:
1006
+ address: 0xdec0000000000000000000000000000000000000
1007
+ function: place(address token, uint128 amount, bool isBid, int16 tick)
1008
+ args: (0x20c0000000000000000000000000000000000005, 10000000000000000000, false, -2001)
1009
+ sender: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
1010
+
1011
+ Docs: https://viem.sh/docs/contract/writeContract
1012
+ Version: viem@2.38.2]
1013
+ `)
1014
+ })
1015
+
1016
+ test('behavior: transfers from wallet', async () => {
1017
+ const { base, quote } = await setupTokenPair()
1018
+
1019
+ // Get balances before placing order
1020
+ const baseBalanceBefore = await Actions.token.getBalance(client, {
1021
+ token: base,
1022
+ })
1023
+ const quoteBalanceBefore = await Actions.token.getBalance(client, {
1024
+ token: quote,
1025
+ })
1026
+
1027
+ // Place a buy order - should transfer quote tokens to escrow
1028
+ const orderAmount = parseEther('100')
1029
+ const tick = Tick.fromPrice('1.001')
1030
+ await Actions.dex.placeSync(client, {
1031
+ token: base,
1032
+ amount: orderAmount,
1033
+ type: 'buy',
1034
+ tick,
1035
+ })
1036
+
1037
+ // Get balances after placing order
1038
+ const baseBalanceAfter = await Actions.token.getBalance(client, {
1039
+ token: base,
1040
+ })
1041
+ const quoteBalanceAfter = await Actions.token.getBalance(client, {
1042
+ token: quote,
1043
+ })
1044
+
1045
+ // Base token balance should be unchanged (we're buying base, not selling)
1046
+ expect(baseBalanceAfter).toBe(baseBalanceBefore)
1047
+
1048
+ // Quote token balance should decrease (escrowed for the bid)
1049
+ // Amount = orderAmount * (1 + tick/1000) for bids
1050
+ const expectedQuoteEscrowed =
1051
+ (orderAmount * BigInt(100000 + tick)) / BigInt(100000)
1052
+ expect(quoteBalanceBefore - quoteBalanceAfter).toBeGreaterThanOrEqual(
1053
+ expectedQuoteEscrowed,
1054
+ )
1055
+ })
1056
+
1057
+ test('behavior: multiple orders at same tick', async () => {
1058
+ const { base } = await setupTokenPair()
1059
+
1060
+ const tick = Tick.fromPrice('1.0005')
1061
+
1062
+ // Place first order
1063
+ const { orderId: orderId1 } = await Actions.dex.placeSync(client, {
1064
+ token: base,
1065
+ amount: parseEther('100'),
1066
+ type: 'buy',
1067
+ tick,
1068
+ })
1069
+
1070
+ // Place second order at same tick
1071
+ const { orderId: orderId2 } = await Actions.dex.placeSync(client, {
1072
+ token: base,
1073
+ amount: parseEther('50'),
1074
+ type: 'buy',
1075
+ tick,
1076
+ })
1077
+
1078
+ // Order IDs should be different and sequential
1079
+ expect(orderId2).toBeGreaterThan(orderId1)
1080
+ })
1081
+ })
1082
+
1083
+ describe('placeFlip', () => {
1084
+ test('default', async () => {
1085
+ const { base } = await setupTokenPair()
1086
+
1087
+ // Place a flip bid order
1088
+ const { receipt, ...result } = await Actions.dex.placeFlipSync(client, {
1089
+ token: base,
1090
+ amount: parseEther('100'),
1091
+ type: 'buy',
1092
+ tick: Tick.fromPrice('1.001'),
1093
+ flipTick: Tick.fromPrice('1.002'),
1094
+ })
1095
+
1096
+ expect(receipt).toBeDefined()
1097
+ expect(receipt.status).toBe('success')
1098
+ expect(result.orderId).toBeGreaterThan(0n)
1099
+ expect(result.flipTick).toBe(Tick.fromPrice('1.002'))
1100
+ expect(result).toMatchInlineSnapshot(`
1101
+ {
1102
+ "amount": 100000000000000000000n,
1103
+ "flipTick": 200,
1104
+ "isBid": true,
1105
+ "maker": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
1106
+ "orderId": 1n,
1107
+ "tick": 100,
1108
+ "token": "0x20c0000000000000000000000000000000000005",
1109
+ }
1110
+ `)
1111
+ })
1112
+
1113
+ test('behavior: flip bid requires flipTick > tick', async () => {
1114
+ const { base } = await setupTokenPair()
1115
+
1116
+ // Valid: flipTick > tick for bid
1117
+ const { receipt: receipt1 } = await Actions.dex.placeFlipSync(client, {
1118
+ token: base,
1119
+ amount: parseEther('10'),
1120
+ type: 'buy',
1121
+ tick: Tick.fromPrice('1.0005'),
1122
+ flipTick: Tick.fromPrice('1.001'),
1123
+ })
1124
+ expect(receipt1.status).toBe('success')
1125
+
1126
+ // Invalid: flipTick <= tick for bid should fail
1127
+ await expect(
1128
+ Actions.dex.placeFlipSync(client, {
1129
+ token: base,
1130
+ amount: parseEther('10'),
1131
+ type: 'buy',
1132
+ tick: Tick.fromPrice('1.001'),
1133
+ flipTick: Tick.fromPrice('1.001'), // Equal
1134
+ }),
1135
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
1136
+ [ContractFunctionExecutionError: The contract function "placeFlip" reverted.
1137
+
1138
+ Error: InvalidFlipTick()
1139
+
1140
+ Contract Call:
1141
+ address: 0xdec0000000000000000000000000000000000000
1142
+ function: placeFlip(address token, uint128 amount, bool isBid, int16 tick, int16 flipTick)
1143
+ args: (0x20c0000000000000000000000000000000000005, 10000000000000000000, true, 100, 100)
1144
+ sender: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
1145
+
1146
+ Docs: https://viem.sh/docs/contract/writeContract
1147
+ Version: viem@2.38.2]
1148
+ `)
1149
+
1150
+ await expect(
1151
+ Actions.dex.placeFlipSync(client, {
1152
+ token: base,
1153
+ amount: parseEther('10'),
1154
+ type: 'buy',
1155
+ tick: Tick.fromPrice('1.001'),
1156
+ flipTick: Tick.fromPrice('1.0005'), // Less than
1157
+ }),
1158
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
1159
+ [ContractFunctionExecutionError: The contract function "placeFlip" reverted.
1160
+
1161
+ Error: InvalidFlipTick()
1162
+
1163
+ Contract Call:
1164
+ address: 0xdec0000000000000000000000000000000000000
1165
+ function: placeFlip(address token, uint128 amount, bool isBid, int16 tick, int16 flipTick)
1166
+ args: (0x20c0000000000000000000000000000000000005, 10000000000000000000, true, 100, 50)
1167
+ sender: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
1168
+
1169
+ Docs: https://viem.sh/docs/contract/writeContract
1170
+ Version: viem@2.38.2]
1171
+ `)
1172
+ })
1173
+
1174
+ test('behavior: flip ask requires flipTick < tick', async () => {
1175
+ const { base } = await setupTokenPair()
1176
+
1177
+ // Valid: flipTick < tick for ask
1178
+ const { receipt: receipt1 } = await Actions.dex.placeFlipSync(client, {
1179
+ token: base,
1180
+ amount: parseEther('10'),
1181
+ type: 'sell',
1182
+ tick: Tick.fromPrice('1.001'),
1183
+ flipTick: Tick.fromPrice('1.0005'),
1184
+ })
1185
+ expect(receipt1.status).toBe('success')
1186
+
1187
+ // Invalid: flipTick >= tick for ask should fail
1188
+ await expect(
1189
+ Actions.dex.placeFlipSync(client, {
1190
+ token: base,
1191
+ amount: parseEther('10'),
1192
+ type: 'sell',
1193
+ tick: Tick.fromPrice('1.0005'),
1194
+ flipTick: Tick.fromPrice('1.0005'), // Equal
1195
+ }),
1196
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
1197
+ [ContractFunctionExecutionError: The contract function "placeFlip" reverted.
1198
+
1199
+ Error: InvalidFlipTick()
1200
+
1201
+ Contract Call:
1202
+ address: 0xdec0000000000000000000000000000000000000
1203
+ function: placeFlip(address token, uint128 amount, bool isBid, int16 tick, int16 flipTick)
1204
+ args: (0x20c0000000000000000000000000000000000005, 10000000000000000000, false, 50, 50)
1205
+ sender: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
1206
+
1207
+ Docs: https://viem.sh/docs/contract/writeContract
1208
+ Version: viem@2.38.2]
1209
+ `)
1210
+
1211
+ await expect(
1212
+ Actions.dex.placeFlipSync(client, {
1213
+ token: base,
1214
+ amount: parseEther('10'),
1215
+ type: 'sell',
1216
+ tick: Tick.fromPrice('1.0005'),
1217
+ flipTick: Tick.fromPrice('1.001'), // Greater than
1218
+ }),
1219
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
1220
+ [ContractFunctionExecutionError: The contract function "placeFlip" reverted.
1221
+
1222
+ Error: InvalidFlipTick()
1223
+
1224
+ Contract Call:
1225
+ address: 0xdec0000000000000000000000000000000000000
1226
+ function: placeFlip(address token, uint128 amount, bool isBid, int16 tick, int16 flipTick)
1227
+ args: (0x20c0000000000000000000000000000000000005, 10000000000000000000, false, 50, 100)
1228
+ sender: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
1229
+
1230
+ Docs: https://viem.sh/docs/contract/writeContract
1231
+ Version: viem@2.38.2]
1232
+ `)
1233
+ })
1234
+
1235
+ test('behavior: flip ticks at boundaries', async () => {
1236
+ const { base } = await setupTokenPair()
1237
+
1238
+ // Flip order with ticks at extreme boundaries
1239
+ const { receipt } = await Actions.dex.placeFlipSync(client, {
1240
+ token: base,
1241
+ amount: parseEther('10'),
1242
+ type: 'buy',
1243
+ tick: Tick.minTick,
1244
+ flipTick: Tick.maxTick,
1245
+ })
1246
+ expect(receipt.status).toBe('success')
1247
+ })
1248
+ })
1249
+
1250
+ describe('sell', () => {
1251
+ test('default', async () => {
1252
+ const { base, quote } = await setupTokenPair()
1253
+
1254
+ // Place bid order to create liquidity
1255
+ await Actions.dex.placeSync(client, {
1256
+ token: base,
1257
+ amount: parseEther('500'),
1258
+ type: 'buy',
1259
+ tick: Tick.fromPrice('0.999'),
1260
+ })
1261
+
1262
+ // Sell base tokens
1263
+ const { receipt } = await Actions.dex.sellSync(client, {
1264
+ tokenIn: base,
1265
+ tokenOut: quote,
1266
+ amountIn: parseEther('100'),
1267
+ minAmountOut: parseEther('50'),
1268
+ })
1269
+
1270
+ expect(receipt).toBeDefined()
1271
+ expect(receipt.status).toBe('success')
1272
+ })
1273
+
1274
+ test('behavior: respects minAmountOut', async () => {
1275
+ const { base, quote } = await setupTokenPair()
1276
+
1277
+ // Place bid order at low price
1278
+ await Actions.dex.placeSync(client, {
1279
+ token: base,
1280
+ amount: parseEther('500'),
1281
+ type: 'buy',
1282
+ tick: Tick.fromPrice('0.99'), // 1% below peg
1283
+ })
1284
+
1285
+ // Try to sell with too high minAmountOut - should fail
1286
+ await expect(
1287
+ Actions.dex.sellSync(client, {
1288
+ tokenIn: base,
1289
+ tokenOut: quote,
1290
+ amountIn: parseEther('100'),
1291
+ minAmountOut: parseEther('150'), // Way too high
1292
+ }),
1293
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
1294
+ [ContractFunctionExecutionError: The contract function "swapExactAmountIn" reverted.
1295
+
1296
+ Error: InsufficientOutput()
1297
+
1298
+ Contract Call:
1299
+ address: 0xdec0000000000000000000000000000000000000
1300
+ function: swapExactAmountIn(address tokenIn, address tokenOut, uint128 amountIn, uint128 minAmountOut)
1301
+ args: (0x20c0000000000000000000000000000000000005, 0x20C0000000000000000000000000000000000004, 100000000000000000000, 150000000000000000000)
1302
+ sender: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
1303
+
1304
+ Docs: https://viem.sh/docs/contract/writeContract
1305
+ Version: viem@2.38.2]
1306
+ `)
1307
+ })
1308
+
1309
+ test('behavior: fails with insufficient liquidity', async () => {
1310
+ const { base, quote } = await setupTokenPair()
1311
+
1312
+ // No orders - no liquidity
1313
+
1314
+ // Try to sell - should fail
1315
+ await expect(
1316
+ Actions.dex.sellSync(client, {
1317
+ tokenIn: base,
1318
+ tokenOut: quote,
1319
+ amountIn: parseEther('100'),
1320
+ minAmountOut: parseEther('50'),
1321
+ }),
1322
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
1323
+ [ContractFunctionExecutionError: The contract function "swapExactAmountIn" reverted.
1324
+
1325
+ Error: InsufficientLiquidity()
1326
+
1327
+ Contract Call:
1328
+ address: 0xdec0000000000000000000000000000000000000
1329
+ function: swapExactAmountIn(address tokenIn, address tokenOut, uint128 amountIn, uint128 minAmountOut)
1330
+ args: (0x20c0000000000000000000000000000000000005, 0x20C0000000000000000000000000000000000004, 100000000000000000000, 50000000000000000000)
1331
+ sender: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
1332
+
1333
+ Docs: https://viem.sh/docs/contract/writeContract
1334
+ Version: viem@2.38.2]
1335
+ `)
1336
+ })
1337
+ })
1338
+
1339
+ describe('watchFlipOrderPlaced', () => {
1340
+ test('default', async () => {
1341
+ const { base } = await setupTokenPair()
1342
+
1343
+ const receivedOrders: Array<{
1344
+ args: Actions.dex.watchFlipOrderPlaced.Args
1345
+ log: Actions.dex.watchFlipOrderPlaced.Log
1346
+ }> = []
1347
+
1348
+ const unwatch = Actions.dex.watchFlipOrderPlaced(client, {
1349
+ onFlipOrderPlaced: (args, log) => {
1350
+ receivedOrders.push({ args, log })
1351
+ },
1352
+ })
1353
+
1354
+ try {
1355
+ // Place flip order
1356
+ await Actions.dex.placeFlipSync(client, {
1357
+ token: base,
1358
+ amount: parseEther('100'),
1359
+ type: 'buy',
1360
+ tick: Tick.fromPrice('1.001'),
1361
+ flipTick: Tick.fromPrice('1.002'),
1362
+ })
1363
+
1364
+ await new Promise((resolve) => setTimeout(resolve, 200))
1365
+
1366
+ expect(receivedOrders).toHaveLength(1)
1367
+ expect(receivedOrders[0]?.args.flipTick).toBe(Tick.fromPrice('1.002'))
1368
+ expect(receivedOrders[0]?.args.tick).toBe(Tick.fromPrice('1.001'))
1369
+ } finally {
1370
+ unwatch()
1371
+ }
1372
+ })
1373
+ })
1374
+
1375
+ describe('watchOrderCancelled', () => {
1376
+ test('default', async () => {
1377
+ const { base } = await setupTokenPair()
1378
+
1379
+ const receivedCancellations: Array<{
1380
+ args: Actions.dex.watchOrderCancelled.Args
1381
+ log: Actions.dex.watchOrderCancelled.Log
1382
+ }> = []
1383
+
1384
+ const unwatch = Actions.dex.watchOrderCancelled(client, {
1385
+ onOrderCancelled: (args, log) => {
1386
+ receivedCancellations.push({ args, log })
1387
+ },
1388
+ })
1389
+
1390
+ try {
1391
+ // Place order
1392
+ const { orderId } = await Actions.dex.placeSync(client, {
1393
+ token: base,
1394
+ amount: parseEther('100'),
1395
+ type: 'buy',
1396
+ tick: Tick.fromPrice('1.001'),
1397
+ })
1398
+
1399
+ // Cancel order
1400
+ await Actions.dex.cancelSync(client, {
1401
+ orderId,
1402
+ })
1403
+
1404
+ await new Promise((resolve) => setTimeout(resolve, 200))
1405
+
1406
+ expect(receivedCancellations).toHaveLength(1)
1407
+ expect(receivedCancellations[0]?.args.orderId).toBe(orderId)
1408
+ } finally {
1409
+ unwatch()
1410
+ }
1411
+ })
1412
+
1413
+ test('behavior: filter by orderId', async () => {
1414
+ const { base } = await setupTokenPair()
1415
+
1416
+ // Place two orders
1417
+ const { orderId: orderId1 } = await Actions.dex.placeSync(client, {
1418
+ token: base,
1419
+ amount: parseEther('100'),
1420
+ type: 'buy',
1421
+ tick: Tick.fromPrice('1.001'),
1422
+ })
1423
+
1424
+ const { orderId: orderId2 } = await Actions.dex.placeSync(client, {
1425
+ token: base,
1426
+ amount: parseEther('50'),
1427
+ type: 'buy',
1428
+ tick: Tick.fromPrice('1.001'),
1429
+ })
1430
+
1431
+ const receivedCancellations: Array<{
1432
+ args: Actions.dex.watchOrderCancelled.Args
1433
+ log: Actions.dex.watchOrderCancelled.Log
1434
+ }> = []
1435
+
1436
+ // Watch only for cancellation of orderId1
1437
+ const unwatch = Actions.dex.watchOrderCancelled(client, {
1438
+ orderId: orderId1,
1439
+ onOrderCancelled: (args, log) => {
1440
+ receivedCancellations.push({ args, log })
1441
+ },
1442
+ })
1443
+
1444
+ try {
1445
+ // Cancel orderId1 (should be captured)
1446
+ await Actions.dex.cancelSync(client, {
1447
+ orderId: orderId1,
1448
+ })
1449
+
1450
+ // Cancel orderId2 (should NOT be captured)
1451
+ await Actions.dex.cancelSync(client, {
1452
+ orderId: orderId2,
1453
+ })
1454
+
1455
+ await new Promise((resolve) => setTimeout(resolve, 200))
1456
+
1457
+ // Should only receive 1 event
1458
+ expect(receivedCancellations).toHaveLength(1)
1459
+ expect(receivedCancellations[0]?.args.orderId).toBe(orderId1)
1460
+ } finally {
1461
+ unwatch()
1462
+ }
1463
+ })
1464
+ })
1465
+
1466
+ describe.todo('watchOrderFilled')
1467
+
1468
+ describe('watchOrderPlaced', () => {
1469
+ test('default', async () => {
1470
+ const { base } = await setupTokenPair()
1471
+
1472
+ const receivedOrders: Array<{
1473
+ args: Actions.dex.watchOrderPlaced.Args
1474
+ log: Actions.dex.watchOrderPlaced.Log
1475
+ }> = []
1476
+
1477
+ const unwatch = Actions.dex.watchOrderPlaced(client, {
1478
+ onOrderPlaced: (args, log) => {
1479
+ receivedOrders.push({ args, log })
1480
+ },
1481
+ })
1482
+
1483
+ try {
1484
+ // Place first order
1485
+ await Actions.dex.placeSync(client, {
1486
+ token: base,
1487
+ amount: parseEther('100'),
1488
+ type: 'buy',
1489
+ tick: Tick.fromPrice('1.001'),
1490
+ })
1491
+
1492
+ // Place second order
1493
+ await Actions.dex.placeSync(client, {
1494
+ token: base,
1495
+ amount: parseEther('50'),
1496
+ type: 'sell',
1497
+ tick: Tick.fromPrice('0.999'),
1498
+ })
1499
+
1500
+ // Wait for events
1501
+ await new Promise((resolve) => setTimeout(resolve, 200))
1502
+
1503
+ expect(receivedOrders).toHaveLength(2)
1504
+ expect(receivedOrders[0]?.args.isBid).toBe(true)
1505
+ expect(receivedOrders[0]?.args.amount).toBe(parseEther('100'))
1506
+ expect(receivedOrders[1]?.args.isBid).toBe(false)
1507
+ expect(receivedOrders[1]?.args.amount).toBe(parseEther('50'))
1508
+ } finally {
1509
+ unwatch()
1510
+ }
1511
+ })
1512
+
1513
+ test('behavior: filter by token', async () => {
1514
+ const { base } = await setupTokenPair()
1515
+ const { base: base2 } = await setupTokenPair()
1516
+
1517
+ const receivedOrders: Array<{
1518
+ args: Actions.dex.watchOrderPlaced.Args
1519
+ log: Actions.dex.watchOrderPlaced.Log
1520
+ }> = []
1521
+
1522
+ // Watch only for orders on base
1523
+ const unwatch = Actions.dex.watchOrderPlaced(client, {
1524
+ token: base,
1525
+ onOrderPlaced: (args, log) => {
1526
+ receivedOrders.push({ args, log })
1527
+ },
1528
+ })
1529
+
1530
+ try {
1531
+ // Place order on base (should be captured)
1532
+ await Actions.dex.placeSync(client, {
1533
+ token: base,
1534
+ amount: parseEther('100'),
1535
+ type: 'buy',
1536
+ tick: Tick.fromPrice('1.001'),
1537
+ })
1538
+
1539
+ // Place order on base2 (should NOT be captured)
1540
+ await Actions.dex.placeSync(client, {
1541
+ token: base2,
1542
+ amount: parseEther('50'),
1543
+ type: 'buy',
1544
+ tick: Tick.fromPrice('1.001'),
1545
+ })
1546
+
1547
+ await new Promise((resolve) => setTimeout(resolve, 200))
1548
+
1549
+ // Should only receive 1 event
1550
+ expect(receivedOrders).toHaveLength(1)
1551
+ expect(receivedOrders[0]?.args.token.toLowerCase()).toBe(
1552
+ base.toLowerCase(),
1553
+ )
1554
+ } finally {
1555
+ unwatch()
1556
+ }
1557
+ })
1558
+ })
1559
+
1560
+ describe('withdraw', () => {
1561
+ test('default', async () => {
1562
+ const { base, quote } = await setupTokenPair()
1563
+
1564
+ // Create internal balance
1565
+ const { orderId } = await Actions.dex.placeSync(client, {
1566
+ token: base,
1567
+ amount: parseEther('100'),
1568
+ type: 'buy',
1569
+ tick: Tick.fromPrice('1.001'),
1570
+ })
1571
+
1572
+ await Actions.dex.cancelSync(client, { orderId })
1573
+
1574
+ // Get DEX balance
1575
+ const dexBalance = await Actions.dex.getBalance(client, {
1576
+ account: account.address,
1577
+ token: quote,
1578
+ })
1579
+ expect(dexBalance).toBeGreaterThan(0n)
1580
+
1581
+ // Get wallet balance before withdraw
1582
+ const walletBalanceBefore = await Actions.token.getBalance(client, {
1583
+ token: quote,
1584
+ })
1585
+
1586
+ // Withdraw from DEX
1587
+ const { receipt } = await Actions.dex.withdrawSync(client, {
1588
+ token: quote,
1589
+ amount: dexBalance,
1590
+ })
1591
+
1592
+ expect(receipt).toBeDefined()
1593
+ expect(receipt.status).toBe('success')
1594
+
1595
+ // Check DEX balance is now 0
1596
+ const dexBalanceAfter = await Actions.dex.getBalance(client, {
1597
+ account: account.address,
1598
+ token: quote,
1599
+ })
1600
+ expect(dexBalanceAfter).toBe(0n)
1601
+
1602
+ // Check wallet balance increased
1603
+ const walletBalanceAfter = await Actions.token.getBalance(client, {
1604
+ token: quote,
1605
+ })
1606
+ expect(walletBalanceAfter).toBeGreaterThan(walletBalanceBefore)
1607
+ })
1608
+ })