tapimo 0.4.3 → 0.5.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 (192) hide show
  1. package/dist/App.d.ts +7222 -6316
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +4 -0
  4. package/dist/App.js.map +1 -1
  5. package/dist/Scope.d.ts +18 -1
  6. package/dist/Scope.d.ts.map +1 -1
  7. package/dist/Scope.js +24 -3
  8. package/dist/Scope.js.map +1 -1
  9. package/dist/admin/apps/verified-tokens.d.ts +116 -116
  10. package/dist/apps/data/App.d.ts +2541 -2607
  11. package/dist/apps/data/App.d.ts.map +1 -1
  12. package/dist/apps/data/App.js +135 -44
  13. package/dist/apps/data/App.js.map +1 -1
  14. package/dist/apps/data/FundingQuoteProviders.d.ts +36 -0
  15. package/dist/apps/data/FundingQuoteProviders.d.ts.map +1 -0
  16. package/dist/apps/data/FundingQuoteProviders.js +190 -0
  17. package/dist/apps/data/FundingQuoteProviders.js.map +1 -0
  18. package/dist/apps/data/index.d.ts +1 -1
  19. package/dist/apps/data/index.js +1 -1
  20. package/dist/apps/data/routes/activities.d.ts +242 -242
  21. package/dist/apps/data/routes/addresses.d.ts +227 -227
  22. package/dist/apps/data/routes/balances.d.ts +12 -12
  23. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  24. package/dist/apps/data/routes/balances.js +67 -149
  25. package/dist/apps/data/routes/balances.js.map +1 -1
  26. package/dist/apps/data/routes/blocks.d.ts +125 -126
  27. package/dist/apps/data/routes/blocks.d.ts.map +1 -1
  28. package/dist/apps/data/routes/blocks.js +3 -5
  29. package/dist/apps/data/routes/blocks.js.map +1 -1
  30. package/dist/apps/data/routes/coingecko.d.ts +55 -54
  31. package/dist/apps/data/routes/coingecko.d.ts.map +1 -1
  32. package/dist/apps/data/routes/coingecko.js +3 -8
  33. package/dist/apps/data/routes/coingecko.js.map +1 -1
  34. package/dist/apps/data/routes/exchanges.d.ts +455 -455
  35. package/dist/apps/data/routes/fee-amm.d.ts +92 -92
  36. package/dist/apps/data/routes/funding.d.ts +91 -864
  37. package/dist/apps/data/routes/funding.d.ts.map +1 -1
  38. package/dist/apps/data/routes/funding.js +138 -450
  39. package/dist/apps/data/routes/funding.js.map +1 -1
  40. package/dist/apps/data/routes/rpc.d.ts.map +1 -1
  41. package/dist/apps/data/routes/rpc.js +86 -16
  42. package/dist/apps/data/routes/rpc.js.map +1 -1
  43. package/dist/apps/data/routes/tokenlist.d.ts +36 -36
  44. package/dist/apps/data/routes/webhooks.d.ts +340 -0
  45. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  46. package/dist/apps/data/routes/webhooks.js +13 -1
  47. package/dist/apps/data/routes/webhooks.js.map +1 -1
  48. package/dist/apps/index.d.ts +1 -1
  49. package/dist/apps/index.js +1 -1
  50. package/dist/apps/management/App.d.ts +1843 -861
  51. package/dist/apps/management/App.d.ts.map +1 -1
  52. package/dist/apps/management/App.js +4 -0
  53. package/dist/apps/management/App.js.map +1 -1
  54. package/dist/apps/management/routes/api-keys.d.ts +121 -121
  55. package/dist/apps/management/routes/api-keys.js +2 -2
  56. package/dist/apps/management/routes/api-keys.js.map +1 -1
  57. package/dist/apps/management/routes/billing.d.ts +2 -2
  58. package/dist/apps/management/routes/invite-links.d.ts +1100 -0
  59. package/dist/apps/management/routes/invite-links.d.ts.map +1 -0
  60. package/dist/apps/management/routes/invite-links.js +411 -0
  61. package/dist/apps/management/routes/invite-links.js.map +1 -0
  62. package/dist/apps/management/routes/me.d.ts +13 -13
  63. package/dist/apps/management/routes/members.d.ts +77 -77
  64. package/dist/apps/management/routes/orgs.d.ts +96 -96
  65. package/dist/apps/management/routes/projects.d.ts +62 -62
  66. package/dist/apps/management/routes/scopes.d.ts +11 -11
  67. package/dist/apps/management/routes/scopes.d.ts.map +1 -1
  68. package/dist/apps/management/routes/scopes.js +2 -2
  69. package/dist/apps/management/routes/scopes.js.map +1 -1
  70. package/dist/apps/management/routes/usage.d.ts +79 -79
  71. package/dist/apps/mcp.d.ts.map +1 -1
  72. package/dist/apps/mcp.js +3 -1
  73. package/dist/apps/mcp.js.map +1 -1
  74. package/dist/apps/relay/App.d.ts.map +1 -1
  75. package/dist/apps/relay/App.js +42 -4
  76. package/dist/apps/relay/App.js.map +1 -1
  77. package/dist/cloudflare.d.ts +2 -0
  78. package/dist/cloudflare.d.ts.map +1 -1
  79. package/dist/cloudflare.js +4 -0
  80. package/dist/cloudflare.js.map +1 -1
  81. package/dist/db/Db.d.ts +3 -0
  82. package/dist/db/Db.d.ts.map +1 -1
  83. package/dist/db/Db.js +57 -0
  84. package/dist/db/Db.js.map +1 -1
  85. package/dist/db/Schema.d.ts +31 -0
  86. package/dist/db/Schema.d.ts.map +1 -1
  87. package/dist/db/Schema.js +31 -0
  88. package/dist/db/Schema.js.map +1 -1
  89. package/dist/db/tables/inviteLinks.d.ts +42 -0
  90. package/dist/db/tables/inviteLinks.d.ts.map +1 -0
  91. package/dist/db/tables/inviteLinks.js +148 -0
  92. package/dist/db/tables/inviteLinks.js.map +1 -0
  93. package/dist/db/tables/organizations.d.ts.map +1 -1
  94. package/dist/db/tables/organizations.js +2 -0
  95. package/dist/db/tables/organizations.js.map +1 -1
  96. package/dist/handlers/relay.d.ts.map +1 -1
  97. package/dist/handlers/relay.js +13 -3
  98. package/dist/handlers/relay.js.map +1 -1
  99. package/dist/internal/Auth.d.ts +42 -23
  100. package/dist/internal/Auth.d.ts.map +1 -1
  101. package/dist/internal/Auth.js +169 -20
  102. package/dist/internal/Auth.js.map +1 -1
  103. package/dist/internal/EdgeCache.d.ts +3 -1
  104. package/dist/internal/EdgeCache.d.ts.map +1 -1
  105. package/dist/internal/EdgeCache.js +17 -2
  106. package/dist/internal/EdgeCache.js.map +1 -1
  107. package/dist/internal/Errors.d.ts +6 -0
  108. package/dist/internal/Errors.d.ts.map +1 -0
  109. package/dist/internal/Errors.js +16 -0
  110. package/dist/internal/Errors.js.map +1 -0
  111. package/dist/internal/FundingQuotes.d.ts +644 -0
  112. package/dist/internal/FundingQuotes.d.ts.map +1 -0
  113. package/dist/internal/{FundingRoutes.js → FundingQuotes.js} +93 -289
  114. package/dist/internal/FundingQuotes.js.map +1 -0
  115. package/dist/internal/Store.d.ts +61 -16
  116. package/dist/internal/Store.d.ts.map +1 -1
  117. package/dist/internal/Store.js +72 -17
  118. package/dist/internal/Store.js.map +1 -1
  119. package/dist/internal/Viem.d.ts +2 -0
  120. package/dist/internal/Viem.d.ts.map +1 -1
  121. package/dist/internal/Viem.js +11 -2
  122. package/dist/internal/Viem.js.map +1 -1
  123. package/dist/internal/index.d.ts +27 -0
  124. package/dist/internal/index.d.ts.map +1 -0
  125. package/dist/internal/index.js +27 -0
  126. package/dist/internal/index.js.map +1 -0
  127. package/package.json +8 -3
  128. package/src/ApiKeys.test.ts +23 -0
  129. package/src/App.test-d.ts +2 -0
  130. package/src/App.test.ts +60 -4
  131. package/src/App.ts +6 -0
  132. package/src/Client.test-d.ts +16 -54
  133. package/src/Scope.test.ts +1 -0
  134. package/src/Scope.ts +30 -4
  135. package/src/apps/data/App.test.ts +354 -0
  136. package/src/apps/data/App.ts +159 -52
  137. package/src/apps/data/FundingQuoteProviders.test.ts +151 -0
  138. package/src/apps/data/FundingQuoteProviders.ts +251 -0
  139. package/src/apps/data/index.ts +1 -1
  140. package/src/apps/data/routes/balances.test.ts +64 -18
  141. package/src/apps/data/routes/balances.ts +76 -177
  142. package/src/apps/data/routes/blocks.ts +3 -5
  143. package/src/apps/data/routes/coingecko.ts +3 -8
  144. package/src/apps/data/routes/funding.test.ts +149 -306
  145. package/src/apps/data/routes/funding.ts +154 -604
  146. package/src/apps/data/routes/indexer.test.ts +99 -3
  147. package/src/apps/data/routes/rpc.test.ts +219 -0
  148. package/src/apps/data/routes/rpc.ts +92 -17
  149. package/src/apps/data/routes/webhooks.test.ts +23 -2
  150. package/src/apps/data/routes/webhooks.ts +13 -1
  151. package/src/apps/index.ts +1 -1
  152. package/src/apps/management/App.ts +4 -0
  153. package/src/apps/management/routes/api-keys.test.ts +47 -3
  154. package/src/apps/management/routes/api-keys.ts +2 -2
  155. package/src/apps/management/routes/invite-links.test.ts +385 -0
  156. package/src/apps/management/routes/invite-links.ts +570 -0
  157. package/src/apps/management/routes/scopes.test.ts +3 -2
  158. package/src/apps/management/routes/scopes.ts +4 -2
  159. package/src/apps/mcp.test.ts +55 -14
  160. package/src/apps/mcp.ts +3 -1
  161. package/src/apps/relay/App.test.ts +324 -4
  162. package/src/apps/relay/App.ts +40 -4
  163. package/src/cloudflare.ts +5 -0
  164. package/src/db/Db.ts +65 -0
  165. package/src/db/Schema.ts +39 -0
  166. package/src/db/tables/inviteLinks.ts +187 -0
  167. package/src/db/tables/organizations.ts +2 -0
  168. package/src/handlers/relay.ts +16 -4
  169. package/src/internal/Auth.test.ts +528 -37
  170. package/src/internal/Auth.ts +246 -45
  171. package/src/internal/EdgeCache.test.ts +24 -3
  172. package/src/internal/EdgeCache.ts +18 -2
  173. package/src/internal/FundingQuotes.test.ts +174 -0
  174. package/src/internal/FundingQuotes.ts +735 -0
  175. package/src/internal/Log.test.ts +4 -4
  176. package/src/internal/Store.test-d.ts +15 -0
  177. package/src/internal/Store.test.ts +153 -2
  178. package/src/internal/Store.ts +157 -41
  179. package/src/internal/Viem.ts +14 -5
  180. package/src/internal/index.test.ts +34 -0
  181. package/src/internal/index.ts +26 -0
  182. package/dist/apps/data/FundingRouteProviders.d.ts +0 -143
  183. package/dist/apps/data/FundingRouteProviders.d.ts.map +0 -1
  184. package/dist/apps/data/FundingRouteProviders.js +0 -546
  185. package/dist/apps/data/FundingRouteProviders.js.map +0 -1
  186. package/dist/internal/FundingRoutes.d.ts +0 -1085
  187. package/dist/internal/FundingRoutes.d.ts.map +0 -1
  188. package/dist/internal/FundingRoutes.js.map +0 -1
  189. package/src/apps/data/FundingRouteProviders.test.ts +0 -485
  190. package/src/apps/data/FundingRouteProviders.ts +0 -744
  191. package/src/internal/FundingRoutes.test.ts +0 -239
  192. package/src/internal/FundingRoutes.ts +0 -1067
