tapimo 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/dist/App.d.ts +4879 -4870
  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 +116 -116
  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 +3199 -3206
  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 +76 -3
  33. package/dist/apps/data/FundingQuoteProviders.d.ts.map +1 -1
  34. package/dist/apps/data/FundingQuoteProviders.js +276 -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/addresses.d.ts +206 -206
  38. package/dist/apps/data/routes/balances.d.ts +46 -46
  39. package/dist/apps/data/routes/blocks.d.ts +123 -123
  40. package/dist/apps/data/routes/coingecko.d.ts +169 -169
  41. package/dist/apps/data/routes/funding.d.ts +230 -146
  42. package/dist/apps/data/routes/funding.d.ts.map +1 -1
  43. package/dist/apps/data/routes/funding.js +23 -12
  44. package/dist/apps/data/routes/funding.js.map +1 -1
  45. package/dist/apps/data/routes/indexer.d.ts +34 -34
  46. package/dist/apps/data/routes/indexer.d.ts.map +1 -1
  47. package/dist/apps/data/routes/indexer.js +9 -2
  48. package/dist/apps/data/routes/indexer.js.map +1 -1
  49. package/dist/apps/data/routes/receipts.d.ts +228 -228
  50. package/dist/apps/data/routes/tokenlist.d.ts +36 -36
  51. package/dist/apps/data/routes/transactions.d.ts +281 -281
  52. package/dist/apps/data/routes/transfers.d.ts +58 -58
  53. package/dist/apps/data/routes/verified-tokens.d.ts +86 -86
  54. package/dist/apps/management/App.d.ts +1042 -1042
  55. package/dist/apps/management/routes/api-keys.d.ts +120 -120
  56. package/dist/apps/management/routes/billing.d.ts +211 -211
  57. package/dist/apps/management/routes/invitations.d.ts +132 -132
  58. package/dist/apps/management/routes/invite-links.d.ts +62 -62
  59. package/dist/apps/management/routes/me.d.ts +17 -17
  60. package/dist/apps/management/routes/members.d.ts +80 -80
  61. package/dist/apps/management/routes/orgs.d.ts +96 -96
  62. package/dist/apps/management/routes/projects.d.ts +61 -61
  63. package/dist/apps/management/routes/scopes.d.ts +11 -11
  64. package/dist/apps/management/routes/usage.d.ts +79 -79
  65. package/dist/apps/mcp.js +2 -2
  66. package/dist/apps/mcp.js.map +1 -1
  67. package/dist/apps/relay/App.d.ts +2 -2
  68. package/dist/apps/relay/App.js +2 -2
  69. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  70. package/dist/db/tables/sponsoredTransactions.js +6 -2
  71. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  72. package/dist/internal/Auth.d.ts.map +1 -1
  73. package/dist/internal/Auth.js +8 -0
  74. package/dist/internal/Auth.js.map +1 -1
  75. package/dist/internal/FundingQuotes.d.ts +170 -56
  76. package/dist/internal/FundingQuotes.d.ts.map +1 -1
  77. package/dist/internal/FundingQuotes.js +91 -5
  78. package/dist/internal/FundingQuotes.js.map +1 -1
  79. package/dist/internal/Log.d.ts +18 -12
  80. package/dist/internal/Log.d.ts.map +1 -1
  81. package/dist/internal/Log.js +2 -0
  82. package/dist/internal/Log.js.map +1 -1
  83. package/dist/internal/Tidx.d.ts +9 -3
  84. package/dist/internal/Tidx.d.ts.map +1 -1
  85. package/dist/internal/Tidx.js +13 -7
  86. package/dist/internal/Tidx.js.map +1 -1
  87. package/dist/internal/Viem.d.ts +14 -1
  88. package/dist/internal/Viem.d.ts.map +1 -1
  89. package/dist/internal/Viem.js +19 -1
  90. package/dist/internal/Viem.js.map +1 -1
  91. package/package.json +2 -2
  92. package/src/ApiKeys.test.ts +2 -0
  93. package/src/App.test.ts +13 -2
  94. package/src/App.ts +31 -5
  95. package/src/Assets.ts +3 -3
  96. package/src/Client.test-d.ts +3 -2
  97. package/src/Scope.ts +4 -2
  98. package/src/Webhooks.ts +5 -10
  99. package/src/admin/apps/api-keys.test.ts +27 -0
  100. package/src/admin/apps/api-keys.ts +8 -1
  101. package/src/admin/apps/scopes.test.ts +7 -2
  102. package/src/admin/apps/scopes.ts +2 -2
  103. package/src/admin/ui/src/components/Dialog.tsx +8 -2
  104. package/src/admin/ui/src/lib/scopes.test.ts +19 -0
  105. package/src/admin/ui/src/lib/scopes.ts +12 -0
  106. package/src/admin/ui/src/routes/api-keys.tsx +128 -27
  107. package/src/admin/ui.generated.ts +33 -33
  108. package/src/apps/composition.test.ts +77 -0
  109. package/src/apps/data/App.ts +25 -45
  110. package/src/apps/data/FundingQuoteProviders.test.ts +431 -5
  111. package/src/apps/data/FundingQuoteProviders.ts +386 -4
  112. package/src/apps/data/routes/funding.test.ts +174 -7
  113. package/src/apps/data/routes/funding.ts +27 -12
  114. package/src/apps/data/routes/indexer.test.ts +82 -20
  115. package/src/apps/data/routes/indexer.ts +8 -2
  116. package/src/apps/management/Billing.test.ts +2 -2
  117. package/src/apps/mcp.test.ts +18 -0
  118. package/src/apps/mcp.ts +2 -2
  119. package/src/apps/relay/App.test.ts +6 -5
  120. package/src/apps/relay/App.ts +2 -2
  121. package/src/apps/relay/Sponsorships.test.ts +2 -2
  122. package/src/db/Db.test.ts +5 -5
  123. package/src/db/tables/billingSettings.test.ts +2 -2
  124. package/src/db/tables/organizations.test.ts +2 -2
  125. package/src/db/tables/sponsoredTransactions.test.ts +19 -2
  126. package/src/db/tables/sponsoredTransactions.ts +21 -14
  127. package/src/db/tables/stripeCustomers.test.ts +2 -2
  128. package/src/internal/Auth.ts +10 -0
  129. package/src/internal/FundingQuotes.test.ts +97 -3
  130. package/src/internal/FundingQuotes.ts +104 -6
  131. package/src/internal/Log.ts +21 -14
  132. package/src/internal/Tidx.test.ts +48 -0
  133. package/src/internal/Tidx.ts +21 -7
  134. package/src/internal/Viem.test.ts +46 -0
  135. package/src/internal/Viem.ts +34 -1
