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.
Files changed (197) hide show
  1. package/dist/App.d.ts +4854 -4829
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +20 -2
  4. package/dist/App.js.map +1 -1
  5. package/dist/Assets.js +3 -3
  6. package/dist/Assets.js.map +1 -1
  7. package/dist/Scope.d.ts.map +1 -1
  8. package/dist/Scope.js +1 -2
  9. package/dist/Scope.js.map +1 -1
  10. package/dist/Webhooks.d.ts +1 -1
  11. package/dist/Webhooks.d.ts.map +1 -1
  12. package/dist/Webhooks.js +5 -10
  13. package/dist/Webhooks.js.map +1 -1
  14. package/dist/admin/App.d.ts +6 -34
  15. package/dist/admin/App.d.ts.map +1 -1
  16. package/dist/admin/apps/api-keys.d.ts +3 -3
  17. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  18. package/dist/admin/apps/api-keys.js +4 -1
  19. package/dist/admin/apps/api-keys.js.map +1 -1
  20. package/dist/admin/apps/scopes.d.ts +6 -34
  21. package/dist/admin/apps/scopes.d.ts.map +1 -1
  22. package/dist/admin/apps/scopes.js +2 -2
  23. package/dist/admin/apps/scopes.js.map +1 -1
  24. package/dist/admin/apps/verified-tokens.d.ts +2 -2
  25. package/dist/admin/ui.generated.d.ts +1 -1
  26. package/dist/admin/ui.generated.js +33 -33
  27. package/dist/admin/ui.generated.js.map +1 -1
  28. package/dist/apps/data/App.d.ts +1398 -1405
  29. package/dist/apps/data/App.d.ts.map +1 -1
  30. package/dist/apps/data/App.js +25 -38
  31. package/dist/apps/data/App.js.map +1 -1
  32. package/dist/apps/data/FundingQuoteProviders.d.ts +108 -3
  33. package/dist/apps/data/FundingQuoteProviders.d.ts.map +1 -1
  34. package/dist/apps/data/FundingQuoteProviders.js +409 -2
  35. package/dist/apps/data/FundingQuoteProviders.js.map +1 -1
  36. package/dist/apps/data/routes/activities.d.ts +245 -245
  37. package/dist/apps/data/routes/blocks.d.ts +123 -123
  38. package/dist/apps/data/routes/funding.d.ts +236 -146
  39. package/dist/apps/data/routes/funding.d.ts.map +1 -1
  40. package/dist/apps/data/routes/funding.js +23 -12
  41. package/dist/apps/data/routes/funding.js.map +1 -1
  42. package/dist/apps/data/routes/indexer.d.ts.map +1 -1
  43. package/dist/apps/data/routes/indexer.js +9 -2
  44. package/dist/apps/data/routes/indexer.js.map +1 -1
  45. package/dist/apps/data/routes/rpc.d.ts +31 -31
  46. package/dist/apps/data/routes/tokenlist.d.ts +36 -36
  47. package/dist/apps/data/routes/transactions.d.ts +281 -281
  48. package/dist/apps/data/routes/transfers.d.ts +66 -73
  49. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  50. package/dist/apps/data/routes/transfers.js +21 -27
  51. package/dist/apps/data/routes/transfers.js.map +1 -1
  52. package/dist/apps/data/routes/verified-tokens.d.ts +86 -86
  53. package/dist/apps/data/routes/webhooks.d.ts +35 -35
  54. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  55. package/dist/apps/data/routes/webhooks.js +176 -42
  56. package/dist/apps/data/routes/webhooks.js.map +1 -1
  57. package/dist/apps/management/App.d.ts +520 -504
  58. package/dist/apps/management/App.d.ts.map +1 -1
  59. package/dist/apps/management/Billing.d.ts.map +1 -1
  60. package/dist/apps/management/Billing.js +5 -4
  61. package/dist/apps/management/Billing.js.map +1 -1
  62. package/dist/apps/management/routes/api-keys.d.ts +120 -120
  63. package/dist/apps/management/routes/billing.d.ts +211 -211
  64. package/dist/apps/management/routes/billing.d.ts.map +1 -1
  65. package/dist/apps/management/routes/billing.js +1 -1
  66. package/dist/apps/management/routes/billing.js.map +1 -1
  67. package/dist/apps/management/routes/invitations.d.ts +72 -72
  68. package/dist/apps/management/routes/invite-links.d.ts +159 -159
  69. package/dist/apps/management/routes/me.d.ts +17 -17
  70. package/dist/apps/management/routes/members.d.ts +80 -80
  71. package/dist/apps/management/routes/orgs.d.ts +59 -43
  72. package/dist/apps/management/routes/orgs.d.ts.map +1 -1
  73. package/dist/apps/management/routes/orgs.js +18 -1
  74. package/dist/apps/management/routes/orgs.js.map +1 -1
  75. package/dist/apps/management/routes/projects.d.ts +109 -109
  76. package/dist/apps/management/routes/scopes.d.ts +11 -11
  77. package/dist/apps/management/routes/usage.d.ts +79 -79
  78. package/dist/apps/mcp.js +2 -2
  79. package/dist/apps/mcp.js.map +1 -1
  80. package/dist/apps/relay/App.d.ts +2 -2
  81. package/dist/apps/relay/App.js +2 -2
  82. package/dist/apps/relay/Sponsorships.d.ts +3 -0
  83. package/dist/apps/relay/Sponsorships.d.ts.map +1 -1
  84. package/dist/apps/relay/Sponsorships.js +15 -1
  85. package/dist/apps/relay/Sponsorships.js.map +1 -1
  86. package/dist/db/Db.d.ts.map +1 -1
  87. package/dist/db/Db.js +4 -0
  88. package/dist/db/Db.js.map +1 -1
  89. package/dist/db/Schema.js +1 -1
  90. package/dist/db/Schema.js.map +1 -1
  91. package/dist/db/tables/organizations.d.ts +6 -0
  92. package/dist/db/tables/organizations.d.ts.map +1 -1
  93. package/dist/db/tables/organizations.js +26 -0
  94. package/dist/db/tables/organizations.js.map +1 -1
  95. package/dist/db/tables/requestUsage.d.ts +2 -11
  96. package/dist/db/tables/requestUsage.d.ts.map +1 -1
  97. package/dist/db/tables/requestUsage.js +1 -20
  98. package/dist/db/tables/requestUsage.js.map +1 -1
  99. package/dist/db/tables/sponsoredTransactions.d.ts +2 -1
  100. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  101. package/dist/db/tables/sponsoredTransactions.js +52 -10
  102. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  103. package/dist/internal/Auth.d.ts +4 -0
  104. package/dist/internal/Auth.d.ts.map +1 -1
  105. package/dist/internal/Auth.js +15 -8
  106. package/dist/internal/Auth.js.map +1 -1
  107. package/dist/internal/Cache.d.ts.map +1 -1
  108. package/dist/internal/Cache.js +9 -1
  109. package/dist/internal/Cache.js.map +1 -1
  110. package/dist/internal/EdgeCache.d.ts.map +1 -1
  111. package/dist/internal/EdgeCache.js +12 -3
  112. package/dist/internal/EdgeCache.js.map +1 -1
  113. package/dist/internal/FundingQuotes.d.ts +176 -56
  114. package/dist/internal/FundingQuotes.d.ts.map +1 -1
  115. package/dist/internal/FundingQuotes.js +100 -5
  116. package/dist/internal/FundingQuotes.js.map +1 -1
  117. package/dist/internal/Log.d.ts +18 -12
  118. package/dist/internal/Log.d.ts.map +1 -1
  119. package/dist/internal/Log.js +2 -0
  120. package/dist/internal/Log.js.map +1 -1
  121. package/dist/internal/RateLimit.d.ts +2 -0
  122. package/dist/internal/RateLimit.d.ts.map +1 -1
  123. package/dist/internal/RateLimit.js +4 -4
  124. package/dist/internal/RateLimit.js.map +1 -1
  125. package/dist/internal/Tidx.d.ts +9 -3
  126. package/dist/internal/Tidx.d.ts.map +1 -1
  127. package/dist/internal/Tidx.js +13 -7
  128. package/dist/internal/Tidx.js.map +1 -1
  129. package/dist/internal/Viem.d.ts +14 -1
  130. package/dist/internal/Viem.d.ts.map +1 -1
  131. package/dist/internal/Viem.js +19 -1
  132. package/dist/internal/Viem.js.map +1 -1
  133. package/package.json +7 -2
  134. package/src/ApiKeys.test.ts +2 -0
  135. package/src/App.test.ts +15 -4
  136. package/src/App.ts +31 -5
  137. package/src/Assets.ts +3 -3
  138. package/src/Client.test-d.ts +30 -3
  139. package/src/Scope.ts +4 -2
  140. package/src/Webhooks.ts +5 -10
  141. package/src/admin/apps/api-keys.test.ts +27 -0
  142. package/src/admin/apps/api-keys.ts +8 -1
  143. package/src/admin/apps/scopes.test.ts +7 -2
  144. package/src/admin/apps/scopes.ts +2 -2
  145. package/src/admin/ui/src/components/Dialog.tsx +8 -2
  146. package/src/admin/ui/src/lib/scopes.test.ts +19 -0
  147. package/src/admin/ui/src/lib/scopes.ts +12 -0
  148. package/src/admin/ui/src/routes/api-keys.tsx +128 -27
  149. package/src/admin/ui.generated.ts +33 -33
  150. package/src/apps/composition.test.ts +77 -0
  151. package/src/apps/data/App.ts +25 -45
  152. package/src/apps/data/FundingQuoteProviders.test.ts +615 -5
  153. package/src/apps/data/FundingQuoteProviders.ts +570 -4
  154. package/src/apps/data/routes/funding.test.ts +181 -7
  155. package/src/apps/data/routes/funding.ts +27 -12
  156. package/src/apps/data/routes/indexer.test.ts +82 -20
  157. package/src/apps/data/routes/indexer.ts +8 -2
  158. package/src/apps/data/routes/tokens.test.ts +54 -0
  159. package/src/apps/data/routes/transfers.test.ts +73 -1
  160. package/src/apps/data/routes/transfers.ts +24 -30
  161. package/src/apps/data/routes/webhooks.test.ts +203 -0
  162. package/src/apps/data/routes/webhooks.ts +191 -54
  163. package/src/apps/management/Billing.test.ts +52 -3
  164. package/src/apps/management/Billing.ts +5 -4
  165. package/src/apps/management/routes/billing.ts +6 -1
  166. package/src/apps/management/routes/orgs.ts +19 -1
  167. package/src/apps/mcp.test.ts +18 -0
  168. package/src/apps/mcp.ts +2 -2
  169. package/src/apps/relay/App.test.ts +6 -5
  170. package/src/apps/relay/App.ts +2 -2
  171. package/src/apps/relay/Sponsorships.test.ts +2 -2
  172. package/src/apps/relay/Sponsorships.ts +24 -1
  173. package/src/db/Db.test.ts +5 -5
  174. package/src/db/Db.ts +4 -0
  175. package/src/db/Schema.ts +1 -1
  176. package/src/db/tables/billingSettings.test.ts +2 -2
  177. package/src/db/tables/organizations.test.ts +32 -2
  178. package/src/db/tables/organizations.ts +28 -0
  179. package/src/db/tables/requestUsage.ts +3 -23
  180. package/src/db/tables/sponsoredTransactions.test.ts +60 -2
  181. package/src/db/tables/sponsoredTransactions.ts +67 -21
  182. package/src/db/tables/stripeCustomers.test.ts +2 -2
  183. package/src/internal/Auth.test.ts +63 -10
  184. package/src/internal/Auth.ts +21 -9
  185. package/src/internal/Cache.test.ts +19 -0
  186. package/src/internal/Cache.ts +13 -1
  187. package/src/internal/EdgeCache.test.ts +40 -0
  188. package/src/internal/EdgeCache.ts +11 -3
  189. package/src/internal/FundingQuotes.test.ts +130 -3
  190. package/src/internal/FundingQuotes.ts +113 -6
  191. package/src/internal/Log.ts +21 -14
  192. package/src/internal/RateLimit.test.ts +12 -0
  193. package/src/internal/RateLimit.ts +4 -4
  194. package/src/internal/Tidx.test.ts +48 -0
  195. package/src/internal/Tidx.ts +21 -7
  196. package/src/internal/Viem.test.ts +46 -0
  197. 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 calls = 0
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
- calls += 1
40
+ relayCalls += 1
36
41
  }),
