tapimo 0.5.0 → 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.
- package/dist/App.d.ts +4879 -4870
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +20 -2
- package/dist/App.js.map +1 -1
- package/dist/Assets.js +3 -3
- package/dist/Assets.js.map +1 -1
- package/dist/Scope.d.ts.map +1 -1
- package/dist/Scope.js +1 -2
- package/dist/Scope.js.map +1 -1
- package/dist/Webhooks.d.ts +1 -1
- package/dist/Webhooks.d.ts.map +1 -1
- package/dist/Webhooks.js +5 -10
- package/dist/Webhooks.js.map +1 -1
- package/dist/admin/App.d.ts +6 -34
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +3 -3
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.js +4 -1
- package/dist/admin/apps/api-keys.js.map +1 -1
- package/dist/admin/apps/scopes.d.ts +6 -34
- package/dist/admin/apps/scopes.d.ts.map +1 -1
- package/dist/admin/apps/scopes.js +2 -2
- package/dist/admin/apps/scopes.js.map +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +116 -116
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.js +33 -33
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/apps/data/App.d.ts +3199 -3206
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +25 -38
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/FundingQuoteProviders.d.ts +76 -3
- package/dist/apps/data/FundingQuoteProviders.d.ts.map +1 -1
- package/dist/apps/data/FundingQuoteProviders.js +276 -2
- package/dist/apps/data/FundingQuoteProviders.js.map +1 -1
- package/dist/apps/data/routes/activities.d.ts +245 -245
- package/dist/apps/data/routes/addresses.d.ts +206 -206
- package/dist/apps/data/routes/balances.d.ts +46 -46
- package/dist/apps/data/routes/blocks.d.ts +123 -123
- package/dist/apps/data/routes/coingecko.d.ts +169 -169
- package/dist/apps/data/routes/funding.d.ts +230 -146
- package/dist/apps/data/routes/funding.d.ts.map +1 -1
- package/dist/apps/data/routes/funding.js +23 -12
- package/dist/apps/data/routes/funding.js.map +1 -1
- package/dist/apps/data/routes/indexer.d.ts +34 -34
- package/dist/apps/data/routes/indexer.d.ts.map +1 -1
- package/dist/apps/data/routes/indexer.js +9 -2
- package/dist/apps/data/routes/indexer.js.map +1 -1
- package/dist/apps/data/routes/receipts.d.ts +228 -228
- package/dist/apps/data/routes/tokenlist.d.ts +36 -36
- package/dist/apps/data/routes/transactions.d.ts +281 -281
- package/dist/apps/data/routes/transfers.d.ts +58 -58
- package/dist/apps/data/routes/verified-tokens.d.ts +86 -86
- package/dist/apps/management/App.d.ts +1042 -1042
- package/dist/apps/management/routes/api-keys.d.ts +120 -120
- package/dist/apps/management/routes/billing.d.ts +211 -211
- package/dist/apps/management/routes/invitations.d.ts +132 -132
- package/dist/apps/management/routes/invite-links.d.ts +62 -62
- package/dist/apps/management/routes/me.d.ts +17 -17
- package/dist/apps/management/routes/members.d.ts +80 -80
- package/dist/apps/management/routes/orgs.d.ts +96 -96
- package/dist/apps/management/routes/projects.d.ts +61 -61
- package/dist/apps/management/routes/scopes.d.ts +11 -11
- package/dist/apps/management/routes/usage.d.ts +79 -79
- package/dist/apps/mcp.js +2 -2
- package/dist/apps/mcp.js.map +1 -1
- package/dist/apps/relay/App.d.ts +2 -2
- package/dist/apps/relay/App.js +2 -2
- package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
- package/dist/db/tables/sponsoredTransactions.js +6 -2
- package/dist/db/tables/sponsoredTransactions.js.map +1 -1
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +8 -0
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/FundingQuotes.d.ts +170 -56
- package/dist/internal/FundingQuotes.d.ts.map +1 -1
- package/dist/internal/FundingQuotes.js +91 -5
- package/dist/internal/FundingQuotes.js.map +1 -1
- package/dist/internal/Log.d.ts +18 -12
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js +2 -0
- package/dist/internal/Log.js.map +1 -1
- package/dist/internal/Tidx.d.ts +9 -3
- package/dist/internal/Tidx.d.ts.map +1 -1
- package/dist/internal/Tidx.js +13 -7
- package/dist/internal/Tidx.js.map +1 -1
- package/dist/internal/Viem.d.ts +14 -1
- package/dist/internal/Viem.d.ts.map +1 -1
- package/dist/internal/Viem.js +19 -1
- package/dist/internal/Viem.js.map +1 -1
- package/package.json +2 -2
- package/src/ApiKeys.test.ts +2 -0
- package/src/App.test.ts +13 -2
- package/src/App.ts +31 -5
- package/src/Assets.ts +3 -3
- package/src/Client.test-d.ts +3 -2
- package/src/Scope.ts +4 -2
- package/src/Webhooks.ts +5 -10
- package/src/admin/apps/api-keys.test.ts +27 -0
- package/src/admin/apps/api-keys.ts +8 -1
- package/src/admin/apps/scopes.test.ts +7 -2
- package/src/admin/apps/scopes.ts +2 -2
- package/src/admin/ui/src/components/Dialog.tsx +8 -2
- package/src/admin/ui/src/lib/scopes.test.ts +19 -0
- package/src/admin/ui/src/lib/scopes.ts +12 -0
- package/src/admin/ui/src/routes/api-keys.tsx +128 -27
- package/src/admin/ui.generated.ts +33 -33
- package/src/apps/composition.test.ts +77 -0
- package/src/apps/data/App.ts +25 -45
- package/src/apps/data/FundingQuoteProviders.test.ts +431 -5
- package/src/apps/data/FundingQuoteProviders.ts +386 -4
- package/src/apps/data/routes/funding.test.ts +174 -7
- package/src/apps/data/routes/funding.ts +27 -12
- package/src/apps/data/routes/indexer.test.ts +82 -20
- package/src/apps/data/routes/indexer.ts +8 -2
- package/src/apps/management/Billing.test.ts +2 -2
- package/src/apps/mcp.test.ts +18 -0
- package/src/apps/mcp.ts +2 -2
- package/src/apps/relay/App.test.ts +6 -5
- package/src/apps/relay/App.ts +2 -2
- package/src/apps/relay/Sponsorships.test.ts +2 -2
- package/src/db/Db.test.ts +5 -5
- package/src/db/tables/billingSettings.test.ts +2 -2
- package/src/db/tables/organizations.test.ts +2 -2
- package/src/db/tables/sponsoredTransactions.test.ts +19 -2
- package/src/db/tables/sponsoredTransactions.ts +21 -14
- package/src/db/tables/stripeCustomers.test.ts +2 -2
- package/src/internal/Auth.ts +10 -0
- package/src/internal/FundingQuotes.test.ts +97 -3
- package/src/internal/FundingQuotes.ts +104 -6
- package/src/internal/Log.ts +21 -14
- package/src/internal/Tidx.test.ts +48 -0
- package/src/internal/Tidx.ts +21 -7
- package/src/internal/Viem.test.ts +46 -0
- package/src/internal/Viem.ts +34 -1
|
@@ -1,23 +1,166 @@
|
|
|
1
|
+
import * as z from 'zod/mini'
|
|
2
|
+
|
|
1
3
|
import * as FundingQuotes from '../../internal/FundingQuotes.js'
|
|
2
4
|
|
|
3
5
|
type Fetch = typeof globalThis.fetch
|
|
4
6
|
|
|
5
7
|
const defaultFetch: Fetch = (input, init) => globalThis.fetch(input, init)
|
|
6
8
|
|
|
9
|
+
const defaultAcrossBaseUrl = 'https://app.across.to/api'
|
|
7
10
|
const defaultRelayBaseUrl = 'https://api.relay.link'
|
|
11
|
+
const defaultRhinoBaseUrl = 'https://api.rhino.fi/bridge'
|
|
12
|
+
const providerOperations = {
|
|
13
|
+
across: 'swap/approval',
|
|
14
|
+
relay: 'quote/v2',
|
|
15
|
+
rhino: 'quote/bridge-swap/public',
|
|
16
|
+
} as const
|
|
17
|
+
|
|
18
|
+
namespace schema {
|
|
19
|
+
const IntegerString = z.string().check(z.regex(/^\d+$/))
|
|
20
|
+
|
|
21
|
+
const AcrossToken = z.object({
|
|
22
|
+
address: z.string(),
|
|
23
|
+
chainId: z.number().check(z.int(), z.nonnegative()),
|
|
24
|
+
decimals: z.number().check(z.int(), z.nonnegative()),
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
export const AcrossQuote = z.object({
|
|
28
|
+
amountType: z.literal('exactInput'),
|
|
29
|
+
expectedFillTime: z.number().check(z.int(), z.nonnegative()),
|
|
30
|
+
expectedOutputAmount: IntegerString,
|
|
31
|
+
inputAmount: IntegerString,
|
|
32
|
+
inputToken: AcrossToken,
|
|
33
|
+
minOutputAmount: IntegerString,
|
|
34
|
+
outputToken: AcrossToken,
|
|
35
|
+
quoteExpiryTimestamp: z.number().check(z.int(), z.nonnegative()),
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
export const RhinoQuote = z.object({
|
|
39
|
+
_tag: z.literal('bridge'),
|
|
40
|
+
chainIn: z.string(),
|
|
41
|
+
chainOut: z.string(),
|
|
42
|
+
estimatedDuration: z.optional(z.nullable(z.number().check(z.nonnegative()))),
|
|
43
|
+
payAmount: z.string().check(z.regex(/^\d+(\.\d+)?$/)),
|
|
44
|
+
receiveAmount: z.string().check(z.regex(/^\d+(\.\d+)?$/)),
|
|
45
|
+
token: z.string(),
|
|
46
|
+
})
|
|
47
|
+
}
|
|
8
48
|
|
|
9
49
|
/** Creates the live funding quote adapters configured for this deployment. */
|
|
10
50
|
export function createAdapters(options: createAdapters.Options = {}) {
|
|
11
|
-
return
|
|
12
|
-
options.
|
|
13
|
-
|
|
51
|
+
return {
|
|
52
|
+
...(options.across ? { across: across(options.across) } : {}),
|
|
53
|
+
...(options.relay ? { relay: relay(options.relay) } : {}),
|
|
54
|
+
...(options.rhino ? { rhino: rhino(options.rhino) } : {}),
|
|
55
|
+
} satisfies FundingQuotes.ProviderAdapters
|
|
14
56
|
}
|
|
15
57
|
|
|
16
58
|
export declare namespace createAdapters {
|
|
17
59
|
/** Provider adapter factory options. Omit a provider to leave it unconfigured. */
|
|
18
60
|
type Options = {
|
|
61
|
+
/** Across quote adapter options. */
|
|
62
|
+
across?: across.Options | false | undefined
|
|
19
63
|
/** Relay quote adapter options. */
|
|
20
64
|
relay?: relay.Options | false | undefined
|
|
65
|
+
/** Rhino quote adapter options. */
|
|
66
|
+
rhino?: rhino.Options | false | undefined
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Resolves all-or-none Across deployment configuration. */
|
|
71
|
+
export function configureAcross(options: configureAcross.Options = {}): across.Options | undefined {
|
|
72
|
+
const configured = [options.apiKey, options.integratorId, options.userAddress].filter(Boolean)
|
|
73
|
+
if (configured.length === 0) return undefined
|
|
74
|
+
if (!options.apiKey || !options.integratorId || !options.userAddress)
|
|
75
|
+
throw new ProviderConfigurationError()
|
|
76
|
+
return {
|
|
77
|
+
apiKey: options.apiKey,
|
|
78
|
+
integratorId: options.integratorId,
|
|
79
|
+
userAddress: options.userAddress,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export declare namespace configureAcross {
|
|
84
|
+
/** Optional deployment bindings required together to enable Across. */
|
|
85
|
+
type Options = {
|
|
86
|
+
/** Across API key binding. */
|
|
87
|
+
apiKey?: string | undefined
|
|
88
|
+
/** Across integrator id binding. */
|
|
89
|
+
integratorId?: string | undefined
|
|
90
|
+
/** Across quote-user binding. */
|
|
91
|
+
userAddress?: string | undefined
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Creates an Across quote adapter backed by `/swap/approval`. */
|
|
96
|
+
export function across(options: across.Options): FundingQuotes.ProviderAdapter {
|
|
97
|
+
assertAcrossConfiguration(options)
|
|
98
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl ?? defaultAcrossBaseUrl)
|
|
99
|
+
const fetcher = options.fetch ?? defaultFetch
|
|
100
|
+
const now = options.now ?? (() => new Date())
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
async probeQuote({ candidate }, signal) {
|
|
104
|
+
const destinationChainId = eip155ChainId(candidate.destinationChain)
|
|
105
|
+
const originChainId = providerChainId(candidate.sourceChain, 'across')
|
|
106
|
+
const url = new URL(`${baseUrl}/swap/approval`)
|
|
107
|
+
url.search = new URLSearchParams({
|
|
108
|
+
amount: candidate.sourceAmount.amount,
|
|
109
|
+
depositor: options.userAddress,
|
|
110
|
+
destinationChainId,
|
|
111
|
+
inputToken: candidate.sourceToken.address,
|
|
112
|
+
integratorId: options.integratorId,
|
|
113
|
+
originChainId,
|
|
114
|
+
outputToken: candidate.destinationToken.address,
|
|
115
|
+
recipient: options.userAddress,
|
|
116
|
+
skipOriginTxEstimation: 'true',
|
|
117
|
+
strictTradeType: 'true',
|
|
118
|
+
tradeType: 'exactInput',
|
|
119
|
+
}).toString()
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const quote = await requestJson(url, {
|
|
123
|
+
fetcher,
|
|
124
|
+
headers: { Authorization: `Bearer ${options.apiKey}` },
|
|
125
|
+
signal,
|
|
126
|
+
})
|
|
127
|
+
return formatAcrossResult(candidate, quote, now())
|
|
128
|
+
} catch (cause) {
|
|
129
|
+
const unavailable = providerUnavailable(cause, [
|
|
130
|
+
'AMOUNT_TOO_HIGH',
|
|
131
|
+
'AMOUNT_TOO_LOW',
|
|
132
|
+
'INSUFFICIENT_LIQUIDITY',
|
|
133
|
+
'NO_QUOTES',
|
|
134
|
+
'NO_ROUTE',
|
|
135
|
+
'UNSUPPORTED_ROUTE',
|
|
136
|
+
])
|
|
137
|
+
if (!unavailable) throw cause
|
|
138
|
+
return unavailableResult({
|
|
139
|
+
detail: 'across:swap-approval',
|
|
140
|
+
message: unavailable,
|
|
141
|
+
now: now(),
|
|
142
|
+
source: 'providerQuote',
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export declare namespace across {
|
|
150
|
+
/** Across quote adapter options. */
|
|
151
|
+
type Options = {
|
|
152
|
+
/** Across API key sent as a bearer token. */
|
|
153
|
+
apiKey: string
|
|
154
|
+
/** Override for tests or Across test environments. */
|
|
155
|
+
baseUrl?: string | undefined
|
|
156
|
+
/** Fetch implementation override for tests. */
|
|
157
|
+
fetch?: Fetch | undefined
|
|
158
|
+
/** Across integrator id sent as the required query parameter. */
|
|
159
|
+
integratorId: string
|
|
160
|
+
/** Clock override for deterministic tests. */
|
|
161
|
+
now?: (() => Date) | undefined
|
|
162
|
+
/** Probe wallet address used as the Across depositor and recipient. */
|
|
163
|
+
userAddress: string
|
|
21
164
|
}
|
|
22
165
|
}
|
|
23
166
|
|
|
@@ -87,6 +230,144 @@ export declare namespace relay {
|
|
|
87
230
|
}
|
|
88
231
|
}
|
|
89
232
|
|
|
233
|
+
/** Creates a Rhino quote adapter backed by the public bridge-and-swap quote endpoint. */
|
|
234
|
+
export function rhino(options: rhino.Options = {}): FundingQuotes.ProviderAdapter {
|
|
235
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl ?? defaultRhinoBaseUrl)
|
|
236
|
+
const fetcher = options.fetch ?? defaultFetch
|
|
237
|
+
const now = options.now ?? (() => new Date())
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
async probeQuote({ cache, candidate }, signal) {
|
|
241
|
+
const chainIn = providerChainReference(candidate.sourceChain, 'rhino')
|
|
242
|
+
const tokenOut = rhinoToken(candidate.destinationToken)
|
|
243
|
+
const url = new URL(`${baseUrl}/quote/bridge-swap/public`)
|
|
244
|
+
url.search = new URLSearchParams({
|
|
245
|
+
amount: candidate.sourceAmount.formatted,
|
|
246
|
+
amountNative: '0',
|
|
247
|
+
chainIn,
|
|
248
|
+
chainOut: 'TEMPO',
|
|
249
|
+
mode: 'pay',
|
|
250
|
+
tokenIn: candidate.sourceToken.symbol,
|
|
251
|
+
tokenOut,
|
|
252
|
+
}).toString()
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
const quote = await requestJson(url, {
|
|
256
|
+
fetcher,
|
|
257
|
+
headers: cache.bypass ? { 'cache-control': 'no-cache' } : undefined,
|
|
258
|
+
signal,
|
|
259
|
+
})
|
|
260
|
+
return formatRhinoResult(candidate, quote, { chainIn, now: now(), tokenOut })
|
|
261
|
+
} catch (cause) {
|
|
262
|
+
const unavailable = providerUnavailable(cause, ['NoRouteFoundError'])
|
|
263
|
+
if (!unavailable) throw cause
|
|
264
|
+
return unavailableResult({
|
|
265
|
+
detail: 'rhino:bridge-swap-public',
|
|
266
|
+
message: unavailable,
|
|
267
|
+
now: now(),
|
|
268
|
+
source: 'providerQuote',
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export declare namespace rhino {
|
|
276
|
+
/** Rhino public quote adapter options. */
|
|
277
|
+
type Options = {
|
|
278
|
+
/** Override for tests or Rhino test environments. */
|
|
279
|
+
baseUrl?: string | undefined
|
|
280
|
+
/** Fetch implementation override for tests. */
|
|
281
|
+
fetch?: Fetch | undefined
|
|
282
|
+
/** Clock override for deterministic tests. */
|
|
283
|
+
now?: (() => Date) | undefined
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Returns bounded provider failure details suitable for structured request logs. */
|
|
288
|
+
export function providerFailure(options: providerFailure.Options): providerFailure.Result {
|
|
289
|
+
const operation = providerOperations[options.provider]
|
|
290
|
+
if (
|
|
291
|
+
options.cause instanceof FundingQuotes.ProviderAdapterTimeoutError ||
|
|
292
|
+
(options.cause instanceof DOMException && options.cause.name === 'AbortError')
|
|
293
|
+
)
|
|
294
|
+
return { failure: 'timeout', id: options.provider, operation }
|
|
295
|
+
if (options.cause instanceof ProviderResponseError) {
|
|
296
|
+
const code = providerErrorCode(options.cause.body)
|
|
297
|
+
return {
|
|
298
|
+
...(code === undefined ? {} : { code }),
|
|
299
|
+
failure: 'http',
|
|
300
|
+
id: options.provider,
|
|
301
|
+
operation,
|
|
302
|
+
status: options.cause.status,
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (options.cause instanceof ProviderPayloadError)
|
|
306
|
+
return { failure: 'payload', id: options.provider, operation }
|
|
307
|
+
if (options.cause instanceof TypeError)
|
|
308
|
+
return { failure: 'network', id: options.provider, operation }
|
|
309
|
+
return { failure: 'unknown', id: options.provider, operation }
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export declare namespace providerFailure {
|
|
313
|
+
/** Inputs used to classify a provider failure. */
|
|
314
|
+
type Options = FundingQuotes.ProviderFailure
|
|
315
|
+
|
|
316
|
+
/** Bounded provider failure fields. */
|
|
317
|
+
type Result = {
|
|
318
|
+
/** Stable provider error code, when one is safe to retain. */
|
|
319
|
+
code?: string | undefined
|
|
320
|
+
/** Failure boundary that rejected the provider request. */
|
|
321
|
+
failure: 'http' | 'network' | 'payload' | 'timeout' | 'unknown'
|
|
322
|
+
/** Provider identifier. */
|
|
323
|
+
id: FundingQuotes.ProviderId
|
|
324
|
+
/** Provider operation identifier. */
|
|
325
|
+
operation: (typeof providerOperations)[FundingQuotes.ProviderId]
|
|
326
|
+
/** Upstream HTTP status, when a response was received. */
|
|
327
|
+
status?: number | undefined
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function formatAcrossResult(
|
|
332
|
+
candidate: FundingQuotes.QuoteCandidate,
|
|
333
|
+
quote: unknown,
|
|
334
|
+
now: Date,
|
|
335
|
+
): FundingQuotes.QuoteProbeResult {
|
|
336
|
+
const parsed = schema.AcrossQuote.safeParse(quote)
|
|
337
|
+
if (!parsed.success) throw new ProviderPayloadError()
|
|
338
|
+
const response = parsed.data
|
|
339
|
+
const expiresAt = new Date(response.quoteExpiryTimestamp * 1_000)
|
|
340
|
+
if (
|
|
341
|
+
response.inputAmount !== candidate.sourceAmount.amount ||
|
|
342
|
+
!matchesToken(response.inputToken, candidate.sourceChain, candidate.sourceToken) ||
|
|
343
|
+
!matchesToken(response.outputToken, candidate.destinationChain, candidate.destinationToken) ||
|
|
344
|
+
BigInt(response.expectedOutputAmount) <= 0n ||
|
|
345
|
+
BigInt(response.minOutputAmount) <= 0n ||
|
|
346
|
+
BigInt(response.minOutputAmount) > BigInt(response.expectedOutputAmount) ||
|
|
347
|
+
Number.isNaN(expiresAt.getTime()) ||
|
|
348
|
+
response.quoteExpiryTimestamp * 1_000 <= now.getTime()
|
|
349
|
+
)
|
|
350
|
+
throw new ProviderPayloadError()
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
expiresAt: expiresAt.toISOString(),
|
|
354
|
+
minimumOutputAmount: response.minOutputAmount,
|
|
355
|
+
outputAmount: response.expectedOutputAmount,
|
|
356
|
+
outputAmountFormatted: decimalFromBaseUnits(
|
|
357
|
+
response.expectedOutputAmount,
|
|
358
|
+
candidate.destinationToken.decimals,
|
|
359
|
+
),
|
|
360
|
+
quality: {
|
|
361
|
+
estimatedSeconds: response.expectedFillTime,
|
|
362
|
+
liquiditySource: 'providerQuote',
|
|
363
|
+
sourceDetail: 'across:swap-approval',
|
|
364
|
+
tier: 'liquid',
|
|
365
|
+
},
|
|
366
|
+
sampledAt: now.toISOString(),
|
|
367
|
+
status: 'available',
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
90
371
|
function formatRelayResult(
|
|
91
372
|
candidate: FundingQuotes.QuoteCandidate,
|
|
92
373
|
quote: unknown,
|
|
@@ -116,6 +397,45 @@ function formatRelayResult(
|
|
|
116
397
|
}
|
|
117
398
|
}
|
|
118
399
|
|
|
400
|
+
function formatRhinoResult(
|
|
401
|
+
candidate: FundingQuotes.QuoteCandidate,
|
|
402
|
+
quote: unknown,
|
|
403
|
+
options: { chainIn: string; now: Date; tokenOut: string },
|
|
404
|
+
): FundingQuotes.QuoteProbeResult {
|
|
405
|
+
const parsed = schema.RhinoQuote.safeParse(quote)
|
|
406
|
+
if (!parsed.success) throw new ProviderPayloadError()
|
|
407
|
+
const response = parsed.data
|
|
408
|
+
const inputAmount = baseUnitsFromDecimal(response.payAmount, candidate.sourceToken.decimals)
|
|
409
|
+
const outputAmount = baseUnitsFromDecimal(
|
|
410
|
+
response.receiveAmount,
|
|
411
|
+
candidate.destinationToken.decimals,
|
|
412
|
+
)
|
|
413
|
+
if (
|
|
414
|
+
response.chainIn !== options.chainIn ||
|
|
415
|
+
response.chainOut !== 'TEMPO' ||
|
|
416
|
+
response.token !== options.tokenOut ||
|
|
417
|
+
inputAmount !== candidate.sourceAmount.amount ||
|
|
418
|
+
outputAmount === undefined ||
|
|
419
|
+
BigInt(outputAmount) <= 0n
|
|
420
|
+
)
|
|
421
|
+
throw new ProviderPayloadError()
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
outputAmount,
|
|
425
|
+
outputAmountFormatted: decimalFromBaseUnits(outputAmount, candidate.destinationToken.decimals),
|
|
426
|
+
quality: {
|
|
427
|
+
...(response.estimatedDuration === undefined || response.estimatedDuration === null
|
|
428
|
+
? {}
|
|
429
|
+
: { estimatedSeconds: Math.ceil(response.estimatedDuration / 1_000) }),
|
|
430
|
+
liquiditySource: 'providerQuote',
|
|
431
|
+
sourceDetail: 'rhino:bridge-swap-public',
|
|
432
|
+
tier: 'liquid',
|
|
433
|
+
},
|
|
434
|
+
sampledAt: options.now.toISOString(),
|
|
435
|
+
status: 'available',
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
119
439
|
function unavailableResult(options: {
|
|
120
440
|
detail: string
|
|
121
441
|
message: string
|
|
@@ -172,7 +492,8 @@ function providerUnavailable(cause: unknown, codes: readonly string[]) {
|
|
|
172
492
|
|
|
173
493
|
function providerErrorCode(input: unknown) {
|
|
174
494
|
const body = record(input)
|
|
175
|
-
const code =
|
|
495
|
+
const code =
|
|
496
|
+
stringValue(body['_tag']) ?? stringValue(body['code']) ?? stringValue(body['errorCode'])
|
|
176
497
|
return code && /^[A-Za-z0-9_.:-]{1,64}$/.test(code) ? code : undefined
|
|
177
498
|
}
|
|
178
499
|
|
|
@@ -186,6 +507,38 @@ function providerChainId(
|
|
|
186
507
|
return eip155ChainId(quoteChain)
|
|
187
508
|
}
|
|
188
509
|
|
|
510
|
+
function providerChainReference(
|
|
511
|
+
quoteChain: FundingQuotes.QuoteCandidate['sourceChain'],
|
|
512
|
+
provider: string,
|
|
513
|
+
) {
|
|
514
|
+
const providerIds = quoteChain.providerIds as Record<string, string> | undefined
|
|
515
|
+
const id = providerIds?.[provider]
|
|
516
|
+
if (!id) throw new UnsupportedProviderChainError()
|
|
517
|
+
return id
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function rhinoToken(token: FundingQuotes.TokenRef) {
|
|
521
|
+
const tokens: Record<string, string> = {
|
|
522
|
+
'0x20c00000000000000000000014f22ca97301eb73': 'USDT',
|
|
523
|
+
'0x20c000000000000000000000b9537d11c60e8b50': 'USDC',
|
|
524
|
+
}
|
|
525
|
+
const id = tokens[token.address.toLowerCase()]
|
|
526
|
+
if (!id) throw new UnsupportedProviderTokenError()
|
|
527
|
+
return id
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function matchesToken(
|
|
531
|
+
token: z.output<typeof schema.AcrossQuote>['inputToken'],
|
|
532
|
+
chain: FundingQuotes.ChainRef,
|
|
533
|
+
expected: FundingQuotes.TokenRef,
|
|
534
|
+
) {
|
|
535
|
+
return (
|
|
536
|
+
token.address.toLowerCase() === expected.address.toLowerCase() &&
|
|
537
|
+
token.chainId === Number(eip155ChainId(chain)) &&
|
|
538
|
+
token.decimals === expected.decimals
|
|
539
|
+
)
|
|
540
|
+
}
|
|
541
|
+
|
|
189
542
|
function eip155ChainId(chain: { id: string; kind: string }) {
|
|
190
543
|
if (chain.kind !== 'evm') throw new UnsupportedProviderChainError()
|
|
191
544
|
const match = /^eip155:(\d+)$/.exec(chain.id)
|
|
@@ -235,6 +588,31 @@ function decimalFromBaseUnits(baseUnits: string, decimals: number) {
|
|
|
235
588
|
return fraction ? `${whole}.${fraction}` : whole
|
|
236
589
|
}
|
|
237
590
|
|
|
591
|
+
function baseUnitsFromDecimal(value: string, decimals: number) {
|
|
592
|
+
if (!/^\d+(\.\d+)?$/.test(value)) return undefined
|
|
593
|
+
const [whole, fraction = ''] = value.split('.')
|
|
594
|
+
const significantFraction = fraction.replace(/0+$/, '')
|
|
595
|
+
if (significantFraction.length > decimals) return undefined
|
|
596
|
+
return BigInt(`${whole}${significantFraction.padEnd(decimals, '0')}`).toString()
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function assertAcrossConfiguration(options: across.Options) {
|
|
600
|
+
if (
|
|
601
|
+
options.apiKey.length === 0 ||
|
|
602
|
+
!/^0x[\da-fA-F]{4}$/.test(options.integratorId) ||
|
|
603
|
+
!/^0x[\da-fA-F]{40}$/.test(options.userAddress)
|
|
604
|
+
)
|
|
605
|
+
throw new ProviderConfigurationError()
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
class ProviderConfigurationError extends Error {
|
|
609
|
+
override name = 'FundingQuoteProviders.ProviderConfigurationError'
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
class ProviderPayloadError extends Error {
|
|
613
|
+
override name = 'FundingQuoteProviders.ProviderPayloadError'
|
|
614
|
+
}
|
|
615
|
+
|
|
238
616
|
class ProviderResponseError extends Error {
|
|
239
617
|
override name = 'FundingQuoteProviders.ProviderResponseError'
|
|
240
618
|
|
|
@@ -249,3 +627,7 @@ class ProviderResponseError extends Error {
|
|
|
249
627
|
class UnsupportedProviderChainError extends Error {
|
|
250
628
|
override name = 'FundingQuoteProviders.UnsupportedProviderChainError'
|
|
251
629
|
}
|
|
630
|
+
|
|
631
|
+
class UnsupportedProviderTokenError extends Error {
|
|
632
|
+
override name = 'FundingQuoteProviders.UnsupportedProviderTokenError'
|
|
633
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Schema } from 'tapimo'
|
|
2
2
|
|
|
3
3
|
import type * as FundingQuotes from '../../../internal/FundingQuotes.js'
|
|
4
|
+
import type * as Log from '../../../internal/Log.js'
|
|
4
5
|
import * as TestApp from '../../../../test/App.js'
|
|
5
6
|
import * as Funding from './funding.js'
|
|
6
7
|
|
|
@@ -27,22 +28,59 @@ describe('GET /funding/quotes', () => {
|
|
|
27
28
|
})
|
|
28
29
|
|
|
29
30
|
test('queries only the requested provider', async () => {
|
|
30
|
-
let
|
|
31
|
+
let acrossCalls = 0
|
|
32
|
+
let relayCalls = 0
|
|
31
33
|
const app = TestApp.create({
|
|
32
34
|
funding: {
|
|
33
35
|
adapters: {
|
|
36
|
+
across: acrossAdapter(() => {
|
|
37
|
+
acrossCalls += 1
|
|
38
|
+
}),
|
|
34
39
|
relay: relayAdapter(() => {
|
|
35
|
-
|
|
40
|
+
relayCalls += 1
|
|
36
41
|
}),
|
|
37
42
|
},
|
|
38
43
|
},
|
|
39
44
|
})
|
|
40
|
-
const response = await app.request(quoteUrl({ ...baseUsdcQuery, provider: '
|
|
45
|
+
const response = await app.request(quoteUrl({ ...baseUsdcQuery, provider: 'across' }), {
|
|
41
46
|
headers: TestApp.auth.headers,
|
|
42
47
|
})
|
|
43
48
|
|
|
44
49
|
expect(response.status).toBe(200)
|
|
45
|
-
expect(
|
|
50
|
+
expect(acrossCalls).toBe(1)
|
|
51
|
+
expect(relayCalls).toBe(0)
|
|
52
|
+
const body = await TestApp.json(response, Funding.schema.quoteFunding.Response)
|
|
53
|
+
expect(body.data[0]?.quote.expiresAt).toBe('2026-01-01T00:01:00Z')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('returns non-EVM Rhino quotes with stable chain and token ids', async () => {
|
|
57
|
+
const app = TestApp.create({ funding: { adapters: { rhino: rhinoAdapter() } } })
|
|
58
|
+
const response = await app.request(
|
|
59
|
+
quoteUrl({
|
|
60
|
+
destinationToken: 'USDC.e',
|
|
61
|
+
provider: 'rhino',
|
|
62
|
+
sourceAmount: '1000000',
|
|
63
|
+
sourceChain: 'solana',
|
|
64
|
+
sourceToken: 'USDC',
|
|
65
|
+
}),
|
|
66
|
+
{ headers: TestApp.auth.headers },
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
expect(response.status).toBe(200)
|
|
70
|
+
const body = await TestApp.json(response, Funding.schema.quoteFunding.Response)
|
|
71
|
+
expect(body.data[0]).toMatchObject({
|
|
72
|
+
id: 'solana-usdc-tempo-usdce-rhino',
|
|
73
|
+
provider: { id: 'rhino' },
|
|
74
|
+
sourceChain: {
|
|
75
|
+
addressFormat: 'base58',
|
|
76
|
+
id: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
|
|
77
|
+
kind: 'solana',
|
|
78
|
+
},
|
|
79
|
+
sourceToken: {
|
|
80
|
+
tokenKey:
|
|
81
|
+
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
|
|
82
|
+
},
|
|
83
|
+
})
|
|
46
84
|
})
|
|
47
85
|
|
|
48
86
|
test('returns an empty list when no provider is configured', async () => {
|
|
@@ -90,6 +128,72 @@ describe('GET /funding/quotes', () => {
|
|
|
90
128
|
expect(body.error.code).toBe('upstream_error')
|
|
91
129
|
})
|
|
92
130
|
|
|
131
|
+
test('logs provider quote deadlines as timeouts', async () => {
|
|
132
|
+
const entries: Log.Entry[] = []
|
|
133
|
+
const app = TestApp.create({
|
|
134
|
+
funding: {
|
|
135
|
+
adapterTimeoutMs: 1,
|
|
136
|
+
adapters: {
|
|
137
|
+
across: {
|
|
138
|
+
probeQuote() {
|
|
139
|
+
return new Promise<FundingQuotes.QuoteProbeResult>(() => {})
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
logger: (entry) => {
|
|
145
|
+
entries.push(entry)
|
|
146
|
+
},
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const response = await app.request(quoteUrl({ ...baseUsdcQuery, provider: 'across' }), {
|
|
150
|
+
headers: TestApp.auth.headers,
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
expect(response.status).toBe(502)
|
|
154
|
+
expect(entries[0]?.provider).toEqual({
|
|
155
|
+
failure: 'timeout',
|
|
156
|
+
id: 'across',
|
|
157
|
+
operation: 'swap/approval',
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test('logs every failure when multiple providers fail', async () => {
|
|
162
|
+
const entries: Log.Entry[] = []
|
|
163
|
+
const app = TestApp.create({
|
|
164
|
+
funding: {
|
|
165
|
+
adapters: {
|
|
166
|
+
across: {
|
|
167
|
+
async probeQuote() {
|
|
168
|
+
throw new TypeError('Across network failure')
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
relay: {
|
|
172
|
+
async probeQuote() {
|
|
173
|
+
throw new Error('Relay failure')
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
logger: (entry) => {
|
|
179
|
+
entries.push(entry)
|
|
180
|
+
},
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
const response = await app.request(quoteUrl(), { headers: TestApp.auth.headers })
|
|
184
|
+
|
|
185
|
+
expect(response.status).toBe(502)
|
|
186
|
+
expect(entries[0]?.provider).toEqual({
|
|
187
|
+
failure: 'network',
|
|
188
|
+
id: 'across',
|
|
189
|
+
operation: 'swap/approval',
|
|
190
|
+
})
|
|
191
|
+
expect(entries[0]?.providerFailures).toEqual([
|
|
192
|
+
{ failure: 'network', id: 'across', operation: 'swap/approval' },
|
|
193
|
+
{ failure: 'unknown', id: 'relay', operation: 'quote/v2' },
|
|
194
|
+
])
|
|
195
|
+
})
|
|
196
|
+
|
|
93
197
|
test('rejects successful provider results without output amounts', async () => {
|
|
94
198
|
const app = TestApp.create({
|
|
95
199
|
funding: {
|
|
@@ -126,9 +230,9 @@ describe('GET /funding/quotes', () => {
|
|
|
126
230
|
expect(body.error.code).toBe('query_invalid')
|
|
127
231
|
})
|
|
128
232
|
|
|
129
|
-
test('rejects source chains
|
|
233
|
+
test('rejects unsupported source chains', async () => {
|
|
130
234
|
const response = await TestApp.create().request(
|
|
131
|
-
quoteUrl({ ...baseUsdcQuery, sourceChain: '
|
|
235
|
+
quoteUrl({ ...baseUsdcQuery, sourceChain: 'bitcoin' }),
|
|
132
236
|
{ headers: TestApp.auth.headers },
|
|
133
237
|
)
|
|
134
238
|
expect(response.status).toBe(400)
|
|
@@ -151,7 +255,13 @@ describe('GET /funding/chains', () => {
|
|
|
151
255
|
expect(response.status).toBe(200)
|
|
152
256
|
const body = await TestApp.json(response, Funding.schema.listFundingChains.Response)
|
|
153
257
|
|
|
154
|
-
expect(body.data.map((chain) => chain.id)).toEqual([
|
|
258
|
+
expect(body.data.map((chain) => chain.id)).toEqual([
|
|
259
|
+
'eip155:1',
|
|
260
|
+
'eip155:8453',
|
|
261
|
+
'eip155:56',
|
|
262
|
+
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
|
|
263
|
+
'tron:0x2b6653dc',
|
|
264
|
+
])
|
|
155
265
|
expect(body.data.flatMap((chain) => chain.tokens.map((token) => token.symbol))).toEqual(
|
|
156
266
|
expect.arrayContaining(['USDC', 'USDT']),
|
|
157
267
|
)
|
|
@@ -168,6 +278,13 @@ describe('GET /funding/providers', () => {
|
|
|
168
278
|
const body = await TestApp.json(response, Funding.schema.listProviders.Response)
|
|
169
279
|
|
|
170
280
|
expect(body.data).toEqual([
|
|
281
|
+
{
|
|
282
|
+
id: 'across',
|
|
283
|
+
kind: 'intentBridge',
|
|
284
|
+
name: 'Across',
|
|
285
|
+
requiresApiKey: true,
|
|
286
|
+
status: 'unconfigured',
|
|
287
|
+
},
|
|
171
288
|
{
|
|
172
289
|
id: 'relay',
|
|
173
290
|
kind: 'solverNetwork',
|
|
@@ -175,6 +292,13 @@ describe('GET /funding/providers', () => {
|
|
|
175
292
|
requiresApiKey: false,
|
|
176
293
|
status: 'configured',
|
|
177
294
|
},
|
|
295
|
+
{
|
|
296
|
+
id: 'rhino',
|
|
297
|
+
kind: 'bridge',
|
|
298
|
+
name: 'Rhino',
|
|
299
|
+
requiresApiKey: false,
|
|
300
|
+
status: 'unconfigured',
|
|
301
|
+
},
|
|
178
302
|
])
|
|
179
303
|
})
|
|
180
304
|
|
|
@@ -222,3 +346,46 @@ function relayAdapter(onProbe: () => void = () => {}): FundingQuotes.ProviderAda
|
|
|
222
346
|
},
|
|
223
347
|
}
|
|
224
348
|
}
|
|
349
|
+
|
|
350
|
+
function acrossAdapter(onProbe: () => void = () => {}): FundingQuotes.ProviderAdapter {
|
|
351
|
+
return {
|
|
352
|
+
async probeQuote(input, signal) {
|
|
353
|
+
onProbe()
|
|
354
|
+
expect(signal.aborted).toBe(false)
|
|
355
|
+
expect(input.candidate.id).toBe('base-usdc-tempo-usdce-across')
|
|
356
|
+
return {
|
|
357
|
+
expiresAt: '2026-01-01T00:01:00Z',
|
|
358
|
+
minimumOutputAmount: '988000',
|
|
359
|
+
outputAmount: '990000',
|
|
360
|
+
outputAmountFormatted: '0.99',
|
|
361
|
+
quality: {
|
|
362
|
+
estimatedSeconds: 2,
|
|
363
|
+
liquiditySource: 'providerQuote',
|
|
364
|
+
sourceDetail: 'across:swap-approval',
|
|
365
|
+
tier: 'liquid',
|
|
366
|
+
},
|
|
367
|
+
sampledAt: '2026-01-01T00:00:00Z',
|
|
368
|
+
status: 'available',
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function rhinoAdapter(): FundingQuotes.ProviderAdapter {
|
|
375
|
+
return {
|
|
376
|
+
async probeQuote(input, signal) {
|
|
377
|
+
expect(signal.aborted).toBe(false)
|
|
378
|
+
expect(input.candidate.id).toBe('solana-usdc-tempo-usdce-rhino')
|
|
379
|
+
return {
|
|
380
|
+
outputAmount: '983000',
|
|
381
|
+
quality: {
|
|
382
|
+
liquiditySource: 'providerQuote',
|
|
383
|
+
sourceDetail: 'rhino:bridge-swap-public',
|
|
384
|
+
tier: 'liquid',
|
|
385
|
+
},
|
|
386
|
+
sampledAt: '2026-01-01T00:00:00Z',
|
|
387
|
+
status: 'available',
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
}
|
|
391
|
+
}
|