@@ -83,9 +83,11 @@ describe('listQuotes', () => {
83
83
  },
84
84
  })
85
85
 
86
- await expect(quotes).rejects.toThrowErrorMatchingInlineSnapshot(
87
- `[FundingQuotes.ProviderQuoteError]`,
88
- )
86
+ const cause = await quotes.catch((error: unknown) => error)
87
+ expect(cause).toMatchObject({
88
+ failures: [{ cause: expect.any(Error), provider: 'relay' }],
89
+ name: 'FundingQuotes.ProviderQuoteError',
90
+ })
89
91
  })
90
92
 
91
93
  test('rejects successful provider responses without output amounts', async () => {
@@ -145,6 +147,32 @@ describe('listQuotes', () => {
145
147
 
146
148
  expect(seen?.cache.bypass).toBe(true)
147
149
  })
150
+
151
+ test('returns successful quotes when another provider fails', async () => {
152
+ const quotes = await FundingQuotes.listQuotes({
153
+ ...baseUsdcRequest,
154
+ adapters: {
155
+ across: {
156
+ async probeQuote() {
157
+ throw new Error('upstream unavailable')
158
+ },
159
+ },
160
+ relay: {
161
+ async probeQuote() {
162
+ return {
163
+ outputAmount: '990000',
164
+ sampledAt: '2026-01-01T00:00:00Z',
165
+ status: 'available',
166
+ }
167
+ },
168
+ },
169
+ },
170
+ provider: undefined,
171
+ })
172
+
173
+ expect(quotes).toHaveLength(1)
174
+ expect(quotes[0]?.provider.id).toBe('relay')
175
+ })
148
176
  })
149
177
 
150
178
  describe('listChains', () => {
@@ -153,14 +181,73 @@ describe('listChains', () => {
153
181
  'eip155:1',
154
182
  'eip155:8453',
155
183
  'eip155:56',
184
+ 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
185
+ 'tron:0x2b6653dc',
156
186
  ])
157
187
  })
