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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Schema } from 'tapimo'
|
|
2
2
|
|
|
3
3
|
import type * as FundingQuotes from '../../../internal/FundingQuotes.js'
|
|
4
|
+
import type * as Log from '../../../internal/Log.js'
|
|
4
5
|
import * as TestApp from '../../../../test/App.js'
|
|
5
6
|
import * as Funding from './funding.js'
|
|
6
7
|
|
|
@@ -27,22 +28,59 @@ describe('GET /funding/quotes', () => {
|
|
|
27
28
|
})
|
|
28
29
|
|
|
29
30
|
test('queries only the requested provider', async () => {
|
|
30
|
-
let
|
|
31
|
+
let acrossCalls = 0
|
|
32
|
+
let relayCalls = 0
|
|
31
33
|
const app = TestApp.create({
|
|
32
34
|
funding: {
|
|
33
35
|
adapters: {
|
|
36
|
+
across: acrossAdapter(() => {
|
|
37
|
+
acrossCalls += 1
|
|
38
|
+
}),
|
|
34
39
|
relay: relayAdapter(() => {
|
|
35
|
-
|
|
40
|
+
relayCalls += 1
|
|
36
41
|
}),
|
|
37
42
|
},
|
|
38
43
|
},
|
|
39
44
|
})
|
|
40
|
-
const response = await app.request(quoteUrl({ ...baseUsdcQuery, provider: '
|
|
45
|
+
const response = await app.request(quoteUrl({ ...baseUsdcQuery, provider: 'across' }), {
|
|
41
46
|
headers: TestApp.auth.headers,
|
|
42
47
|
})
|
|
43
48
|
|
|
44
49
|
expect(response.status).toBe(200)
|
|
45
|
-
expect(
|
|
50
|
+
expect(acrossCalls).toBe(1)
|
|
51
|
+
expect(relayCalls).toBe(0)
|
|
52
|
+
const body = await TestApp.json(response, Funding.schema.quoteFunding.Response)
|
|
53
|
+
expect(body.data[0]?.quote.expiresAt).toBe('2026-01-01T00:01:00Z')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('returns non-EVM Rhino quotes with stable chain and token ids', async () => {
|
|
57
|
+
const app = TestApp.create({ funding: { adapters: { rhino: rhinoAdapter() } } })
|
|
58
|
+
const response = await app.request(
|
|
59
|
+
quoteUrl({
|
|
60
|
+
destinationToken: 'USDC.e',
|
|
61
|
+
provider: 'rhino',
|
|
62
|
+
sourceAmount: '1000000',
|
|
63
|
+
sourceChain: 'solana',
|
|
64
|
+
sourceToken: 'USDC',
|
|
65
|
+
}),
|
|
66
|
+
{ headers: TestApp.auth.headers },
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
expect(response.status).toBe(200)
|
|
70
|
+
const body = await TestApp.json(response, Funding.schema.quoteFunding.Response)
|
|
71
|
+
expect(body.data[0]).toMatchObject({
|
|
72
|
+
id: 'solana-usdc-tempo-usdce-rhino',
|
|
73
|
+
provider: { id: 'rhino' },
|
|
74
|
+
sourceChain: {
|
|
75
|
+
addressFormat: 'base58',
|
|
76
|
+
id: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
|
|
77
|
+
kind: 'solana',
|
|
78
|
+
},
|
|
79
|
+
sourceToken: {
|
|
80
|
+
tokenKey:
|
|
81
|
+
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
|
|
82
|
+
},
|
|
83
|
+
})
|
|
46
84
|
})
|
|
47
85
|
|
|
48
86
|
test('returns an empty list when no provider is configured', async () => {
|
|
@@ -90,6 +128,72 @@ describe('GET /funding/quotes', () => {
|
|
|
90
128
|
expect(body.error.code).toBe('upstream_error')
|
|
91
129
|
})
|
|
92
130
|
|
|
131
|
+
test('logs provider quote deadlines as timeouts', async () => {
|
|
132
|
+
const entries: Log.Entry[] = []
|
|
133
|
+
const app = TestApp.create({
|
|
134
|
+
funding: {
|
|
135
|
+
adapterTimeoutMs: 1,
|
|
136
|
+
adapters: {
|
|
137
|
+
across: {
|
|
138
|
+
probeQuote() {
|
|
139
|
+
return new Promise<FundingQuotes.QuoteProbeResult>(() => {})
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
logger: (entry) => {
|
|
145
|
+
entries.push(entry)
|
|
146
|
+
},
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const response = await app.request(quoteUrl({ ...baseUsdcQuery, provider: 'across' }), {
|
|
150
|
+
headers: TestApp.auth.headers,
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
expect(response.status).toBe(502)
|
|
154
|
+
expect(entries[0]?.provider).toEqual({
|
|
155
|
+
failure: 'timeout',
|
|
156
|
+
id: 'across',
|
|
157
|
+
operation: 'swap/approval',
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test('logs every failure when multiple providers fail', async () => {
|
|
162
|
+
const entries: Log.Entry[] = []
|
|
163
|
+
const app = TestApp.create({
|
|
164
|
+
funding: {
|
|
165
|
+
adapters: {
|
|
166
|
+
across: {
|
|
167
|
+
async probeQuote() {
|
|
168
|
+
throw new TypeError('Across network failure')
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
relay: {
|
|
172
|
+
async probeQuote() {
|
|
173
|
+
throw new Error('Relay failure')
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
logger: (entry) => {
|
|
179
|
+
entries.push(entry)
|
|
180
|
+
},
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
const response = await app.request(quoteUrl(), { headers: TestApp.auth.headers })
|
|
184
|
+
|
|
185
|
+
expect(response.status).toBe(502)
|
|
186
|
+
expect(entries[0]?.provider).toEqual({
|
|
187
|
+
failure: 'network',
|
|
188
|
+
id: 'across',
|
|
189
|
+
operation: 'swap/approval',
|
|
190
|
+
})
|
|
191
|
+
expect(entries[0]?.providerFailures).toEqual([
|
|
192
|
+
{ failure: 'network', id: 'across', operation: 'swap/approval' },
|
|
193
|
+
{ failure: 'unknown', id: 'relay', operation: 'quote/v2' },
|
|
194
|
+
])
|
|
195
|
+
})
|
|
196
|
+
|
|
93
197
|
test('rejects successful provider results without output amounts', async () => {
|
|
94
198
|
const app = TestApp.create({
|
|
95
199
|
funding: {
|
|
@@ -126,9 +230,9 @@ describe('GET /funding/quotes', () => {
|
|
|
126
230
|
expect(body.error.code).toBe('query_invalid')
|
|
127
231
|
})
|
|
128
232
|
|
|
129
|
-
test('rejects source chains
|
|
233
|
+
test('rejects unsupported source chains', async () => {
|
|
130
234
|
const response = await TestApp.create().request(
|
|
131
|
-
quoteUrl({ ...baseUsdcQuery, sourceChain: '
|
|
235
|
+
quoteUrl({ ...baseUsdcQuery, sourceChain: 'bitcoin' }),
|
|
132
236
|
{ headers: TestApp.auth.headers },
|
|
133
237
|
)
|
|
134
238
|
expect(response.status).toBe(400)
|
|
@@ -151,7 +255,13 @@ describe('GET /funding/chains', () => {
|
|
|
151
255
|
expect(response.status).toBe(200)
|
|
152
256
|
const body = await TestApp.json(response, Funding.schema.listFundingChains.Response)
|
|
153
257
|
|
|
154
|
-
expect(body.data.map((chain) => chain.id)).toEqual([
|
|
258
|
+
expect(body.data.map((chain) => chain.id)).toEqual([
|
|
259
|
+
'eip155:1',
|
|
260
|
+
'eip155:8453',
|
|
261
|
+
'eip155:56',
|
|
262
|
+
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
|
|
263
|
+
'tron:0x2b6653dc',
|
|
264
|
+
])
|
|
155
265
|
expect(body.data.flatMap((chain) => chain.tokens.map((token) => token.symbol))).toEqual(
|
|
156
266
|
expect.arrayContaining(['USDC', 'USDT']),
|
|
157
267
|
)
|
|
@@ -168,6 +278,13 @@ describe('GET /funding/providers', () => {
|
|
|
168
278
|
const body = await TestApp.json(response, Funding.schema.listProviders.Response)
|
|
169
279
|
|
|
170
280
|
expect(body.data).toEqual([
|
|
281
|
+
{
|
|
282
|
+
id: 'across',
|
|
283
|
+
kind: 'intentBridge',
|
|
284
|
+
name: 'Across',
|
|
285
|
+
requiresApiKey: true,
|
|
286
|
+
status: 'unconfigured',
|
|
287
|
+
},
|
|
171
288
|
{
|
|
172
289
|
id: 'relay',
|
|
173
290
|
kind: 'solverNetwork',
|
|
@@ -175,6 +292,20 @@ describe('GET /funding/providers', () => {
|
|
|
175
292
|
requiresApiKey: false,
|
|
176
293
|
status: 'configured',
|
|
177
294
|
},
|
|
295
|
+
{
|
|
296
|
+
id: 'rhino',
|
|
297
|
+
kind: 'bridge',
|
|
298
|
+
name: 'Rhino',
|
|
299
|
+
requiresApiKey: false,
|
|
300
|
+
status: 'unconfigured',
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
id: 'squid',
|
|
304
|
+
kind: 'solverNetwork',
|
|
305
|
+
name: 'Squid',
|
|
306
|
+
requiresApiKey: true,
|
|
307
|
+
status: 'unconfigured',
|
|
308
|
+
},
|
|
178
309
|
])
|
|
179
310
|
})
|
|
180
311
|
|
|
@@ -222,3 +353,46 @@ function relayAdapter(onProbe: () => void = () => {}): FundingQuotes.ProviderAda
|
|
|
222
353
|
},
|
|
223
354
|
}
|
|
224
355
|
}
|
|
356
|
+
|
|
357
|
+
function acrossAdapter(onProbe: () => void = () => {}): FundingQuotes.ProviderAdapter {
|
|
358
|
+
return {
|
|
359
|
+
async probeQuote(input, signal) {
|
|
360
|
+
onProbe()
|
|
361
|
+
expect(signal.aborted).toBe(false)
|
|
362
|
+
expect(input.candidate.id).toBe('base-usdc-tempo-usdce-across')
|
|
363
|
+
return {
|
|
364
|
+
expiresAt: '2026-01-01T00:01:00Z',
|
|
365
|
+
minimumOutputAmount: '988000',
|
|
366
|
+
outputAmount: '990000',
|
|
367
|
+
outputAmountFormatted: '0.99',
|
|
368
|
+
quality: {
|
|
369
|
+
estimatedSeconds: 2,
|
|
370
|
+
liquiditySource: 'providerQuote',
|
|
371
|
+
sourceDetail: 'across:swap-approval',
|
|
372
|
+
tier: 'liquid',
|
|
373
|
+
},
|
|
374
|
+
sampledAt: '2026-01-01T00:00:00Z',
|
|
375
|
+
status: 'available',
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function rhinoAdapter(): FundingQuotes.ProviderAdapter {
|
|
382
|
+
return {
|
|
383
|
+
async probeQuote(input, signal) {
|
|
384
|
+
expect(signal.aborted).toBe(false)
|
|
385
|
+
expect(input.candidate.id).toBe('solana-usdc-tempo-usdce-rhino')
|
|
386
|
+
return {
|
|
387
|
+
outputAmount: '983000',
|
|
388
|
+
quality: {
|
|
389
|
+
liquiditySource: 'providerQuote',
|
|
390
|
+
sourceDetail: 'rhino:bridge-swap-public',
|
|
391
|
+
tier: 'liquid',
|
|
392
|
+
},
|
|
393
|
+
sampledAt: '2026-01-01T00:00:00Z',
|
|
394
|
+
status: 'available',
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
}
|
|
398
|
+
}
|
|
@@ -8,6 +8,7 @@ import * as FundingQuotes from '../../../internal/FundingQuotes.js'
|
|
|
8
8
|
import * as OpenApi from '../../../internal/OpenApi.js'
|
|
9
9
|
import * as Response from '../../../internal/Response.js'
|
|
10
10
|
import * as Schema from '../../../internal/Schema.js'
|
|
11
|
+
import * as FundingQuoteProviders from '../FundingQuoteProviders.js'
|
|
11
12
|
|
|
12
13
|
/** Zod schemas owned by funding quote resources. */
|
|
13
14
|
export namespace schema {
|
|
@@ -15,7 +16,7 @@ export namespace schema {
|
|
|
15
16
|
export const Chain = Schema.describe(
|
|
16
17
|
z.object({
|
|
17
18
|
addressFormat: z
|
|
18
|
-
.
|
|
19
|
+
.enum(['base58', 'base58check', 'evm'])
|
|
19
20
|
.check(
|
|
20
21
|
z.describe('Address family used by accounts and token identifiers on this chain.'),
|
|
21
22
|
z.meta({ examples: ['evm'] }),
|
|
@@ -24,12 +25,12 @@ export namespace schema {
|
|
|
24
25
|
.string()
|
|
25
26
|
.check(
|
|
26
27
|
z.describe(
|
|
27
|
-
'Stable
|
|
28
|
+
'Stable chain id. Uses CAIP-2 where standardized and a Tempo-defined compatible id otherwise.',
|
|
28
29
|
),
|
|
29
30
|
z.meta({ examples: ['eip155:8453'] }),
|
|
30
31
|
),
|
|
31
32
|
kind: z
|
|
32
|
-
.
|
|
33
|
+
.enum(['evm', 'solana', 'tron'])
|
|
33
34
|
.check(
|
|
34
35
|
z.describe('Chain execution family used for provider routing.'),
|
|
35
36
|
z.meta({ examples: ['evm'] }),
|
|
@@ -41,7 +42,7 @@ export namespace schema {
|
|
|
41
42
|
.optional(z.record(z.string(), z.string()))
|
|
42
43
|
.check(
|
|
43
44
|
z.describe('Provider-specific chain ids Tempo uses when requesting quotes.'),
|
|
44
|
-
z.meta({ examples: [{ relay: '8453' }] }),
|
|
45
|
+
z.meta({ examples: [{ across: '8453', relay: '8453', rhino: 'BASE' }] }),
|
|
45
46
|
),
|
|
46
47
|
}),
|
|
47
48
|
'A normalized chain reference for funding quotes.',
|
|
@@ -117,7 +118,7 @@ export namespace schema {
|
|
|
117
118
|
export const ProviderRef = Schema.describe(
|
|
118
119
|
z.object({
|
|
119
120
|
id: z
|
|
120
|
-
.
|
|
121
|
+
.enum(['across', 'relay', 'rhino', 'squid'])
|
|
121
122
|
.check(z.describe('Stable provider id.'), z.meta({ examples: ['relay'] })),
|
|
122
123
|
kind: z
|
|
123
124
|
.string()
|
|
@@ -172,6 +173,12 @@ export namespace schema {
|
|
|
172
173
|
/** Provider-derived output, limits, and freshness data. */
|
|
173
174
|
export const QuoteDetails = Schema.describe(
|
|
174
175
|
z.object({
|
|
176
|
+
expiresAt: z
|
|
177
|
+
.optional(z.iso.datetime())
|
|
178
|
+
.check(
|
|
179
|
+
z.describe('Timestamp after which the provider quote is no longer valid.'),
|
|
180
|
+
z.meta({ examples: ['2026-01-01T00:01:00Z'] }),
|
|
181
|
+
),
|
|
175
182
|
inputAmount: Schema.DecimalString.check(
|
|
176
183
|
z.describe('Source amount sent to the provider in base units.'),
|
|
177
184
|
z.meta({ examples: ['1000000'] }),
|
|
@@ -230,7 +237,7 @@ export namespace schema {
|
|
|
230
237
|
export const Provider = Schema.describe(
|
|
231
238
|
z.object({
|
|
232
239
|
id: z
|
|
233
|
-
.
|
|
240
|
+
.enum(['across', 'relay', 'rhino', 'squid'])
|
|
234
241
|
.check(z.describe('Stable provider id.'), z.meta({ examples: ['relay'] })),
|
|
235
242
|
kind: z
|
|
236
243
|
.string()
|
|
@@ -278,10 +285,10 @@ export namespace schema {
|
|
|
278
285
|
z.meta({ examples: ['USDC.e'] }),
|
|
279
286
|
),
|
|
280
287
|
provider: z
|
|
281
|
-
.optional(z.
|
|
288
|
+
.optional(z.enum(['across', 'relay', 'rhino', 'squid']))
|
|
282
289
|
.check(
|
|
283
290
|
z.describe(
|
|
284
|
-
'Only request a quote from this provider.
|
|
291
|
+
'Only request a quote from this provider. Supported ids are `across`, `relay`, `rhino`, and `squid`; omit it to query every configured provider.',
|
|
285
292
|
),
|
|
286
293
|
z.meta({ examples: ['relay'] }),
|
|
287
294
|
),
|
|
@@ -304,7 +311,9 @@ export namespace schema {
|
|
|
304
311
|
.string()
|
|
305
312
|
.check(
|
|
306
313
|
z.minLength(1),
|
|
307
|
-
z.describe(
|
|
314
|
+
z.describe(
|
|
315
|
+
'Source chain alias or stable id, such as `base`, `solana`, or `eip155:8453`.',
|
|
316
|
+
),
|
|
308
317
|
z.meta({ examples: ['base', 'eip155:8453'] }),
|
|
309
318
|
),
|
|
310
319
|
sourceToken: z
|
|
@@ -312,7 +321,7 @@ export namespace schema {
|
|
|
312
321
|
.check(
|
|
313
322
|
z.minLength(1),
|
|
314
323
|
z.describe(
|
|
315
|
-
'Source token symbol, contract address, or Tempo token key.
|
|
324
|
+
'Source token symbol, contract address, or Tempo token key. Lookup is scoped to `sourceChain`; non-EVM addresses are case-sensitive. An unknown token returns an empty list.',
|
|
316
325
|
),
|
|
317
326
|
z.meta({ examples: ['USDC', '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913'] }),
|
|
318
327
|
),
|
|
@@ -414,7 +423,8 @@ export function funding(options: funding.Options = {}) {
|
|
|
414
423
|
400: { codes: ['query_invalid'] },
|
|
415
424
|
502: {
|
|
416
425
|
codes: ['upstream_error'],
|
|
417
|
-
description:
|
|
426
|
+
description:
|
|
427
|
+
'No quote succeeded and at least one matching provider failed unexpectedly.',
|
|
418
428
|
},
|
|
419
429
|
},
|
|
420
430
|
success: {
|
|
@@ -456,12 +466,17 @@ export function funding(options: funding.Options = {}) {
|
|
|
456
466
|
} catch (cause) {
|
|
457
467
|
const message = requestErrorMessage(cause)
|
|
458
468
|
if (message) return Response.error(c, { code: 'query_invalid', message, status: 400 })
|
|
459
|
-
if (cause instanceof FundingQuotes.ProviderQuoteError)
|
|
469
|
+
if (cause instanceof FundingQuotes.ProviderQuoteError) {
|
|
470
|
+
const failures = cause.failures.map(FundingQuoteProviders.providerFailure)
|
|
471
|
+
const failure = failures[0]
|
|
472
|
+
if (failure) c.set('providerFailure', failure)
|
|
473
|
+
if (failures.length > 1) c.set('providerFailures', failures)
|
|
460
474
|
return Response.error(c, {
|
|
461
475
|
code: 'upstream_error',
|
|
462
476
|
message: 'Quote providers could not complete the request.',
|
|
463
477
|
status: 502,
|
|
464
478
|
})
|
|
479
|
+
}
|
|
465
480
|
throw cause
|
|
466
481
|
}
|
|
467
482
|
},
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Challenge } from 'mppx'
|
|
2
2
|
import { sessionManager } from 'mppx/client'
|
|
3
|
-
import {
|
|
3
|
+
import { ZoneRpcAuthentication } from 'ox/tempo'
|
|
4
4
|
import { Actions } from 'viem/tempo'
|
|
5
5
|
import * as TestApp from '../../../../test/App.js'
|
|
6
6
|
import * as Mppx from '../../../../test/Mppx.js'
|
|
7
7
|
import * as Runtime from '../../../../test/runtime.js'
|
|
8
8
|
import * as Tempo from '../../../../test/Tempo.js'
|
|
9
|
+
import * as Auth from '../../../internal/Auth.js'
|
|
9
10
|
import * as Tidx from '../../../internal/Tidx.js'
|
|
10
11
|
import * as Viem from '../../../internal/Viem.js'
|
|
11
12
|
import * as Indexer from './indexer.js'
|
|
@@ -83,13 +84,52 @@ describe('GET /indexer/query', () => {
|
|
|
83
84
|
})
|
|
84
85
|
const response = await client.v1.indexer.query.$get(
|
|
85
86
|
{ query: { chainId: String(zone), sql: 'select 1' } },
|
|
86
|
-
{
|
|
87
|
+
{
|
|
88
|
+
headers: {
|
|
89
|
+
authorization: `Bearer ${zoneReader.token}`,
|
|
90
|
+
[ZoneRpcAuthentication.headerName]: 'zone-rpc-secret',
|
|
91
|
+
},
|
|
92
|
+
},
|
|
87
93
|
)
|
|
88
94
|
const upstream = fetch.mock.calls[0]![0] as Request
|
|
89
95
|
|
|
90
96
|
expect(response.status).toBe(200)
|
|
91
97
|
expect(new URL(upstream.url).origin).toBe('https://zone.tidx.test')
|
|
92
98
|
expect(upstream.headers.get('authorization')).toBe('Basic em9uZS1zZWNyZXQ=')
|
|
99
|
+
expect(upstream.headers.get(ZoneRpcAuthentication.headerName)).toBeNull()
|
|
100
|
+
} finally {
|
|
101
|
+
fetch.mockRestore()
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('routes Zone queries to the Zone TIDX with its bearer auth', async () => {
|
|
106
|
+
const zone = runtime.zone.chainId
|
|
107
|
+
const zoneReader = {
|
|
108
|
+
id: 'key_zone_bearer_indexer',
|
|
109
|
+
orgId: 'org_test',
|
|
110
|
+
scopes: ['data:read', 'indexer:query', `zone:${zone}:read`],
|
|
111
|
+
token: 'secret_zone_bearer_indexer',
|
|
112
|
+
} satisfies TestApp.kvStore.Key
|
|
113
|
+
const fetch = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
114
|
+
new Response(JSON.stringify({ columns: [], ok: true, row_count: 0, rows: [] }), {
|
|
115
|
+
headers: { 'content-type': 'application/json' },
|
|
116
|
+
}),
|
|
117
|
+
)
|
|
118
|
+
try {
|
|
119
|
+
const client = TestApp.client({
|
|
120
|
+
auth: { keys: [zoneReader] },
|
|
121
|
+
tidx: { baseUrl: 'https://zone.tidx.test', bearerAuth: 'zone-secret' },
|
|
122
|
+
zones: [{ chainId: zone, forwardRpcUrl: runtime.zone.forwardRpcUrl }],
|
|
123
|
+
})
|
|
124
|
+
const response = await client.v1.indexer.query.$get(
|
|
125
|
+
{ query: { chainId: String(zone), sql: 'select 1' } },
|
|
126
|
+
{ headers: { authorization: `Bearer ${zoneReader.token}` } },
|
|
127
|
+
)
|
|
128
|
+
const upstream = fetch.mock.calls[0]![0] as Request
|
|
129
|
+
|
|
130
|
+
expect(response.status).toBe(200)
|
|
131
|
+
expect(new URL(upstream.url).origin).toBe('https://zone.tidx.test')
|
|
132
|
+
expect(upstream.headers.get('authorization')).toBe('Bearer zone-secret')
|
|
93
133
|
} finally {
|
|
94
134
|
fetch.mockRestore()
|
|
95
135
|
}
|
|
@@ -156,6 +196,20 @@ describe('GET /indexer/query', () => {
|
|
|
156
196
|
expect(response.status).toMatchInlineSnapshot(`401`)
|
|
157
197
|
})
|
|
158
198
|
|
|
199
|
+
test('does not expose anonymous access', () => {
|
|
200
|
+
expect(Auth.describeAccess(Indexer.indexer())['indexerQuery']).toMatchInlineSnapshot(`
|
|
201
|
+
{
|
|
202
|
+
"apiKey": true,
|
|
203
|
+
"mpp": true,
|
|
204
|
+
"public": false,
|
|
205
|
+
"scopes": [
|
|
206
|
+
"indexer:query",
|
|
207
|
+
],
|
|
208
|
+
"session": false,
|
|
209
|
+
}
|
|
210
|
+
`)
|
|
211
|
+
})
|
|
212
|
+
|
|
159
213
|
test('proxies without auth middleware', async () => {
|
|
160
214
|
const client = TestApp.client({ auth: false })
|
|
161
215
|
const response = await client.v1.indexer.query.$get({
|
|
@@ -167,10 +221,8 @@ describe('GET /indexer/query', () => {
|
|
|
167
221
|
expect(body.ok).toMatchInlineSnapshot(`true`)
|
|
168
222
|
})
|
|
169
223
|
|
|
170
|
-
test('
|
|
171
|
-
const rateLimitStore = Store.memory()
|
|
224
|
+
test('challenges anonymous callers for MPP payment', async () => {
|
|
172
225
|
const app = TestApp.create({
|
|
173
|
-
rateLimit: { store: rateLimitStore },
|
|
174
226
|
mpp: {
|
|
175
227
|
secretKey: 'secret_test_key_0123456789abcdef',
|
|
176
228
|
session: {
|
|
@@ -182,13 +234,9 @@ describe('GET /indexer/query', () => {
|
|
|
182
234
|
},
|
|
183
235
|
},
|
|
184
236
|
})
|
|
185
|
-
await exhaustQuota(RateLimit.memory({ store: rateLimitStore }), 'public:anonymous', {
|
|
186
|
-
perMinute: 60,
|
|
187
|
-
})
|
|
188
237
|
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
// through `app.fetch` rather than the typed route client.
|
|
238
|
+
// MPP challenges and receipts ride on real HTTP headers, so drive the
|
|
239
|
+
// lifecycle through `app.fetch` rather than the typed route client.
|
|
192
240
|
const challenge = await app.fetch(
|
|
193
241
|
new Request('http://tempo-api.test/v1/indexer/query?sql=select%201'),
|
|
194
242
|
)
|
|
@@ -209,10 +257,31 @@ describe('GET /indexer/query', () => {
|
|
|
209
257
|
expect(body.ok).toMatchInlineSnapshot(`true`)
|
|
210
258
|
})
|
|
211
259
|
|
|
260
|
+
test('rejects an unsupported chain before issuing an MPP challenge', async () => {
|
|
261
|
+
const app = TestApp.create({
|
|
262
|
+
mpp: {
|
|
263
|
+
secretKey: 'secret_test_key_0123456789abcdef',
|
|
264
|
+
session: {
|
|
265
|
+
chainId: Tempo.chain.id,
|
|
266
|
+
currency: Tempo.currency,
|
|
267
|
+
decimals: 6,
|
|
268
|
+
getClient: () => Tempo.client,
|
|
269
|
+
recipient: Tempo.accounts[2].address,
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
})
|
|
273
|
+
const response = await app.fetch(
|
|
274
|
+
new Request('http://tempo-api.test/v1/indexer/query?sql=select%201&chainId=31318'),
|
|
275
|
+
)
|
|
276
|
+
const body = (await response.json()) as { error: { code: string } }
|
|
277
|
+
|
|
278
|
+
expect(response.status).toBe(400)
|
|
279
|
+
expect(response.headers.has('www-authenticate')).toBe(false)
|
|
280
|
+
expect(body.error.code).toBe('chain_id_unsupported')
|
|
281
|
+
})
|
|
282
|
+
|
|
212
283
|
test('accepts session management POSTs', { timeout: 120_000 }, async () => {
|
|
213
|
-
const rateLimitStore = Store.memory()
|
|
214
284
|
const app = TestApp.create({
|
|
215
|
-
rateLimit: { store: rateLimitStore },
|
|
216
285
|
mpp: {
|
|
217
286
|
secretKey: 'secret_test_key_0123456789abcdef',
|
|
218
287
|
session: {
|
|
@@ -225,9 +294,6 @@ describe('GET /indexer/query', () => {
|
|
|
225
294
|
},
|
|
226
295
|
},
|
|
227
296
|
})
|
|
228
|
-
await exhaustQuota(RateLimit.memory({ store: rateLimitStore }), 'public:anonymous', {
|
|
229
|
-
perMinute: 60,
|
|
230
|
-
})
|
|
231
297
|
await Actions.faucet.fundSync(Tempo.client, {
|
|
232
298
|
account: Tempo.accounts[2],
|
|
233
299
|
timeout: 60_000,
|
|
@@ -264,7 +330,3 @@ describe('GET /indexer/query', () => {
|
|
|
264
330
|
expect(state.deposit).toMatchInlineSnapshot(`0n`)
|
|
265
331
|
})
|
|
266
332
|
})
|
|
267
|
-
|
|
268
|
-
async function exhaustQuota(rateLimit: RateLimit.Store, key: string, limit: RateLimit.Limit) {
|
|
269
|
-
for (let index = 0; index < limit.perMinute; index++) await rateLimit.consume({ key, limit })
|
|
270
|
-
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Hono, type Context, type TypedResponse } from 'hono'
|
|
2
|
+
import { ZoneRpcAuthentication } from 'ox/tempo'
|
|
2
3
|
import * as z from 'zod/mini'
|
|
3
4
|
|
|
4
5
|
import type * as App from '../../../App.js'
|
|
@@ -25,6 +26,7 @@ const strippedHeaders = new Set([
|
|
|
25
26
|
'transfer-encoding',
|
|
26
27
|
'upgrade',
|
|
27
28
|
'tempo-api-key',
|
|
29
|
+
ZoneRpcAuthentication.headerName.toLowerCase(),
|
|
28
30
|
])
|
|
29
31
|
|
|
30
32
|
/** Never reached; carries typed passthrough response variants into Hono client inference. */
|
|
@@ -99,7 +101,7 @@ export function indexer() {
|
|
|
99
101
|
|
|
100
102
|
return new Hono<App.Environment>().get(
|
|
101
103
|
'/v1/indexer/query',
|
|
102
|
-
Auth.policy({ apiKey: { scopes: ['indexer:query'] }, mpp: true
|
|
104
|
+
Auth.policy({ apiKey: { scopes: ['indexer:query'] }, mpp: true }),
|
|
103
105
|
OpenApi.describeRoute({
|
|
104
106
|
description:
|
|
105
107
|
'Runs a read-only SQL-style query against Tempo’s indexed chain data, with optional live streaming as new blocks arrive.',
|
|
@@ -256,7 +258,10 @@ async function passthrough(
|
|
|
256
258
|
// Resolve the upstream for this caller. A resolver `tidx` can route by
|
|
257
259
|
// principal (e.g. anonymous `type: 'public'` callers to a public, no-auth
|
|
258
260
|
// indexer); a static config ignores the principal.
|
|
259
|
-
const { baseUrl, basicAuth } = Tidx.resolve(tidx, {
|
|
261
|
+
const { baseUrl, basicAuth, bearerAuth } = Tidx.resolve(tidx, {
|
|
262
|
+
chainId,
|
|
263
|
+
principal: Auth.getPrincipal(c),
|
|
264
|
+
})
|
|
260
265
|
// The chain id is syntactically valid but this deployment has no indexer
|
|
261
266
|
// upstream configured for it (no `tidx` config and no built-in default).
|
|
262
267
|
if (!baseUrl)
|
|
@@ -290,6 +295,7 @@ async function passthrough(
|
|
|
290
295
|
for (const [name, value] of c.req.raw.headers)
|
|
291
296
|
if (!strippedHeaders.has(name.toLowerCase())) headers.set(name, value)
|
|
292
297
|
if (basicAuth) headers.set('authorization', `Basic ${btoa(basicAuth)}`)
|
|
298
|
+
else if (bearerAuth) headers.set('authorization', `Bearer ${bearerAuth}`)
|
|
293
299
|
|
|
294
300
|
const method = c.req.method
|
|
295
301
|
|
|
@@ -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(
|