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.
- package/dist/App.d.ts +4701 -3416
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +24 -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 +18 -1
- package/dist/Scope.d.ts.map +1 -1
- package/dist/Scope.js +23 -3
- 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 +57 -57
- 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 +4574 -4277
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +155 -77
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/FundingQuoteProviders.d.ts +109 -0
- package/dist/apps/data/FundingQuoteProviders.d.ts.map +1 -0
- package/dist/apps/data/FundingQuoteProviders.js +464 -0
- package/dist/apps/data/FundingQuoteProviders.js.map +1 -0
- package/dist/apps/data/index.d.ts +1 -1
- package/dist/apps/data/index.js +1 -1
- package/dist/apps/data/routes/activities.d.ts +26 -26
- package/dist/apps/data/routes/addresses.d.ts +38 -38
- package/dist/apps/data/routes/balances.d.ts +45 -45
- package/dist/apps/data/routes/balances.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.js +67 -149
- package/dist/apps/data/routes/balances.js.map +1 -1
- package/dist/apps/data/routes/blocks.d.ts +2 -3
- package/dist/apps/data/routes/blocks.d.ts.map +1 -1
- package/dist/apps/data/routes/blocks.js +3 -5
- package/dist/apps/data/routes/blocks.js.map +1 -1
- package/dist/apps/data/routes/coingecko.d.ts +177 -176
- package/dist/apps/data/routes/coingecko.d.ts.map +1 -1
- package/dist/apps/data/routes/coingecko.js +3 -8
- package/dist/apps/data/routes/coingecko.js.map +1 -1
- package/dist/apps/data/routes/funding.d.ts +131 -228
- package/dist/apps/data/routes/funding.d.ts.map +1 -1
- package/dist/apps/data/routes/funding.js +152 -219
- 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/rpc.d.ts.map +1 -1
- package/dist/apps/data/routes/rpc.js +86 -16
- package/dist/apps/data/routes/rpc.js.map +1 -1
- package/dist/apps/data/routes/tokenlist.d.ts +12 -12
- package/dist/apps/data/routes/verified-tokens.d.ts +5 -5
- package/dist/apps/data/routes/webhooks.d.ts +340 -0
- package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/data/routes/webhooks.js +13 -1
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/index.d.ts +1 -1
- package/dist/apps/index.js +1 -1
- package/dist/apps/management/App.d.ts +1833 -851
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/App.js +4 -0
- package/dist/apps/management/App.js.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +41 -41
- package/dist/apps/management/routes/api-keys.js +2 -2
- package/dist/apps/management/routes/api-keys.js.map +1 -1
- package/dist/apps/management/routes/billing.d.ts +48 -48
- package/dist/apps/management/routes/invitations.d.ts +132 -132
- package/dist/apps/management/routes/invite-links.d.ts +1100 -0
- package/dist/apps/management/routes/invite-links.d.ts.map +1 -0
- package/dist/apps/management/routes/invite-links.js +411 -0
- package/dist/apps/management/routes/invite-links.js.map +1 -0
- package/dist/apps/management/routes/members.d.ts +41 -41
- package/dist/apps/management/routes/orgs.d.ts +92 -92
- package/dist/apps/management/routes/projects.d.ts +9 -9
- package/dist/apps/management/routes/scopes.d.ts.map +1 -1
- package/dist/apps/management/routes/scopes.js +2 -2
- package/dist/apps/management/routes/scopes.js.map +1 -1
- package/dist/apps/management/routes/usage.d.ts +24 -24
- 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.d.ts.map +1 -1
- package/dist/apps/relay/App.js +44 -6
- package/dist/apps/relay/App.js.map +1 -1
- package/dist/db/Db.d.ts +3 -0
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +57 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +31 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +31 -0
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/inviteLinks.d.ts +42 -0
- package/dist/db/tables/inviteLinks.d.ts.map +1 -0
- package/dist/db/tables/inviteLinks.js +148 -0
- package/dist/db/tables/inviteLinks.js.map +1 -0
- package/dist/db/tables/organizations.d.ts.map +1 -1
- package/dist/db/tables/organizations.js +2 -0
- package/dist/db/tables/organizations.js.map +1 -1
- 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/handlers/relay.d.ts.map +1 -1
- package/dist/handlers/relay.js +13 -3
- package/dist/handlers/relay.js.map +1 -1
- package/dist/internal/Auth.d.ts +9 -1
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +138 -10
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/EdgeCache.d.ts +3 -1
- package/dist/internal/EdgeCache.d.ts.map +1 -1
- package/dist/internal/EdgeCache.js +17 -2
- package/dist/internal/EdgeCache.js.map +1 -1
- package/dist/internal/{FundingRoutes.d.ts → FundingQuotes.d.ts} +240 -369
- package/dist/internal/FundingQuotes.d.ts.map +1 -0
- package/dist/internal/{FundingRoutes.js → FundingQuotes.js} +144 -183
- package/dist/internal/FundingQuotes.js.map +1 -0
- 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 +17 -2
- package/dist/internal/Viem.d.ts.map +1 -1
- package/dist/internal/Viem.js +30 -3
- package/dist/internal/Viem.js.map +1 -1
- package/dist/internal/index.d.ts +1 -1
- package/dist/internal/index.js +1 -1
- package/package.json +2 -2
- package/src/ApiKeys.test.ts +25 -0
- package/src/App.test-d.ts +2 -0
- package/src/App.test.ts +13 -2
- package/src/App.ts +37 -5
- package/src/Assets.ts +3 -3
- package/src/Client.test-d.ts +21 -11
- package/src/Scope.test.ts +1 -0
- package/src/Scope.ts +32 -4
- 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.test.ts +354 -0
- package/src/apps/data/App.ts +178 -91
- package/src/apps/data/FundingQuoteProviders.test.ts +577 -0
- package/src/apps/data/FundingQuoteProviders.ts +633 -0
- package/src/apps/data/index.ts +1 -1
- package/src/apps/data/routes/balances.test.ts +64 -18
- package/src/apps/data/routes/balances.ts +76 -177
- package/src/apps/data/routes/blocks.ts +3 -5
- package/src/apps/data/routes/coingecko.ts +3 -8
- package/src/apps/data/routes/funding.test.ts +304 -192
- package/src/apps/data/routes/funding.ts +166 -286
- package/src/apps/data/routes/indexer.test.ts +173 -15
- package/src/apps/data/routes/indexer.ts +8 -2
- package/src/apps/data/routes/rpc.test.ts +219 -0
- package/src/apps/data/routes/rpc.ts +92 -17
- package/src/apps/data/routes/webhooks.test.ts +21 -0
- package/src/apps/data/routes/webhooks.ts +13 -1
- package/src/apps/index.ts +1 -1
- package/src/apps/management/App.ts +4 -0
- package/src/apps/management/Billing.test.ts +2 -2
- package/src/apps/management/routes/api-keys.test.ts +47 -3
- package/src/apps/management/routes/api-keys.ts +2 -2
- package/src/apps/management/routes/invite-links.test.ts +385 -0
- package/src/apps/management/routes/invite-links.ts +570 -0
- package/src/apps/management/routes/scopes.test.ts +3 -2
- package/src/apps/management/routes/scopes.ts +4 -2
- package/src/apps/mcp.test.ts +18 -0
- package/src/apps/mcp.ts +2 -2
- package/src/apps/relay/App.test.ts +328 -7
- package/src/apps/relay/App.ts +42 -6
- package/src/apps/relay/Sponsorships.test.ts +2 -2
- package/src/db/Db.test.ts +5 -5
- package/src/db/Db.ts +65 -0
- package/src/db/Schema.ts +39 -0
- package/src/db/tables/billingSettings.test.ts +2 -2
- package/src/db/tables/inviteLinks.ts +187 -0
- package/src/db/tables/organizations.test.ts +2 -2
- package/src/db/tables/organizations.ts +2 -0
- 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/handlers/relay.ts +16 -4
- package/src/internal/Auth.test.ts +132 -22
- package/src/internal/Auth.ts +181 -14
- package/src/internal/EdgeCache.test.ts +24 -3
- package/src/internal/EdgeCache.ts +18 -2
- package/src/internal/FundingQuotes.test.ts +268 -0
- package/src/internal/{FundingRoutes.ts → FundingQuotes.ts} +216 -277
- 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 +48 -6
- package/src/internal/index.test.ts +1 -1
- package/src/internal/index.ts +1 -1
- package/dist/apps/data/FundingRouteProviders.d.ts +0 -36
- package/dist/apps/data/FundingRouteProviders.d.ts.map +0 -1
- package/dist/apps/data/FundingRouteProviders.js +0 -245
- package/dist/apps/data/FundingRouteProviders.js.map +0 -1
- package/dist/internal/FundingRoutes.d.ts.map +0 -1
- package/dist/internal/FundingRoutes.js.map +0 -1
- package/src/apps/data/FundingRouteProviders.test.ts +0 -225
- package/src/apps/data/FundingRouteProviders.ts +0 -312
- package/src/internal/FundingRoutes.test.ts +0 -213
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
import * as z from 'zod/mini'
|
|
2
|
+
|
|
3
|
+
import * as FundingQuotes from '../../internal/FundingQuotes.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
|
+
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
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Creates the live funding quote adapters configured for this deployment. */
|
|
50
|
+
export function createAdapters(options: createAdapters.Options = {}) {
|
|
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
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export declare namespace createAdapters {
|
|
59
|
+
/** Provider adapter factory options. Omit a provider to leave it unconfigured. */
|
|
60
|
+
type Options = {
|
|
61
|
+
/** Across quote adapter options. */
|
|
62
|
+
across?: across.Options | false | undefined
|
|
63
|
+
/** Relay quote adapter options. */
|
|
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
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Creates a Relay quote adapter backed by `/quote/v2`. */
|
|
168
|
+
export function relay(options: relay.Options): FundingQuotes.ProviderAdapter {
|
|
169
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl ?? defaultRelayBaseUrl)
|
|
170
|
+
const fetcher = options.fetch ?? defaultFetch
|
|
171
|
+
const now = options.now ?? (() => new Date())
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
async probeQuote({ candidate }, signal) {
|
|
175
|
+
const originChainId = providerChainId(candidate.sourceChain, 'relay')
|
|
176
|
+
const destinationChainId = eip155ChainId(candidate.destinationChain)
|
|
177
|
+
const headers = relayHeaders(options.apiKey)
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const quote = await requestJson(new URL(`${baseUrl}/quote/v2`), {
|
|
181
|
+
body: {
|
|
182
|
+
amount: candidate.sourceAmount.amount,
|
|
183
|
+
destinationChainId: Number(destinationChainId),
|
|
184
|
+
destinationCurrency: candidate.destinationToken.address,
|
|
185
|
+
originChainId: Number(originChainId),
|
|
186
|
+
originCurrency: candidate.sourceToken.address,
|
|
187
|
+
recipient: options.recipientAddress ?? options.userAddress,
|
|
188
|
+
tradeType: 'EXACT_INPUT',
|
|
189
|
+
user: options.userAddress,
|
|
190
|
+
},
|
|
191
|
+
fetcher,
|
|
192
|
+
headers,
|
|
193
|
+
method: 'POST',
|
|
194
|
+
signal,
|
|
195
|
+
})
|
|
196
|
+
return formatRelayResult(candidate, quote, now())
|
|
197
|
+
} catch (cause) {
|
|
198
|
+
const unavailable = providerUnavailable(cause, [
|
|
199
|
+
'NO_QUOTES',
|
|
200
|
+
'NO_ROUTE',
|
|
201
|
+
'UNSUPPORTED_ROUTE',
|
|
202
|
+
])
|
|
203
|
+
if (!unavailable) throw cause
|
|
204
|
+
return unavailableResult({
|
|
205
|
+
detail: 'relay:quote-v2',
|
|
206
|
+
message: unavailable,
|
|
207
|
+
now: now(),
|
|
208
|
+
source: 'providerQuote',
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export declare namespace relay {
|
|
216
|
+
/** Relay quote adapter options. */
|
|
217
|
+
type Options = {
|
|
218
|
+
/** Probe wallet address required by Relay's quote API. */
|
|
219
|
+
userAddress: string
|
|
220
|
+
/** Optional recipient override; defaults to `userAddress`. */
|
|
221
|
+
recipientAddress?: string | undefined
|
|
222
|
+
/** Optional Relay API key for higher rate limits. */
|
|
223
|
+
apiKey?: string | undefined
|
|
224
|
+
/** Override for tests or Relay testnet environments. */
|
|
225
|
+
baseUrl?: string | undefined
|
|
226
|
+
/** Fetch implementation override for tests. */
|
|
227
|
+
fetch?: Fetch | undefined
|
|
228
|
+
/** Clock override for deterministic tests. */
|
|
229
|
+
now?: (() => Date) | undefined
|
|
230
|
+
}
|
|
231
|
+
}
|
|
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
|
+
|
|
371
|
+
function formatRelayResult(
|
|
372
|
+
candidate: FundingQuotes.QuoteCandidate,
|
|
373
|
+
quote: unknown,
|
|
374
|
+
now: Date,
|
|
375
|
+
): FundingQuotes.QuoteProbeResult {
|
|
376
|
+
const details = record(record(quote)['details'])
|
|
377
|
+
const currencyOut = record(details['currencyOut'])
|
|
378
|
+
const outputAmount = decimalString(currencyOut['amount'])
|
|
379
|
+
const status = outputAmount
|
|
380
|
+
? { status: 'available' as const, tier: 'liquid' as const }
|
|
381
|
+
: { status: 'unknown' as const, tier: 'unknown' as const }
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
minimumOutputAmount: decimalString(currencyOut['minimumAmount']),
|
|
385
|
+
outputAmount,
|
|
386
|
+
outputAmountFormatted: outputAmount
|
|
387
|
+
? decimalFromBaseUnits(outputAmount, candidate.destinationToken.decimals)
|
|
388
|
+
: undefined,
|
|
389
|
+
quality: {
|
|
390
|
+
estimatedSeconds: nonnegativeInteger(details['timeEstimate']),
|
|
391
|
+
liquiditySource: 'providerQuote',
|
|
392
|
+
sourceDetail: 'relay:quote-v2',
|
|
393
|
+
tier: status.tier,
|
|
394
|
+
},
|
|
395
|
+
sampledAt: now.toISOString(),
|
|
396
|
+
status: status.status,
|
|
397
|
+
}
|
|
398
|
+
}
|
|
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
|
+
|
|
439
|
+
function unavailableResult(options: {
|
|
440
|
+
detail: string
|
|
441
|
+
message: string
|
|
442
|
+
now: Date
|
|
443
|
+
source: string
|
|
444
|
+
}): FundingQuotes.QuoteProbeResult {
|
|
445
|
+
return {
|
|
446
|
+
quality: {
|
|
447
|
+
liquiditySource: options.source,
|
|
448
|
+
sourceDetail: `${options.detail}:${options.message}`,
|
|
449
|
+
tier: 'unavailable',
|
|
450
|
+
},
|
|
451
|
+
sampledAt: options.now.toISOString(),
|
|
452
|
+
status: 'unavailable',
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async function requestJson(
|
|
457
|
+
url: URL,
|
|
458
|
+
options: {
|
|
459
|
+
body?: unknown
|
|
460
|
+
fetcher: Fetch
|
|
461
|
+
headers?: Record<string, string> | undefined
|
|
462
|
+
method?: string | undefined
|
|
463
|
+
signal: AbortSignal
|
|
464
|
+
},
|
|
465
|
+
) {
|
|
466
|
+
const headers = { accept: 'application/json', ...options.headers }
|
|
467
|
+
const init: RequestInit = {
|
|
468
|
+
headers:
|
|
469
|
+
options.body === undefined ? headers : { ...headers, 'content-type': 'application/json' },
|
|
470
|
+
method: options.method ?? 'GET',
|
|
471
|
+
signal: options.signal,
|
|
472
|
+
}
|
|
473
|
+
if (options.body !== undefined) init.body = JSON.stringify(options.body)
|
|
474
|
+
const response = await options.fetcher(url, init)
|
|
475
|
+
const text = await response.text()
|
|
476
|
+
const body = parseJson(text)
|
|
477
|
+
if (!response.ok) throw new ProviderResponseError(response.status, body)
|
|
478
|
+
return body
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function relayHeaders(apiKey: string | undefined): Record<string, string> | undefined {
|
|
482
|
+
return apiKey ? { 'x-api-key': apiKey } : undefined
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function providerUnavailable(cause: unknown, codes: readonly string[]) {
|
|
486
|
+
if (!(cause instanceof ProviderResponseError) || cause.status < 400 || cause.status >= 500)
|
|
487
|
+
return undefined
|
|
488
|
+
const code = providerErrorCode(cause.body)
|
|
489
|
+
if (code && codes.includes(code)) return code
|
|
490
|
+
return undefined
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function providerErrorCode(input: unknown) {
|
|
494
|
+
const body = record(input)
|
|
495
|
+
const code =
|
|
496
|
+
stringValue(body['_tag']) ?? stringValue(body['code']) ?? stringValue(body['errorCode'])
|
|
497
|
+
return code && /^[A-Za-z0-9_.:-]{1,64}$/.test(code) ? code : undefined
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function providerChainId(
|
|
501
|
+
quoteChain: FundingQuotes.QuoteCandidate['sourceChain'],
|
|
502
|
+
provider: string,
|
|
503
|
+
) {
|
|
504
|
+
const providerIds = quoteChain.providerIds as Record<string, string> | undefined
|
|
505
|
+
const id = providerIds?.[provider]
|
|
506
|
+
if (id && /^\d+$/.test(id)) return id
|
|
507
|
+
return eip155ChainId(quoteChain)
|
|
508
|
+
}
|
|
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
|
+
|
|
542
|
+
function eip155ChainId(chain: { id: string; kind: string }) {
|
|
543
|
+
if (chain.kind !== 'evm') throw new UnsupportedProviderChainError()
|
|
544
|
+
const match = /^eip155:(\d+)$/.exec(chain.id)
|
|
545
|
+
if (!match?.[1]) throw new UnsupportedProviderChainError()
|
|
546
|
+
return match[1]
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function normalizeBaseUrl(input: string) {
|
|
550
|
+
return input.replace(/\/+$/, '')
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function parseJson(input: string) {
|
|
554
|
+
if (!input) return null
|
|
555
|
+
try {
|
|
556
|
+
return JSON.parse(input) as unknown
|
|
557
|
+
} catch {
|
|
558
|
+
return input
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function record(input: unknown): Record<string, unknown> {
|
|
563
|
+
return input && typeof input === 'object' && !Array.isArray(input)
|
|
564
|
+
? (input as Record<string, unknown>)
|
|
565
|
+
: {}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function stringValue(input: unknown) {
|
|
569
|
+
return typeof input === 'string' && input.length > 0 ? input : undefined
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function decimalString(input: unknown) {
|
|
573
|
+
if (typeof input === 'string' && /^\d+(\.\d+)?$/.test(input)) return input
|
|
574
|
+
if (typeof input === 'number' && Number.isFinite(input) && input >= 0) return String(input)
|
|
575
|
+
return undefined
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function nonnegativeInteger(input: unknown) {
|
|
579
|
+
if (typeof input === 'number' && Number.isSafeInteger(input) && input >= 0) return input
|
|
580
|
+
if (typeof input === 'string' && /^\d+$/.test(input)) return Number(input)
|
|
581
|
+
return undefined
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function decimalFromBaseUnits(baseUnits: string, decimals: number) {
|
|
585
|
+
const padded = baseUnits.padStart(decimals + 1, '0')
|
|
586
|
+
const whole = decimals === 0 ? padded : padded.slice(0, -decimals)
|
|
587
|
+
const fraction = decimals === 0 ? '' : padded.slice(-decimals).replace(/0+$/, '')
|
|
588
|
+
return fraction ? `${whole}.${fraction}` : whole
|
|
589
|
+
}
|
|
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
|
+
|
|
616
|
+
class ProviderResponseError extends Error {
|
|
617
|
+
override name = 'FundingQuoteProviders.ProviderResponseError'
|
|
618
|
+
|
|
619
|
+
constructor(
|
|
620
|
+
readonly status: number,
|
|
621
|
+
readonly body: unknown,
|
|
622
|
+
) {
|
|
623
|
+
super(`Provider request failed with status ${status}`)
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
class UnsupportedProviderChainError extends Error {
|
|
628
|
+
override name = 'FundingQuoteProviders.UnsupportedProviderChainError'
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
class UnsupportedProviderTokenError extends Error {
|
|
632
|
+
override name = 'FundingQuoteProviders.UnsupportedProviderTokenError'
|
|
633
|
+
}
|
package/src/apps/data/index.ts
CHANGED