tapimo 0.5.1 → 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 +34 -18
- package/dist/App.d.ts.map +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +117 -117
- package/dist/apps/data/App.d.ts +3217 -3217
- package/dist/apps/data/FundingQuoteProviders.d.ts +32 -0
- package/dist/apps/data/FundingQuoteProviders.d.ts.map +1 -1
- package/dist/apps/data/FundingQuoteProviders.js +134 -1
- package/dist/apps/data/FundingQuoteProviders.js.map +1 -1
- 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/coingecko.d.ts +169 -169
- package/dist/apps/data/routes/funding.d.ts +8 -2
- package/dist/apps/data/routes/funding.d.ts.map +1 -1
- package/dist/apps/data/routes/funding.js +4 -4
- 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/receipts.d.ts +228 -228
- package/dist/apps/data/routes/rpc.d.ts +31 -31
- package/dist/apps/data/routes/transfers.d.ts +8 -15
- 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/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 +982 -966
- 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/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 +121 -121
- package/dist/apps/management/routes/invite-links.d.ts +174 -174
- package/dist/apps/management/routes/orgs.d.ts +102 -86
- 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 +124 -124
- 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 +46 -8
- 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 +7 -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 +16 -10
- package/dist/internal/FundingQuotes.d.ts.map +1 -1
- package/dist/internal/FundingQuotes.js +9 -0
- package/dist/internal/FundingQuotes.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/package.json +6 -1
- package/src/App.test.ts +2 -2
- package/src/Client.test-d.ts +28 -2
- package/src/apps/data/FundingQuoteProviders.test.ts +184 -0
- package/src/apps/data/FundingQuoteProviders.ts +185 -1
- package/src/apps/data/routes/funding.test.ts +7 -0
- package/src/apps/data/routes/funding.ts +4 -4
- 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 +50 -1
- 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/relay/Sponsorships.ts +24 -1
- package/src/db/Db.ts +4 -0
- package/src/db/Schema.ts +1 -1
- package/src/db/tables/organizations.test.ts +30 -0
- package/src/db/tables/organizations.ts +28 -0
- package/src/db/tables/requestUsage.ts +3 -23
- package/src/db/tables/sponsoredTransactions.test.ts +41 -0
- package/src/db/tables/sponsoredTransactions.ts +46 -7
- package/src/internal/Auth.test.ts +63 -10
- package/src/internal/Auth.ts +11 -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 +33 -0
- package/src/internal/FundingQuotes.ts +9 -0
- package/src/internal/RateLimit.test.ts +12 -0
- package/src/internal/RateLimit.ts +4 -4
|
@@ -22,6 +22,22 @@ describe('FundingQuoteProviders', () => {
|
|
|
22
22
|
})
|
|
23
23
|
})
|
|
24
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
|
+
|
|
25
41
|
test('maps successful Across quotes to available quote probes', async () => {
|
|
26
42
|
const requests: RecordedRequest[] = []
|
|
27
43
|
const candidate = quoteCandidate('across')
|
|
@@ -172,6 +188,80 @@ describe('FundingQuoteProviders', () => {
|
|
|
172
188
|
})
|
|
173
189
|
})
|
|
174
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
|
+
|
|
175
265
|
test('maps successful Rhino quotes to available quote probes', async () => {
|
|
176
266
|
const requests: RecordedRequest[] = []
|
|
177
267
|
const candidate = quoteCandidate('rhino')
|
|
@@ -437,6 +527,64 @@ describe('FundingQuoteProviders', () => {
|
|
|
437
527
|
).rejects.toThrowErrorMatchingInlineSnapshot(`[FundingQuoteProviders.ProviderPayloadError]`)
|
|
438
528
|
})
|
|
439
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
|
+
|
|
440
588
|
test('returns bounded Across failure diagnostics', async () => {
|
|
441
589
|
const candidate = quoteCandidate('across')
|
|
442
590
|
const adapter = FundingQuoteProviders.across({
|
|
@@ -473,6 +621,19 @@ describe('FundingQuoteProviders', () => {
|
|
|
473
621
|
operation: 'swap/approval',
|
|
474
622
|
})
|
|
475
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
|
+
})
|
|
476
637
|
})
|
|
477
638
|
|
|
478
639
|
type RecordedRequest = {
|
|
@@ -527,6 +688,29 @@ function acrossQuote(candidate: FundingQuotes.QuoteProbeInput['candidate']) {
|
|
|
527
688
|
}
|
|
528
689
|
}
|
|
529
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
|
+
|
|
530
714
|
function probeInput(
|
|
531
715
|
candidate: FundingQuotes.QuoteProbeInput['candidate'],
|
|
532
716
|
): FundingQuotes.QuoteProbeInput {
|
|
@@ -9,10 +9,12 @@ const defaultFetch: Fetch = (input, init) => globalThis.fetch(input, init)
|
|
|
9
9
|
const defaultAcrossBaseUrl = 'https://app.across.to/api'
|
|
10
10
|
const defaultRelayBaseUrl = 'https://api.relay.link'
|
|
11
11
|
const defaultRhinoBaseUrl = 'https://api.rhino.fi/bridge'
|
|
12
|
+
const defaultSquidBaseUrl = 'https://v2.api.squidrouter.com/v2'
|
|
12
13
|
const providerOperations = {
|
|
13
14
|
across: 'swap/approval',
|
|
14
15
|
relay: 'quote/v2',
|
|
15
16
|
rhino: 'quote/bridge-swap/public',
|
|
17
|
+
squid: 'v2/route',
|
|
16
18
|
} as const
|
|
17
19
|
|
|
18
20
|
namespace schema {
|
|
@@ -44,6 +46,25 @@ namespace schema {
|
|
|
44
46
|
receiveAmount: z.string().check(z.regex(/^\d+(\.\d+)?$/)),
|
|
45
47
|
token: z.string(),
|
|
46
48
|
})
|
|
49
|
+
|
|
50
|
+
const SquidToken = z.object({
|
|
51
|
+
address: z.string(),
|
|
52
|
+
chainId: z.union([z.string(), z.number().check(z.int(), z.nonnegative())]),
|
|
53
|
+
decimals: z.number().check(z.int(), z.nonnegative()),
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
export const SquidQuote = z.object({
|
|
57
|
+
route: z.object({
|
|
58
|
+
estimate: z.object({
|
|
59
|
+
estimatedRouteDuration: z.number().check(z.nonnegative()),
|
|
60
|
+
fromAmount: IntegerString,
|
|
61
|
+
fromToken: SquidToken,
|
|
62
|
+
toAmount: IntegerString,
|
|
63
|
+
toAmountMin: IntegerString,
|
|
64
|
+
toToken: SquidToken,
|
|
65
|
+
}),
|
|
66
|
+
}),
|
|
67
|
+
})
|
|
47
68
|
}
|
|
48
69
|
|
|
49
70
|
/** Creates the live funding quote adapters configured for this deployment. */
|
|
@@ -52,6 +73,7 @@ export function createAdapters(options: createAdapters.Options = {}) {
|
|
|
52
73
|
...(options.across ? { across: across(options.across) } : {}),
|
|
53
74
|
...(options.relay ? { relay: relay(options.relay) } : {}),
|
|
54
75
|
...(options.rhino ? { rhino: rhino(options.rhino) } : {}),
|
|
76
|
+
...(options.squid ? { squid: squid(options.squid) } : {}),
|
|
55
77
|
} satisfies FundingQuotes.ProviderAdapters
|
|
56
78
|
}
|
|
57
79
|
|
|
@@ -64,6 +86,8 @@ export declare namespace createAdapters {
|
|
|
64
86
|
relay?: relay.Options | false | undefined
|
|
65
87
|
/** Rhino quote adapter options. */
|
|
66
88
|
rhino?: rhino.Options | false | undefined
|
|
89
|
+
/** Squid quote adapter options. */
|
|
90
|
+
squid?: squid.Options | false | undefined
|
|
67
91
|
}
|
|
68
92
|
}
|
|
69
93
|
|
|
@@ -92,6 +116,27 @@ export declare namespace configureAcross {
|
|
|
92
116
|
}
|
|
93
117
|
}
|
|
94
118
|
|
|
119
|
+
/** Resolves all-or-none Squid deployment configuration. */
|
|
120
|
+
export function configureSquid(options: configureSquid.Options = {}): squid.Options | undefined {
|
|
121
|
+
const configured = [options.integratorId, options.userAddress].filter(Boolean)
|
|
122
|
+
if (configured.length === 0) return undefined
|
|
123
|
+
if (!options.integratorId || !options.userAddress) throw new ProviderConfigurationError()
|
|
124
|
+
return {
|
|
125
|
+
integratorId: options.integratorId,
|
|
126
|
+
userAddress: options.userAddress,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export declare namespace configureSquid {
|
|
131
|
+
/** Optional deployment bindings required together to enable Squid. */
|
|
132
|
+
type Options = {
|
|
133
|
+
/** Squid integrator id binding. */
|
|
134
|
+
integratorId?: string | undefined
|
|
135
|
+
/** Squid quote-user binding. */
|
|
136
|
+
userAddress?: string | undefined
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
95
140
|
/** Creates an Across quote adapter backed by `/swap/approval`. */
|
|
96
141
|
export function across(options: across.Options): FundingQuotes.ProviderAdapter {
|
|
97
142
|
assertAcrossConfiguration(options)
|
|
@@ -284,6 +329,74 @@ export declare namespace rhino {
|
|
|
284
329
|
}
|
|
285
330
|
}
|
|
286
331
|
|
|
332
|
+
/** Creates a Squid quote adapter backed by `/v2/route`. */
|
|
333
|
+
export function squid(options: squid.Options): FundingQuotes.ProviderAdapter {
|
|
334
|
+
assertSquidConfiguration(options)
|
|
335
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl ?? defaultSquidBaseUrl)
|
|
336
|
+
const fetcher = options.fetch ?? defaultFetch
|
|
337
|
+
const now = options.now ?? (() => new Date())
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
async probeQuote({ candidate }, signal) {
|
|
341
|
+
const fromChain = eip155ChainId(candidate.sourceChain)
|
|
342
|
+
const toChain = eip155ChainId(candidate.destinationChain)
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
const quote = await requestJson(new URL(`${baseUrl}/route`), {
|
|
346
|
+
body: {
|
|
347
|
+
fromAddress: options.userAddress,
|
|
348
|
+
fromAmount: candidate.sourceAmount.amount,
|
|
349
|
+
fromChain,
|
|
350
|
+
fromToken: candidate.sourceToken.address,
|
|
351
|
+
quoteOnly: true,
|
|
352
|
+
// V1 quotes use a fixed 1% tolerance; callers cannot customize execution policy.
|
|
353
|
+
slippage: 1,
|
|
354
|
+
toAddress: options.userAddress,
|
|
355
|
+
toChain,
|
|
356
|
+
toToken: candidate.destinationToken.address,
|
|
357
|
+
},
|
|
358
|
+
fetcher,
|
|
359
|
+
headers: { 'x-integrator-id': options.integratorId },
|
|
360
|
+
method: 'POST',
|
|
361
|
+
signal,
|
|
362
|
+
})
|
|
363
|
+
return formatSquidResult(candidate, quote, now())
|
|
364
|
+
} catch (cause) {
|
|
365
|
+
const unavailable =
|
|
366
|
+
providerUnavailable(cause, [
|
|
367
|
+
'NO_ROUTE',
|
|
368
|
+
'NO_ROUTE_FOUND',
|
|
369
|
+
'ROUTE_NOT_FOUND',
|
|
370
|
+
'UNSUPPORTED_ROUTE',
|
|
371
|
+
]) ?? squidUnavailable(cause)
|
|
372
|
+
if (!unavailable) throw cause
|
|
373
|
+
return unavailableResult({
|
|
374
|
+
detail: 'squid:route-v2',
|
|
375
|
+
message: unavailable,
|
|
376
|
+
now: now(),
|
|
377
|
+
source: 'providerQuote',
|
|
378
|
+
})
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export declare namespace squid {
|
|
385
|
+
/** Squid quote adapter options. */
|
|
386
|
+
type Options = {
|
|
387
|
+
/** Override for tests or Squid test environments. */
|
|
388
|
+
baseUrl?: string | undefined
|
|
389
|
+
/** Fetch implementation override for tests. */
|
|
390
|
+
fetch?: Fetch | undefined
|
|
391
|
+
/** Squid integrator id sent in the required request header. */
|
|
392
|
+
integratorId: string
|
|
393
|
+
/** Clock override for deterministic tests. */
|
|
394
|
+
now?: (() => Date) | undefined
|
|
395
|
+
/** Probe wallet address used as the Squid sender and recipient. */
|
|
396
|
+
userAddress: string
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
287
400
|
/** Returns bounded provider failure details suitable for structured request logs. */
|
|
288
401
|
export function providerFailure(options: providerFailure.Options): providerFailure.Result {
|
|
289
402
|
const operation = providerOperations[options.provider]
|
|
@@ -436,6 +549,42 @@ function formatRhinoResult(
|
|
|
436
549
|
}
|
|
437
550
|
}
|
|
438
551
|
|
|
552
|
+
function formatSquidResult(
|
|
553
|
+
candidate: FundingQuotes.QuoteCandidate,
|
|
554
|
+
quote: unknown,
|
|
555
|
+
now: Date,
|
|
556
|
+
): FundingQuotes.QuoteProbeResult {
|
|
557
|
+
const parsed = schema.SquidQuote.safeParse(quote)
|
|
558
|
+
if (!parsed.success) throw new ProviderPayloadError()
|
|
559
|
+
const response = parsed.data.route.estimate
|
|
560
|
+
if (
|
|
561
|
+
response.fromAmount !== candidate.sourceAmount.amount ||
|
|
562
|
+
!matchesSquidToken(response.fromToken, candidate.sourceChain, candidate.sourceToken) ||
|
|
563
|
+
!matchesSquidToken(response.toToken, candidate.destinationChain, candidate.destinationToken) ||
|
|
564
|
+
BigInt(response.toAmount) <= 0n ||
|
|
565
|
+
BigInt(response.toAmountMin) <= 0n ||
|
|
566
|
+
BigInt(response.toAmountMin) > BigInt(response.toAmount)
|
|
567
|
+
)
|
|
568
|
+
throw new ProviderPayloadError()
|
|
569
|
+
|
|
570
|
+
return {
|
|
571
|
+
minimumOutputAmount: response.toAmountMin,
|
|
572
|
+
outputAmount: response.toAmount,
|
|
573
|
+
outputAmountFormatted: decimalFromBaseUnits(
|
|
574
|
+
response.toAmount,
|
|
575
|
+
candidate.destinationToken.decimals,
|
|
576
|
+
),
|
|
577
|
+
quality: {
|
|
578
|
+
estimatedSeconds: Math.ceil(response.estimatedRouteDuration),
|
|
579
|
+
liquiditySource: 'providerQuote',
|
|
580
|
+
sourceDetail: 'squid:route-v2',
|
|
581
|
+
tier: 'liquid',
|
|
582
|
+
},
|
|
583
|
+
sampledAt: now.toISOString(),
|
|
584
|
+
status: 'available',
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
439
588
|
function unavailableResult(options: {
|
|
440
589
|
detail: string
|
|
441
590
|
message: string
|
|
@@ -492,11 +641,29 @@ function providerUnavailable(cause: unknown, codes: readonly string[]) {
|
|
|
492
641
|
|
|
493
642
|
function providerErrorCode(input: unknown) {
|
|
494
643
|
const body = record(input)
|
|
644
|
+
const error = record(body['error'])
|
|
495
645
|
const code =
|
|
496
|
-
stringValue(body['_tag']) ??
|
|
646
|
+
stringValue(body['_tag']) ??
|
|
647
|
+
stringValue(body['code']) ??
|
|
648
|
+
stringValue(body['errorCode']) ??
|
|
649
|
+
stringValue(body['errorType']) ??
|
|
650
|
+
stringValue(body['type']) ??
|
|
651
|
+
stringValue(error['code']) ??
|
|
652
|
+
stringValue(error['type'])
|
|
497
653
|
return code && /^[A-Za-z0-9_.:-]{1,64}$/.test(code) ? code : undefined
|
|
498
654
|
}
|
|
499
655
|
|
|
656
|
+
function squidUnavailable(cause: unknown) {
|
|
657
|
+
if (!(cause instanceof ProviderResponseError) || cause.status !== 400) return undefined
|
|
658
|
+
const body = record(cause.body)
|
|
659
|
+
if (
|
|
660
|
+
body['type'] === 'BAD_REQUEST' &&
|
|
661
|
+
body['message'] === 'Please increase the swap amount and try again.'
|
|
662
|
+
)
|
|
663
|
+
return 'AMOUNT_TOO_LOW'
|
|
664
|
+
return undefined
|
|
665
|
+
}
|
|
666
|
+
|
|
500
667
|
function providerChainId(
|
|
501
668
|
quoteChain: FundingQuotes.QuoteCandidate['sourceChain'],
|
|
502
669
|
provider: string,
|
|
@@ -539,6 +706,18 @@ function matchesToken(
|
|
|
539
706
|
)
|
|
540
707
|
}
|
|
541
708
|
|
|
709
|
+
function matchesSquidToken(
|
|
710
|
+
token: z.output<typeof schema.SquidQuote>['route']['estimate']['fromToken'],
|
|
711
|
+
chain: FundingQuotes.ChainRef,
|
|
712
|
+
expected: FundingQuotes.TokenRef,
|
|
713
|
+
) {
|
|
714
|
+
return (
|
|
715
|
+
token.address.toLowerCase() === expected.address.toLowerCase() &&
|
|
716
|
+
String(token.chainId) === eip155ChainId(chain) &&
|
|
717
|
+
token.decimals === expected.decimals
|
|
718
|
+
)
|
|
719
|
+
}
|
|
720
|
+
|
|
542
721
|
function eip155ChainId(chain: { id: string; kind: string }) {
|
|
543
722
|
if (chain.kind !== 'evm') throw new UnsupportedProviderChainError()
|
|
544
723
|
const match = /^eip155:(\d+)$/.exec(chain.id)
|
|
@@ -605,6 +784,11 @@ function assertAcrossConfiguration(options: across.Options) {
|
|
|
605
784
|
throw new ProviderConfigurationError()
|
|
606
785
|
}
|
|
607
786
|
|
|
787
|
+
function assertSquidConfiguration(options: squid.Options) {
|
|
788
|
+
if (options.integratorId.length === 0 || !/^0x[\da-fA-F]{40}$/.test(options.userAddress))
|
|
789
|
+
throw new ProviderConfigurationError()
|
|
790
|
+
}
|
|
791
|
+
|
|
608
792
|
class ProviderConfigurationError extends Error {
|
|
609
793
|
override name = 'FundingQuoteProviders.ProviderConfigurationError'
|
|
610
794
|
}
|
|
@@ -299,6 +299,13 @@ describe('GET /funding/providers', () => {
|
|
|
299
299
|
requiresApiKey: false,
|
|
300
300
|
status: 'unconfigured',
|
|
301
301
|
},
|
|
302
|
+
{
|
|
303
|
+
id: 'squid',
|
|
304
|
+
kind: 'solverNetwork',
|
|
305
|
+
name: 'Squid',
|
|
306
|
+
requiresApiKey: true,
|
|
307
|
+
status: 'unconfigured',
|
|
308
|
+
},
|
|
302
309
|
])
|
|
303
310
|
})
|
|
304
311
|
|
|
@@ -118,7 +118,7 @@ export namespace schema {
|
|
|
118
118
|
export const ProviderRef = Schema.describe(
|
|
119
119
|
z.object({
|
|
120
120
|
id: z
|
|
121
|
-
.enum(['across', 'relay', 'rhino'])
|
|
121
|
+
.enum(['across', 'relay', 'rhino', 'squid'])
|
|
122
122
|
.check(z.describe('Stable provider id.'), z.meta({ examples: ['relay'] })),
|
|
123
123
|
kind: z
|
|
124
124
|
.string()
|
|
@@ -237,7 +237,7 @@ export namespace schema {
|
|
|
237
237
|
export const Provider = Schema.describe(
|
|
238
238
|
z.object({
|
|
239
239
|
id: z
|
|
240
|
-
.enum(['across', 'relay', 'rhino'])
|
|
240
|
+
.enum(['across', 'relay', 'rhino', 'squid'])
|
|
241
241
|
.check(z.describe('Stable provider id.'), z.meta({ examples: ['relay'] })),
|
|
242
242
|
kind: z
|
|
243
243
|
.string()
|
|
@@ -285,10 +285,10 @@ export namespace schema {
|
|
|
285
285
|
z.meta({ examples: ['USDC.e'] }),
|
|
286
286
|
),
|
|
287
287
|
provider: z
|
|
288
|
-
.optional(z.enum(['across', 'relay', 'rhino']))
|
|
288
|
+
.optional(z.enum(['across', 'relay', 'rhino', 'squid']))
|
|
289
289
|
.check(
|
|
290
290
|
z.describe(
|
|
291
|
-
'Only request a quote from this provider. Supported ids are `across`, `relay`, and `
|
|
291
|
+
'Only request a quote from this provider. Supported ids are `across`, `relay`, `rhino`, and `squid`; omit it to query every configured provider.',
|
|
292
292
|
),
|
|
293
293
|
z.meta({ examples: ['relay'] }),
|
|
294
294
|
),
|
|
@@ -272,6 +272,60 @@ describe('GET /tokens?verified=true', () => {
|
|
|
272
272
|
})
|
|
273
273
|
|
|
274
274
|
describe('GET /tokens?addresses=', () => {
|
|
275
|
+
test('separates repeated addresses in the response cache', async () => {
|
|
276
|
+
const routed = TestApp.create({ db })
|
|
277
|
+
const headers = { authorization: `Bearer ${TestApp.key.token}` }
|
|
278
|
+
const repeated = new URLSearchParams()
|
|
279
|
+
repeated.append('addresses', TestApp.token)
|
|
280
|
+
repeated.append('addresses', TestApp.tokenWithHolders)
|
|
281
|
+
const single = new URLSearchParams({
|
|
282
|
+
addresses: TestApp.tokenWithHolders,
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
const primed = await routed.request(`/v1/tokens?${repeated}`, { headers })
|
|
286
|
+
const miss = await routed.request(`/v1/tokens?${single}`, { headers })
|
|
287
|
+
const hit = await routed.request(`/v1/tokens?${single}`, { headers })
|
|
288
|
+
const primedBody = await TestApp.json(primed, Tokens.schema.getTokens.Response)
|
|
289
|
+
const body = await TestApp.json(hit, Tokens.schema.getTokens.Response)
|
|
290
|
+
|
|
291
|
+
expect(primed.status).toBe(200)
|
|
292
|
+
expect(miss.status).toBe(200)
|
|
293
|
+
expect(hit.status).toBe(200)
|
|
294
|
+
expect(primedBody.data.map((token) => token.address)).toEqual([
|
|
295
|
+
TestApp.token,
|
|
296
|
+
TestApp.tokenWithHolders,
|
|
297
|
+
])
|
|
298
|
+
expect(miss.headers.has('RateLimit-Limit')).toBe(true)
|
|
299
|
+
expect(hit.headers.has('RateLimit-Limit')).toBe(false)
|
|
300
|
+
expect(body.data.map((token) => token.address)).toEqual([TestApp.tokenWithHolders])
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
test('separates repeated includes in the response cache', async () => {
|
|
304
|
+
const routed = TestApp.create({ db })
|
|
305
|
+
const headers = { authorization: `Bearer ${TestApp.key.token}` }
|
|
306
|
+
const repeated = new URLSearchParams({ limit: '5' })
|
|
307
|
+
repeated.append('include', 'createdAt')
|
|
308
|
+
repeated.append('include', 'holderCount')
|
|
309
|
+
const single = new URLSearchParams({
|
|
310
|
+
include: 'holderCount',
|
|
311
|
+
limit: '5',
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
const primed = await routed.request(`/v1/tokens?${repeated}`, { headers })
|
|
315
|
+
const miss = await routed.request(`/v1/tokens?${single}`, { headers })
|
|
316
|
+
const hit = await routed.request(`/v1/tokens?${single}`, { headers })
|
|
317
|
+
const primedBody = await TestApp.json(primed, Tokens.schema.getTokens.Response)
|
|
318
|
+
const body = await TestApp.json(hit, Tokens.schema.getTokens.Response)
|
|
319
|
+
|
|
320
|
+
expect(primed.status).toBe(200)
|
|
321
|
+
expect(miss.status).toBe(200)
|
|
322
|
+
expect(hit.status).toBe(200)
|
|
323
|
+
expect(primedBody.data.every((token) => typeof token.createdAt === 'string')).toBe(true)
|
|
324
|
+
expect(miss.headers.has('RateLimit-Limit')).toBe(true)
|
|
325
|
+
expect(hit.headers.has('RateLimit-Limit')).toBe(false)
|
|
326
|
+
expect(body.data.every((token) => token.createdAt === undefined)).toBe(true)
|
|
327
|
+
})
|
|
328
|
+
|
|
275
329
|
test('returns the requested tokens as a single page in input order', async () => {
|
|
276
330
|
const client = app()
|
|
277
331
|
const response = await client.v1.tokens.$get(
|
|
@@ -271,7 +271,24 @@ describe('GET /transfers', () => {
|
|
|
271
271
|
},
|
|
272
272
|
)
|
|
273
273
|
|
|
274
|
-
test('embeds a capped total count on demand
|
|
274
|
+
test('embeds a capped unscoped total count on demand', async () => {
|
|
275
|
+
const client = TestApp.client()
|
|
276
|
+
const response = await client.v1.transfers.$get(
|
|
277
|
+
{ query: { include: 'totalCount', limit: '5' } },
|
|
278
|
+
TestApp.auth,
|
|
279
|
+
)
|
|
280
|
+
const body = await TestApp.json(response, Transfers.schema.getTransfers.Response)
|
|
281
|
+
|
|
282
|
+
expect(response.status).toBe(200)
|
|
283
|
+
expect(response.headers.get('server-timing')?.includes('transfers_count;dur=')).toBe(true)
|
|
284
|
+
expect(body.meta).toBeDefined()
|
|
285
|
+
expect(Number.isInteger(body.meta?.totalCount)).toBe(true)
|
|
286
|
+
expect(body.meta!.totalCount).toBeGreaterThanOrEqual(body.data.length)
|
|
287
|
+
expect(body.meta!.totalCount).toBeLessThanOrEqual(Schema.countCap)
|
|
288
|
+
expect(body.meta!.totalCountCapped).toBe(body.meta!.totalCount >= Schema.countCap)
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
test('embeds a capped token-scoped total count on demand', async () => {
|
|
275
292
|
const client = TestApp.client()
|
|
276
293
|
|
|
277
294
|
if (Runtime.get().mode === 'localnet') expect(TestApp.token).toBeDefined()
|
|
@@ -350,6 +367,61 @@ describe('GET /transfers', () => {
|
|
|
350
367
|
},
|
|
351
368
|
)
|
|
352
369
|
|
|
370
|
+
test.skipIf(Runtime.get().mode !== 'localnet')(
|
|
371
|
+
'counts unscoped cross-token transfers',
|
|
372
|
+
async () => {
|
|
373
|
+
const crossToken = Runtime.get().fixtures?.crossTokenTransfer
|
|
374
|
+
expect(crossToken).toBeDefined()
|
|
375
|
+
if (!crossToken) return
|
|
376
|
+
|
|
377
|
+
const client = TestApp.client()
|
|
378
|
+
const response = await client.v1.transfers.$get(
|
|
379
|
+
{
|
|
380
|
+
query: {
|
|
381
|
+
address: crossToken.sender,
|
|
382
|
+
include: 'crossToken,totalCount',
|
|
383
|
+
limit: '50',
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
TestApp.auth,
|
|
387
|
+
)
|
|
388
|
+
const body = await TestApp.json(response, Transfers.schema.getTransfers.Response)
|
|
389
|
+
|
|
390
|
+
expect(response.status).toBe(200)
|
|
391
|
+
expect(response.headers.get('server-timing')?.includes('transfers_count;dur=')).toBe(true)
|
|
392
|
+
expect(body.data.some((entry) => entry.transactionHash === crossToken.hash)).toBe(true)
|
|
393
|
+
expect(body.meta).toBeDefined()
|
|
394
|
+
expect(body.meta!.totalCount).toBeGreaterThanOrEqual(body.data.length)
|
|
395
|
+
},
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
test.skipIf(Runtime.get().mode !== 'localnet')(
|
|
399
|
+
'counts token-scoped cross-token transfers',
|
|
400
|
+
async () => {
|
|
401
|
+
const crossToken = Runtime.get().fixtures?.crossTokenTransfer
|
|
402
|
+
expect(crossToken).toBeDefined()
|
|
403
|
+
if (!crossToken) return
|
|
404
|
+
|
|
405
|
+
const client = TestApp.client()
|
|
406
|
+
const response = await client.v1.transfers.$get(
|
|
407
|
+
{
|
|
408
|
+
query: {
|
|
409
|
+
include: 'crossToken,totalCount',
|
|
410
|
+
limit: '50',
|
|
411
|
+
token: crossToken.destinationToken,
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
TestApp.auth,
|
|
415
|
+
)
|
|
416
|
+
const body = await TestApp.json(response, Transfers.schema.getTransfers.Response)
|
|
417
|
+
|
|
418
|
+
expect(response.status).toBe(200)
|
|
419
|
+
expect(response.headers.get('server-timing')?.includes('transfers_count;dur=')).toBe(true)
|
|
420
|
+
expect(body.meta).toBeDefined()
|
|
421
|
+
expect(body.meta!.totalCount).toBeGreaterThanOrEqual(body.data.length)
|
|
422
|
+
},
|
|
423
|
+
)
|
|
424
|
+
|
|
353
425
|
test('surfaces a 502 when transfer data is unavailable', async () => {
|
|
354
426
|
// Point at an unreachable indexer so the upstream query fails deterministically.
|
|
355
427
|
const client = TestApp.client({ tidx: { baseUrl: 'http://127.0.0.1:1' } })
|