tapimo 0.4.4 → 0.5.0

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 (162) hide show
  1. package/dist/App.d.ts +7403 -6127
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +4 -0
  4. package/dist/App.js.map +1 -1
  5. package/dist/Scope.d.ts +18 -1
  6. package/dist/Scope.d.ts.map +1 -1
  7. package/dist/Scope.js +24 -3
  8. package/dist/Scope.js.map +1 -1
  9. package/dist/admin/apps/verified-tokens.d.ts +115 -115
  10. package/dist/apps/data/App.d.ts +1838 -1534
  11. package/dist/apps/data/App.d.ts.map +1 -1
  12. package/dist/apps/data/App.js +135 -44
  13. package/dist/apps/data/App.js.map +1 -1
  14. package/dist/apps/data/{FundingRouteProviders.d.ts → FundingQuoteProviders.d.ts} +7 -7
  15. package/dist/apps/data/{FundingRouteProviders.d.ts.map → FundingQuoteProviders.d.ts.map} +1 -1
  16. package/dist/apps/data/{FundingRouteProviders.js → FundingQuoteProviders.js} +36 -91
  17. package/dist/apps/data/FundingQuoteProviders.js.map +1 -0
  18. package/dist/apps/data/index.d.ts +1 -1
  19. package/dist/apps/data/index.js +1 -1
  20. package/dist/apps/data/routes/activities.d.ts +242 -242
  21. package/dist/apps/data/routes/addresses.d.ts +227 -227
  22. package/dist/apps/data/routes/balances.d.ts +12 -12
  23. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  24. package/dist/apps/data/routes/balances.js +67 -149
  25. package/dist/apps/data/routes/balances.js.map +1 -1
  26. package/dist/apps/data/routes/blocks.d.ts +125 -126
  27. package/dist/apps/data/routes/blocks.d.ts.map +1 -1
  28. package/dist/apps/data/routes/blocks.js +3 -5
  29. package/dist/apps/data/routes/blocks.js.map +1 -1
  30. package/dist/apps/data/routes/coingecko.d.ts +55 -54
  31. package/dist/apps/data/routes/coingecko.d.ts.map +1 -1
  32. package/dist/apps/data/routes/coingecko.js +3 -8
  33. package/dist/apps/data/routes/coingecko.js.map +1 -1
  34. package/dist/apps/data/routes/funding.d.ts +207 -388
  35. package/dist/apps/data/routes/funding.d.ts.map +1 -1
  36. package/dist/apps/data/routes/funding.js +143 -221
  37. package/dist/apps/data/routes/funding.js.map +1 -1
  38. package/dist/apps/data/routes/rpc.d.ts.map +1 -1
  39. package/dist/apps/data/routes/rpc.js +86 -16
  40. package/dist/apps/data/routes/rpc.js.map +1 -1
  41. package/dist/apps/data/routes/tokenlist.d.ts +33 -33
  42. package/dist/apps/data/routes/transactions.d.ts +281 -281
  43. package/dist/apps/data/routes/transfers.d.ts +58 -58
  44. package/dist/apps/data/routes/verified-tokens.d.ts +86 -86
  45. package/dist/apps/data/routes/webhooks.d.ts +340 -0
  46. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  47. package/dist/apps/data/routes/webhooks.js +13 -1
  48. package/dist/apps/data/routes/webhooks.js.map +1 -1
  49. package/dist/apps/index.d.ts +1 -1
  50. package/dist/apps/index.js +1 -1
  51. package/dist/apps/management/App.d.ts +1090 -108
  52. package/dist/apps/management/App.d.ts.map +1 -1
  53. package/dist/apps/management/App.js +4 -0
  54. package/dist/apps/management/App.js.map +1 -1
  55. package/dist/apps/management/routes/api-keys.d.ts +118 -118
  56. package/dist/apps/management/routes/api-keys.js +2 -2
  57. package/dist/apps/management/routes/api-keys.js.map +1 -1
  58. package/dist/apps/management/routes/billing.d.ts +207 -207
  59. package/dist/apps/management/routes/invite-links.d.ts +1100 -0
  60. package/dist/apps/management/routes/invite-links.d.ts.map +1 -0
  61. package/dist/apps/management/routes/invite-links.js +411 -0
  62. package/dist/apps/management/routes/invite-links.js.map +1 -0
  63. package/dist/apps/management/routes/me.d.ts +17 -17
  64. package/dist/apps/management/routes/members.d.ts +71 -71
  65. package/dist/apps/management/routes/orgs.d.ts +12 -12
  66. package/dist/apps/management/routes/projects.d.ts +70 -70
  67. package/dist/apps/management/routes/scopes.d.ts +11 -11
  68. package/dist/apps/management/routes/scopes.d.ts.map +1 -1
  69. package/dist/apps/management/routes/scopes.js +2 -2
  70. package/dist/apps/management/routes/scopes.js.map +1 -1
  71. package/dist/apps/management/routes/usage.d.ts +75 -75
  72. package/dist/apps/relay/App.d.ts.map +1 -1
  73. package/dist/apps/relay/App.js +42 -4
  74. package/dist/apps/relay/App.js.map +1 -1
  75. package/dist/db/Db.d.ts +3 -0
  76. package/dist/db/Db.d.ts.map +1 -1
  77. package/dist/db/Db.js +57 -0
  78. package/dist/db/Db.js.map +1 -1
  79. package/dist/db/Schema.d.ts +31 -0
  80. package/dist/db/Schema.d.ts.map +1 -1
  81. package/dist/db/Schema.js +31 -0
  82. package/dist/db/Schema.js.map +1 -1
  83. package/dist/db/tables/inviteLinks.d.ts +42 -0
  84. package/dist/db/tables/inviteLinks.d.ts.map +1 -0
  85. package/dist/db/tables/inviteLinks.js +148 -0
  86. package/dist/db/tables/inviteLinks.js.map +1 -0
  87. package/dist/db/tables/organizations.d.ts.map +1 -1
  88. package/dist/db/tables/organizations.js +2 -0
  89. package/dist/db/tables/organizations.js.map +1 -1
  90. package/dist/handlers/relay.d.ts.map +1 -1
  91. package/dist/handlers/relay.js +13 -3
  92. package/dist/handlers/relay.js.map +1 -1
  93. package/dist/internal/Auth.d.ts +9 -1
  94. package/dist/internal/Auth.d.ts.map +1 -1
  95. package/dist/internal/Auth.js +130 -10
  96. package/dist/internal/Auth.js.map +1 -1
  97. package/dist/internal/EdgeCache.d.ts +3 -1
  98. package/dist/internal/EdgeCache.d.ts.map +1 -1
  99. package/dist/internal/EdgeCache.js +17 -2
  100. package/dist/internal/EdgeCache.js.map +1 -1
  101. package/dist/internal/{FundingRoutes.d.ts → FundingQuotes.d.ts} +185 -428
  102. package/dist/internal/FundingQuotes.d.ts.map +1 -0
  103. package/dist/internal/{FundingRoutes.js → FundingQuotes.js} +93 -218
  104. package/dist/internal/FundingQuotes.js.map +1 -0
  105. package/dist/internal/Viem.d.ts +3 -1
  106. package/dist/internal/Viem.d.ts.map +1 -1
  107. package/dist/internal/Viem.js +11 -2
  108. package/dist/internal/Viem.js.map +1 -1
  109. package/dist/internal/index.d.ts +1 -1
  110. package/dist/internal/index.js +1 -1
  111. package/package.json +1 -1
  112. package/src/ApiKeys.test.ts +23 -0
  113. package/src/App.test-d.ts +2 -0
  114. package/src/App.ts +6 -0
  115. package/src/Client.test-d.ts +20 -11
  116. package/src/Scope.test.ts +1 -0
  117. package/src/Scope.ts +30 -4
  118. package/src/apps/data/App.test.ts +354 -0
  119. package/src/apps/data/App.ts +159 -52
  120. package/src/apps/data/FundingQuoteProviders.test.ts +151 -0
  121. package/src/apps/data/{FundingRouteProviders.ts → FundingQuoteProviders.ts} +40 -101
  122. package/src/apps/data/index.ts +1 -1
  123. package/src/apps/data/routes/balances.test.ts +64 -18
  124. package/src/apps/data/routes/balances.ts +76 -177
  125. package/src/apps/data/routes/blocks.ts +3 -5
  126. package/src/apps/data/routes/coingecko.ts +3 -8
  127. package/src/apps/data/routes/funding.test.ts +151 -206
  128. package/src/apps/data/routes/funding.ts +156 -291
  129. package/src/apps/data/routes/indexer.test.ts +97 -1
  130. package/src/apps/data/routes/rpc.test.ts +219 -0
  131. package/src/apps/data/routes/rpc.ts +92 -17
  132. package/src/apps/data/routes/webhooks.test.ts +21 -0
  133. package/src/apps/data/routes/webhooks.ts +13 -1
  134. package/src/apps/index.ts +1 -1
  135. package/src/apps/management/App.ts +4 -0
  136. package/src/apps/management/routes/api-keys.test.ts +47 -3
  137. package/src/apps/management/routes/api-keys.ts +2 -2
  138. package/src/apps/management/routes/invite-links.test.ts +385 -0
  139. package/src/apps/management/routes/invite-links.ts +570 -0
  140. package/src/apps/management/routes/scopes.test.ts +3 -2
  141. package/src/apps/management/routes/scopes.ts +4 -2
  142. package/src/apps/relay/App.test.ts +324 -4
  143. package/src/apps/relay/App.ts +40 -4
  144. package/src/db/Db.ts +65 -0
  145. package/src/db/Schema.ts +39 -0
  146. package/src/db/tables/inviteLinks.ts +187 -0
  147. package/src/db/tables/organizations.ts +2 -0
  148. package/src/handlers/relay.ts +16 -4
  149. package/src/internal/Auth.test.ts +132 -22
  150. package/src/internal/Auth.ts +171 -14
  151. package/src/internal/EdgeCache.test.ts +24 -3
  152. package/src/internal/EdgeCache.ts +18 -2
  153. package/src/internal/FundingQuotes.test.ts +174 -0
  154. package/src/internal/{FundingRoutes.ts → FundingQuotes.ts} +153 -312
  155. package/src/internal/Viem.ts +14 -5
  156. package/src/internal/index.test.ts +1 -1
  157. package/src/internal/index.ts +1 -1
  158. package/dist/apps/data/FundingRouteProviders.js.map +0 -1
  159. package/dist/internal/FundingRoutes.d.ts.map +0 -1
  160. package/dist/internal/FundingRoutes.js.map +0 -1
  161. package/src/apps/data/FundingRouteProviders.test.ts +0 -225
  162. package/src/internal/FundingRoutes.test.ts +0 -213
