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