188
+
189
+ test('uses chain-specific token keys for non-EVM inventory', () => {
190
+ const chains = FundingQuotes.listChains()
191
+ expect(chains.find((chain) => chain.kind === 'solana')?.tokens[0]?.tokenKey).toBe(
192
+ 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
193
+ )
194
+ expect(chains.find((chain) => chain.kind === 'tron')?.tokens[0]?.tokenKey).toBe(
195
+ 'tron:0x2b6653dc/trc20:TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t',
196
+ )
197
+ })
198
+ })
199
+
200
+ describe('listQuoteCandidates', () => {
201
+ test('round-trips the advertised Solana chain and token ids', () => {
202
+ const [candidate] = FundingQuotes.listQuoteCandidates({
203
+ provider: 'rhino',
204
+ sourceAmount: '1000000',
205
+ sourceAmountUnits: 'baseUnits',
206
+ sourceChain: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
207
+ sourceToken:
208
+ 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
209
+ })
210
+
211
+ expect(candidate?.id).toBe('solana-usdc-tempo-usdce-rhino')
212
+ })
213
+
214
+ test('keeps non-EVM token identifiers case-sensitive', () => {
215
+ expect(
216
+ FundingQuotes.listQuoteCandidates({
217
+ provider: 'rhino',
218
+ sourceAmount: '1000000',
219
+ sourceAmountUnits: 'baseUnits',
220
+ sourceChain: 'solana',
221
+ sourceToken: 'epjfwdd5aufqssqem2qn1xzybapc8g4wegkzwytdt1v',
222
+ }),
223
+ ).toEqual([])
224
+ })
225
+
226
+ test('resolves the complete Tron token key', () => {
227
+ const [candidate] = FundingQuotes.listQuoteCandidates({
228
+ destinationToken: 'USDT0',
229
+ provider: 'rhino',
230
+ sourceAmount: '1000000',
231
+ sourceAmountUnits: 'baseUnits',
232
+ sourceChain: 'tron:0x2b6653dc',
233
+ sourceToken: 'tron:0x2b6653dc/trc20:TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t',
234
+ })
235
+
236
+ expect(candidate?.id).toBe('tron-usdt-tempo-usdt0-rhino')
237
+ })
158
238
  })
159
239
 