@@ -1,4 +1,4 @@
1
- import * as FundingRoutes from '../../internal/FundingRoutes.js'
1
+ import * as FundingQuotes from '../../internal/FundingQuotes.js'
2
2
 
3
3
  type Fetch = typeof globalThis.fetch
4
4
 
@@ -6,41 +6,41 @@ const defaultFetch: Fetch = (input, init) => globalThis.fetch(input, init)
6
6
 
7
7
  const defaultRelayBaseUrl = 'https://api.relay.link'
8
8
 
9
- /** Creates the live funding route adapters configured for this deployment. */
9
+ /** Creates the live funding quote adapters configured for this deployment. */
10
10
  export function createAdapters(options: createAdapters.Options = {}) {
11
11
  return (
12
12
  options.relay ? { relay: relay(options.relay) } : {}
13
- ) satisfies FundingRoutes.ProviderAdapters
13
+ ) satisfies FundingQuotes.ProviderAdapters
14
14
  }
15
15
 
16
16
  export declare namespace createAdapters {
17
- /** Provider adapter factory options. Omit a provider to keep its routes static. */
17
+ /** Provider adapter factory options. Omit a provider to leave it unconfigured. */
18
18
  type Options = {
19
19
  /** Relay quote adapter options. */
20
20
  relay?: relay.Options | false | undefined
21
21
  }
22
22
  }
