tapimo 0.5.0 → 0.5.2

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 (197) hide show
  1. package/dist/App.d.ts +4854 -4829
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +20 -2
  4. package/dist/App.js.map +1 -1
  5. package/dist/Assets.js +3 -3
  6. package/dist/Assets.js.map +1 -1
  7. package/dist/Scope.d.ts.map +1 -1
  8. package/dist/Scope.js +1 -2
  9. package/dist/Scope.js.map +1 -1
  10. package/dist/Webhooks.d.ts +1 -1
  11. package/dist/Webhooks.d.ts.map +1 -1
  12. package/dist/Webhooks.js +5 -10
  13. package/dist/Webhooks.js.map +1 -1
  14. package/dist/admin/App.d.ts +6 -34
  15. package/dist/admin/App.d.ts.map +1 -1
  16. package/dist/admin/apps/api-keys.d.ts +3 -3
  17. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  18. package/dist/admin/apps/api-keys.js +4 -1
  19. package/dist/admin/apps/api-keys.js.map +1 -1
  20. package/dist/admin/apps/scopes.d.ts +6 -34
  21. package/dist/admin/apps/scopes.d.ts.map +1 -1
  22. package/dist/admin/apps/scopes.js +2 -2
  23. package/dist/admin/apps/scopes.js.map +1 -1
  24. package/dist/admin/apps/verified-tokens.d.ts +2 -2
  25. package/dist/admin/ui.generated.d.ts +1 -1
  26. package/dist/admin/ui.generated.js +33 -33
  27. package/dist/admin/ui.generated.js.map +1 -1
  28. package/dist/apps/data/App.d.ts +1398 -1405
  29. package/dist/apps/data/App.d.ts.map +1 -1
  30. package/dist/apps/data/App.js +25 -38
  31. package/dist/apps/data/App.js.map +1 -1
  32. package/dist/apps/data/FundingQuoteProviders.d.ts +108 -3
  33. package/dist/apps/data/FundingQuoteProviders.d.ts.map +1 -1
  34. package/dist/apps/data/FundingQuoteProviders.js +409 -2
  35. package/dist/apps/data/FundingQuoteProviders.js.map +1 -1
  36. package/dist/apps/data/routes/activities.d.ts +245 -245
  37. package/dist/apps/data/routes/blocks.d.ts +123 -123
  38. package/dist/apps/data/routes/funding.d.ts +236 -146
  39. package/dist/apps/data/routes/funding.d.ts.map +1 -1
  40. package/dist/apps/data/routes/funding.js +23 -12
  41. package/dist/apps/data/routes/funding.js.map +1 -1
  42. package/dist/apps/data/routes/indexer.d.ts.map +1 -1
  43. package/dist/apps/data/routes/indexer.js +9 -2
  44. package/dist/apps/data/routes/indexer.js.map +1 -1
  45. package/dist/apps/data/routes/rpc.d.ts +31 -31
  46. package/dist/apps/data/routes/tokenlist.d.ts +36 -36
  47. package/dist/apps/data/routes/transactions.d.ts +281 -281
  48. package/dist/apps/data/routes/transfers.d.ts +66 -73
  49. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  50. package/dist/apps/data/routes/transfers.js +21 -27
  51. package/dist/apps/data/routes/transfers.js.map +1 -1
  52. package/dist/apps/data/routes/verified-tokens.d.ts +86 -86
  53. package/dist/apps/data/routes/webhooks.d.ts +35 -35
  54. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  55. package/dist/apps/data/routes/webhooks.js +176 -42
  56. package/dist/apps/data/routes/webhooks.js.map +1 -1
  57. package/dist/apps/management/App.d.ts +520 -504
  58. package/dist/apps/management/App.d.ts.map +1 -1
  59. package/dist/apps/management/Billing.d.ts.map +1 -1
  60. package/dist/apps/management/Billing.js +5 -4
  61. package/dist/apps/management/Billing.js.map +1 -1
  62. package/dist/apps/management/routes/api-keys.d.ts +120 -120
  63. package/dist/apps/management/routes/billing.d.ts +211 -211
  64. package/dist/apps/management/routes/billing.d.ts.map +1 -1
  65. package/dist/apps/management/routes/billing.js +1 -1
  66. package/dist/apps/management/routes/billing.js.map +1 -1
  67. package/dist/apps/management/routes/invitations.d.ts +72 -72
  68. package/dist/apps/management/routes/invite-links.d.ts +159 -159
  69. package/dist/apps/management/routes/me.d.ts +17 -17
  70. package/dist/apps/management/routes/members.d.ts +80 -80
  71. package/dist/apps/management/routes/orgs.d.ts +59 -43
  72. package/dist/apps/management/routes/orgs.d.ts.map +1 -1
  73. package/dist/apps/management/routes/orgs.js +18 -1
  74. package/dist/apps/management/routes/orgs.js.map +1 -1
  75. package/dist/apps/management/routes/projects.d.ts +109 -109
  76. package/dist/apps/management/routes/scopes.d.ts +11 -11
  77. package/dist/apps/management/routes/usage.d.ts +79 -79
  78. package/dist/apps/mcp.js +2 -2
  79. package/dist/apps/mcp.js.map +1 -1
  80. package/dist/apps/relay/App.d.ts +2 -2
  81. package/dist/apps/relay/App.js +2 -2
  82. package/dist/apps/relay/Sponsorships.d.ts +3 -0
  83. package/dist/apps/relay/Sponsorships.d.ts.map +1 -1
  84. package/dist/apps/relay/Sponsorships.js +15 -1
  85. package/dist/apps/relay/Sponsorships.js.map +1 -1
  86. package/dist/db/Db.d.ts.map +1 -1
  87. package/dist/db/Db.js +4 -0
  88. package/dist/db/Db.js.map +1 -1
  89. package/dist/db/Schema.js +1 -1
  90. package/dist/db/Schema.js.map +1 -1
  91. package/dist/db/tables/organizations.d.ts +6 -0
  92. package/dist/db/tables/organizations.d.ts.map +1 -1
  93. package/dist/db/tables/organizations.js +26 -0
  94. package/dist/db/tables/organizations.js.map +1 -1
  95. package/dist/db/tables/requestUsage.d.ts +2 -11
  96. package/dist/db/tables/requestUsage.d.ts.map +1 -1
  97. package/dist/db/tables/requestUsage.js +1 -20
  98. package/dist/db/tables/requestUsage.js.map +1 -1
  99. package/dist/db/tables/sponsoredTransactions.d.ts +2 -1
  100. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  101. package/dist/db/tables/sponsoredTransactions.js +52 -10
  102. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  103. package/dist/internal/Auth.d.ts +4 -0
  104. package/dist/internal/Auth.d.ts.map +1 -1
  105. package/dist/internal/Auth.js +15 -8
  106. package/dist/internal/Auth.js.map +1 -1
  107. package/dist/internal/Cache.d.ts.map +1 -1
  108. package/dist/internal/Cache.js +9 -1
  109. package/dist/internal/Cache.js.map +1 -1
  110. package/dist/internal/EdgeCache.d.ts.map +1 -1
  111. package/dist/internal/EdgeCache.js +12 -3
  112. package/dist/internal/EdgeCache.js.map +1 -1
  113. package/dist/internal/FundingQuotes.d.ts +176 -56
  114. package/dist/internal/FundingQuotes.d.ts.map +1 -1
  115. package/dist/internal/FundingQuotes.js +100 -5
  116. package/dist/internal/FundingQuotes.js.map +1 -1
  117. package/dist/internal/Log.d.ts +18 -12
  118. package/dist/internal/Log.d.ts.map +1 -1
  119. package/dist/internal/Log.js +2 -0
  120. package/dist/internal/Log.js.map +1 -1
  121. package/dist/internal/RateLimit.d.ts +2 -0
  122. package/dist/internal/RateLimit.d.ts.map +1 -1
  123. package/dist/internal/RateLimit.js +4 -4
  124. package/dist/internal/RateLimit.js.map +1 -1
  125. package/dist/internal/Tidx.d.ts +9 -3
  126. package/dist/internal/Tidx.d.ts.map +1 -1
  127. package/dist/internal/Tidx.js +13 -7
  128. package/dist/internal/Tidx.js.map +1 -1
  129. package/dist/internal/Viem.d.ts +14 -1
  130. package/dist/internal/Viem.d.ts.map +1 -1
  131. package/dist/internal/Viem.js +19 -1
  132. package/dist/internal/Viem.js.map +1 -1
  133. package/package.json +7 -2
  134. package/src/ApiKeys.test.ts +2 -0
  135. package/src/App.test.ts +15 -4
  136. package/src/App.ts +31 -5
  137. package/src/Assets.ts +3 -3
  138. package/src/Client.test-d.ts +30 -3
  139. package/src/Scope.ts +4 -2
  140. package/src/Webhooks.ts +5 -10
  141. package/src/admin/apps/api-keys.test.ts +27 -0
  142. package/src/admin/apps/api-keys.ts +8 -1
  143. package/src/admin/apps/scopes.test.ts +7 -2
  144. package/src/admin/apps/scopes.ts +2 -2
  145. package/src/admin/ui/src/components/Dialog.tsx +8 -2
  146. package/src/admin/ui/src/lib/scopes.test.ts +19 -0
  147. package/src/admin/ui/src/lib/scopes.ts +12 -0
  148. package/src/admin/ui/src/routes/api-keys.tsx +128 -27
  149. package/src/admin/ui.generated.ts +33 -33
  150. package/src/apps/composition.test.ts +77 -0
  151. package/src/apps/data/App.ts +25 -45
  152. package/src/apps/data/FundingQuoteProviders.test.ts +615 -5
  153. package/src/apps/data/FundingQuoteProviders.ts +570 -4
  154. package/src/apps/data/routes/funding.test.ts +181 -7
  155. package/src/apps/data/routes/funding.ts +27 -12
  156. package/src/apps/data/routes/indexer.test.ts +82 -20
  157. package/src/apps/data/routes/indexer.ts +8 -2
  158. package/src/apps/data/routes/tokens.test.ts +54 -0
  159. package/src/apps/data/routes/transfers.test.ts +73 -1
  160. package/src/apps/data/routes/transfers.ts +24 -30
  161. package/src/apps/data/routes/webhooks.test.ts +203 -0
  162. package/src/apps/data/routes/webhooks.ts +191 -54
  163. package/src/apps/management/Billing.test.ts +52 -3
  164. package/src/apps/management/Billing.ts +5 -4
  165. package/src/apps/management/routes/billing.ts +6 -1
  166. package/src/apps/management/routes/orgs.ts +19 -1
  167. package/src/apps/mcp.test.ts +18 -0
  168. package/src/apps/mcp.ts +2 -2
  169. package/src/apps/relay/App.test.ts +6 -5
  170. package/src/apps/relay/App.ts +2 -2
  171. package/src/apps/relay/Sponsorships.test.ts +2 -2
  172. package/src/apps/relay/Sponsorships.ts +24 -1
  173. package/src/db/Db.test.ts +5 -5
  174. package/src/db/Db.ts +4 -0
  175. package/src/db/Schema.ts +1 -1
  176. package/src/db/tables/billingSettings.test.ts +2 -2
  177. package/src/db/tables/organizations.test.ts +32 -2
  178. package/src/db/tables/organizations.ts +28 -0
  179. package/src/db/tables/requestUsage.ts +3 -23
  180. package/src/db/tables/sponsoredTransactions.test.ts +60 -2
  181. package/src/db/tables/sponsoredTransactions.ts +67 -21
  182. package/src/db/tables/stripeCustomers.test.ts +2 -2
  183. package/src/internal/Auth.test.ts +63 -10
  184. package/src/internal/Auth.ts +21 -9
  185. package/src/internal/Cache.test.ts +19 -0
  186. package/src/internal/Cache.ts +13 -1
  187. package/src/internal/EdgeCache.test.ts +40 -0
  188. package/src/internal/EdgeCache.ts +11 -3
  189. package/src/internal/FundingQuotes.test.ts +130 -3
  190. package/src/internal/FundingQuotes.ts +113 -6
  191. package/src/internal/Log.ts +21 -14
  192. package/src/internal/RateLimit.test.ts +12 -0
  193. package/src/internal/RateLimit.ts +4 -4
  194. package/src/internal/Tidx.test.ts +48 -0
  195. package/src/internal/Tidx.ts +21 -7
  196. package/src/internal/Viem.test.ts +46 -0
  197. package/src/internal/Viem.ts +34 -1
