tapimo 0.4.4 → 0.5.1

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 (229) hide show
  1. package/dist/App.d.ts +4701 -3416
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +24 -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 +18 -1
  8. package/dist/Scope.d.ts.map +1 -1
  9. package/dist/Scope.js +23 -3
  10. package/dist/Scope.js.map +1 -1
  11. package/dist/Webhooks.d.ts +1 -1
  12. package/dist/Webhooks.d.ts.map +1 -1
  13. package/dist/Webhooks.js +5 -10
  14. package/dist/Webhooks.js.map +1 -1
  15. package/dist/admin/App.d.ts +6 -34
  16. package/dist/admin/App.d.ts.map +1 -1
  17. package/dist/admin/apps/api-keys.d.ts +3 -3
  18. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  19. package/dist/admin/apps/api-keys.js +4 -1
  20. package/dist/admin/apps/api-keys.js.map +1 -1
  21. package/dist/admin/apps/scopes.d.ts +6 -34
  22. package/dist/admin/apps/scopes.d.ts.map +1 -1
  23. package/dist/admin/apps/scopes.js +2 -2
  24. package/dist/admin/apps/scopes.js.map +1 -1
  25. package/dist/admin/apps/verified-tokens.d.ts +57 -57
  26. package/dist/admin/ui.generated.d.ts +1 -1
  27. package/dist/admin/ui.generated.js +33 -33
  28. package/dist/admin/ui.generated.js.map +1 -1
  29. package/dist/apps/data/App.d.ts +4574 -4277
  30. package/dist/apps/data/App.d.ts.map +1 -1
  31. package/dist/apps/data/App.js +155 -77
  32. package/dist/apps/data/App.js.map +1 -1
  33. package/dist/apps/data/FundingQuoteProviders.d.ts +109 -0
  34. package/dist/apps/data/FundingQuoteProviders.d.ts.map +1 -0
  35. package/dist/apps/data/FundingQuoteProviders.js +464 -0
  36. package/dist/apps/data/FundingQuoteProviders.js.map +1 -0
  37. package/dist/apps/data/index.d.ts +1 -1
  38. package/dist/apps/data/index.js +1 -1
  39. package/dist/apps/data/routes/activities.d.ts +26 -26
  40. package/dist/apps/data/routes/addresses.d.ts +38 -38
  41. package/dist/apps/data/routes/balances.d.ts +45 -45
  42. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  43. package/dist/apps/data/routes/balances.js +67 -149
  44. package/dist/apps/data/routes/balances.js.map +1 -1
  45. package/dist/apps/data/routes/blocks.d.ts +2 -3
  46. package/dist/apps/data/routes/blocks.d.ts.map +1 -1
  47. package/dist/apps/data/routes/blocks.js +3 -5
  48. package/dist/apps/data/routes/blocks.js.map +1 -1
  49. package/dist/apps/data/routes/coingecko.d.ts +177 -176
  50. package/dist/apps/data/routes/coingecko.d.ts.map +1 -1
  51. package/dist/apps/data/routes/coingecko.js +3 -8
  52. package/dist/apps/data/routes/coingecko.js.map +1 -1
  53. package/dist/apps/data/routes/funding.d.ts +131 -228
  54. package/dist/apps/data/routes/funding.d.ts.map +1 -1
  55. package/dist/apps/data/routes/funding.js +152 -219
  56. package/dist/apps/data/routes/funding.js.map +1 -1
  57. package/dist/apps/data/routes/indexer.d.ts +34 -34
  58. package/dist/apps/data/routes/indexer.d.ts.map +1 -1
  59. package/dist/apps/data/routes/indexer.js +9 -2
  60. package/dist/apps/data/routes/indexer.js.map +1 -1
  61. package/dist/apps/data/routes/receipts.d.ts +228 -228
  62. package/dist/apps/data/routes/rpc.d.ts.map +1 -1
  63. package/dist/apps/data/routes/rpc.js +86 -16
  64. package/dist/apps/data/routes/rpc.js.map +1 -1
  65. package/dist/apps/data/routes/tokenlist.d.ts +12 -12
  66. package/dist/apps/data/routes/verified-tokens.d.ts +5 -5
  67. package/dist/apps/data/routes/webhooks.d.ts +340 -0
  68. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  69. package/dist/apps/data/routes/webhooks.js +13 -1
  70. package/dist/apps/data/routes/webhooks.js.map +1 -1
  71. package/dist/apps/index.d.ts +1 -1
  72. package/dist/apps/index.js +1 -1
  73. package/dist/apps/management/App.d.ts +1833 -851
  74. package/dist/apps/management/App.d.ts.map +1 -1
  75. package/dist/apps/management/App.js +4 -0
  76. package/dist/apps/management/App.js.map +1 -1
  77. package/dist/apps/management/routes/api-keys.d.ts +41 -41
  78. package/dist/apps/management/routes/api-keys.js +2 -2
  79. package/dist/apps/management/routes/api-keys.js.map +1 -1
  80. package/dist/apps/management/routes/billing.d.ts +48 -48
  81. package/dist/apps/management/routes/invitations.d.ts +132 -132
  82. package/dist/apps/management/routes/invite-links.d.ts +1100 -0
  83. package/dist/apps/management/routes/invite-links.d.ts.map +1 -0
  84. package/dist/apps/management/routes/invite-links.js +411 -0
  85. package/dist/apps/management/routes/invite-links.js.map +1 -0
  86. package/dist/apps/management/routes/members.d.ts +41 -41
  87. package/dist/apps/management/routes/orgs.d.ts +92 -92
  88. package/dist/apps/management/routes/projects.d.ts +9 -9
  89. package/dist/apps/management/routes/scopes.d.ts.map +1 -1
  90. package/dist/apps/management/routes/scopes.js +2 -2
  91. package/dist/apps/management/routes/scopes.js.map +1 -1
  92. package/dist/apps/management/routes/usage.d.ts +24 -24
  93. package/dist/apps/mcp.js +2 -2
  94. package/dist/apps/mcp.js.map +1 -1
  95. package/dist/apps/relay/App.d.ts +2 -2
  96. package/dist/apps/relay/App.d.ts.map +1 -1
  97. package/dist/apps/relay/App.js +44 -6
  98. package/dist/apps/relay/App.js.map +1 -1
  99. package/dist/db/Db.d.ts +3 -0
  100. package/dist/db/Db.d.ts.map +1 -1
  101. package/dist/db/Db.js +57 -0
  102. package/dist/db/Db.js.map +1 -1
  103. package/dist/db/Schema.d.ts +31 -0
  104. package/dist/db/Schema.d.ts.map +1 -1
  105. package/dist/db/Schema.js +31 -0
  106. package/dist/db/Schema.js.map +1 -1
  107. package/dist/db/tables/inviteLinks.d.ts +42 -0
  108. package/dist/db/tables/inviteLinks.d.ts.map +1 -0
  109. package/dist/db/tables/inviteLinks.js +148 -0
  110. package/dist/db/tables/inviteLinks.js.map +1 -0
  111. package/dist/db/tables/organizations.d.ts.map +1 -1
  112. package/dist/db/tables/organizations.js +2 -0
  113. package/dist/db/tables/organizations.js.map +1 -1
  114. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  115. package/dist/db/tables/sponsoredTransactions.js +6 -2
  116. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  117. package/dist/handlers/relay.d.ts.map +1 -1
  118. package/dist/handlers/relay.js +13 -3
  119. package/dist/handlers/relay.js.map +1 -1
  120. package/dist/internal/Auth.d.ts +9 -1
  121. package/dist/internal/Auth.d.ts.map +1 -1
  122. package/dist/internal/Auth.js +138 -10
  123. package/dist/internal/Auth.js.map +1 -1
  124. package/dist/internal/EdgeCache.d.ts +3 -1
  125. package/dist/internal/EdgeCache.d.ts.map +1 -1
  126. package/dist/internal/EdgeCache.js +17 -2
  127. package/dist/internal/EdgeCache.js.map +1 -1
  128. package/dist/internal/{FundingRoutes.d.ts → FundingQuotes.d.ts} +240 -369
  129. package/dist/internal/FundingQuotes.d.ts.map +1 -0
  130. package/dist/internal/{FundingRoutes.js → FundingQuotes.js} +144 -183
  131. package/dist/internal/FundingQuotes.js.map +1 -0
  132. package/dist/internal/Log.d.ts +18 -12
  133. package/dist/internal/Log.d.ts.map +1 -1
  134. package/dist/internal/Log.js +2 -0
  135. package/dist/internal/Log.js.map +1 -1
  136. package/dist/internal/Tidx.d.ts +9 -3
  137. package/dist/internal/Tidx.d.ts.map +1 -1
  138. package/dist/internal/Tidx.js +13 -7
  139. package/dist/internal/Tidx.js.map +1 -1
  140. package/dist/internal/Viem.d.ts +17 -2
  141. package/dist/internal/Viem.d.ts.map +1 -1
  142. package/dist/internal/Viem.js +30 -3
  143. package/dist/internal/Viem.js.map +1 -1
  144. package/dist/internal/index.d.ts +1 -1
  145. package/dist/internal/index.js +1 -1
  146. package/package.json +2 -2
  147. package/src/ApiKeys.test.ts +25 -0
  148. package/src/App.test-d.ts +2 -0
  149. package/src/App.test.ts +13 -2
  150. package/src/App.ts +37 -5
  151. package/src/Assets.ts +3 -3
  152. package/src/Client.test-d.ts +21 -11
  153. package/src/Scope.test.ts +1 -0
  154. package/src/Scope.ts +32 -4
  155. package/src/Webhooks.ts +5 -10
  156. package/src/admin/apps/api-keys.test.ts +27 -0
  157. package/src/admin/apps/api-keys.ts +8 -1
  158. package/src/admin/apps/scopes.test.ts +7 -2
  159. package/src/admin/apps/scopes.ts +2 -2
  160. package/src/admin/ui/src/components/Dialog.tsx +8 -2
  161. package/src/admin/ui/src/lib/scopes.test.ts +19 -0
  162. package/src/admin/ui/src/lib/scopes.ts +12 -0
  163. package/src/admin/ui/src/routes/api-keys.tsx +128 -27
  164. package/src/admin/ui.generated.ts +33 -33
  165. package/src/apps/composition.test.ts +77 -0
  166. package/src/apps/data/App.test.ts +354 -0
  167. package/src/apps/data/App.ts +178 -91
  168. package/src/apps/data/FundingQuoteProviders.test.ts +577 -0
  169. package/src/apps/data/FundingQuoteProviders.ts +633 -0
  170. package/src/apps/data/index.ts +1 -1
  171. package/src/apps/data/routes/balances.test.ts +64 -18
  172. package/src/apps/data/routes/balances.ts +76 -177
  173. package/src/apps/data/routes/blocks.ts +3 -5
  174. package/src/apps/data/routes/coingecko.ts +3 -8
  175. package/src/apps/data/routes/funding.test.ts +304 -192
  176. package/src/apps/data/routes/funding.ts +166 -286
  177. package/src/apps/data/routes/indexer.test.ts +173 -15
  178. package/src/apps/data/routes/indexer.ts +8 -2
  179. package/src/apps/data/routes/rpc.test.ts +219 -0
  180. package/src/apps/data/routes/rpc.ts +92 -17
  181. package/src/apps/data/routes/webhooks.test.ts +21 -0
  182. package/src/apps/data/routes/webhooks.ts +13 -1
  183. package/src/apps/index.ts +1 -1
  184. package/src/apps/management/App.ts +4 -0
  185. package/src/apps/management/Billing.test.ts +2 -2
  186. package/src/apps/management/routes/api-keys.test.ts +47 -3
  187. package/src/apps/management/routes/api-keys.ts +2 -2
  188. package/src/apps/management/routes/invite-links.test.ts +385 -0
  189. package/src/apps/management/routes/invite-links.ts +570 -0
  190. package/src/apps/management/routes/scopes.test.ts +3 -2
  191. package/src/apps/management/routes/scopes.ts +4 -2
  192. package/src/apps/mcp.test.ts +18 -0
  193. package/src/apps/mcp.ts +2 -2
  194. package/src/apps/relay/App.test.ts +328 -7
  195. package/src/apps/relay/App.ts +42 -6
  196. package/src/apps/relay/Sponsorships.test.ts +2 -2
  197. package/src/db/Db.test.ts +5 -5
  198. package/src/db/Db.ts +65 -0
  199. package/src/db/Schema.ts +39 -0
  200. package/src/db/tables/billingSettings.test.ts +2 -2
  201. package/src/db/tables/inviteLinks.ts +187 -0
  202. package/src/db/tables/organizations.test.ts +2 -2
  203. package/src/db/tables/organizations.ts +2 -0
  204. package/src/db/tables/sponsoredTransactions.test.ts +19 -2
  205. package/src/db/tables/sponsoredTransactions.ts +21 -14
  206. package/src/db/tables/stripeCustomers.test.ts +2 -2
  207. package/src/handlers/relay.ts +16 -4
  208. package/src/internal/Auth.test.ts +132 -22
  209. package/src/internal/Auth.ts +181 -14
  210. package/src/internal/EdgeCache.test.ts +24 -3
  211. package/src/internal/EdgeCache.ts +18 -2
  212. package/src/internal/FundingQuotes.test.ts +268 -0
  213. package/src/internal/{FundingRoutes.ts → FundingQuotes.ts} +216 -277
  214. package/src/internal/Log.ts +21 -14
  215. package/src/internal/Tidx.test.ts +48 -0
  216. package/src/internal/Tidx.ts +21 -7
  217. package/src/internal/Viem.test.ts +46 -0
  218. package/src/internal/Viem.ts +48 -6
  219. package/src/internal/index.test.ts +1 -1
  220. package/src/internal/index.ts +1 -1
  221. package/dist/apps/data/FundingRouteProviders.d.ts +0 -36
  222. package/dist/apps/data/FundingRouteProviders.d.ts.map +0 -1
  223. package/dist/apps/data/FundingRouteProviders.js +0 -245
  224. package/dist/apps/data/FundingRouteProviders.js.map +0 -1
  225. package/dist/internal/FundingRoutes.d.ts.map +0 -1
  226. package/dist/internal/FundingRoutes.js.map +0 -1
  227. package/src/apps/data/FundingRouteProviders.test.ts +0 -225
  228. package/src/apps/data/FundingRouteProviders.ts +0 -312
  229. package/src/internal/FundingRoutes.test.ts +0 -213