23
23
 
24
- /** Creates a Relay quote adapter backed by `/chains/liquidity` and `/quote/v2`. */
25
- export function relay(options: relay.Options): FundingRoutes.ProviderAdapter {
24
+ /** Creates a Relay quote adapter backed by `/quote/v2`. */
25
+ export function relay(options: relay.Options): FundingQuotes.ProviderAdapter {
26
26
  const baseUrl = normalizeBaseUrl(options.baseUrl ?? defaultRelayBaseUrl)
27
27
  const fetcher = options.fetch ?? defaultFetch
28
28
  const now = options.now ?? (() => new Date())
29
29
 
30
30
  return {
31
- async probeQuote({ route }, signal) {
32
- const originChainId = providerChainId(route.sourceChain, 'relay')
33
- const destinationChainId = eip155ChainId(route.destinationChain)
31
+ async probeQuote({ candidate }, signal) {
32
+ const originChainId = providerChainId(candidate.sourceChain, 'relay')
33
+ const destinationChainId = eip155ChainId(candidate.destinationChain)
34
34
  const headers = relayHeaders(options.apiKey)
35
35
 
36
36
  try {
37
37
  const quote = await requestJson(new URL(`${baseUrl}/quote/v2`), {
38
38
  body: {
39
- amount: route.sourceAmount.amount,
39
+ amount: candidate.sourceAmount.amount,
40
40
  destinationChainId: Number(destinationChainId),
41
- destinationCurrency: route.destinationToken.address,
41
+ destinationCurrency: candidate.destinationToken.address,
42
42
  originChainId: Number(originChainId),
43
- originCurrency: route.sourceToken.address,
43
+ originCurrency: candidate.sourceToken.address,
44
44
  recipient: options.recipientAddress ?? options.userAddress,
45
45
  tradeType: 'EXACT_INPUT',
46
46
  user: options.userAddress,
@@ -50,12 +50,7 @@ export function relay(options: relay.Options): FundingRoutes.ProviderAdapter {
50
50
  method: 'POST',
51
51
  signal,
52
52
  })
53
- const liquidityUrl = new URL(`${baseUrl}/chains/liquidity`)
54
- liquidityUrl.searchParams.set('chainId', destinationChainId)
55
- const liquidity = await requestJson(liquidityUrl, { fetcher, headers, signal }).catch(
56
- () => undefined,
57
- )
58
- return formatRelayResult(route, quote, liquidity, now())
53
+ return formatRelayResult(candidate, quote, now())
59
54
  } catch (cause) {
60
55
  const unavailable = providerUnavailable(cause, [
61
56
  'NO_QUOTES',
@@ -93,68 +88,43 @@ export declare namespace relay {
93
88
  }
94
89
 
95
90
  function formatRelayResult(
96
- route: FundingRoutes.RouteCandidate,
91
+ candidate: FundingQuotes.QuoteCandidate,
97
92
  quote: unknown,
98
- liquidity: unknown,
99
93
  now: Date,
100
- ): FundingRoutes.QuoteProbeResult {
94
+ ): FundingQuotes.QuoteProbeResult {
101
95
  const details = record(record(quote)['details'])
102
96
  const currencyOut = record(details['currencyOut'])
103
97
  const outputAmount = decimalString(currencyOut['amount'])
104
- const liquidityRow = relayLiquidity(liquidity, route)
105
- const destinationLiquidity = decimalString(liquidityRow?.['balance'])
106
- const availableVolumeUsd = decimalString(liquidityRow?.['amountUsd']) ?? null
107
- const liquidityStatus = relayLiquidityStatus({ destinationLiquidity, outputAmount })
98
+ const status = outputAmount
99
+ ? { status: 'available' as const, tier: 'liquid' as const }
100
+ : { status: 'unknown' as const, tier: 'unknown' as const }
108
101
 
109
102
  return {
110
- limits: null,
111
103
  minimumOutputAmount: decimalString(currencyOut['minimumAmount']),
112
104
  outputAmount,
113
105
  outputAmountFormatted: outputAmount
114
- ? decimalFromBaseUnits(outputAmount, route.destinationToken.decimals)
115
- : null,
106
+ ? decimalFromBaseUnits(outputAmount, candidate.destinationToken.decimals)
107
+ : undefined,
116
108
  quality: {
117
- availableVolumeUsd: availableVolumeUsd,
118
109
  estimatedSeconds: nonnegativeInteger(details['timeEstimate']),
119
- feeBps: null,
120
- historicalVolumeUsd24h: null,
121
- instantVolumeUsd: availableVolumeUsd,
122
110
  liquiditySource: 'providerQuote',
123
- score: scoreFor(liquidityStatus.tier),
124
111
  sourceDetail: 'relay:quote-v2',
125
- tier: liquidityStatus.tier,
112
+ tier: status.tier,
126
113
  },
127
114
  sampledAt: now.toISOString(),
128
- status: liquidityStatus.status,
115
+ status: status.status,
129
116
  }
130
117
  }
131
118
 
132
- function relayLiquidityStatus(options: {
133
- destinationLiquidity: string | undefined
134
- outputAmount: string | undefined
135
- }): { status: FundingRoutes.RouteStatus; tier: FundingRoutes.QualityTier } {
136
- if (!options.outputAmount) return { status: 'unknown', tier: 'unknown' }
137
- if (!options.destinationLiquidity) return { status: 'unknown', tier: 'unknown' }
138
- if (compareIntegerStrings(options.destinationLiquidity, options.outputAmount) < 0)
139
- return { status: 'thin', tier: 'thin' }
140
- return { status: 'available', tier: 'liquid' }
141
- }
142
-
143
119
  function unavailableResult(options: {
144
120
  detail: string
145
121
  message: string
146
122
  now: Date
147
123
  source: string
148
- }): FundingRoutes.QuoteProbeResult {
124
+ }): FundingQuotes.QuoteProbeResult {
149
125
  return {
150
126
  quality: {
151
- availableVolumeUsd: null,
152
- estimatedSeconds: null,
153
- feeBps: null,
154
- historicalVolumeUsd24h: null,
155
- instantVolumeUsd: null,
156
127
  liquiditySource: options.source,
157
- score: 0,
158
128
  sourceDetail: `${options.detail}:${options.message}`,
159
129
  tier: 'unavailable',
160
130
  },
@@ -163,20 +133,6 @@ function unavailableResult(options: {
163
133
  }
164
134
  }
165
135
 
166
- function relayLiquidity(input: unknown, route: FundingRoutes.RouteCandidate) {
167
- const response = record(input)
168
- const rows = Array.isArray(response['liquidity']) ? response['liquidity'] : []
169
- const destinationChainId = eip155ChainId(route.destinationChain)
170
- return rows.map(record).find((row) => {
171
- const rowChainId = integerString(row['chainId'])
172
- const rowAddress = typeof row['address'] === 'string' ? row['address'].toLowerCase() : undefined
173
- return (
174
- rowChainId === destinationChainId &&
175
- rowAddress === route.destinationToken.address.toLowerCase()
176
- )
177
- })
178
- }
179
-
180
136
  async function requestJson(
181
137
  url: URL,
182
138
  options: {
@@ -220,38 +176,23 @@ function providerErrorCode(input: unknown) {
220
176
  return code && /^[A-Za-z0-9_.:-]{1,64}$/.test(code) ? code : undefined
221
177
  }
222
178
 
223
- class ProviderResponseError extends Error {
224
- override name = 'FundingRouteProviders.ProviderResponseError'
225
-
226
- constructor(
227
- readonly status: number,
228
- readonly body: unknown,
229
- ) {
230
- super(`Provider request failed with status ${status}`)
231
- }
232
- }
233
-
234
179
  function providerChainId(
235
- routeChain: FundingRoutes.RouteCandidate['sourceChain'],
180
+ quoteChain: FundingQuotes.QuoteCandidate['sourceChain'],
236
181
  provider: string,
237
182
  ) {
238
- const providerIds = routeChain.providerIds as Record<string, string> | undefined
183
+ const providerIds = quoteChain.providerIds as Record<string, string> | undefined
239
184
  const id = providerIds?.[provider]
240
185
  if (id && /^\d+$/.test(id)) return id
241
- return eip155ChainId(routeChain)
186
+ return eip155ChainId(quoteChain)
242
187
  }
243
188
 
244
189
  function eip155ChainId(chain: { id: string; kind: string }) {
245
- if (chain.kind !== 'evm') throw new UnsupportedProviderRouteError()
190
+ if (chain.kind !== 'evm') throw new UnsupportedProviderChainError()
246
191
  const match = /^eip155:(\d+)$/.exec(chain.id)
247
- if (!match?.[1]) throw new UnsupportedProviderRouteError()
192
+ if (!match?.[1]) throw new UnsupportedProviderChainError()
248
193
  return match[1]
249
194
  }
250
195
 
251
- class UnsupportedProviderRouteError extends Error {
252
- override name = 'FundingRouteProviders.UnsupportedProviderRouteError'
253
- }
254
-
255
196
  function normalizeBaseUrl(input: string) {
256
197
  return input.replace(/\/+$/, '')
257
198
  }
@@ -281,16 +222,10 @@ function decimalString(input: unknown) {
281
222
  return undefined
282
223
  }
283
224
 
284
- function integerString(input: unknown) {
285
- if (typeof input === 'string' && /^\d+$/.test(input)) return input
286
- if (typeof input === 'number' && Number.isSafeInteger(input) && input >= 0) return String(input)
287
- return undefined
288
- }
289
-
290
225
  function nonnegativeInteger(input: unknown) {
291
226
  if (typeof input === 'number' && Number.isSafeInteger(input) && input >= 0) return input
292
227
  if (typeof input === 'string' && /^\d+$/.test(input)) return Number(input)
293
- return null
228
+ return undefined
294
229
  }
295
230
 
296
231
  function decimalFromBaseUnits(baseUnits: string, decimals: number) {
@@ -300,13 +235,17 @@ function decimalFromBaseUnits(baseUnits: string, decimals: number) {
300
235
  return fraction ? `${whole}.${fraction}` : whole
301
236
  }
302
237
 
303
- function compareIntegerStrings(left: string, right: string) {
304
- return BigInt(left) === BigInt(right) ? 0 : BigInt(left) > BigInt(right) ? 1 : -1
238
+ class ProviderResponseError extends Error {
239
+ override name = 'FundingQuoteProviders.ProviderResponseError'
240
+
241
+ constructor(
242
+ readonly status: number,
243
+ readonly body: unknown,
244
+ ) {
245
+ super(`Provider request failed with status ${status}`)
246
+ }
305
247
  }
306
248
 
307
- function scoreFor(tier: FundingRoutes.QualityTier) {
308
- if (tier === 'liquid') return 90
309
- if (tier === 'thin') return 65
310
- if (tier === 'unavailable') return 0
311
- return null
249
+ class UnsupportedProviderChainError extends Error {
250
+ override name = 'FundingQuoteProviders.UnsupportedProviderChainError'
312
251
  }
@@ -1,3 +1,3 @@
1
1
  export { data } from './App.js'
2
- export * as FundingRouteProviders from './FundingRouteProviders.js'
2
+ export * as FundingQuoteProviders from './FundingQuoteProviders.js'
3
3
  export * as Sponsorships from '../relay/Sponsorships.js'
@@ -1,8 +1,10 @@
1
1
  import { Schema } from 'tapimo'
2
2
  import { Value as core_Value } from 'ox'
3
+ import { Actions } from 'viem/tempo'
3
4
 
4
5
  import * as TestApp from '../../../../test/App.js'
5
6
  import * as Runtime from '../../../../test/runtime.js'
7
+ import * as TestViem from '../../../../test/Viem.js'
6
8
  import * as Balances from './balances.js'
7
9
  import * as Tokens from './tokens.js'
8
10
 
@@ -45,7 +47,7 @@ describe('GET /addresses/:address/balances', () => {
45
47
  const body = await TestApp.json(response, Balances.schema.getAddressBalances.Response)
46
48
 
47
49
  expect(response.status).toBe(200)
48
- expect(response.headers.get('server-timing')?.includes('address_balances;dur=')).toBe(true)
50
+ expect(response.headers.get('server-timing')?.includes('balances_discovery;dur=')).toBe(true)
49
51
  expect(Array.isArray(body.data)).toBe(true)
50
52
  expect(body.data.length).toBeLessThanOrEqual(5)
51
53
  // Pagination fields stay at the response root; cursor pagination has no
@@ -72,16 +74,48 @@ describe('GET /addresses/:address/balances', () => {
72
74
  ),
73
75
  ).toBe(true)
74
76
  // Results are restricted to TIP-20 tokens (deterministic `0x20c0…` factory
75
- // addresses), so non-TIP-20 contracts in `token_balances_snapshot` are
76
- // excluded.
77
+ // addresses), so non-TIP-20 contracts in the delta history are excluded.
77
78
  expect(body.data.every((entry) => entry.token.address.startsWith('0x20c0'))).toBe(true)
78
79
  // Amounts are returned largest-first.
79
80
  const amounts = body.data.map((entry) => BigInt(entry.amount))
80
81
  expect(amounts.every((amount, index) => index === 0 || amounts[index - 1]! >= amount)).toBe(
81
82
  true,
82
83
  )
84
+
85
+ const rpc = TestViem.getClient()
86
+ const current = await Promise.all(
87
+ body.data.map((entry) =>
88
+ Actions.token.getBalance(rpc, { account: holder, token: entry.token.address }),
89
+ ),
90
+ )
91
+ expect(body.data.map((entry) => entry.amount)).toEqual(
92
+ current.map((balance) => balance.amount.toString()),
93
+ )
83
94
  })
84
95
 
96
+ test.skipIf(runtime.mode !== 'localnet')(
97
+ 'discovers freshly transferred assets from deltas',
98
+ async () => {
99
+ const fixture = runtime.fixtures?.crossTokenTransfer
100
+ if (!fixture) throw new Error('expected cross-token transfer fixture')
101
+ const client = app()
102
+
103
+ const response = await client.v1.addresses[':address'].balances.$get(
104
+ { param: { address: fixture.recipient }, query: { limit: '5' } },
105
+ TestApp.auth,
106
+ )
107
+ const body = await TestApp.json(response, Balances.schema.getAddressBalances.Response)
108
+
109
+ expect(response.status).toBe(200)
110
+ expect(body.data).toContainEqual(
111
+ expect.objectContaining({
112
+ amount: '1000000',
113
+ id: fixture.destinationToken,
114
+ }),
115
+ )
116
+ },
117
+ )
118
+
85
119
  test('embeds an exact balance total on demand via include=totalCount', async () => {
86
120
  const client = app()
87
121
  const holder = await holderWithBalance(client)
@@ -93,15 +127,14 @@ describe('GET /addresses/:address/balances', () => {
93
127
  const body = await TestApp.json(response, Balances.schema.getAddressBalances.Response)
94
128
 
95
129
  expect(response.status).toBe(200)
96
- expect(response.headers.get('server-timing')?.includes('address_balances_count;dur=')).toBe(
97
- true,
98
- )
130
+ expect(response.headers.get('server-timing')?.includes('balances_discovery;dur=')).toBe(true)
99
131
  expect(Number.isInteger(body.meta?.totalCount)).toBe(true)
100
132
  expect(body.meta!.totalCount).toBeGreaterThanOrEqual(body.data.length)
101
- // The count is sort-key-pruned on ClickHouse, so it is exact (never capped).
133
+ // The count comes from the corrected candidate set and is never capped.
102
134
  expect(body.meta!.totalCountCapped).toBe(false)
103
135
 
104
- // Without the include, `meta` is omitted and no count query runs.
136
+ // Without the include, `meta` is omitted. The corrected candidate set
137
+ // already supplies the count, so no second indexer query is needed.
105
138
  const bareResponse = await client.v1.addresses[':address'].balances.$get(
106
139
  { param: { address: holder }, query: { limit: '5' } },
107
140
  TestApp.auth,
@@ -109,8 +142,8 @@ describe('GET /addresses/:address/balances', () => {
109
142
  const bare = await TestApp.json(bareResponse, Balances.schema.getAddressBalances.Response)
110
143
  expect(bareResponse.status).toBe(200)
111
144
  expect(bare.meta).toBeUndefined()
112
- expect(bareResponse.headers.get('server-timing')?.includes('address_balances_count;dur=')).toBe(
113
- false,
145
+ expect(bareResponse.headers.get('server-timing')?.includes('balances_discovery;dur=')).toBe(
146
+ true,
114
147
  )
115
148
  })
116
149
 
@@ -138,6 +171,27 @@ describe('GET /addresses/:address/balances', () => {
138
171
  `)
139
172
  })
140
173
 
174
+ test('surfaces a 502 when RPC correction is unavailable', async () => {
175
+ const client = app({ rpc: { url: () => 'http://127.0.0.1:1' } })
176
+ const holder = await holderWithBalance(client)
177
+ const response = await client.v1.addresses[':address'].balances.$get(
178
+ { param: { address: holder }, query: {} },
179
+ TestApp.auth,
180
+ )
181
+ const body = await TestApp.json(response, Schema.ErrorResponse)
182
+ const { requestId, error } = body
183
+ const { message, ...stableError } = error
184
+
185
+ expect(response.status).toMatchInlineSnapshot(`502`)
186
+ expect(typeof requestId).toMatchInlineSnapshot(`"string"`)
187
+ expect(typeof message).toMatchInlineSnapshot(`"string"`)
188
+ expect(stableError).toMatchInlineSnapshot(`
189
+ {
190
+ "code": "upstream_error",
191
+ }
192
+ `)
193
+ })
194
+
141
195
  test('rejects invalid account addresses', async () => {
142
196
  // The typed client requires `address: `Hex.Hex`` so an invalid literal
143
197
  // cannot pass through it; exercise the validator via `app.request`.
@@ -250,14 +304,6 @@ describe('GET /addresses/:address/balances', () => {
250
304
  },
251
305
  TestApp.auth,
252
306
  )
253
- if (runtime.mode === 'testnet' && response.status === 502) {
254
- // Hosted TIDX can intermittently reject this live filtered balance query
255
- // with a 422. Localnet still exercises the behavior strictly; on testnet,
256
- // accept the documented upstream-error envelope instead of flaking CI.
257
- const body = await TestApp.json(response, Schema.ErrorResponse)
258
- expect(body.error.code).toBe('upstream_error')
259
- return
260
- }
261
307
  const body = await TestApp.json(response, Balances.schema.getAddressBalances.Response)
262
308
  expect(response.status).toBe(200)
263
309