160
240
  describe('listProviders', () => {
161
241
  test('reports deployment configuration', () => {
162
242
  expect(FundingQuotes.listProviders()).toMatchInlineSnapshot(`
163
243
  [
244
+ {
245
+ "id": "across",
246
+ "kind": "intentBridge",
247
+ "name": "Across",
248
+ "requiresApiKey": true,
249
+ "status": "unconfigured",
250
+ },
164
251
  {
165
252
  "id": "relay",
166
253
  "kind": "solverNetwork",
@@ -168,6 +255,13 @@ describe('listProviders', () => {
168
255
  "requiresApiKey": false,
169
256
  "status": "unconfigured",
170
257
  },
258
+ {
259
+ "id": "rhino",
260
+ "kind": "bridge",
261
+ "name": "Rhino",
262
+ "requiresApiKey": false,
263
+ "status": "unconfigured",
264
+ },
171
265
  ]
172
266
  `)
173
267
  })
@@ -18,6 +18,7 @@ const sourceChains = [
18
18
  kind: 'evm',
19
19
  name: 'Ethereum',
20
20
  providerIds: {
21
+ across: '1',
21
22
  relay: '1',
22
23
  },
23
24
  slug: 'ethereum',
@@ -71,7 +72,9 @@ const sourceChains = [
71
72
  kind: 'evm',
72
73
  name: 'Base',
73
74
  providerIds: {
75
+ across: '8453',
74
76
  relay: '8453',
77
+ rhino: 'BASE',
75
78
  },
76
79
  slug: 'base',
77
80
  tokens: [
@@ -108,6 +111,7 @@ const sourceChains = [
108
111
  kind: 'evm',
109
112
  name: 'BNB Smart Chain',
110
113
  providerIds: {
114
+ across: '56',
111
115
  relay: '56',
112
116
  },
113
117
  slug: 'bnb',
@@ -130,6 +134,48 @@ const sourceChains = [
130
134
  },
131
135
  ],
132
136
  },
137
+ {
138
+ addressFormat: 'base58',
139
+ aliases: ['solana', 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'],
140
+ id: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
141
+ kind: 'solana',
142
+ name: 'Solana',
143
+ providerIds: {
144
+ rhino: 'SOLANA',
145
+ },
146
+ slug: 'solana',
147
+ tokens: [
148
+ {
149
+ address: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
150
+ coingeckoId: 'usd-coin',
151
+ decimals: 6,
152
+ name: 'USD Coin',
153
+ standard: 'SPL',
154
+ symbol: 'USDC',
155
+ },
156
+ ],
157
+ },
158
+ {
159
+ addressFormat: 'base58check',
160
+ aliases: ['tron', 'tron:0x2b6653dc'],
161
+ id: 'tron:0x2b6653dc',
162
+ kind: 'tron',
163
+ name: 'Tron',
164
+ providerIds: {
165
+ rhino: 'TRON',
166
+ },
167
+ slug: 'tron',
168
+ tokens: [
169
+ {
170
+ address: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t',
171
+ coingeckoId: 'tether',
172
+ decimals: 6,
173
+ name: 'Tether USD',
174
+ standard: 'TRC-20',
175
+ symbol: 'USDT',
176
+ },
177
+ ],
178
+ },
133
179
  ] as const
134
180
 
135
181
  /** Tempo destination tokens supported by funding quote providers. */
@@ -178,6 +224,13 @@ const destinationTokens = [
178
224
 
179
225
  /** Providers supported by live funding quotes. */
180
226
  const providers = [
227
+ {
228
+ id: 'across',
229
+ kind: 'intentBridge',
230
+ name: 'Across',
231
+ requiresApiKey: true,
232
+ status: 'unconfigured',
233
+ },
181
234
  {
182
235
  id: 'relay',
183
236
  kind: 'solverNetwork',
@@ -185,6 +238,13 @@ const providers = [
185
238
  requiresApiKey: false,
186
239
  status: 'unconfigured',
187
240
  },
241
+ {
242
+ id: 'rhino',
243
+ kind: 'bridge',
244
+ name: 'Rhino',
245
+ requiresApiKey: false,
246
+ status: 'unconfigured',
247
+ },
188
248
  ] as const
189
249
 
190
250
  type TokenInput = (typeof sourceChains)[number]['tokens'][number]
@@ -262,6 +322,8 @@ export type QuoteProbeInput = {
262
322
 
263
323
  /** Quote probe data returned by a provider adapter. */
264
324
  export type QuoteProbeResult = {
325
+ /** Timestamp after which the provider quote is no longer valid. */
326
+ expiresAt?: string | undefined
265
327
  /** Slippage-adjusted minimum destination amount in base units. */
266
328
  minimumOutputAmount?: string | undefined
267
329
  /** Expected destination amount in base units. */
@@ -294,9 +356,20 @@ export type ProviderAdapter = {
294
356
  /** Provider adapter registry, keyed by provider id. */
295
357
  export type ProviderAdapters = Partial<Record<ProviderId, ProviderAdapter>>
296
358
 
359
+ /** A provider adapter failure retained for bounded request diagnostics. */
360
+ export type ProviderFailure = {
361
+ /** Error thrown by the provider boundary. */
362
+ cause: unknown
363
+ /** Provider that failed. */
364
+ provider: ProviderId
365
+ }
366
+
297
367
  /** Curated provider support rows used to select live quote probes. */
298
368
  const inventory: readonly InventoryEntry[] = [
369
+ { destinationToken: 'usdce', provider: 'across', sourceChains: ['ethereum', 'base', 'bnb'], sourceTokens: ['USDC'] }, // prettier-ignore
299
370
  { destinationToken: 'usdce', provider: 'relay', sourceChains: ['ethereum', 'base', 'bnb'], sourceTokens: ['USDC', 'USDT'] }, // prettier-ignore
371
+ { destinationToken: 'usdce', provider: 'rhino', sourceChains: ['base', 'solana'], sourceTokens: ['USDC'] }, // prettier-ignore
372
+ { destinationToken: 'usdt0', provider: 'rhino', sourceChains: ['tron'], sourceTokens: ['USDT'] }, // prettier-ignore
300
373
  ]
301
374
 
302
375
  type InventoryEntry = {
@@ -367,8 +440,12 @@ export async function listQuotes(options: listQuotes.Options) {
367
440
  const quotes = results.flatMap((result) =>
368
441
  result.status === 'fulfilled' && result.value ? [result.value] : [],
369
442
  )
370
- if (quotes.length === 0 && results.some((result) => result.status === 'rejected'))
371
- throw new ProviderQuoteError()
443
+ const failures = results.flatMap((result, index) =>
444
+ result.status === 'rejected'
445
+ ? [{ cause: result.reason, provider: candidates[index]!.provider.id }]
446
+ : [],
447
+ )
448
+ if (quotes.length === 0 && failures.length > 0) throw new ProviderQuoteError(failures)
372
449
  return quotes
373
450
  }
374
451
 
@@ -439,8 +516,13 @@ export declare namespace listQuotes {
439
516
  /** Source amount unit accepted by the funding quote request. */
440
517
  export type SourceAmountUnits = 'baseUnits' | 'decimal'
441
518
 
442
- function tokenKey(chain: { id: string }, token: TokenInput | DestinationTokenInput) {
443
- return `${chain.id}/erc20:${token.address}`
519
+ function tokenKey(chain: { id: string; kind: string }, token: TokenInput | DestinationTokenInput) {
520
+ const namespace = (() => {
521
+ if (chain.kind === 'solana') return 'token'
522
+ if (chain.kind === 'tron') return 'trc20'
523
+ return 'erc20'
524
+ })()
525
+ return `${chain.id}/${namespace}:${token.address}`
444
526
  }
445
527
 
446
528
  function decimalFromBaseUnits(baseUnits: string, decimals: number) {
@@ -485,7 +567,7 @@ async function quoteCandidate(
485
567
  quote.outputAmount === undefined ||
486
568
  quote.outputAmount === null
487
569
  )
488
- throw new ProviderQuoteError()
570
+ throw new InvalidProviderQuoteError()
489
571
  return formatFundingQuote(candidate, {
490
572
  ...quote,
491
573
  outputAmount: quote.outputAmount,
@@ -595,6 +677,7 @@ function formatFundingQuote(candidate: QuoteCandidate, quote: SuccessfulQuotePro
595
677
  tier: quote.status === 'thin' ? ('thin' as const) : ('liquid' as const),
596
678
  },
597
679
  quote: {
680
+ ...(quote.expiresAt !== undefined ? { expiresAt: quote.expiresAt } : {}),
598
681
  inputAmount: candidate.sourceAmount.amount,
599
682
  ...(quote.minimumOutputAmount !== undefined
600
683
  ? { minimumOutputAmount: quote.minimumOutputAmount }
@@ -685,10 +768,13 @@ function matchesTokenIdentifier(
685
768
  }
686
769
 
687
770
  function resolveSourceChain(input: string) {
771
+ const trimmed = input.trim()
688
772
  const normalized = normalizeInput(input)
689
773
  const chain = sourceChains.find(
690
774
  (candidate) =>
691
- candidate.id === normalized || (candidate.aliases as readonly string[]).includes(normalized),
775
+ candidate.id === trimmed ||
776
+ candidate.slug === normalized ||
777
+ (candidate.kind === 'evm' && (candidate.aliases as readonly string[]).includes(normalized)),
692
778
  )
693
779
  if (!chain) throw new UnsupportedSourceChainError()
694
780
  return chain
@@ -722,6 +808,14 @@ export class ProviderAdapterTimeoutError extends Error {
722
808
  /** Error thrown when every matching provider fails to produce a quote. */
723
809
  export class ProviderQuoteError extends Error {
724
810
  override name = 'FundingQuotes.ProviderQuoteError'
811
+
812
+ /** Provider failures that prevented a successful quote. */
813
+ failures: readonly ProviderFailure[]
814
+
815
+ constructor(failures: readonly ProviderFailure[]) {
816
+ super()
817
+ this.failures = failures
818
+ }
725
819
  }
726
820
 
727
821
  /** Error thrown when the caller filters to an unknown provider. */
@@ -733,3 +827,7 @@ export class UnsupportedProviderError extends Error {
733
827
  export class UnsupportedSourceChainError extends Error {
734
828
  override name = 'FundingQuotes.UnsupportedSourceChainError'
735
829
  }
830
+
831
+ class InvalidProviderQuoteError extends Error {
832
+ override name = 'FundingQuotes.InvalidProviderQuoteError'
833
+ }
@@ -12,6 +12,8 @@ export type Variables = {
12
12
  errorCode?: string | undefined
13
13
  /** Redacted provider failure details recorded by an upstream adapter. */
14
14
  providerFailure?: Entry['provider'] | undefined
15
+ /** Redacted provider failures when multiple upstream adapters fail. */
16
+ providerFailures?: Entry['providerFailures'] | undefined
15
17
  }
16
18
 
17
19
  /** Hono environment shape used by the request logger. */
@@ -28,6 +30,20 @@ export type Environment = {
28
30
  /** Log level, derived from the response status. */
29
31
  export type Level = 'error' | 'info' | 'warn'
30
32
 
33
+ /** Redacted upstream provider failure details. */
34
+ export type ProviderFailure = {
35
+ /** Stable provider error code, when one is safe to retain. */
36
+ code?: string | undefined
37
+ /** Failure boundary that rejected the provider request. */
38
+ failure: 'http' | 'network' | 'payload' | 'timeout' | 'unknown'
39
+ /** Provider identifier. */
40
+ id: string
41
+ /** Provider operation identifier. */
42
+ operation: string
43
+ /** Upstream HTTP status, when a response was received. */
44
+ status?: number | undefined
45
+ }
46
+
31
47
  /** One canonical request log entry. */
32
48
  export type Entry = {
33
49
  /** `'hit'` when the edge response cache served the request without reaching the origin handlers. */
@@ -64,20 +80,9 @@ export type Entry = {
64
80
  }
65
81
  | undefined
66
82
  /** Redacted upstream provider failure details, when an adapter request fails. */
67
- provider?:
68
- | {
69
- /** Stable provider error code, when one is safe to retain. */
70
- code?: string | undefined
71
- /** Failure boundary that rejected the provider request. */
72
- failure: 'http' | 'network' | 'payload' | 'unknown'
73
- /** Provider identifier. */
74
- id: string
75
- /** Provider operation identifier. */
76
- operation: string
77
- /** Upstream HTTP status, when a response was received. */
78
- status?: number | undefined
79
- }
80
- | undefined
83
+ provider?: ProviderFailure | undefined
84
+ /** Redacted failures for every provider when multiple adapters fail. */
85
+ providerFailures?: readonly ProviderFailure[] | undefined
81
86
  /** Raw query string (no leading `?`) with API-key tokens redacted, when present. */
82
87
  query?: string | undefined
83
88
  /** Rate limit state from the `RateLimit-*` response headers, when enforced. */
@@ -136,6 +141,7 @@ export function middleware<environment extends Environment = Environment>(
136
141
  const principal = c.get('principal' as never) as Auth.Principal | undefined
137
142
  const errorCode = c.get('errorCode' as never) as string | undefined
138
143
  const provider = c.get('providerFailure' as never) as Entry['provider']
144
+ const providerFailures = c.get('providerFailures' as never) as Entry['providerFailures']
139
145
 
140
146
  // The `request` total metric duplicates `duration` (measured here, one
141
147
  // layer further out), so only operation timings are kept.
@@ -186,6 +192,7 @@ export function middleware<environment extends Environment = Environment>(
186
192
  },
187
193
  }),
188
194
  ...(provider === undefined ? {} : { provider }),
195
+ ...(providerFailures === undefined ? {} : { providerFailures }),
189
196
  ...(query ? { query } : {}),
190
197
  ...(rateLimit === undefined ? {} : { rateLimit }),
191
198
  requestId: c.get('requestId' as never) as string,
@@ -203,6 +203,21 @@ describe('resolve', () => {
203
203
  `)
204
204
  })
205
205
 
206
+ test('resolves Bearer auth', () => {
207
+ expect(
208
+ Tidx.resolve(
209
+ { baseUrl: 'https://internal.indexer', bearerAuth: 'zone-secret' },
210
+ { chainId: 4217, principal: apiKeyPrincipal },
211
+ ),
212
+ ).toMatchInlineSnapshot(`
213
+ {
214
+ "baseUrl": "https://internal.indexer",
215
+ "basicAuth": undefined,
216
+ "bearerAuth": "zone-secret",
217
+ }
218
+ `)
219
+ })
220
+
206
221
  test('the resolver form routes anonymous callers to a separate upstream', () => {
207
222
  const tidx = ({ principal }: Viem.resolve.Context) =>
208
223
  principal?.type === 'public'
@@ -243,6 +258,28 @@ describe('resolve', () => {
243
258
  })
244
259
  })
245
260
 
261
+ describe('getClient', () => {
262
+ test('sends Bearer auth', async () => {
263
+ const fetch = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
264
+ new Response(JSON.stringify({ columns: [], ok: true, row_count: 0, rows: [] }), {
265
+ headers: { 'content-type': 'application/json' },
266
+ }),
267
+ )
268
+ try {
269
+ const client = Tidx.getClient({
270
+ chainId: 4217,
271
+ tidx: { baseUrl: 'https://internal.indexer', bearerAuth: 'zone-secret' },
272
+ })
273
+ await client.fetch({ query: 'select 1' })
274
+
275
+ const upstream = fetch.mock.calls[0]![0] as Request
276
+ expect(upstream.headers.get('authorization')).toBe('Bearer zone-secret')
277
+ } finally {
278
+ fetch.mockRestore()
279
+ }
280
+ })
281
+ })
282
+
246
283
  describe('createGetClient', () => {
247
284
  test('memoizes one client per resolved upstream', () => {
248
285
  const getTidx = Tidx.createGetClient({
@@ -267,6 +304,17 @@ describe('createGetClient', () => {
267
304
  expect(getTidx(4217, null)).toBe(getTidx(4217, publicPrincipal))
268
305
  })
269
306
 
307
+ test('does not share clients across Bearer credentials', () => {
308
+ const getTidx = Tidx.createGetClient({
309
+ tidx: ({ principal }) => ({
310
+ baseUrl: 'https://internal.indexer',
311
+ bearerAuth: principal?.type === 'public' ? 'public-secret' : 'private-secret',
312
+ }),
313
+ })
314
+
315
+ expect(getTidx(4217, publicPrincipal)).not.toBe(getTidx(4217, apiKeyPrincipal))
316
+ })
317
+
270
318
  test('consults the resolver on every call but builds one client per upstream', () => {
271
319
  const seen: (string | undefined)[] = []
272
320
  const getTidx = Tidx.createGetClient({
@@ -29,8 +29,8 @@ export type UrlResolver = (chainId: Viem.ChainId) => string | undefined
29
29
  * receives a {@link Viem.resolve.Context} (`chainId` plus the request
30
30
  * `principal`), so callers can route traffic per caller — e.g. send anonymous
31
31
  * (`principal.type === 'public'`) traffic to a separate, no-auth indexer. A
32
- * missing `baseUrl` falls back to the built-in public host map ({@link url}); a
33
- * missing `basicAuth` means no Basic auth is sent.
32
+ * missing `baseUrl` falls back to the built-in public host map ({@link url});
33
+ * missing auth fields send no Authorization header.
34
34
  */
35
35
  export function resolve(
36
36
  tidx: getClient.Tidx | undefined,
@@ -42,6 +42,7 @@ export function resolve(
42
42
  (typeof config?.baseUrl === 'function' ? config.baseUrl(context.chainId) : config?.baseUrl) ??
43
43
  url[context.chainId],
44
44
  basicAuth: config?.basicAuth,
45
+ ...(config?.bearerAuth ? { bearerAuth: config.bearerAuth } : {}),
45
46
  }
46
47
  }
47
48
 
@@ -56,13 +57,22 @@ export declare namespace resolve {
56
57
  baseUrl: string | undefined
57
58
  /** Basic auth credentials, or undefined to send no Basic auth. */
58
59
  basicAuth?: string | undefined
60
+ /** Bearer auth credential, or undefined to send no Bearer auth. */
61
+ bearerAuth?: string | undefined
59
62
  }
60
63
  }
61
64
 
62
65
  export function getClient(options: getClient.Options): Client {
63
66
  const { chainId = Viem.defaultChainId, principal = null, tidx } = options
64
- const { baseUrl, basicAuth } = resolve(tidx, { chainId, principal })
65
- return withTransientRetry(Tidx.create({ baseUrl, basicAuth, chainId }))
67
+ const { baseUrl, basicAuth, bearerAuth } = resolve(tidx, { chainId, principal })
68
+ return withTransientRetry(
69
+ Tidx.create({
70
+ baseUrl,
71
+ basicAuth,
72
+ bearerAuth,
73
+ chainId,
74
+ }),
75
+ )
66
76
  }
67
77
 
68
78
  /**
@@ -72,7 +82,7 @@ export function getClient(options: getClient.Options): Client {
72
82
  * output (`baseUrl` + `basicAuth` + `chainId`), not the principal, so a resolver
73
83
  * `tidx` may branch on any principal field while client cardinality stays
74
84
  * bounded by the number of distinct upstreams — callers that resolve to the same
75
- * upstream share a client. Shared by `App.create` (per-request reads) and
85
+ * upstream share a client. Shared by `data()` (per-request reads) and
76
86
  * `Webhooks.createPoller` (the scheduled poller) so the resolver logic lives in
77
87
  * one place.
78
88
  */
@@ -83,8 +93,8 @@ export function createGetClient(options: createGetClient.Options = {}): GetClien
83
93
  return (chainId = defaultChainId, principal = null) => {
84
94
  // Resolve to derive the cache key; `getClient` re-resolves to build the
85
95
  // client on a miss. `resolve` is pure and cheap, so the extra call is free.
86
- const { baseUrl, basicAuth } = resolve(tidx, { chainId, principal })
87
- const cacheKey = `${baseUrl}|${basicAuth ?? ''}|${chainId}`
96
+ const { baseUrl, basicAuth, bearerAuth } = resolve(tidx, { chainId, principal })
97
+ const cacheKey = `${baseUrl}|${basicAuth ?? ''}|${bearerAuth ?? ''}|${chainId}`
88
98
  const existing = clients.get(cacheKey)
89
99
  if (existing) return existing
90
100
 
@@ -218,12 +228,16 @@ export declare namespace getClient {
218
228
  baseUrl?: UrlResolver | string | undefined
219
229
  /** Basic auth credentials for TIDX requests. */
220
230
  basicAuth?: string | undefined
231
+ /** Bearer auth credential for TIDX requests. */
232
+ bearerAuth?: string | undefined
221
233
  }
222
234
  | ((context: Viem.resolve.Context) => {
223
235
  /** TIDX base URL to use; falls back to the built-in public host when omitted. */
224
236
  baseUrl?: string | undefined
225
237
  /** Basic auth credentials for TIDX requests, or undefined for none. */
226
238
  basicAuth?: string | undefined
239
+ /** Bearer auth credential for TIDX requests, or undefined for none. */
240
+ bearerAuth?: string | undefined
227
241
  })
228
242
  }
229
243
 
@@ -113,3 +113,49 @@ describe('resolve', () => {
113
113
  `)
114
114
  })
115
115
  })
116
+
117
+ describe('createGetClient', () => {
118
+ test('memoizes one client per resolved upstream', () => {
119
+ const getClient = Viem.createGetClient({
120
+ rpc: ({ principal }) =>
121
+ principal?.type === 'public'
122
+ ? { url: 'https://rpc.tempo.xyz' }
123
+ : { basicAuth: 'user:pass', url: 'https://internal.rpc' },
124
+ })
125
+
126
+ expect(getClient(4217, apiKeyPrincipal)).toBe(getClient(4217, apiKeyPrincipal))
127
+ expect(getClient(4217, publicPrincipal)).toBe(getClient(4217, publicPrincipal))
128
+ expect(getClient(4217, publicPrincipal)).not.toBe(getClient(4217, apiKeyPrincipal))
129
+ expect(getClient(4217, apiKeyPrincipal)).not.toBe(getClient(42431, apiKeyPrincipal))
130
+ })
131
+
132
+ test('shares a client across principals when they resolve identically', () => {
133
+ const getClient = Viem.createGetClient()
134
+
135
+ expect(getClient(4217, publicPrincipal)).toBe(getClient(4217, apiKeyPrincipal))
136
+ expect(getClient(4217, null)).toBe(getClient(4217, publicPrincipal))
137
+ })
138
+
139
+ test('consults the resolver on cache hits', () => {
140
+ const seen: (string | undefined)[] = []
141
+ const getClient = Viem.createGetClient({
142
+ rpc: ({ principal }) => {
143
+ seen.push(principal?.type)
144
+ return { url: 'https://internal.rpc' }
145
+ },
146
+ })
147
+
148
+ const first = getClient(4217, apiKeyPrincipal)
149
+ const second = getClient(4217, apiKeyPrincipal)
150
+ getClient(4217, publicPrincipal)
151
+
152
+ expect(seen).toMatchInlineSnapshot(`
153
+ [
154
+ "api_key",
155
+ "api_key",
156
+ "public",
157
+ ]
158
+ `)
159
+ expect(first).toBe(second)
160
+ })
161
+ })
@@ -58,7 +58,7 @@ export type UrlResolver = (chainId: ChainId) => string | undefined
58
58
  * webhook poller). A missing `url` falls back to the built-in public host map
59
59
  * ({@link url}); a missing `basicAuth` means no Basic auth is sent.
60
60
  *
61
- * Memoization is keyed on the resolved output (see `App.create` and
61
+ * Memoization is keyed on the resolved output (see {@link createGetClient} and
62
62
  * {@link Tidx.createGetClient}), not the context, so a resolver may branch on
63
63
  * any principal field without unbounded client cardinality: callers that
64
64
  * resolve to the same upstream share a client. Pass the result to
@@ -130,6 +130,39 @@ export function getClient(options: getClient.Options): getClient.ReturnType {
130
130
  .extend(tempoActions())
131
131
  }
132
132
 
133
+ /** Builds a memoized Tempo client resolver. */
134
+ export function createGetClient(options: createGetClient.Options = {}): createGetClient.ReturnType {
135
+ const { defaultChainId: defaultChainId_option = defaultChainId, rpc } = options
136
+ const clients = new Map<string, getClient.ReturnType>()
137
+
138
+ return (chainId = defaultChainId_option, principal = null) => {
139
+ const { basicAuth, url } = resolve(rpc, { chainId, principal })
140
+ const cacheKey = JSON.stringify([url, basicAuth, chainId])
141
+ const existing = clients.get(cacheKey)
142
+ if (existing) return existing
143
+
144
+ const client = getClient({
145
+ chainId,
146
+ rpc: { basicAuth, url: () => url },
147
+ })
148
+ clients.set(cacheKey, client)
149
+ return client
150
+ }
151
+ }
152
+
153
+ export declare namespace createGetClient {
154
+ /** Options for building a memoized Tempo client resolver. */
155
+ type Options = {
156
+ /** Tempo chain id used when a caller omits one. */
157
+ defaultChainId?: ChainId | undefined
158
+ /** Tempo RPC options applied to every constructed client. */
159
+ rpc?: getClient.Rpc | undefined
160
+ }
161
+
162
+ /** Memoized Tempo client resolver. */
163
+ type ReturnType = (chainId?: ChainId, principal?: Auth.Principal | null) => getClient.ReturnType
164
+ }
165
+
133
166
  export declare namespace getClient {
134
167
  /** Tempo viem client with public and Tempo action helpers. */
135
168
  type ReturnType = core_Client<