@@ -0,0 +1,577 @@
1
+ import * as FundingQuotes from '../../internal/FundingQuotes.js'
2
+ import * as FundingQuoteProviders from './FundingQuoteProviders.js'
3
+
4
+ const now = () => new Date('2026-01-01T00:00:00.000Z')
5
+
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('maps successful Across quotes to available quote probes', async () => {
26
+ const requests: RecordedRequest[] = []
27
+ const candidate = quoteCandidate('across')
28
+ const fetch = recordingFetch(requests, async () => jsonResponse(acrossQuote(candidate)))
29
+ const adapter = FundingQuoteProviders.across({
30
+ apiKey: 'across-api-key',
31
+ baseUrl: 'https://across.test/api/',
32
+ fetch,
33
+ integratorId: '0xdead',
34
+ now,
35
+ userAddress: '0x1111111111111111111111111111111111111111',
36
+ })
37
+
38
+ const result = await adapter.probeQuote(probeInput(candidate), new AbortController().signal)
39
+
40
+ expect(requests).toHaveLength(1)
41
+ expect(requests[0]?.url.pathname).toBe('/api/swap/approval')
42
+ expect(requests[0]?.url.searchParams.get('originChainId')).toBe('8453')
43
+ expect(requests[0]?.url.searchParams.get('destinationChainId')).toBe('4217')
44
+ expect(requests[0]?.url.searchParams.get('amount')).toBe('1000000')
45
+ expect(requests[0]?.url.searchParams.get('integratorId')).toBe('0xdead')
46
+ expect(requests[0]?.url.searchParams.get('tradeType')).toBe('exactInput')
47
+ expect(requests[0]?.url.searchParams.get('skipOriginTxEstimation')).toBe('true')
48
+ expect(requests[0]?.url.searchParams.get('strictTradeType')).toBe('true')
49
+ expect(requests[0]?.url.searchParams.get('depositor')).toBe(
50
+ '0x1111111111111111111111111111111111111111',
51
+ )
52
+ expect(requests[0]?.url.searchParams.get('recipient')).toBe(
53
+ '0x1111111111111111111111111111111111111111',
54
+ )
55
+ expect(new Headers(requests[0]?.init?.headers).get('authorization')).toBe(
56
+ 'Bearer across-api-key',
57
+ )
58
+ expect(result).toMatchObject({
59
+ expiresAt: '2026-01-01T00:01:00.000Z',
60
+ minimumOutputAmount: '988000',
61
+ outputAmount: '990000',
62
+ outputAmountFormatted: '0.99',
63
+ quality: {
64
+ estimatedSeconds: 2,
65
+ liquiditySource: 'providerQuote',
66
+ sourceDetail: 'across:swap-approval',
67
+ tier: 'liquid',
68
+ },
69
+ sampledAt: '2026-01-01T00:00:00.000Z',
70
+ status: 'available',
71
+ })
72
+ })
73
+
74
+ test('maps recognized Across quote errors to unavailable probes', async () => {
75
+ const candidate = quoteCandidate('across')
76
+ const fetch = recordingFetch([], async () => jsonResponse({ code: 'NO_ROUTE' }, 400))
77
+ const adapter = FundingQuoteProviders.across({
78
+ apiKey: 'across-api-key',
79
+ fetch,
80
+ integratorId: '0xdead',
81
+ now,
82
+ userAddress: '0x1111111111111111111111111111111111111111',
83
+ })
84
+
85
+ await expect(
86
+ adapter.probeQuote(probeInput(candidate), new AbortController().signal),
87
+ ).resolves.toMatchObject({
88
+ quality: {
89
+ liquiditySource: 'providerQuote',
90
+ sourceDetail: 'across:swap-approval:NO_ROUTE',
91
+ tier: 'unavailable',
92
+ },
93
+ status: 'unavailable',
94
+ })
95
+ })
96
+
97
+ test('rejects malformed Across quote payloads', async () => {
98
+ const candidate = quoteCandidate('across')
99
+ const fetch = recordingFetch([], async () =>
100
+ jsonResponse({ ...acrossQuote(candidate), expectedOutputAmount: '0' }),
101
+ )
102
+ const adapter = FundingQuoteProviders.across({
103
+ apiKey: 'across-api-key',
104
+ fetch,
105
+ integratorId: '0xdead',
106
+ now,
107
+ userAddress: '0x1111111111111111111111111111111111111111',
108
+ })
109
+
110
+ await expect(
111
+ adapter.probeQuote(probeInput(candidate), new AbortController().signal),
112
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`[FundingQuoteProviders.ProviderPayloadError]`)
113
+ })
114
+
115
+ test('rejects invalid Across configuration', () => {
116
+ expect(() =>
117
+ FundingQuoteProviders.across({
118
+ apiKey: 'across-api-key',
119
+ integratorId: 'tempo',
120
+ userAddress: '0x1111111111111111111111111111111111111111',
121
+ }),
122
+ ).toThrowErrorMatchingInlineSnapshot(`[FundingQuoteProviders.ProviderConfigurationError]`)
123
+ })
124
+
125
+ test('maps successful Relay quotes to available quote probes', async () => {
126
+ const requests: RecordedRequest[] = []
127
+ const candidate = quoteCandidate()
128
+ const fetch = recordingFetch(requests, async () =>
129
+ jsonResponse({
130
+ details: {
131
+ currencyOut: {
132
+ amount: '990000',
133
+ minimumAmount: '980000',
134
+ },
135
+ timeEstimate: 15,
136
+ totalImpact: { percent: '-1.20' },
137
+ },
138
+ }),
139
+ )
140
+ const adapter = FundingQuoteProviders.relay({
141
+ apiKey: 'relay-api-key',
142
+ fetch,
143
+ now,
144
+ userAddress: '0x1111111111111111111111111111111111111111',
145
+ })
146
+
147
+ const result = await adapter.probeQuote(probeInput(candidate), new AbortController().signal)
148
+
149
+ expect(requests.map((request) => request.url.pathname)).toEqual(['/quote/v2'])
150
+ expect(new Headers(requests[0]?.init?.headers).get('x-api-key')).toBe('relay-api-key')
151
+ expect(jsonRequestBody(requests[0])).toMatchObject({
152
+ amount: '1000000',
153
+ destinationChainId: 4217,
154
+ destinationCurrency: candidate.destinationToken.address,
155
+ originChainId: 8453,
156
+ originCurrency: candidate.sourceToken.address,
157
+ tradeType: 'EXACT_INPUT',
158
+ user: '0x1111111111111111111111111111111111111111',
159
+ })
160
+ expect(result).toMatchObject({
161
+ minimumOutputAmount: '980000',
162
+ outputAmount: '990000',
163
+ outputAmountFormatted: '0.99',
164
+ quality: {
165
+ estimatedSeconds: 15,
166
+ liquiditySource: 'providerQuote',
167
+ sourceDetail: 'relay:quote-v2',
168
+ tier: 'liquid',
169
+ },
170
+ sampledAt: '2026-01-01T00:00:00.000Z',
171
+ status: 'available',
172
+ })
173
+ })
174
+
175
+ test('maps successful Rhino quotes to available quote probes', async () => {
176
+ const requests: RecordedRequest[] = []
177
+ const candidate = quoteCandidate('rhino')
178
+ const fetch = recordingFetch(requests, async () =>
179
+ jsonResponse({
180
+ _tag: 'bridge',
181
+ chainIn: 'BASE',
182
+ chainOut: 'TEMPO',
183
+ estimatedDuration: 1_500,
184
+ payAmount: '1.0000000',
185
+ receiveAmount: '0.9830000',
186
+ token: 'USDC',
187
+ }),
188
+ )
189
+ const adapter = FundingQuoteProviders.rhino({
190
+ baseUrl: 'https://rhino.test/bridge/',
191
+ fetch,
192
+ now,
193
+ })
194
+
195
+ const result = await adapter.probeQuote(probeInput(candidate), new AbortController().signal)
196
+
197
+ expect(requests).toHaveLength(1)
198
+ expect(requests[0]?.url.pathname).toBe('/bridge/quote/bridge-swap/public')
199
+ expect(Object.fromEntries(requests[0]!.url.searchParams)).toEqual({
200
+ amount: '1',
201
+ amountNative: '0',
202
+ chainIn: 'BASE',
203
+ chainOut: 'TEMPO',
204
+ mode: 'pay',
205
+ tokenIn: 'USDC',
206
+ tokenOut: 'USDC',
207
+ })
208
+ expect(result).toEqual({
209
+ outputAmount: '983000',
210
+ outputAmountFormatted: '0.983',
211
+ quality: {
212
+ estimatedSeconds: 2,
213
+ liquiditySource: 'providerQuote',
214
+ sourceDetail: 'rhino:bridge-swap-public',
215
+ tier: 'liquid',
216
+ },
217
+ sampledAt: '2026-01-01T00:00:00.000Z',
218
+ status: 'available',
219
+ })
220
+ })
221
+
222
+ test('maps unavailable Rhino routes to unavailable quote probes', async () => {
223
+ const candidate = quoteCandidate('rhino')
224
+ const adapter = FundingQuoteProviders.rhino({
225
+ fetch: recordingFetch([], async () => jsonResponse({ _tag: 'NoRouteFoundError' }, 404)),
226
+ now,
227
+ })
228
+
229
+ await expect(
230
+ adapter.probeQuote(probeInput(candidate), new AbortController().signal),
231
+ ).resolves.toMatchObject({
232
+ quality: {
233
+ sourceDetail: 'rhino:bridge-swap-public:NoRouteFoundError',
234
+ tier: 'unavailable',
235
+ },
236
+ status: 'unavailable',
237
+ })
238
+ })
239
+
240
+ test('maps Solana and Tron identifiers into Rhino quote requests', async () => {
241
+ const cases = [
242
+ {
243
+ candidate: quoteCandidate('rhino', { sourceChain: 'solana' }),
244
+ chainIn: 'SOLANA',
245
+ tokenOut: 'USDC',
246
+ },
247
+ {
248
+ candidate: quoteCandidate('rhino', {
249
+ destinationToken: 'USDT0',
250
+ sourceChain: 'tron',
251
+ sourceToken: 'USDT',
252
+ }),
253
+ chainIn: 'TRON',
254
+ tokenOut: 'USDT',
255
+ },
256
+ ] as const
257
+
258
+ for (const testCase of cases) {
259
+ const requests: RecordedRequest[] = []
260
+ const adapter = FundingQuoteProviders.rhino({
261
+ fetch: recordingFetch(requests, async () =>
262
+ jsonResponse({
263
+ _tag: 'bridge',
264
+ chainIn: testCase.chainIn,
265
+ chainOut: 'TEMPO',
266
+ payAmount: '1',
267
+ receiveAmount: '0.983',
268
+ token: testCase.tokenOut,
269
+ }),
270
+ ),
271
+ now,
272
+ })
273
+
274
+ await expect(
275
+ adapter.probeQuote(probeInput(testCase.candidate), new AbortController().signal),
276
+ ).resolves.toMatchObject({ outputAmount: '983000', status: 'available' })
277
+ expect(requests[0]?.url.searchParams.get('chainIn')).toBe(testCase.chainIn)
278
+ expect(requests[0]?.url.searchParams.get('tokenOut')).toBe(testCase.tokenOut)
279
+ }
280
+ })
281
+
282
+ test('surfaces Rhino request decoding failures', async () => {
283
+ const candidate = quoteCandidate('rhino')
284
+ const adapter = FundingQuoteProviders.rhino({
285
+ fetch: recordingFetch([], async () => jsonResponse({ _tag: 'HttpApiDecodeError' }, 400)),
286
+ now,
287
+ })
288
+
289
+ await expect(
290
+ adapter.probeQuote(probeInput(candidate), new AbortController().signal),
291
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
292
+ `[FundingQuoteProviders.ProviderResponseError: Provider request failed with status 400]`,
293
+ )
294
+ })
295
+
296
+ test('surfaces unrecognized Rhino route errors', async () => {
297
+ const candidate = quoteCandidate('rhino')
298
+ const failures = [
299
+ { body: null, status: 404 },
300
+ { body: { _tag: 'InvalidRequest' }, status: 422 },
301
+ ] as const
302
+
303
+ for (const failure of failures) {
304
+ const adapter = FundingQuoteProviders.rhino({
305
+ fetch: recordingFetch([], async () => jsonResponse(failure.body, failure.status)),
306
+ now,
307
+ })
308
+ const cause = await adapter
309
+ .probeQuote(probeInput(candidate), new AbortController().signal)
310
+ .catch((error: unknown) => error)
311
+ expect(cause).toMatchObject({
312
+ name: 'FundingQuoteProviders.ProviderResponseError',
313
+ status: failure.status,
314
+ })
315
+ }
316
+ })
317
+
318
+ test('rejects mismatched Rhino quote payloads', async () => {
319
+ const candidate = quoteCandidate('rhino')
320
+ const adapter = FundingQuoteProviders.rhino({
321
+ fetch: recordingFetch([], async () =>
322
+ jsonResponse({
323
+ _tag: 'bridge',
324
+ chainIn: 'SOLANA',
325
+ chainOut: 'TEMPO',
326
+ payAmount: '1',
327
+ receiveAmount: '0.983',
328
+ token: 'USDC',
329
+ }),
330
+ ),
331
+ now,
332
+ })
333
+
334
+ await expect(
335
+ adapter.probeQuote(probeInput(candidate), new AbortController().signal),
336
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`[FundingQuoteProviders.ProviderPayloadError]`)
337
+ })
338
+
339
+ test('rejects non-representable Rhino output precision', async () => {
340
+ const candidate = quoteCandidate('rhino')
341
+ const adapter = FundingQuoteProviders.rhino({
342
+ fetch: recordingFetch([], async () =>
343
+ jsonResponse({
344
+ _tag: 'bridge',
345
+ chainIn: 'BASE',
346
+ chainOut: 'TEMPO',
347
+ payAmount: '1',
348
+ receiveAmount: '0.9830001',
349
+ token: 'USDC',
350
+ }),
351
+ ),
352
+ now,
353
+ })
354
+
355
+ await expect(
356
+ adapter.probeQuote(probeInput(candidate), new AbortController().signal),
357
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`[FundingQuoteProviders.ProviderPayloadError]`)
358
+ })
359
+
360
+ test('calls the runtime fetch method with its global receiver', async () => {
361
+ const candidate = quoteCandidate()
362
+ const fetch = vi.fn(function (this: unknown) {
363
+ expect(this).toBe(globalThis)
364
+ return Promise.resolve(
365
+ jsonResponse({
366
+ details: {
367
+ currencyOut: { amount: '990000', minimumAmount: '980000' },
368
+ timeEstimate: 15,
369
+ },
370
+ }),
371
+ )
372
+ })
373
+ vi.stubGlobal('fetch', fetch)
374
+
375
+ try {
376
+ const adapter = FundingQuoteProviders.relay({
377
+ now,
378
+ userAddress: '0x1111111111111111111111111111111111111111',
379
+ })
380
+ const result = await adapter.probeQuote(probeInput(candidate), new AbortController().signal)
381
+
382
+ expect(result.outputAmount).toBe('990000')
383
+ expect(fetch).toHaveBeenCalledTimes(1)
384
+ } finally {
385
+ vi.unstubAllGlobals()
386
+ }
387
+ })
388
+
389
+ const invalidAcrossQuotes = [
390
+ [
391
+ 'mismatched input amounts',
392
+ (body: ReturnType<typeof acrossQuote>) => (body.inputAmount = '1'),
393
+ ],
394
+ [
395
+ 'mismatched token addresses',
396
+ (body: ReturnType<typeof acrossQuote>) =>
397
+ (body.inputToken.address = '0xfde4c96c8593536e31f229ea8f37b2ada2699bb2'),
398
+ ],
399
+ [
400
+ 'mismatched token chains',
401
+ (body: ReturnType<typeof acrossQuote>) => (body.inputToken.chainId = 1),
402
+ ],
403
+ [
404
+ 'mismatched token decimals',
405
+ (body: ReturnType<typeof acrossQuote>) => (body.inputToken.decimals = 18),
406
+ ],
407
+ [
408
+ 'expired quotes',
409
+ (body: ReturnType<typeof acrossQuote>) => (body.quoteExpiryTimestamp = 1767225600),
410
+ ],
411
+ [
412
+ 'out-of-range expiries',
413
+ (body: ReturnType<typeof acrossQuote>) =>
414
+ (body.quoteExpiryTimestamp = Number.MAX_SAFE_INTEGER),
415
+ ],
416
+ [
417
+ 'invalid output bounds',
418
+ (body: ReturnType<typeof acrossQuote>) => (body.minOutputAmount = '990001'),
419
+ ],
420
+ ] as const
421
+
422
+ for (const [name, mutate] of invalidAcrossQuotes)
423
+ test(`rejects Across quotes with ${name}`, async () => {
424
+ const candidate = quoteCandidate('across')
425
+ const body = acrossQuote(candidate)
426
+ mutate(body)
427
+ const adapter = FundingQuoteProviders.across({
428
+ apiKey: 'across-api-key',
429
+ fetch: recordingFetch([], async () => jsonResponse(body)),
430
+ integratorId: '0xdead',
431
+ now,
432
+ userAddress: '0x1111111111111111111111111111111111111111',
433
+ })
434
+
435
+ await expect(
436
+ adapter.probeQuote(probeInput(candidate), new AbortController().signal),
437
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`[FundingQuoteProviders.ProviderPayloadError]`)
438
+ })
439
+
440
+ test('returns bounded Across failure diagnostics', async () => {
441
+ const candidate = quoteCandidate('across')
442
+ const adapter = FundingQuoteProviders.across({
443
+ apiKey: 'across-api-key',
444
+ fetch: recordingFetch([], async () =>
445
+ jsonResponse({ apiKey: 'secret', code: 'UNAUTHORIZED' }, 401),
446
+ ),
447
+ integratorId: '0xdead',
448
+ now,
449
+ userAddress: '0x1111111111111111111111111111111111111111',
450
+ })
451
+ const cause = await adapter
452
+ .probeQuote(probeInput(candidate), new AbortController().signal)
453
+ .catch((error: unknown) => error)
454
+
455
+ expect(FundingQuoteProviders.providerFailure({ cause, provider: 'across' })).toEqual({
456
+ code: 'UNAUTHORIZED',
457
+ failure: 'http',
458
+ id: 'across',
459
+ operation: 'swap/approval',
460
+ status: 401,
461
+ })
462
+ })
463
+
464
+ test('returns explicit timeout failure diagnostics', () => {
465
+ expect(
466
+ FundingQuoteProviders.providerFailure({
467
+ cause: new FundingQuotes.ProviderAdapterTimeoutError(),
468
+ provider: 'across',
469
+ }),
470
+ ).toEqual({
471
+ failure: 'timeout',
472
+ id: 'across',
473
+ operation: 'swap/approval',
474
+ })
475
+ })
476
+ })
477
+
478
+ type RecordedRequest = {
479
+ init: RequestInit | undefined
480
+ url: URL
481
+ }
482
+
483
+ function quoteCandidate(
484
+ provider: FundingQuotes.ProviderId = 'relay',
485
+ options: quoteCandidate.Options = {},
486
+ ): FundingQuotes.QuoteProbeInput['candidate'] {
487
+ const [candidate] = FundingQuotes.listQuoteCandidates({
488
+ destinationToken: options.destinationToken,
489
+ provider,
490
+ sourceAmount: '1000000',
491
+ sourceAmountUnits: 'baseUnits',
492
+ sourceChain: options.sourceChain ?? 'base',
493
+ sourceToken: options.sourceToken ?? 'USDC',
494
+ })
495
+ if (!candidate) throw new Error(`Missing ${provider} quote candidate`)
496
+ return candidate
497
+ }
498
+
499
+ declare namespace quoteCandidate {
500
+ type Options = {
501
+ destinationToken?: string | undefined
502
+ sourceChain?: string | undefined
503
+ sourceToken?: string | undefined
504
+ }
505
+ }
506
+
507
+ function acrossQuote(candidate: FundingQuotes.QuoteProbeInput['candidate']) {
508
+ return {
509
+ amountType: 'exactInput',
510
+ expectedFillTime: 2,
511
+ expectedOutputAmount: '990000',
512
+ inputAmount: candidate.sourceAmount.amount,
513
+ inputToken: {
514
+ address: candidate.sourceToken.address,
515
+ chainId: 8453,
516
+ decimals: candidate.sourceToken.decimals,
517
+ symbol: candidate.sourceToken.symbol,
518
+ },
519
+ minOutputAmount: '988000',
520
+ outputToken: {
521
+ address: candidate.destinationToken.address,
522
+ chainId: 4217,
523
+ decimals: candidate.destinationToken.decimals,
524
+ symbol: candidate.destinationToken.symbol,
525
+ },
526
+ quoteExpiryTimestamp: 1767225660,
527
+ }
528
+ }
529
+
530
+ function probeInput(
531
+ candidate: FundingQuotes.QuoteProbeInput['candidate'],
532
+ ): FundingQuotes.QuoteProbeInput {
533
+ return {
534
+ candidate,
535
+ cache: { bypass: false },
536
+ destinationChain: candidate.destinationChain,
537
+ destinationToken: candidate.destinationToken,
538
+ request: {
539
+ provider: candidate.provider.id,
540
+ sourceAmount: candidate.sourceAmount.amount,
541
+ sourceAmountUnits: 'baseUnits',
542
+ sourceChain: candidate.sourceChain.id,
543
+ sourceToken: candidate.sourceToken.symbol,
544
+ },
545
+ sourceAmount: candidate.sourceAmount,
546
+ sourceChain: candidate.sourceChain,
547
+ sourceToken: candidate.sourceToken,
548
+ }
549
+ }
550
+
551
+ function recordingFetch(
552
+ requests: RecordedRequest[],
553
+ respond: (request: RecordedRequest) => Response | Promise<Response>,
554
+ ): typeof globalThis.fetch {
555
+ return async (input, init) => {
556
+ const request = { init, url: toUrl(input) }
557
+ requests.push(request)
558
+ return respond(request)
559
+ }
560
+ }
561
+
562
+ function toUrl(input: Parameters<typeof globalThis.fetch>[0]) {
563
+ if (input instanceof Request) return new URL(input.url)
564
+ return new URL(String(input))
565
+ }
566
+
567
+ function jsonResponse(body: unknown, status = 200) {
568
+ return new Response(JSON.stringify(body), {
569
+ headers: { 'content-type': 'application/json' },
570
+ status,
571
+ })
572
+ }
573
+
574
+ function jsonRequestBody(request: RecordedRequest | undefined) {
575
+ if (typeof request?.init?.body !== 'string') throw new Error('Expected JSON request body')
576
+ return JSON.parse(request.init.body) as unknown
577
+ }