37
42
  },
38
43
  },
39
44
  })
40
- const response = await app.request(quoteUrl({ ...baseUsdcQuery, provider: 'relay' }), {
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(calls).toBe(1)
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 without a live quote adapter', async () => {
233
+ test('rejects unsupported source chains', async () => {
130
234
  const response = await TestApp.create().request(
131
- quoteUrl({ ...baseUsdcQuery, sourceChain: 'solana' }),
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(['eip155:1', 'eip155:8453', 'eip155:56'])
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
- .literal('evm')
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 CAIP-2 chain id, represented as a string instead of the numeric `chainId` used elsewhere.',
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
- .literal('evm')
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
- .literal('relay')
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
- .literal('relay')
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.literal('relay'))
288
+ .optional(z.enum(['across', 'relay', 'rhino', 'squid']))
282
289
  .check(
283
290
  z.describe(
284
- 'Only request a quote from this provider. The supported id is `relay`; omit it to query every configured 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('Source chain alias or CAIP-2 id, such as `base` or `eip155:8453`.'),
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. Symbol and address lookup is scoped to `sourceChain` and case-insensitive. An unknown token returns an empty list.',
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: 'Every matching quote provider failed unexpectedly.',
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 { RateLimit, Store } from 'tapimo'
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
- { headers: { authorization: `Bearer ${zoneReader.token}` } },
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('completes the MPP payment lifecycle when public quota is exhausted', async () => {
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
- // An unpaid over-quota request is challenged for payment. MPP challenges
190
- // and receipts ride on real HTTP headers, so we drive the lifecycle
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, public: 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, { chainId, principal: Auth.getPrincipal(c) })
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(