@@ -1,744 +0,0 @@
1
- import * as z from 'zod/mini'
2
-
3
- import * as FundingRoutes from '../../internal/FundingRoutes.js'
4
-
5
- type Fetch = typeof globalThis.fetch
6
-
7
- const defaultFetch: Fetch = (input, init) => globalThis.fetch(input, init)
8
-
9
- const defaultAcrossBaseUrl = 'https://app.across.to/api'
10
- const defaultRelayBaseUrl = 'https://api.relay.link'
11
-
12
- namespace schema {
13
- const DecimalString = z.string().check(z.regex(/^\d+(\.\d+)?$/))
14
- const IntegerString = z.string().check(z.regex(/^\d+$/))
15
-
16
- const AcrossToken = z.object({
17
- address: z.string(),
18
- chainId: z.number().check(z.int(), z.nonnegative()),
19
- decimals: z.number().check(z.int(), z.nonnegative()),
20
- symbol: z.string(),
21
- })
22
-
23
- const AcrossFee = z.object({
24
- amount: IntegerString,
25
- amountUsd: DecimalString,
26
- pct: IntegerString,
27
- token: AcrossToken,
28
- })
29
-
30
- export const AcrossQuote = z.object({
31
- amountType: z.literal('exactInput'),
32
- expectedFillTime: z.number().check(z.int(), z.nonnegative()),
33
- expectedOutputAmount: IntegerString,
34
- fees: z.object({
35
- total: AcrossFee,
36
- totalMax: AcrossFee,
37
- }),
38
- inputAmount: IntegerString,
39
- inputToken: AcrossToken,
40
- minOutputAmount: IntegerString,
41
- outputToken: AcrossToken,
42
- quoteExpiryTimestamp: z.number().check(z.int(), z.nonnegative()),
43
- })
44
- }
45
-
46
- /** Creates the live funding route adapters configured for this deployment. */
47
- export function createAdapters(options: createAdapters.Options = {}) {
48
- return {
49
- ...(options.across ? { across: across(options.across) } : {}),
50
- ...(options.relay ? { relay: relay(options.relay) } : {}),
51
- } satisfies FundingRoutes.ProviderAdapters
52
- }
53
-
54
- export declare namespace createAdapters {
55
- /** Provider adapter factory options. Omit a provider to keep its routes static. */
56
- type Options = {
57
- /** Across discovery adapter options. */
58
- across?: across.Options | false | undefined
59
- /** Relay quote adapter options. */
60
- relay?: relay.Options | false | undefined
61
- }
62
- }
63
-
64
- /** Creates an Across discovery adapter backed by `/suggested-fees`. */
65
- export function across(options: across.Options): FundingRoutes.ProviderAdapter {
66
- const baseUrl = normalizeBaseUrl(options.baseUrl ?? defaultAcrossBaseUrl)
67
- const fetcher = options.fetch ?? defaultFetch
68
- const now = options.now ?? (() => new Date())
69
-
70
- return {
71
- async probeQuote({ route }, signal) {
72
- const sourceChainId = providerChainId(route.sourceChain, 'across')
73
- const destinationChainId = eip155ChainId(route.destinationChain)
74
- const url = new URL(`${baseUrl}/suggested-fees`)
75
- url.search = new URLSearchParams({
76
- allowUnmatchedDecimals: 'true',
77
- amount: route.sourceAmount.amount,
78
- destinationChainId,
79
- inputToken: route.sourceToken.address,
80
- integratorId: options.integratorId,
81
- originChainId: sourceChainId,
82
- outputToken: route.destinationToken.address,
83
- }).toString()
84
-
85
- try {
86
- const body = await requestJson(url, {
87
- fetcher,
88
- headers: { Authorization: `Bearer ${options.apiKey}` },
89
- signal,
90
- })
91
- return formatAcrossResult(route, body, now())
92
- } catch (cause) {
93
- const unavailable = providerUnavailable(cause, ['AMOUNT_TOO_HIGH', 'AMOUNT_TOO_LOW'])
94
- if (!unavailable) throw cause
95
- return unavailableResult({
96
- detail: 'across:suggested-fees',
97
- message: unavailable,
98
- now: now(),
99
- source: 'providerLimit',
100
- })
101
- }
102
- },
103
- }
104
- }
105
-
106
- /** Builds the authenticated Across `/swap/approval` request for an exact-input quote. */
107
- export function createAcrossQuoteRequest(options: createAcrossQuoteRequest.Options) {
108
- assertAcrossIntegratorId(options.integratorId)
109
- const baseUrl = normalizeBaseUrl(options.baseUrl ?? defaultAcrossBaseUrl)
110
- const sourceChainId = providerChainId(options.route.sourceChain, 'across')
111
- const destinationChainId = eip155ChainId(options.route.destinationChain)
112
- const url = new URL(`${baseUrl}/swap/approval`)
113
- url.search = new URLSearchParams({
114
- amount: options.route.sourceAmount.amount,
115
- depositor: options.depositor,
116
- destinationChainId,
117
- inputToken: options.route.sourceToken.address,
118
- integratorId: options.integratorId,
119
- originChainId: sourceChainId,
120
- outputToken: options.route.destinationToken.address,
121
- skipOriginTxEstimation: 'true',
122
- strictTradeType: 'true',
123
- tradeType: 'exactInput',
124
- }).toString()
125
- if (options.recipient !== undefined) url.searchParams.set('recipient', options.recipient)
126
- return { headers: { Authorization: `Bearer ${options.apiKey}` }, url }
127
- }
128
-
129
- export declare namespace createAcrossQuoteRequest {
130
- /** Inputs required to construct an Across exact-input quote request. */
131
- type Options = {
132
- /** Across API key sent as a bearer token. */
133
- apiKey: string
134
- /** Optional Across API base URL. */
135
- baseUrl?: string | undefined
136
- /** Address that would submit the source transaction. */
137
- depositor: string
138
- /** Across integrator id sent as the required query parameter. */
139
- integratorId: string
140
- /** Optional destination recipient. */
141
- recipient?: string | undefined
142
- /** Curated route selected for the quote. */
143
- route: FundingRoutes.RouteCandidate
144
- }
145
- }
146
-
147
- /** Creates an explicit Across quote adapter backed by `/swap/approval`. */
148
- export function acrossQuote(options: acrossQuote.Options): FundingRoutes.ProviderQuote {
149
- assertAcrossIntegratorId(options.integratorId)
150
-
151
- return async ({ depositor, recipient, route }, signal) => {
152
- const request = createAcrossQuoteRequest({
153
- apiKey: options.apiKey,
154
- baseUrl: options.baseUrl,
155
- depositor,
156
- integratorId: options.integratorId,
157
- recipient,
158
- route,
159
- })
160
-
161
- try {
162
- const body = await requestJson(request.url, {
163
- fetcher: defaultFetch,
164
- headers: request.headers,
165
- signal,
166
- })
167
- return parseAcrossQuote({ body, now: new Date(), route })
168
- } catch (cause) {
169
- if (!(cause instanceof ProviderResponseError)) throw cause
170
- throw mapAcrossQuoteError({ body: cause.body, status: cause.status })
171
- }
172
- }
173
- }
174
-
175
- export declare namespace acrossQuote {
176
- /** Across Swap API adapter options. */
177
- type Options = {
178
- /** Across API key sent as a bearer token. */
179
- apiKey: string
180
- /** Optional Across API base URL. */
181
- baseUrl?: string | undefined
182
- /** Across integrator id sent as the required query parameter. */
183
- integratorId: string
184
- }
185
- }
186
-
187
- /** Returns bounded Across quote failure details suitable for structured request logs. */
188
- export function acrossQuoteFailure(options: acrossQuoteFailure.Options): acrossQuoteFailure.Result {
189
- const { cause } = options
190
- const base = { id: 'across', operation: 'swap/approval' } as const
191
- if (cause instanceof ProviderResponseError) {
192
- const code = providerErrorCode(cause.body)
193
- return {
194
- ...(code === undefined ? {} : { code }),
195
- failure: 'http' as const,
196
- ...base,
197
- status: cause.status,
198
- }
199
- }
200
- if (cause instanceof ProviderPayloadError) return { failure: 'payload', ...base }
201
- if (cause instanceof TypeError) return { failure: 'network', ...base }
202
- if (cause instanceof Error) return { failure: 'unknown', ...base }
203
- return undefined
204
- }
205
-
206
- export declare namespace acrossQuoteFailure {
207
- /** Inputs used to classify an Across quote failure. */
208
- type Options = {
209
- /** Error thrown by the Across quote boundary. */
210
- cause: unknown
211
- }
212
-
213
- /** Bounded diagnostic fields, or undefined when the thrown value is not an error. */
214
- type Result =
215
- | {
216
- /** Stable provider error code, when one is safe to retain. */
217
- code?: string | undefined
218
- /** Failure boundary that rejected the provider request. */
219
- failure: 'http' | 'network' | 'payload' | 'unknown'
220
- /** Provider identifier. */
221
- id: 'across'
222
- /** Across operation identifier. */
223
- operation: 'swap/approval'
224
- /** Upstream HTTP status, when a response was received. */
225
- status?: number | undefined
226
- }
227
- | undefined
228
- }
229
-
230
- export declare namespace across {
231
- /** Across API adapter options. */
232
- type Options = {
233
- /** Override for tests or non-production Across environments. */
234
- baseUrl?: string | undefined
235
- /** Fetch implementation override for tests. */
236
- fetch?: Fetch | undefined
237
- /** Across API key sent as a bearer token. */
238
- apiKey: string
239
- /** Across integrator id sent as the required query parameter. */
240
- integratorId: string
241
- /** Clock override for deterministic tests. */
242
- now?: (() => Date) | undefined
243
- }
244
- }
245
-
246
- /** Creates a Relay quote adapter backed by `/chains/liquidity` and `/quote/v2`. */
247
- export function relay(options: relay.Options): FundingRoutes.ProviderAdapter {
248
- const baseUrl = normalizeBaseUrl(options.baseUrl ?? defaultRelayBaseUrl)
249
- const fetcher = options.fetch ?? defaultFetch
250
- const now = options.now ?? (() => new Date())
251
-
252
- return {
253
- async probeQuote({ route }, signal) {
254
- const originChainId = providerChainId(route.sourceChain, 'relay')
255
- const destinationChainId = eip155ChainId(route.destinationChain)
256
- const headers = relayHeaders(options.apiKey)
257
-
258
- try {
259
- const quote = await requestJson(new URL(`${baseUrl}/quote/v2`), {
260
- body: {
261
- amount: route.sourceAmount.amount,
262
- destinationChainId: Number(destinationChainId),
263
- destinationCurrency: route.destinationToken.address,
264
- originChainId: Number(originChainId),
265
- originCurrency: route.sourceToken.address,
266
- recipient: options.recipientAddress ?? options.userAddress,
267
- tradeType: 'EXACT_INPUT',
268
- user: options.userAddress,
269
- },
270
- fetcher,
271
- headers,
272
- method: 'POST',
273
- signal,
274
- })
275
- const liquidityUrl = new URL(`${baseUrl}/chains/liquidity`)
276
- liquidityUrl.searchParams.set('chainId', destinationChainId)
277
- const liquidity = await requestJson(liquidityUrl, { fetcher, headers, signal }).catch(
278
- () => undefined,
279
- )
280
- return formatRelayResult(route, quote, liquidity, now())
281
- } catch (cause) {
282
- const unavailable = providerUnavailable(cause, [
283
- 'NO_QUOTES',
284
- 'NO_ROUTE',
285
- 'UNSUPPORTED_ROUTE',
286
- ])
287
- if (!unavailable) throw cause
288
- return unavailableResult({
289
- detail: 'relay:quote-v2',
290
- message: unavailable,
291
- now: now(),
292
- source: 'providerQuote',
293
- })
294
- }
295
- },
296
- }
297
- }
298
-
299
- export declare namespace relay {
300
- /** Relay quote adapter options. */
301
- type Options = {
302
- /** Probe wallet address required by Relay's quote API. */
303
- userAddress: string
304
- /** Optional recipient override; defaults to `userAddress`. */
305
- recipientAddress?: string | undefined
306
- /** Optional Relay API key for higher rate limits. */
307
- apiKey?: string | undefined
308
- /** Override for tests or Relay testnet environments. */
309
- baseUrl?: string | undefined
310
- /** Fetch implementation override for tests. */
311
- fetch?: Fetch | undefined
312
- /** Clock override for deterministic tests. */
313
- now?: (() => Date) | undefined
314
- }
315
- }
316
-
317
- /** Maps an Across HTTP error into the stable funding-provider error contract. */
318
- export function mapAcrossQuoteError(options: mapAcrossQuoteError.Options) {
319
- if (options.status === 429) return new FundingRoutes.ProviderRateLimitError()
320
- const cause = new ProviderResponseError(options.status, options.body)
321
- const unavailable = providerUnavailable(cause, [
322
- 'AMOUNT_TOO_HIGH',
323
- 'AMOUNT_TOO_LOW',
324
- 'INSUFFICIENT_LIQUIDITY',
325
- 'NO_QUOTES',
326
- 'NO_ROUTE',
327
- 'UNSUPPORTED_ROUTE',
328
- ])
329
- if (unavailable) return new FundingRoutes.ProviderQuoteUnavailableError()
330
- return cause
331
- }
332
-
333
- export declare namespace mapAcrossQuoteError {
334
- /** Across HTTP error response to classify. */
335
- type Options = {
336
- /** Parsed response body returned by Across. */
337
- body: unknown
338
- /** Across HTTP response status. */
339
- status: number
340
- }
341
- }
342
-
343
- /** Validates and normalizes an Across quote payload against its curated route. */
344
- export function parseAcrossQuote(
345
- options: parseAcrossQuote.Options,
346
- ): FundingRoutes.ProviderQuoteResult {
347
- const parsed = schema.AcrossQuote.safeParse(options.body)
348
- if (!parsed.success) throw new ProviderPayloadError()
349
- const response = parsed.data
350
- const total = response.fees.total
351
- const totalMax = response.fees.totalMax
352
- if (
353
- response.inputAmount !== options.route.sourceAmount.amount ||
354
- response.inputToken.address.toLowerCase() !== options.route.sourceToken.address.toLowerCase() ||
355
- response.inputToken.chainId !== Number(eip155ChainId(options.route.sourceChain)) ||
356
- response.inputToken.decimals !== options.route.sourceToken.decimals ||
357
- response.outputToken.address.toLowerCase() !==
358
- options.route.destinationToken.address.toLowerCase() ||
359
- response.outputToken.chainId !== Number(eip155ChainId(options.route.destinationChain)) ||
360
- response.outputToken.decimals !== options.route.destinationToken.decimals ||
361
- BigInt(response.expectedOutputAmount) <= 0n ||
362
- BigInt(response.minOutputAmount) <= 0n ||
363
- BigInt(response.minOutputAmount) > BigInt(response.expectedOutputAmount) ||
364
- response.quoteExpiryTimestamp * 1_000 <= options.now.getTime() ||
365
- !matchesFeeToken(total.token, options.route) ||
366
- !matchesFeeToken(totalMax.token, options.route) ||
367
- BigInt(totalMax.amount) < BigInt(total.amount) ||
368
- BigInt(totalMax.pct) < BigInt(total.pct)
369
- )
370
- throw new ProviderPayloadError()
371
- return {
372
- estimatedSeconds: response.expectedFillTime,
373
- expiresAt: isoFromUnixSeconds(String(response.quoteExpiryTimestamp)),
374
- fees: {
375
- total: formatAcrossFee(response.fees.total),
376
- totalMax: formatAcrossFee(response.fees.totalMax),
377
- },
378
- minimumOutputAmount: response.minOutputAmount,
379
- outputAmount: response.expectedOutputAmount,
380
- sampledAt: options.now.toISOString(),
381
- }
382
- }
383
-
384
- export declare namespace parseAcrossQuote {
385
- /** Inputs required to validate and normalize an Across quote payload. */
386
- type Options = {
387
- /** Untrusted JSON body returned by Across. */
388
- body: unknown
389
- /** Timestamp when Tempo sampled the response. */
390
- now: Date
391
- /** Curated route the response must match. */
392
- route: FundingRoutes.RouteCandidate
393
- }
394
- }
395
-
396
- function formatAcrossResult(
397
- route: FundingRoutes.RouteCandidate,
398
- body: unknown,
399
- now: Date,
400
- ): FundingRoutes.QuoteProbeResult {
401
- const response = record(body)
402
- const timestamp = integerString(response['timestamp'])
403
- const sampledAt = timestamp ? isoFromUnixSeconds(timestamp) : now.toISOString()
404
- const limits = acrossLimits(response['limits'])
405
- const amountStatus = statusFromLimits(route.sourceAmount.amount, limits)
406
- const status = response['isAmountTooLow'] === true ? 'unavailable' : amountStatus.status
407
- const tier = response['isAmountTooLow'] === true ? 'unavailable' : amountStatus.tier
408
- const outputAmount = decimalString(response['outputAmount'])
409
-
410
- return {
411
- expiresAt: timestamp ? isoFromUnixSeconds((BigInt(timestamp) + 600n).toString()) : null,
412
- limits,
413
- outputAmount,
414
- outputAmountFormatted: outputAmount
415
- ? decimalFromBaseUnits(outputAmount, route.destinationToken.decimals)
416
- : null,
417
- quality: {
418
- availableVolumeUsd: stablecoinUsdCapacity(route, limits?.maxSourceAmount),
419
- estimatedSeconds: nonnegativeInteger(response['estimatedFillTimeSec']),
420
- feeBps: scaledPctToBps(
421
- nestedString(response['totalRelayFee'], 'pct') ?? decimalString(response['relayFeePct']),
422
- ),
423
- historicalVolumeUsd24h: null,
424
- instantVolumeUsd: stablecoinUsdCapacity(route, limits?.instantSourceAmount),
425
- liquiditySource: 'providerLimit',
426
- score: scoreFor(tier),
427
- sourceDetail: 'across:suggested-fees',
428
- tier,
429
- },
430
- sampledAt,
431
- status,
432
- }
433
- }
434
-
435
- function formatAcrossFee(
436
- fee: z.output<typeof schema.AcrossQuote>['fees']['total'],
437
- ): FundingRoutes.ProviderQuoteFee {
438
- return {
439
- amount: fee.amount,
440
- amountUsd: fee.amountUsd,
441
- bps: scaledPctToBps(fee.pct),
442
- token: {
443
- address: fee.token.address,
444
- chainId: `eip155:${fee.token.chainId}`,
445
- decimals: fee.token.decimals,
446
- symbol: fee.token.symbol,
447
- },
448
- }
449
- }
450
-
451
- function formatRelayResult(
452
- route: FundingRoutes.RouteCandidate,
453
- quote: unknown,
454
- liquidity: unknown,
455
- now: Date,
456
- ): FundingRoutes.QuoteProbeResult {
457
- const details = record(record(quote)['details'])
458
- const currencyOut = record(details['currencyOut'])
459
- const outputAmount = decimalString(currencyOut['amount'])
460
- const liquidityRow = relayLiquidity(liquidity, route)
461
- const destinationLiquidity = decimalString(liquidityRow?.['balance'])
462
- const availableVolumeUsd = decimalString(liquidityRow?.['amountUsd']) ?? null
463
- const liquidityStatus = relayLiquidityStatus({ destinationLiquidity, outputAmount })
464
-
465
- return {
466
- limits: null,
467
- minimumOutputAmount: decimalString(currencyOut['minimumAmount']),
468
- outputAmount,
469
- outputAmountFormatted: outputAmount
470
- ? decimalFromBaseUnits(outputAmount, route.destinationToken.decimals)
471
- : null,
472
- quality: {
473
- availableVolumeUsd: availableVolumeUsd,
474
- estimatedSeconds: nonnegativeInteger(details['timeEstimate']),
475
- feeBps: null,
476
- historicalVolumeUsd24h: null,
477
- instantVolumeUsd: availableVolumeUsd,
478
- liquiditySource: 'providerQuote',
479
- score: scoreFor(liquidityStatus.tier),
480
- sourceDetail: 'relay:quote-v2',
481
- tier: liquidityStatus.tier,
482
- },
483
- sampledAt: now.toISOString(),
484
- status: liquidityStatus.status,
485
- }
486
- }
487
-
488
- function relayLiquidityStatus(options: {
489
- destinationLiquidity: string | undefined
490
- outputAmount: string | undefined
491
- }): { status: FundingRoutes.RouteStatus; tier: FundingRoutes.QualityTier } {
492
- if (!options.outputAmount) return { status: 'unknown', tier: 'unknown' }
493
- if (!options.destinationLiquidity) return { status: 'unknown', tier: 'unknown' }
494
- if (compareIntegerStrings(options.destinationLiquidity, options.outputAmount) < 0)
495
- return { status: 'thin', tier: 'thin' }
496
- return { status: 'available', tier: 'liquid' }
497
- }
498
-
499
- function unavailableResult(options: {
500
- detail: string
501
- message: string
502
- now: Date
503
- source: string
504
- }): FundingRoutes.QuoteProbeResult {
505
- return {
506
- quality: {
507
- availableVolumeUsd: null,
508
- estimatedSeconds: null,
509
- feeBps: null,
510
- historicalVolumeUsd24h: null,
511
- instantVolumeUsd: null,
512
- liquiditySource: options.source,
513
- score: 0,
514
- sourceDetail: `${options.detail}:${options.message}`,
515
- tier: 'unavailable',
516
- },
517
- sampledAt: options.now.toISOString(),
518
- status: 'unavailable',
519
- }
520
- }
521
-
522
- function acrossLimits(input: unknown): FundingRoutes.QuoteProbeLimits | null {
523
- const limits = record(input)
524
- const minSourceAmount = decimalString(limits['minDeposit'])
525
- const maxSourceAmount = decimalString(limits['maxDeposit'])
526
- const instantSourceAmount =
527
- decimalString(limits['maxDepositInstant']) ?? decimalString(limits['recommendedDepositInstant'])
528
- return minSourceAmount || maxSourceAmount || instantSourceAmount
529
- ? { instantSourceAmount, maxSourceAmount, minSourceAmount }
530
- : null
531
- }
532
-
533
- function statusFromLimits(
534
- amount: string,
535
- limits: FundingRoutes.QuoteProbeLimits | null,
536
- ): { status: FundingRoutes.RouteStatus; tier: FundingRoutes.QualityTier } {
537
- if (!limits) return { status: 'available', tier: 'liquid' }
538
- if (limits.minSourceAmount && compareIntegerStrings(amount, limits.minSourceAmount) < 0)
539
- return { status: 'unavailable', tier: 'unavailable' }
540
- if (limits.maxSourceAmount && compareIntegerStrings(amount, limits.maxSourceAmount) > 0)
541
- return { status: 'unavailable', tier: 'unavailable' }
542
- if (limits.instantSourceAmount && compareIntegerStrings(amount, limits.instantSourceAmount) > 0)
543
- return { status: 'thin', tier: 'thin' }
544
- return { status: 'available', tier: 'liquid' }
545
- }
546
-
547
- function relayLiquidity(input: unknown, route: FundingRoutes.RouteCandidate) {
548
- const response = record(input)
549
- const rows = Array.isArray(response['liquidity']) ? response['liquidity'] : []
550
- const destinationChainId = eip155ChainId(route.destinationChain)
551
- return rows.map(record).find((row) => {
552
- const rowChainId = integerString(row['chainId'])
553
- const rowAddress = typeof row['address'] === 'string' ? row['address'].toLowerCase() : undefined
554
- return (
555
- rowChainId === destinationChainId &&
556
- rowAddress === route.destinationToken.address.toLowerCase()
557
- )
558
- })
559
- }
560
-
561
- async function requestJson(
562
- url: URL,
563
- options: {
564
- body?: unknown
565
- fetcher: Fetch
566
- headers?: Record<string, string> | undefined
567
- method?: string | undefined
568
- signal: AbortSignal
569
- },
570
- ) {
571
- const headers = { accept: 'application/json', ...options.headers }
572
- const init: RequestInit = {
573
- headers:
574
- options.body === undefined ? headers : { ...headers, 'content-type': 'application/json' },
575
- method: options.method ?? 'GET',
576
- signal: options.signal,
577
- }
578
- if (options.body !== undefined) init.body = JSON.stringify(options.body)
579
- const response = await options.fetcher(url, init)
580
- const text = await response.text()
581
- const body = parseJson(text)
582
- if (!response.ok) throw new ProviderResponseError(response.status, body)
583
- return body
584
- }
585
-
586
- function relayHeaders(apiKey: string | undefined): Record<string, string> | undefined {
587
- return apiKey ? { 'x-api-key': apiKey } : undefined
588
- }
589
-
590
- function providerUnavailable(cause: unknown, codes: readonly string[]) {
591
- if (!(cause instanceof ProviderResponseError) || cause.status < 400 || cause.status >= 500)
592
- return undefined
593
- const code = providerErrorCode(cause.body)
594
- if (code && codes.includes(code)) return code
595
- return undefined
596
- }
597
-
598
- function providerErrorCode(input: unknown) {
599
- const body = record(input)
600
- const code = stringValue(body['code']) ?? stringValue(body['errorCode'])
601
- return code && /^[A-Za-z0-9_.:-]{1,64}$/.test(code) ? code : undefined
602
- }
603
-
604
- class ProviderResponseError extends Error {
605
- override name = 'FundingRouteProviders.ProviderResponseError'
606
-
607
- constructor(
608
- readonly status: number,
609
- readonly body: unknown,
610
- ) {
611
- super(`Provider request failed with status ${status}`)
612
- }
613
- }
614
-
615
- class ProviderPayloadError extends Error {
616
- override name = 'FundingRouteProviders.ProviderPayloadError'
617
- }
618
-
619
- class ProviderConfigurationError extends Error {
620
- override name = 'FundingRouteProviders.ProviderConfigurationError'
621
- }
622
-
623
- function providerChainId(
624
- routeChain: FundingRoutes.RouteCandidate['sourceChain'],
625
- provider: string,
626
- ) {
627
- const providerIds = routeChain.providerIds as Record<string, string> | undefined
628
- const id = providerIds?.[provider]
629
- if (id && /^\d+$/.test(id)) return id
630
- return eip155ChainId(routeChain)
631
- }
632
-
633
- function eip155ChainId(chain: { id: string; kind: string }) {
634
- if (chain.kind !== 'evm') throw new UnsupportedProviderRouteError()
635
- const match = /^eip155:(\d+)$/.exec(chain.id)
636
- if (!match?.[1]) throw new UnsupportedProviderRouteError()
637
- return match[1]
638
- }
639
-
640
- class UnsupportedProviderRouteError extends Error {
641
- override name = 'FundingRouteProviders.UnsupportedProviderRouteError'
642
- }
643
-
644
- function assertAcrossIntegratorId(integratorId: string) {
645
- if (!/^0x[\da-fA-F]{4}$/.test(integratorId)) throw new ProviderConfigurationError()
646
- }
647
-
648
- function matchesFeeToken(
649
- token: z.output<typeof schema.AcrossQuote>['fees']['total']['token'],
650
- route: FundingRoutes.RouteCandidate,
651
- ) {
652
- return (
653
- token.address.toLowerCase() === route.sourceToken.address.toLowerCase() &&
654
- token.chainId === Number(eip155ChainId(route.sourceChain)) &&
655
- token.decimals === route.sourceToken.decimals
656
- )
657
- }
658
-
659
- function normalizeBaseUrl(input: string) {
660
- return input.replace(/\/+$/, '')
661
- }
662
-
663
- function parseJson(input: string) {
664
- if (!input) return null
665
- try {
666
- return JSON.parse(input) as unknown
667
- } catch {
668
- return input
669
- }
670
- }
671
-
672
- function record(input: unknown): Record<string, unknown> {
673
- return input && typeof input === 'object' && !Array.isArray(input)
674
- ? (input as Record<string, unknown>)
675
- : {}
676
- }
677
-
678
- function stringValue(input: unknown) {
679
- return typeof input === 'string' && input.length > 0 ? input : undefined
680
- }
681
-
682
- function decimalString(input: unknown) {
683
- if (typeof input === 'string' && /^\d+(\.\d+)?$/.test(input)) return input
684
- if (typeof input === 'number' && Number.isFinite(input) && input >= 0) return String(input)
685
- return undefined
686
- }
687
-
688
- function integerString(input: unknown) {
689
- if (typeof input === 'string' && /^\d+$/.test(input)) return input
690
- if (typeof input === 'number' && Number.isSafeInteger(input) && input >= 0) return String(input)
691
- return undefined
692
- }
693
-
694
- function nestedString(input: unknown, key: string) {
695
- return decimalString(record(input)[key])
696
- }
697
-
698
- function nonnegativeInteger(input: unknown) {
699
- if (typeof input === 'number' && Number.isSafeInteger(input) && input >= 0) return input
700
- if (typeof input === 'string' && /^\d+$/.test(input)) return Number(input)
701
- return null
702
- }
703
-
704
- function decimalFromBaseUnits(baseUnits: string, decimals: number) {
705
- const padded = baseUnits.padStart(decimals + 1, '0')
706
- const whole = decimals === 0 ? padded : padded.slice(0, -decimals)
707
- const fraction = decimals === 0 ? '' : padded.slice(-decimals).replace(/0+$/, '')
708
- return fraction ? `${whole}.${fraction}` : whole
709
- }
710
-
711
- function compareIntegerStrings(left: string, right: string) {
712
- return BigInt(left) === BigInt(right) ? 0 : BigInt(left) > BigInt(right) ? 1 : -1
713
- }
714
-
715
- function isoFromUnixSeconds(input: string) {
716
- return new Date(Number(input) * 1_000).toISOString()
717
- }
718
-
719
- function stablecoinUsdCapacity(
720
- route: FundingRoutes.RouteCandidate,
721
- baseUnits: string | null | undefined,
722
- ) {
723
- if (!baseUnits || !isDollarStablecoin(route.sourceToken.symbol)) return null
724
- return decimalFromBaseUnits(baseUnits, route.sourceToken.decimals)
725
- }
726
-
727
- function isDollarStablecoin(symbol: string) {
728
- return ['DAI', 'FRXUSD', 'PYUSD', 'USDC', 'USDC.E', 'USDT', 'USDT0'].includes(
729
- symbol.toUpperCase(),
730
- )
731
- }
732
-
733
- function scaledPctToBps(input: string | undefined) {
734
- if (!input || !/^\d+$/.test(input)) return null
735
- const denominator = 1_000_000_000_000_000_000n
736
- return Number((BigInt(input) * 10_000n + denominator / 2n) / denominator)
737
- }
738
-
739
- function scoreFor(tier: FundingRoutes.QualityTier) {
740
- if (tier === 'liquid') return 90
741
- if (tier === 'thin') return 65
742
- if (tier === 'unavailable') return 0
743
- return null
744
- }