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.
- package/dist/App.d.ts +4879 -4870
- 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 +116 -116
- 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 +3199 -3206
- 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 +76 -3
- package/dist/apps/data/FundingQuoteProviders.d.ts.map +1 -1
- package/dist/apps/data/FundingQuoteProviders.js +276 -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/addresses.d.ts +206 -206
- package/dist/apps/data/routes/balances.d.ts +46 -46
- package/dist/apps/data/routes/blocks.d.ts +123 -123
- package/dist/apps/data/routes/coingecko.d.ts +169 -169
- package/dist/apps/data/routes/funding.d.ts +230 -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 +34 -34
- 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/receipts.d.ts +228 -228
- 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 +58 -58
- package/dist/apps/data/routes/verified-tokens.d.ts +86 -86
- package/dist/apps/management/App.d.ts +1042 -1042
- 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/invitations.d.ts +132 -132
- package/dist/apps/management/routes/invite-links.d.ts +62 -62
- 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 +96 -96
- package/dist/apps/management/routes/projects.d.ts +61 -61
- 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/db/tables/sponsoredTransactions.d.ts.map +1 -1
- package/dist/db/tables/sponsoredTransactions.js +6 -2
- package/dist/db/tables/sponsoredTransactions.js.map +1 -1
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +8 -0
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/FundingQuotes.d.ts +170 -56
- package/dist/internal/FundingQuotes.d.ts.map +1 -1
- package/dist/internal/FundingQuotes.js +91 -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/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 +2 -2
- package/src/ApiKeys.test.ts +2 -0
- package/src/App.test.ts +13 -2
- package/src/App.ts +31 -5
- package/src/Assets.ts +3 -3
- package/src/Client.test-d.ts +3 -2
- 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 +431 -5
- package/src/apps/data/FundingQuoteProviders.ts +386 -4
- package/src/apps/data/routes/funding.test.ts +174 -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/management/Billing.test.ts +2 -2
- 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/db/Db.test.ts +5 -5
- package/src/db/tables/billingSettings.test.ts +2 -2
- package/src/db/tables/organizations.test.ts +2 -2
- package/src/db/tables/sponsoredTransactions.test.ts +19 -2
- package/src/db/tables/sponsoredTransactions.ts +21 -14
- package/src/db/tables/stripeCustomers.test.ts +2 -2
- package/src/internal/Auth.ts +10 -0
- package/src/internal/FundingQuotes.test.ts +97 -3
- package/src/internal/FundingQuotes.ts +104 -6
- package/src/internal/Log.ts +21 -14
- 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
|
@@ -83,9 +83,11 @@ describe('listQuotes', () => {
|
|
|
83
83
|
},
|
|
84
84
|
})
|
|
85
85
|
|
|
86
|
-
await
|
|
87
|
-
|
|
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
|
-
|
|
371
|
-
|
|
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
|
-
|
|
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
|
|
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 ===
|
|
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
|
+
}
|
package/src/internal/Log.ts
CHANGED
|
@@ -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
|
-
|
|
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({
|
package/src/internal/Tidx.ts
CHANGED
|
@@ -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});
|
|
33
|
-
* missing
|
|
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(
|
|
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 `
|
|
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
|
+
})
|
package/src/internal/Viem.ts
CHANGED
|
@@ -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
|
|
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<
|