@@ -4,6 +4,140 @@ import * as FundingQuoteProviders from './FundingQuoteProviders.js'
4
4
  const now = () => new Date('2026-01-01T00:00:00.000Z')
5
5
 
6
6
  describe('FundingQuoteProviders', () => {
7
+ test('requires every Across deployment binding when any is configured', () => {
8
+ expect(FundingQuoteProviders.configureAcross()).toBeUndefined()
9
+ expect(() =>
10
+ FundingQuoteProviders.configureAcross({ apiKey: 'across-api-key' }),
11
+ ).toThrowErrorMatchingInlineSnapshot(`[FundingQuoteProviders.ProviderConfigurationError]`)
12
+ expect(
13
+ FundingQuoteProviders.configureAcross({
14
+ apiKey: 'across-api-key',
15
+ integratorId: '0xdead',
16
+ userAddress: '0x1111111111111111111111111111111111111111',
17
+ }),
18
+ ).toEqual({
19
+ apiKey: 'across-api-key',
20
+ integratorId: '0xdead',
21
+ userAddress: '0x1111111111111111111111111111111111111111',
22
+ })
23
+ })
24
+
25
+ test('requires every Squid deployment binding when any is configured', () => {
26
+ expect(FundingQuoteProviders.configureSquid()).toBeUndefined()
27
+ expect(() =>
28
+ FundingQuoteProviders.configureSquid({ integratorId: 'squid-integrator-id' }),
29
+ ).toThrowErrorMatchingInlineSnapshot(`[FundingQuoteProviders.ProviderConfigurationError]`)
30
+ expect(
31
+ FundingQuoteProviders.configureSquid({
32
+ integratorId: 'squid-integrator-id',
33
+ userAddress: '0x1111111111111111111111111111111111111111',
34
+ }),
35
+ ).toEqual({
36
+ integratorId: 'squid-integrator-id',
37
+ userAddress: '0x1111111111111111111111111111111111111111',
38
+ })
39
+ })
40
+
41
+ test('maps successful Across quotes to available quote probes', async () => {
42
+ const requests: RecordedRequest[] = []
43
+ const candidate = quoteCandidate('across')
44
+ const fetch = recordingFetch(requests, async () => jsonResponse(acrossQuote(candidate)))
45
+ const adapter = FundingQuoteProviders.across({
46
+ apiKey: 'across-api-key',
47
+ baseUrl: 'https://across.test/api/',
48
+ fetch,
49
+ integratorId: '0xdead',
50
+ now,
51
+ userAddress: '0x1111111111111111111111111111111111111111',
52
+ })
53
+
54
+ const result = await adapter.probeQuote(probeInput(candidate), new AbortController().signal)
55
+
56
+ expect(requests).toHaveLength(1)
57
+ expect(requests[0]?.url.pathname).toBe('/api/swap/approval')
58
+ expect(requests[0]?.url.searchParams.get('originChainId')).toBe('8453')
59
+ expect(requests[0]?.url.searchParams.get('destinationChainId')).toBe('4217')
60
+ expect(requests[0]?.url.searchParams.get('amount')).toBe('1000000')
61
+ expect(requests[0]?.url.searchParams.get('integratorId')).toBe('0xdead')
62
+ expect(requests[0]?.url.searchParams.get('tradeType')).toBe('exactInput')
63
+ expect(requests[0]?.url.searchParams.get('skipOriginTxEstimation')).toBe('true')
64
+ expect(requests[0]?.url.searchParams.get('strictTradeType')).toBe('true')
65
+ expect(requests[0]?.url.searchParams.get('depositor')).toBe(
66
+ '0x1111111111111111111111111111111111111111',
67
+ )
68
+ expect(requests[0]?.url.searchParams.get('recipient')).toBe(
69
+ '0x1111111111111111111111111111111111111111',
70
+ )
71
+ expect(new Headers(requests[0]?.init?.headers).get('authorization')).toBe(
72
+ 'Bearer across-api-key',
73
+ )
74
+ expect(result).toMatchObject({
75
+ expiresAt: '2026-01-01T00:01:00.000Z',
76
+ minimumOutputAmount: '988000',
77
+ outputAmount: '990000',
78
+ outputAmountFormatted: '0.99',
79
+ quality: {
80
+ estimatedSeconds: 2,
81
+ liquiditySource: 'providerQuote',
82
+ sourceDetail: 'across:swap-approval',
83
+ tier: 'liquid',
84
+ },
85
+ sampledAt: '2026-01-01T00:00:00.000Z',
86
+ status: 'available',
87
+ })
88
+ })
89
+
90
+ test('maps recognized Across quote errors to unavailable probes', async () => {
91
+ const candidate = quoteCandidate('across')
92
+ const fetch = recordingFetch([], async () => jsonResponse({ code: 'NO_ROUTE' }, 400))
93
+ const adapter = FundingQuoteProviders.across({
94
+ apiKey: 'across-api-key',
95
+ fetch,
96
+ integratorId: '0xdead',
97
+ now,
98
+ userAddress: '0x1111111111111111111111111111111111111111',
99
+ })
100
+
101
+ await expect(
102
+ adapter.probeQuote(probeInput(candidate), new AbortController().signal),
103
+ ).resolves.toMatchObject({
104
+ quality: {
105
+ liquiditySource: 'providerQuote',
106
+ sourceDetail: 'across:swap-approval:NO_ROUTE',
107
+ tier: 'unavailable',
108
+ },
109
+ status: 'unavailable',
110
+ })
111
+ })
112
+
113
+ test('rejects malformed Across quote payloads', async () => {
114
+ const candidate = quoteCandidate('across')
115
+ const fetch = recordingFetch([], async () =>
116
+ jsonResponse({ ...acrossQuote(candidate), expectedOutputAmount: '0' }),
117
+ )
118
+ const adapter = FundingQuoteProviders.across({
119
+ apiKey: 'across-api-key',
120
+ fetch,
121
+ integratorId: '0xdead',
122
+ now,
123
+ userAddress: '0x1111111111111111111111111111111111111111',
124
+ })
125
+
126
+ await expect(
127
+ adapter.probeQuote(probeInput(candidate), new AbortController().signal),
128
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`[FundingQuoteProviders.ProviderPayloadError]`)
129
+ })
130
+
131
+ test('rejects invalid Across configuration', () => {
132
+ expect(() =>
133
+ FundingQuoteProviders.across({
134
+ apiKey: 'across-api-key',
135
+ integratorId: 'tempo',
136
+ userAddress: '0x1111111111111111111111111111111111111111',
137
+ }),
138
+ ).toThrowErrorMatchingInlineSnapshot(`[FundingQuoteProviders.ProviderConfigurationError]`)
139
+ })
140
+
7
141
  test('maps successful Relay quotes to available quote probes', async () => {
8
142
  const requests: RecordedRequest[] = []
9
143
  const candidate = quoteCandidate()
@@ -54,6 +188,265 @@ describe('FundingQuoteProviders', () => {
54
188
  })
55
189
  })
56
190
 
191
+ test('maps successful Squid quotes to available quote probes', async () => {
192
+ const requests: RecordedRequest[] = []
193
+ const candidate = quoteCandidate('squid')
194
+ const fetch = recordingFetch(requests, async () => jsonResponse(squidQuote(candidate)))
195
+ const adapter = FundingQuoteProviders.squid({
196
+ baseUrl: 'https://squid.test/v2/',
197
+ fetch,
198
+ integratorId: 'squid-integrator-id',
199
+ now,
200
+ userAddress: '0x1111111111111111111111111111111111111111',
201
+ })
202
+
203
+ const result = await adapter.probeQuote(probeInput(candidate), new AbortController().signal)
204
+
205
+ expect(requests).toHaveLength(1)
206
+ expect(requests[0]?.url.pathname).toBe('/v2/route')
207
+ expect(new Headers(requests[0]?.init?.headers).get('x-integrator-id')).toBe(
208
+ 'squid-integrator-id',
209
+ )
210
+ expect(jsonRequestBody(requests[0])).toEqual({
211
+ fromAddress: '0x1111111111111111111111111111111111111111',
212
+ fromAmount: '1000000',
213
+ fromChain: '8453',
214
+ fromToken: candidate.sourceToken.address,
215
+ quoteOnly: true,
216
+ slippage: 1,
217
+ toAddress: '0x1111111111111111111111111111111111111111',
218
+ toChain: '4217',
219
+ toToken: candidate.destinationToken.address,
220
+ })
221
+ expect(result).toEqual({
222
+ minimumOutputAmount: '989622',
223
+ outputAmount: '999620',
224
+ outputAmountFormatted: '0.99962',
225
+ quality: {
226
+ estimatedSeconds: 10,
227
+ liquiditySource: 'providerQuote',
228
+ sourceDetail: 'squid:route-v2',
229
+ tier: 'liquid',
230
+ },
231
+ sampledAt: '2026-01-01T00:00:00.000Z',
232
+ status: 'available',
233
+ })
234
+ })
235
+
236
+ test('maps unavailable Squid routes to unavailable quote probes', async () => {
237
+ const candidate = quoteCandidate('squid')
238
+ const adapter = FundingQuoteProviders.squid({
239
+ fetch: recordingFetch([], async () =>
240
+ jsonResponse(
241
+ {
242
+ message: 'Please increase the swap amount and try again.',
243
+ statusCode: 400,
244
+ type: 'BAD_REQUEST',
245
+ },
246
+ 400,
247
+ ),
248
+ ),
249
+ integratorId: 'squid-integrator-id',
250
+ now,
251
+ userAddress: '0x1111111111111111111111111111111111111111',
252
+ })
253
+
254
+ await expect(
255
+ adapter.probeQuote(probeInput(candidate), new AbortController().signal),
256
+ ).resolves.toMatchObject({
257
+ quality: {
258
+ sourceDetail: 'squid:route-v2:AMOUNT_TOO_LOW',
259
+ tier: 'unavailable',
260
+ },
261
+ status: 'unavailable',
262
+ })
263
+ })
264
+
265
+ test('maps successful Rhino quotes to available quote probes', async () => {
266
+ const requests: RecordedRequest[] = []
267
+ const candidate = quoteCandidate('rhino')
268
+ const fetch = recordingFetch(requests, async () =>
269
+ jsonResponse({
270
+ _tag: 'bridge',
271
+ chainIn: 'BASE',
272
+ chainOut: 'TEMPO',
273
+ estimatedDuration: 1_500,
274
+ payAmount: '1.0000000',
275
+ receiveAmount: '0.9830000',
276
+ token: 'USDC',
277
+ }),
278
+ )
279
+ const adapter = FundingQuoteProviders.rhino({
280
+ baseUrl: 'https://rhino.test/bridge/',
281
+ fetch,
282
+ now,
283
+ })
284
+
285
+ const result = await adapter.probeQuote(probeInput(candidate), new AbortController().signal)
286
+
287
+ expect(requests).toHaveLength(1)
288
+ expect(requests[0]?.url.pathname).toBe('/bridge/quote/bridge-swap/public')
289
+ expect(Object.fromEntries(requests[0]!.url.searchParams)).toEqual({
290
+ amount: '1',
291
+ amountNative: '0',
292
+ chainIn: 'BASE',
293
+ chainOut: 'TEMPO',
294
+ mode: 'pay',
295
+ tokenIn: 'USDC',
296
+ tokenOut: 'USDC',
297
+ })
298
+ expect(result).toEqual({
299
+ outputAmount: '983000',
300
+ outputAmountFormatted: '0.983',
301
+ quality: {
302
+ estimatedSeconds: 2,
303
+ liquiditySource: 'providerQuote',
304
+ sourceDetail: 'rhino:bridge-swap-public',
305
+ tier: 'liquid',
306
+ },
307
+ sampledAt: '2026-01-01T00:00:00.000Z',
308
+ status: 'available',
309
+ })
310
+ })
311
+
312
+ test('maps unavailable Rhino routes to unavailable quote probes', async () => {
313
+ const candidate = quoteCandidate('rhino')
314
+ const adapter = FundingQuoteProviders.rhino({
315
+ fetch: recordingFetch([], async () => jsonResponse({ _tag: 'NoRouteFoundError' }, 404)),
316
+ now,
317
+ })
318
+
319
+ await expect(
320
+ adapter.probeQuote(probeInput(candidate), new AbortController().signal),
321
+ ).resolves.toMatchObject({
322
+ quality: {
323
+ sourceDetail: 'rhino:bridge-swap-public:NoRouteFoundError',
324
+ tier: 'unavailable',
325
+ },
326
+ status: 'unavailable',
327
+ })
328
+ })
329
+
330
+ test('maps Solana and Tron identifiers into Rhino quote requests', async () => {
331
+ const cases = [
332
+ {
333
+ candidate: quoteCandidate('rhino', { sourceChain: 'solana' }),
334
+ chainIn: 'SOLANA',
335
+ tokenOut: 'USDC',
336
+ },
337
+ {
338
+ candidate: quoteCandidate('rhino', {
339
+ destinationToken: 'USDT0',
340
+ sourceChain: 'tron',
341
+ sourceToken: 'USDT',
342
+ }),
343
+ chainIn: 'TRON',
344
+ tokenOut: 'USDT',
345
+ },
346
+ ] as const
347
+
348
+ for (const testCase of cases) {
349
+ const requests: RecordedRequest[] = []
350
+ const adapter = FundingQuoteProviders.rhino({
351
+ fetch: recordingFetch(requests, async () =>
352
+ jsonResponse({
353
+ _tag: 'bridge',
354
+ chainIn: testCase.chainIn,
355
+ chainOut: 'TEMPO',
356
+ payAmount: '1',
357
+ receiveAmount: '0.983',
358
+ token: testCase.tokenOut,
359
+ }),
360
+ ),
361
+ now,
362
+ })
363
+
364
+ await expect(
365
+ adapter.probeQuote(probeInput(testCase.candidate), new AbortController().signal),
366
+ ).resolves.toMatchObject({ outputAmount: '983000', status: 'available' })
367
+ expect(requests[0]?.url.searchParams.get('chainIn')).toBe(testCase.chainIn)
368
+ expect(requests[0]?.url.searchParams.get('tokenOut')).toBe(testCase.tokenOut)
369
+ }
370
+ })
371
+
372
+ test('surfaces Rhino request decoding failures', async () => {
373
+ const candidate = quoteCandidate('rhino')
374
+ const adapter = FundingQuoteProviders.rhino({
375
+ fetch: recordingFetch([], async () => jsonResponse({ _tag: 'HttpApiDecodeError' }, 400)),
376
+ now,
377
+ })
378
+
379
+ await expect(
380
+ adapter.probeQuote(probeInput(candidate), new AbortController().signal),
381
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
382
+ `[FundingQuoteProviders.ProviderResponseError: Provider request failed with status 400]`,
383
+ )
384
+ })
385
+
386
+ test('surfaces unrecognized Rhino route errors', async () => {
387
+ const candidate = quoteCandidate('rhino')
388
+ const failures = [
389
+ { body: null, status: 404 },
390
+ { body: { _tag: 'InvalidRequest' }, status: 422 },
391
+ ] as const
392
+
393
+ for (const failure of failures) {
394
+ const adapter = FundingQuoteProviders.rhino({
395
+ fetch: recordingFetch([], async () => jsonResponse(failure.body, failure.status)),
396
+ now,
397
+ })
398
+ const cause = await adapter
399
+ .probeQuote(probeInput(candidate), new AbortController().signal)
400
+ .catch((error: unknown) => error)
401
+ expect(cause).toMatchObject({
402
+ name: 'FundingQuoteProviders.ProviderResponseError',
403
+ status: failure.status,
404
+ })
405
+ }
406
+ })
407
+
408
+ test('rejects mismatched Rhino quote payloads', async () => {
409
+ const candidate = quoteCandidate('rhino')
410
+ const adapter = FundingQuoteProviders.rhino({
411
+ fetch: recordingFetch([], async () =>
412
+ jsonResponse({
413
+ _tag: 'bridge',
414
+ chainIn: 'SOLANA',
415
+ chainOut: 'TEMPO',
416
+ payAmount: '1',
417
+ receiveAmount: '0.983',
418
+ token: 'USDC',
419
+ }),
420
+ ),
421
+ now,
422
+ })
423
+
424
+ await expect(
425
+ adapter.probeQuote(probeInput(candidate), new AbortController().signal),
426
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`[FundingQuoteProviders.ProviderPayloadError]`)
427
+ })
428
+
429
+ test('rejects non-representable Rhino output precision', async () => {
430
+ const candidate = quoteCandidate('rhino')
431
+ const adapter = FundingQuoteProviders.rhino({
432
+ fetch: recordingFetch([], async () =>
433
+ jsonResponse({
434
+ _tag: 'bridge',
435
+ chainIn: 'BASE',
436
+ chainOut: 'TEMPO',
437
+ payAmount: '1',
438
+ receiveAmount: '0.9830001',
439
+ token: 'USDC',
440
+ }),
441
+ ),
442
+ now,
443
+ })
444
+
445
+ await expect(
446
+ adapter.probeQuote(probeInput(candidate), new AbortController().signal),
447
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`[FundingQuoteProviders.ProviderPayloadError]`)
448
+ })
449
+
57
450
  test('calls the runtime fetch method with its global receiver', async () => {
58
451
  const candidate = quoteCandidate()
59
452
  const fetch = vi.fn(function (this: unknown) {
@@ -82,6 +475,165 @@ describe('FundingQuoteProviders', () => {
82
475
  vi.unstubAllGlobals()
83
476
  }
84
477
  })
478
+
479
+ const invalidAcrossQuotes = [
480
+ [
481
+ 'mismatched input amounts',
482
+ (body: ReturnType<typeof acrossQuote>) => (body.inputAmount = '1'),
483
+ ],
484
+ [
485
+ 'mismatched token addresses',
486
+ (body: ReturnType<typeof acrossQuote>) =>
487
+ (body.inputToken.address = '0xfde4c96c8593536e31f229ea8f37b2ada2699bb2'),
488
+ ],
489
+ [
490
+ 'mismatched token chains',
491
+ (body: ReturnType<typeof acrossQuote>) => (body.inputToken.chainId = 1),
492
+ ],
493
+ [
494
+ 'mismatched token decimals',
495
+ (body: ReturnType<typeof acrossQuote>) => (body.inputToken.decimals = 18),
496
+ ],
497
+ [
498
+ 'expired quotes',
499
+ (body: ReturnType<typeof acrossQuote>) => (body.quoteExpiryTimestamp = 1767225600),
500
+ ],
501
+ [
502
+ 'out-of-range expiries',
503
+ (body: ReturnType<typeof acrossQuote>) =>
504
+ (body.quoteExpiryTimestamp = Number.MAX_SAFE_INTEGER),
505
+ ],
506
+ [
507
+ 'invalid output bounds',
508
+ (body: ReturnType<typeof acrossQuote>) => (body.minOutputAmount = '990001'),
509
+ ],
510
+ ] as const
511
+
512
+ for (const [name, mutate] of invalidAcrossQuotes)
513
+ test(`rejects Across quotes with ${name}`, async () => {
514
+ const candidate = quoteCandidate('across')
515
+ const body = acrossQuote(candidate)
516
+ mutate(body)
517
+ const adapter = FundingQuoteProviders.across({
518
+ apiKey: 'across-api-key',
519
+ fetch: recordingFetch([], async () => jsonResponse(body)),
520
+ integratorId: '0xdead',
521
+ now,
522
+ userAddress: '0x1111111111111111111111111111111111111111',
523
+ })
524
+
525
+ await expect(
526
+ adapter.probeQuote(probeInput(candidate), new AbortController().signal),
527
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`[FundingQuoteProviders.ProviderPayloadError]`)
528
+ })
529
+
530
+ const invalidSquidQuotes = [
531
+ [
532
+ 'mismatched input amounts',
533
+ (body: ReturnType<typeof squidQuote>) => (body.route.estimate.fromAmount = '1'),
534
+ ],
535
+ [
536
+ 'mismatched input token addresses',
537
+ (body: ReturnType<typeof squidQuote>) =>
538
+ (body.route.estimate.fromToken.address = '0xfde4c96c8593536e31f229ea8f37b2ada2699bb2'),
539
+ ],
540
+ [
541
+ 'mismatched input chains',
542
+ (body: ReturnType<typeof squidQuote>) => (body.route.estimate.fromToken.chainId = 1),
543
+ ],
544
+ [
545
+ 'mismatched input decimals',
546
+ (body: ReturnType<typeof squidQuote>) => (body.route.estimate.fromToken.decimals = 18),
547
+ ],
548
+ [
549
+ 'mismatched output token addresses',
550
+ (body: ReturnType<typeof squidQuote>) =>
551
+ (body.route.estimate.toToken.address = '0x20c00000000000000000000014f22ca97301eb73'),
552
+ ],
553
+ [
554
+ 'mismatched output chains',
555
+ (body: ReturnType<typeof squidQuote>) => (body.route.estimate.toToken.chainId = '1'),
556
+ ],
557
+ [
558
+ 'mismatched output decimals',
559
+ (body: ReturnType<typeof squidQuote>) => (body.route.estimate.toToken.decimals = 18 as never),
560
+ ],
561
+ [
562
+ 'zero output amounts',
563
+ (body: ReturnType<typeof squidQuote>) => (body.route.estimate.toAmount = '0'),
564
+ ],
565
+ [
566
+ 'invalid output bounds',
567
+ (body: ReturnType<typeof squidQuote>) => (body.route.estimate.toAmountMin = '1000000'),
568
+ ],
569
+ ] as const
570
+
571
+ for (const [name, mutate] of invalidSquidQuotes)
572
+ test(`rejects Squid quotes with ${name}`, async () => {
573
+ const candidate = quoteCandidate('squid')
574
+ const body = squidQuote(candidate)
575
+ mutate(body)
576
+ const adapter = FundingQuoteProviders.squid({
577
+ fetch: recordingFetch([], async () => jsonResponse(body)),
578
+ integratorId: 'squid-integrator-id',
579
+ now,
580
+ userAddress: '0x1111111111111111111111111111111111111111',
581
+ })
582
+
583
+ await expect(
584
+ adapter.probeQuote(probeInput(candidate), new AbortController().signal),
585
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`[FundingQuoteProviders.ProviderPayloadError]`)
586
+ })
587
+
588
+ test('returns bounded Across failure diagnostics', async () => {
589
+ const candidate = quoteCandidate('across')
590
+ const adapter = FundingQuoteProviders.across({
591
+ apiKey: 'across-api-key',
592
+ fetch: recordingFetch([], async () =>
593
+ jsonResponse({ apiKey: 'secret', code: 'UNAUTHORIZED' }, 401),
594
+ ),
595
+ integratorId: '0xdead',
596
+ now,
597
+ userAddress: '0x1111111111111111111111111111111111111111',
598
+ })
599
+ const cause = await adapter
600
+ .probeQuote(probeInput(candidate), new AbortController().signal)
601
+ .catch((error: unknown) => error)
602
+
603
+ expect(FundingQuoteProviders.providerFailure({ cause, provider: 'across' })).toEqual({
604
+ code: 'UNAUTHORIZED',
605
+ failure: 'http',
606
+ id: 'across',
607
+ operation: 'swap/approval',
608
+ status: 401,
609
+ })
610
+ })
611
+
612
+ test('returns explicit timeout failure diagnostics', () => {
613
+ expect(
614
+ FundingQuoteProviders.providerFailure({
615
+ cause: new FundingQuotes.ProviderAdapterTimeoutError(),
616
+ provider: 'across',
617
+ }),
618
+ ).toEqual({
619
+ failure: 'timeout',
620
+ id: 'across',
621
+ operation: 'swap/approval',
622
+ })
623
+ })
624
+
625
+ test('returns bounded Squid failure diagnostics', () => {
626
+ expect(
627
+ FundingQuoteProviders.providerFailure({
628
+ cause: new TypeError('network failure'),
629
+ provider: 'squid',
630
+ }),
631
+ ).toEqual({
632
+ failure: 'network',
633
+ id: 'squid',
634
+ operation: 'v2/route',
635
+ })
636
+ })
85
637
  })
86
638
 
87
639
  type RecordedRequest = {
@@ -89,18 +641,76 @@ type RecordedRequest = {
89
641
  url: URL
90
642
  }
91
643
 
92
- function quoteCandidate(): FundingQuotes.QuoteProbeInput['candidate'] {
644
+ function quoteCandidate(
645
+ provider: FundingQuotes.ProviderId = 'relay',
646
+ options: quoteCandidate.Options = {},
647
+ ): FundingQuotes.QuoteProbeInput['candidate'] {
93
648
  const [candidate] = FundingQuotes.listQuoteCandidates({
94
- provider: 'relay',
649
+ destinationToken: options.destinationToken,
650
+ provider,
95
651
  sourceAmount: '1000000',
96
652
  sourceAmountUnits: 'baseUnits',
97
- sourceToken: 'USDC',
98
- sourceChain: 'base',
653
+ sourceChain: options.sourceChain ?? 'base',
654
+ sourceToken: options.sourceToken ?? 'USDC',
99
655
  })
100
- if (!candidate) throw new Error('Missing Relay quote candidate')
656
+ if (!candidate) throw new Error(`Missing ${provider} quote candidate`)
101
657
  return candidate
102
658
  }
103
659
 
660
+ declare namespace quoteCandidate {
661
+ type Options = {
662
+ destinationToken?: string | undefined
663
+ sourceChain?: string | undefined
664
+ sourceToken?: string | undefined
665
+ }
666
+ }
667
+
668
+ function acrossQuote(candidate: FundingQuotes.QuoteProbeInput['candidate']) {
669
+ return {
670
+ amountType: 'exactInput',
671
+ expectedFillTime: 2,
672
+ expectedOutputAmount: '990000',
673
+ inputAmount: candidate.sourceAmount.amount,
674
+ inputToken: {
675
+ address: candidate.sourceToken.address,
676
+ chainId: 8453,
677
+ decimals: candidate.sourceToken.decimals,
678
+ symbol: candidate.sourceToken.symbol,
679
+ },
680
+ minOutputAmount: '988000',
681
+ outputToken: {
682
+ address: candidate.destinationToken.address,
683
+ chainId: 4217,
684
+ decimals: candidate.destinationToken.decimals,
685
+ symbol: candidate.destinationToken.symbol,
686
+ },
687
+ quoteExpiryTimestamp: 1767225660,
688
+ }
689
+ }
690
+
691
+ function squidQuote(candidate: FundingQuotes.QuoteProbeInput['candidate']) {
692
+ return {
693
+ route: {
694
+ estimate: {
695
+ estimatedRouteDuration: 10,
696
+ fromAmount: candidate.sourceAmount.amount,
697
+ fromToken: {
698
+ address: candidate.sourceToken.address,
699
+ chainId: 8453,
700
+ decimals: candidate.sourceToken.decimals,
701
+ },
702
+ toAmount: '999620',
703
+ toAmountMin: '989622',
704
+ toToken: {
705
+ address: candidate.destinationToken.address,
706
+ chainId: '4217',
707
+ decimals: candidate.destinationToken.decimals,
708
+ },
709
+ },
710
+ },
711
+ }
712
+ }
713
+
104
714
  function probeInput(
105
715
  candidate: FundingQuotes.QuoteProbeInput['candidate'],
106
716
  ): FundingQuotes.QuoteProbeInput {