tapimo 0.10.0 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/dist/ApiKey.d.ts +2 -1
  2. package/dist/ApiKey.d.ts.map +1 -1
  3. package/dist/ApiKeys.d.ts +4 -2
  4. package/dist/ApiKeys.d.ts.map +1 -1
  5. package/dist/ApiKeys.js +26 -1
  6. package/dist/ApiKeys.js.map +1 -1
  7. package/dist/App.d.ts +6007 -5003
  8. package/dist/App.d.ts.map +1 -1
  9. package/dist/App.js +1 -0
  10. package/dist/App.js.map +1 -1
  11. package/dist/Webhooks.d.ts +1 -1
  12. package/dist/Webhooks.d.ts.map +1 -1
  13. package/dist/Webhooks.js +6 -6
  14. package/dist/Webhooks.js.map +1 -1
  15. package/dist/admin/App.d.ts +91 -84
  16. package/dist/admin/App.d.ts.map +1 -1
  17. package/dist/admin/apps/api-keys.d.ts +22 -13
  18. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  19. package/dist/admin/apps/api-keys.js +6 -0
  20. package/dist/admin/apps/api-keys.js.map +1 -1
  21. package/dist/admin/apps/organizations.d.ts +4 -2
  22. package/dist/admin/apps/organizations.d.ts.map +1 -1
  23. package/dist/admin/ui.generated.d.ts +1 -1
  24. package/dist/admin/ui.generated.js +69 -69
  25. package/dist/admin/ui.generated.js.map +1 -1
  26. package/dist/analytics/Analytics.d.ts.map +1 -1
  27. package/dist/analytics/Analytics.js +232 -0
  28. package/dist/analytics/Analytics.js.map +1 -1
  29. package/dist/analytics/tables/requestEvents.d.ts +4 -0
  30. package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
  31. package/dist/analytics/tables/requestEvents.js +2 -0
  32. package/dist/analytics/tables/requestEvents.js.map +1 -1
  33. package/dist/apps/Zones.d.ts +4266 -82
  34. package/dist/apps/Zones.d.ts.map +1 -1
  35. package/dist/apps/Zones.js +27 -8
  36. package/dist/apps/Zones.js.map +1 -1
  37. package/dist/apps/data/App.d.ts +3467 -2696
  38. package/dist/apps/data/App.d.ts.map +1 -1
  39. package/dist/apps/data/App.js +2 -0
  40. package/dist/apps/data/App.js.map +1 -1
  41. package/dist/apps/data/routes/activities.d.ts +2999 -100
  42. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  43. package/dist/apps/data/routes/activities.js +340 -2
  44. package/dist/apps/data/routes/activities.js.map +1 -1
  45. package/dist/apps/data/routes/addresses.d.ts +514 -0
  46. package/dist/apps/data/routes/addresses.d.ts.map +1 -1
  47. package/dist/apps/data/routes/balances.d.ts +26 -26
  48. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  49. package/dist/apps/data/routes/balances.js +7 -3
  50. package/dist/apps/data/routes/balances.js.map +1 -1
  51. package/dist/apps/data/routes/blocks.d.ts +4 -4
  52. package/dist/apps/data/routes/earn.d.ts +75 -75
  53. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  54. package/dist/apps/data/routes/earn.js +65 -36
  55. package/dist/apps/data/routes/earn.js.map +1 -1
  56. package/dist/apps/data/routes/exchanges.d.ts +124 -120
  57. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  58. package/dist/apps/data/routes/exchanges.js +115 -77
  59. package/dist/apps/data/routes/exchanges.js.map +1 -1
  60. package/dist/apps/data/routes/fee-amm.d.ts +24 -24
  61. package/dist/apps/data/routes/indexer.d.ts +2 -1
  62. package/dist/apps/data/routes/indexer.d.ts.map +1 -1
  63. package/dist/apps/data/routes/indexer.js +3 -2
  64. package/dist/apps/data/routes/indexer.js.map +1 -1
  65. package/dist/apps/data/routes/receipts.d.ts +4 -3
  66. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  67. package/dist/apps/data/routes/receipts.js +15 -14
  68. package/dist/apps/data/routes/receipts.js.map +1 -1
  69. package/dist/apps/data/routes/rpc.d.ts.map +1 -1
  70. package/dist/apps/data/routes/rpc.js +5 -1
  71. package/dist/apps/data/routes/rpc.js.map +1 -1
  72. package/dist/apps/data/routes/tokenlist.d.ts +12 -12
  73. package/dist/apps/data/routes/tokens.d.ts +1 -1
  74. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  75. package/dist/apps/data/routes/tokens.js +4 -3
  76. package/dist/apps/data/routes/tokens.js.map +1 -1
  77. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  78. package/dist/apps/data/routes/transfers.js +25 -7
  79. package/dist/apps/data/routes/transfers.js.map +1 -1
  80. package/dist/apps/data/routes/verified-tokens.d.ts +37 -37
  81. package/dist/apps/data/routes/webhooks.d.ts +71 -1
  82. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  83. package/dist/apps/data/routes/webhooks.js +177 -23
  84. package/dist/apps/data/routes/webhooks.js.map +1 -1
  85. package/dist/apps/data/routes/zones.js +1 -1
  86. package/dist/apps/data/routes/zones.js.map +1 -1
  87. package/dist/apps/funding/App.d.ts +322 -89
  88. package/dist/apps/funding/App.d.ts.map +1 -1
  89. package/dist/apps/funding/routes/deposit-addresses.d.ts +375 -45
  90. package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
  91. package/dist/apps/funding/routes/deposit-addresses.js +208 -12
  92. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
  93. package/dist/apps/funding/routes/transfers.d.ts +32 -32
  94. package/dist/apps/funding/routes/transfers.js +1 -1
  95. package/dist/apps/funding/routes/transfers.js.map +1 -1
  96. package/dist/apps/management/App.d.ts +418 -418
  97. package/dist/apps/management/routes/api-keys.d.ts +50 -50
  98. package/dist/apps/management/routes/billing.d.ts +72 -72
  99. package/dist/apps/management/routes/billing.js +1 -1
  100. package/dist/apps/management/routes/billing.js.map +1 -1
  101. package/dist/apps/management/routes/faucet.d.ts +8 -8
  102. package/dist/apps/management/routes/faucet.js +2 -2
  103. package/dist/apps/management/routes/faucet.js.map +1 -1
  104. package/dist/apps/management/routes/invite-links.d.ts +42 -42
  105. package/dist/apps/management/routes/me.d.ts +6 -6
  106. package/dist/apps/management/routes/members.d.ts +14 -14
  107. package/dist/apps/management/routes/projects.d.ts +40 -40
  108. package/dist/apps/management/routes/usage.d.ts +8 -8
  109. package/dist/apps/management/routes/webhooks.d.ts +49 -49
  110. package/dist/apps/mcp.d.ts.map +1 -1
  111. package/dist/apps/mcp.js +4 -1
  112. package/dist/apps/mcp.js.map +1 -1
  113. package/dist/db/Db.d.ts +2 -0
  114. package/dist/db/Db.d.ts.map +1 -1
  115. package/dist/db/Db.js +90 -0
  116. package/dist/db/Db.js.map +1 -1
  117. package/dist/db/Schema.d.ts +19 -0
  118. package/dist/db/Schema.d.ts.map +1 -1
  119. package/dist/db/Schema.js +24 -0
  120. package/dist/db/Schema.js.map +1 -1
  121. package/dist/db/tables/fundingDepositAddresses.d.ts +20 -0
  122. package/dist/db/tables/fundingDepositAddresses.d.ts.map +1 -1
  123. package/dist/db/tables/fundingDepositAddresses.js +17 -0
  124. package/dist/db/tables/fundingDepositAddresses.js.map +1 -1
  125. package/dist/db/tables/fundingDeposits.d.ts +41 -0
  126. package/dist/db/tables/fundingDeposits.d.ts.map +1 -1
  127. package/dist/db/tables/fundingDeposits.js +69 -0
  128. package/dist/db/tables/fundingDeposits.js.map +1 -1
  129. package/dist/db/tables/webhookQueueEvents.d.ts +122 -0
  130. package/dist/db/tables/webhookQueueEvents.d.ts.map +1 -0
  131. package/dist/db/tables/webhookQueueEvents.js +160 -0
  132. package/dist/db/tables/webhookQueueEvents.js.map +1 -0
  133. package/dist/db/tables/webhookSubscriptions.d.ts +2 -0
  134. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  135. package/dist/db/tables/webhookSubscriptions.js +10 -0
  136. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  137. package/dist/internal/Auth.d.ts +3 -26
  138. package/dist/internal/Auth.d.ts.map +1 -1
  139. package/dist/internal/Auth.js +6 -37
  140. package/dist/internal/Auth.js.map +1 -1
  141. package/dist/internal/Credential.d.ts +27 -0
  142. package/dist/internal/Credential.d.ts.map +1 -0
  143. package/dist/internal/Credential.js +34 -0
  144. package/dist/internal/Credential.js.map +1 -0
  145. package/dist/internal/EarnRates.js +11 -7
  146. package/dist/internal/EarnRates.js.map +1 -1
  147. package/dist/internal/Log.d.ts +8 -0
  148. package/dist/internal/Log.d.ts.map +1 -1
  149. package/dist/internal/Log.js +4 -0
  150. package/dist/internal/Log.js.map +1 -1
  151. package/dist/internal/MetricSink.d.ts +51 -0
  152. package/dist/internal/MetricSink.d.ts.map +1 -1
  153. package/dist/internal/MetricSink.js +54 -2
  154. package/dist/internal/MetricSink.js.map +1 -1
  155. package/dist/internal/RateLimit.d.ts +4 -6
  156. package/dist/internal/RateLimit.d.ts.map +1 -1
  157. package/dist/internal/RateLimit.js +15 -14
  158. package/dist/internal/RateLimit.js.map +1 -1
  159. package/dist/internal/Store.d.ts +2 -1
  160. package/dist/internal/Store.d.ts.map +1 -1
  161. package/dist/internal/Store.js +57 -18
  162. package/dist/internal/Store.js.map +1 -1
  163. package/dist/internal/Tidx.d.ts +4 -3
  164. package/dist/internal/Tidx.d.ts.map +1 -1
  165. package/dist/internal/Tidx.js +2 -2
  166. package/dist/internal/Tidx.js.map +1 -1
  167. package/dist/internal/Value.d.ts +2 -0
  168. package/dist/internal/Value.d.ts.map +1 -1
  169. package/dist/internal/Value.js +25 -0
  170. package/dist/internal/Value.js.map +1 -1
  171. package/dist/internal/Viem.d.ts +4 -3
  172. package/dist/internal/Viem.d.ts.map +1 -1
  173. package/dist/internal/Viem.js +3 -2
  174. package/dist/internal/Viem.js.map +1 -1
  175. package/dist/internal/WebhookTransfer.d.ts +20 -0
  176. package/dist/internal/WebhookTransfer.d.ts.map +1 -1
  177. package/dist/internal/WebhookTransfer.js +7 -4
  178. package/dist/internal/WebhookTransfer.js.map +1 -1
  179. package/dist/internal/Webhooks.d.ts +141 -0
  180. package/dist/internal/Webhooks.d.ts.map +1 -1
  181. package/dist/internal/Webhooks.js +139 -10
  182. package/dist/internal/Webhooks.js.map +1 -1
  183. package/dist/internal/funding/providers/relay.d.ts +3 -4
  184. package/dist/internal/funding/providers/relay.d.ts.map +1 -1
  185. package/dist/internal/funding/providers/relay.js +59 -8
  186. package/dist/internal/funding/providers/relay.js.map +1 -1
  187. package/dist/internal/index.d.ts +1 -0
  188. package/dist/internal/index.d.ts.map +1 -1
  189. package/dist/internal/index.js +1 -0
  190. package/dist/internal/index.js.map +1 -1
  191. package/package.json +3 -3
  192. package/src/ApiKeys.test.ts +20 -0
  193. package/src/ApiKeys.ts +28 -1
  194. package/src/App.test.ts +40 -8
  195. package/src/App.ts +1 -0
  196. package/src/Client.test-d.ts +34 -1
  197. package/src/Webhooks.ts +20 -18
  198. package/src/admin/apps/api-keys.test.ts +9 -0
  199. package/src/admin/apps/api-keys.ts +15 -0
  200. package/src/admin/ui/src/lib/query-client.ts +3 -2
  201. package/src/admin/ui/src/routes/api-keys.tsx +31 -9
  202. package/src/admin/ui.generated.ts +69 -69
  203. package/src/analytics/Analytics.test.ts +42 -0
  204. package/src/analytics/Analytics.ts +232 -0
  205. package/src/analytics/tables/requestEvents.ts +6 -0
  206. package/src/apps/Zones.test.ts +20 -3
  207. package/src/apps/Zones.ts +28 -8
  208. package/src/apps/composition.test.ts +2 -0
  209. package/src/apps/data/App.test.ts +2 -2
  210. package/src/apps/data/App.ts +2 -0
  211. package/src/apps/data/routes/activities.bench.ts +41 -0
  212. package/src/apps/data/routes/activities.test.ts +103 -2
  213. package/src/apps/data/routes/activities.ts +401 -2
  214. package/src/apps/data/routes/balances.bench.ts +21 -0
  215. package/src/apps/data/routes/balances.ts +7 -3
  216. package/src/apps/data/routes/blocks.bench.ts +34 -0
  217. package/src/apps/data/routes/coingecko.bench.ts +53 -0
  218. package/src/apps/data/routes/earn.bench.ts +71 -0
  219. package/src/apps/data/routes/earn.ts +66 -36
  220. package/src/apps/data/routes/exchanges.bench.ts +85 -0
  221. package/src/apps/data/routes/exchanges.test.ts +8 -3
  222. package/src/apps/data/routes/exchanges.ts +134 -82
  223. package/src/apps/data/routes/fee-amm.bench.ts +13 -0
  224. package/src/apps/data/routes/indexer.bench.ts +8 -0
  225. package/src/apps/data/routes/indexer.test.ts +2 -1
  226. package/src/apps/data/routes/indexer.ts +3 -2
  227. package/src/apps/data/routes/receipts.bench.ts +39 -0
  228. package/src/apps/data/routes/receipts.ts +16 -14
  229. package/src/apps/data/routes/rpc.bench.ts +14 -0
  230. package/src/apps/data/routes/rpc.test.ts +27 -0
  231. package/src/apps/data/routes/rpc.ts +5 -1
  232. package/src/apps/data/routes/tokenlist.bench.ts +3 -0
  233. package/src/apps/data/routes/tokens.bench.ts +76 -0
  234. package/src/apps/data/routes/tokens.ts +3 -2
  235. package/src/apps/data/routes/transactions.bench.ts +48 -0
  236. package/src/apps/data/routes/transfers.bench.ts +28 -0
  237. package/src/apps/data/routes/transfers.ts +36 -10
  238. package/src/apps/data/routes/valuation.bench.ts +14 -0
  239. package/src/apps/data/routes/verified-tokens.bench.ts +8 -0
  240. package/src/apps/data/routes/webhooks.bench.ts +26 -0
  241. package/src/apps/data/routes/webhooks.scan.test.ts +326 -0
  242. package/src/apps/data/routes/webhooks.test.ts +53 -6
  243. package/src/apps/data/routes/webhooks.ts +252 -24
  244. package/src/apps/data/routes/zones.bench.ts +32 -0
  245. package/src/apps/data/routes/zones.test.ts +1 -1
  246. package/src/apps/data/routes/zones.ts +1 -1
  247. package/src/apps/funding/routes/chains.test.ts +1 -1
  248. package/src/apps/funding/routes/deposit-addresses.test.ts +217 -3
  249. package/src/apps/funding/routes/deposit-addresses.ts +255 -19
  250. package/src/apps/funding/routes/providers.test.ts +1 -1
  251. package/src/apps/funding/routes/quotes.test.ts +1 -1
  252. package/src/apps/funding/routes/transfers.test.ts +20 -8
  253. package/src/apps/funding/routes/transfers.ts +1 -1
  254. package/src/apps/management/routes/billing.ts +1 -1
  255. package/src/apps/management/routes/faucet.ts +2 -2
  256. package/src/apps/management/routes/me.test.ts +31 -0
  257. package/src/apps/mcp.ts +4 -1
  258. package/src/db/Db.ts +117 -0
  259. package/src/db/Schema.ts +32 -0
  260. package/src/db/tables/fundingDepositAddresses.test.ts +28 -0
  261. package/src/db/tables/fundingDepositAddresses.ts +39 -0
  262. package/src/db/tables/fundingDeposits.test.ts +82 -0
  263. package/src/db/tables/fundingDeposits.ts +130 -0
  264. package/src/db/tables/webhookQueueEvents.ts +285 -0
  265. package/src/db/tables/webhookSubscriptions.ts +15 -0
  266. package/src/internal/Auth.test.ts +61 -98
  267. package/src/internal/Auth.ts +9 -63
  268. package/src/internal/Credential.test.ts +51 -0
  269. package/src/internal/Credential.ts +58 -0
  270. package/src/internal/EarnRates.ts +13 -7
  271. package/src/internal/EarnVaults.test.ts +2 -2
  272. package/src/internal/Log.test.ts +3 -1
  273. package/src/internal/Log.ts +14 -0
  274. package/src/internal/MetricSink.test.ts +108 -2
  275. package/src/internal/MetricSink.ts +112 -2
  276. package/src/internal/RateLimit.test.ts +23 -18
  277. package/src/internal/RateLimit.ts +23 -15
  278. package/src/internal/Store.test.ts +99 -13
  279. package/src/internal/Store.ts +52 -17
  280. package/src/internal/Tidx.test.ts +4 -4
  281. package/src/internal/Tidx.ts +5 -4
  282. package/src/internal/Value.test.ts +25 -0
  283. package/src/internal/Value.ts +29 -0
  284. package/src/internal/Viem.test.ts +4 -0
  285. package/src/internal/Viem.ts +6 -4
  286. package/src/internal/WebhookTransfer.test.ts +89 -0
  287. package/src/internal/WebhookTransfer.ts +11 -7
  288. package/src/internal/Webhooks.test.ts +488 -0
  289. package/src/internal/Webhooks.ts +291 -10
  290. package/src/internal/funding/Provider.test.ts +1 -1
  291. package/src/internal/funding/providers/index.test.ts +73 -3
  292. package/src/internal/funding/providers/relay.test.ts +1 -5
  293. package/src/internal/funding/providers/relay.ts +56 -12
  294. package/src/internal/index.test.ts +1 -0
  295. package/src/internal/index.ts +1 -0
@@ -389,6 +389,39 @@ describe('durableObject', () => {
389
389
  `)
390
390
  })
391
391
 
392
+ test('preserves bigint values and collision-shaped strings', async () => {
393
+ const { namespace } = TestStore.durableObjectNamespace()
394
+ const store = Store.durableObject<{
395
+ value: { bigint: bigint; legacyMarker: string; valueMarker: string }
396
+ }>(namespace)
397
+ const value = {
398
+ bigint: 42n,
399
+ legacyMarker: '123#__bigint',
400
+ valueMarker: 'tempo-api:store:value:bigint:123',
401
+ }
402
+
403
+ await store.put('value', value)
404
+ await store.update('value', (current) => ({
405
+ op: 'set',
406
+ result: undefined,
407
+ value: { ...current!, bigint: current!.bigint + 1n },
408
+ }))
409
+
410
+ expect(await store.get('value')).toEqual({ ...value, bigint: 43n })
411
+ })
412
+
413
+ test('reads typed values written with the legacy codec', async () => {
414
+ const { getState, namespace } = TestStore.durableObjectNamespace()
415
+ await getState('channel').put('channel', 'tempo-api:store:json:v1:{"spent":"42#__bigint"}')
416
+ const store = Store.durableObject<TypedItems>(namespace)
417
+
418
+ expect(await store.get('channel')).toMatchInlineSnapshot(`
419
+ {
420
+ "spent": 42n,
421
+ }
422
+ `)
423
+ })
424
+
392
425
  test('keeps typed updates linearizable', async () => {
393
426
  const { namespace } = TestStore.durableObjectNamespace()
394
427
  const storeA = Store.durableObject<TypedItems>(namespace)
@@ -439,11 +472,11 @@ describe('durableObject', () => {
439
472
 
440
473
  test('rejects typed values written with an unknown codec version', async () => {
441
474
  const { getState, namespace } = TestStore.durableObjectNamespace()
442
- await getState('channel').put('channel', 'tempo-api:store:json:v2:{}')
475
+ await getState('channel').put('channel', 'tempo-api:store:json:v3:{}')
443
476
  const store = Store.durableObject<TypedItems>(namespace)
444
477
 
445
478
  await expect(store.get('channel')).rejects.toThrowErrorMatchingInlineSnapshot(
446
- `[Store.DurableObjectCodecError: Unsupported Durable Object store codec version: v2]`,
479
+ `[Store.DurableObjectCodecError: Unsupported Durable Object store codec version: v3]`,
447
480
  )
448
481
  })
449
482
 
@@ -744,11 +777,9 @@ describe('memoize', () => {
744
777
  const values = await Promise.all(requests)
745
778
 
746
779
  expect(calls).toMatchInlineSnapshot(`1`)
747
- expect(JSON.parse((await store.get('memo:test')) ?? 'null')).toMatchInlineSnapshot(`
748
- {
749
- "value": "fresh",
750
- }
751
- `)
780
+ expect(await store.get('memo:test')).toMatchInlineSnapshot(
781
+ `"tempo-api:store:json:v2:{"value":"fresh"}"`,
782
+ )
752
783
  expect(values).toMatchInlineSnapshot(`
753
784
  [
754
785
  {
@@ -805,8 +836,65 @@ describe('memoize', () => {
805
836
  `)
806
837
  })
807
838
 
839
+ test('preserves bigint values and collision-shaped strings on cache hits', async () => {
840
+ const store = Store.memory()
841
+ let calls = 0
842
+ const fetch = async () => {
843
+ calls += 1
844
+ return {
845
+ bigint: 42n,
846
+ legacyMarker: '123#__bigint',
847
+ valueMarker: 'tempo-api:store:value:bigint:123',
848
+ }
849
+ }
850
+
851
+ const first = await Store.memoize(fetch, { key: 'memo:test', store, ttl: 60_000 })
852
+ const second = await Store.memoize(fetch, { key: 'memo:test', store, ttl: 60_000 })
853
+
854
+ expect(first).toEqual(second)
855
+ expect(second).toEqual({
856
+ bigint: 42n,
857
+ legacyMarker: '123#__bigint',
858
+ valueMarker: 'tempo-api:store:value:bigint:123',
859
+ })
860
+ expect(calls).toBe(1)
861
+ })
862
+
863
+ test('does not cache undefined results', async () => {
864
+ const store = Store.memory()
865
+ let calls = 0
866
+ const fetch = async () => {
867
+ calls += 1
868
+ return undefined
869
+ }
870
+
871
+ const first = await Store.memoize(fetch, { key: 'memo:test', store, ttl: 60_000 })
872
+ const second = await Store.memoize(fetch, { key: 'memo:test', store, ttl: 60_000 })
873
+
874
+ expect(first).toBeUndefined()
875
+ expect(second).toBeUndefined()
876
+ expect(await store.get('memo:test')).toBeNull()
877
+ expect(calls).toBe(2)
878
+ })
879
+
880
+ test('refreshes legacy untagged cache entries', async () => {
881
+ const store = Store.memory({ entries: [['memo:test', '{"value":"123#__bigint"}']] })
882
+ let calls = 0
883
+
884
+ const value = await Store.memoize(
885
+ async () => {
886
+ calls += 1
887
+ return { value: '123#__bigint' }
888
+ },
889
+ { key: 'memo:test', store, ttl: 60_000 },
890
+ )
891
+
892
+ expect(value).toEqual({ value: '123#__bigint' })
893
+ expect(calls).toBe(1)
894
+ })
895
+
808
896
  test('refreshes corrupt cache entries', async () => {
809
- const store = Store.memory({ entries: [['memo:test', 'not-json']] })
897
+ const store = Store.memory({ entries: [['memo:test', 'tempo-api:store:json:v2:not-json']] })
810
898
 
811
899
  const value = await Store.memoize(async () => ({ value: 'fresh' }), {
812
900
  key: 'memo:test',
@@ -819,11 +907,9 @@ describe('memoize', () => {
819
907
  "value": "fresh",
820
908
  }
821
909
  `)
822
- expect(JSON.parse((await store.get('memo:test')) ?? 'null')).toMatchInlineSnapshot(`
823
- {
824
- "value": "fresh",
825
- }
826
- `)
910
+ expect(await store.get('memo:test')).toMatchInlineSnapshot(
911
+ `"tempo-api:store:json:v2:{"value":"fresh"}"`,
912
+ )
827
913
  })
828
914
  })
829
915
 
@@ -578,7 +578,8 @@ export declare namespace durableObject {
578
578
  /**
579
579
  * Wraps an async producer with store-backed caching. Returns the cached value if
580
580
  * present and valid; otherwise runs `fetch`, stores the JSON-encoded result
581
- * under `key` with `ttl` milliseconds, and returns it.
581
+ * under `key` with `ttl` milliseconds, and returns it. Undefined results are
582
+ * returned without being cached.
582
583
  */
583
584
  export async function memoize<value>(
584
585
  fetch: () => Promise<value>,
@@ -587,9 +588,11 @@ export async function memoize<value>(
587
588
  const { key, store, ttl } = options
588
589
 
589
590
  const raw = await store.get(key)
590
- if (raw) {
591
+ // Old cache entries used ambiguous untagged bigint strings. Refresh them
592
+ // instead of guessing whether a matching string was originally a bigint.
593
+ if (raw?.startsWith(jsonCodecPrefix)) {
591
594
  try {
592
- return Json.parse(raw) as value
595
+ return jsonDecode(raw) as value
593
596
  } catch {
594
597
  // Corrupt entries are treated as misses so the next call refreshes.
595
598
  }
@@ -604,7 +607,8 @@ export async function memoize<value>(
604
607
  // fetch is still in flight.
605
608
  const flight = (async () => {
606
609
  const value = await fetch()
607
- await store.put(key, Json.stringify(value), { ttl })
610
+ if (value === undefined) return value
611
+ await store.put(key, jsonEncode(value), { ttl })
608
612
  return value
609
613
  })()
610
614
  flights.set(key, flight)
@@ -721,24 +725,55 @@ async function durableObjectRetry<value>(call: () => Promise<value>): Promise<va
721
725
  }
722
726
  }
723
727
 
724
- const durableObjectCodecRoot = 'tempo-api:store:json:'
725
- const durableObjectCodecVersion = 'v1'
726
- const durableObjectCodecPrefix = `${durableObjectCodecRoot}${durableObjectCodecVersion}:`
728
+ const jsonCodecRoot = 'tempo-api:store:json:'
729
+ const jsonCodecVersion = 'v2'
730
+ const jsonCodecPrefix = `${jsonCodecRoot}${jsonCodecVersion}:`
731
+ const jsonValuePrefix = 'tempo-api:store:value:'
732
+ const legacyDurableObjectCodecPrefix = `${jsonCodecRoot}v1:`
733
+
734
+ function jsonEncode(value: unknown): string {
735
+ const encoded = JSON.stringify(value, (_key, item) => {
736
+ if (typeof item === 'bigint') return `${jsonValuePrefix}bigint:${item}`
737
+ if (typeof item === 'string' && item.startsWith(jsonValuePrefix))
738
+ return `${jsonValuePrefix}string:${item}`
739
+ return item
740
+ })
741
+ if (encoded === undefined) throw new TypeError('Store value is not serializable')
742
+ return `${jsonCodecPrefix}${encoded}`
743
+ }
744
+
745
+ function jsonDecode(value: string): unknown {
746
+ return JSON.parse(value.slice(jsonCodecPrefix.length), (_key, item) => {
747
+ if (typeof item !== 'string' || !item.startsWith(jsonValuePrefix)) return item
748
+ if (item.startsWith(`${jsonValuePrefix}string:`))
749
+ return item.slice(`${jsonValuePrefix}string:`.length)
750
+ if (item.startsWith(`${jsonValuePrefix}bigint:`)) {
751
+ const bigint = item.slice(`${jsonValuePrefix}bigint:`.length)
752
+ if (!/^-?\d+$/.test(bigint)) throw new TypeError('Store bigint is malformed')
753
+ return BigInt(bigint)
754
+ }
755
+ throw new TypeError('Store value tag is unsupported')
756
+ })
757
+ }
727
758
 
728
759
  function durableObjectEncode(value: unknown): string {
729
- if (typeof value === 'string' && !value.startsWith(durableObjectCodecRoot)) return value
730
- const encoded = Json.stringify(value)
731
- if (encoded === undefined) throw new TypeError('Durable Object store value is not serializable')
732
- return `${durableObjectCodecPrefix}${encoded}`
760
+ if (typeof value === 'string' && !value.startsWith(jsonCodecRoot)) return value
761
+ try {
762
+ return jsonEncode(value)
763
+ } catch (cause) {
764
+ throw new TypeError('Durable Object store value is not serializable', {
765
+ cause,
766
+ })
767
+ }
733
768
  }
734
769
 
735
770
  function durableObjectDecode(value: null | string): unknown {
736
- if (value === null || !value.startsWith(durableObjectCodecRoot)) return value
737
- if (!value.startsWith(durableObjectCodecPrefix)) {
738
- const [version] = value.slice(durableObjectCodecRoot.length).split(':')
739
- throw new DurableObjectCodecError(version)
740
- }
741
- return Json.parse(value.slice(durableObjectCodecPrefix.length))
771
+ if (value === null || !value.startsWith(jsonCodecRoot)) return value
772
+ if (value.startsWith(jsonCodecPrefix)) return jsonDecode(value)
773
+ if (value.startsWith(legacyDurableObjectCodecPrefix))
774
+ return Json.parse(value.slice(legacyDurableObjectCodecPrefix.length))
775
+ const [version] = value.slice(jsonCodecRoot.length).split(':')
776
+ throw new DurableObjectCodecError(version)
742
777
  }
743
778
 
744
779
  function cloudflareKvExpirationTtl(ttl: number | undefined) {
@@ -330,9 +330,9 @@ describe('resolve', () => {
330
330
  test('passes Zone metadata to the resolver', () => {
331
331
  expect(
332
332
  Tidx.resolve(({ zone }) => ({ baseUrl: zone ? 'http://tidx:8080' : undefined }), {
333
- chainId: Zones.zoneModerato.id,
333
+ chainId: Zones.zoneModeratoInternal.id,
334
334
  principal: null,
335
- zone: Zones.zoneModerato,
335
+ zone: Zones.zoneModeratoInternal,
336
336
  }),
337
337
  ).toMatchInlineSnapshot(`
338
338
  {
@@ -469,7 +469,7 @@ describe('getClient', () => {
469
469
  )
470
470
  try {
471
471
  const client = Tidx.getClient({
472
- chainId: Zones.zoneModerato.id,
472
+ chainId: Zones.zoneModeratoInternal.id,
473
473
  tidx: {
474
474
  baseUrl: 'https://internal.indexer',
475
475
  zoneHeaders: {
@@ -477,7 +477,7 @@ describe('getClient', () => {
477
477
  'CF-Access-Client-Secret': 'client-secret',
478
478
  },
479
479
  },
480
- zone: Zones.zoneModerato,
480
+ zone: Zones.zoneModeratoInternal,
481
481
  })
482
482
  await client.fetch({ query: 'select 1' })
483
483
 
@@ -1,7 +1,8 @@
1
1
  import { Tidx } from 'tidx.ts'
2
2
  import type { Chain } from 'viem'
3
3
 
4
- import * as Auth from './Auth.js'
4
+ import type * as Auth from './Auth.js'
5
+ import * as Credential from './Credential.js'
5
6
  import * as Errors from './Errors.js'
6
7
  import type * as Log from './Log.js'
7
8
  import * as Viem from './Viem.js'
@@ -40,7 +41,7 @@ export function resolve(
40
41
  ): resolve.Resolved {
41
42
  const config = typeof tidx === 'function' ? tidx(context) : tidx
42
43
  return {
43
- ...Auth.resolveCredential({ chainId: context.chainId, value: config?.auth }),
44
+ ...Credential.resolveCredential({ chainId: context.chainId, value: config?.auth }),
44
45
  ...(context.zone && config?.zoneHeaders ? { zoneHeaders: config.zoneHeaders } : {}),
45
46
  baseUrl:
46
47
  Viem.resolveUrl(config?.baseUrl, context.chainId) ??
@@ -319,7 +320,7 @@ export declare namespace getClient {
319
320
  type Tidx =
320
321
  | {
321
322
  /** Shared or per-chain TIDX credentials. Values containing `:` use Basic auth; others use Bearer auth. */
322
- auth?: Auth.ChainCredential | undefined
323
+ auth?: Credential.ChainCredential | undefined
323
324
  /** Resolves the TIDX base URL for a Tempo chain id, or a static base URL. */
324
325
  baseUrl?: UrlResolver | undefined
325
326
  /** Headers sent to Zone indexer upstreams. */
@@ -327,7 +328,7 @@ export declare namespace getClient {
327
328
  }
328
329
  | ((context: Viem.resolveRpc.Context) => {
329
330
  /** Shared or per-chain TIDX credentials, or undefined for none. */
330
- auth?: Auth.ChainCredential | undefined
331
+ auth?: Credential.ChainCredential | undefined
331
332
  /** TIDX base URL to use; falls back to the built-in public host when omitted. */
332
333
  baseUrl?: string | undefined
333
334
  /** Headers sent to Zone indexer upstreams. */
@@ -1,5 +1,30 @@
1
1
  import * as Value from './Value.js'
2
2
 
3
+ describe('blockToIso', () => {
4
+ test('prefers the millisecond timestamp on a raw header', () => {
5
+ expect(
6
+ Value.blockToIso({ timestamp: '0x6a7b5571', timestampMillis: '0x19ff1c5c312' }),
7
+ ).toMatchInlineSnapshot(`"2026-08-11T17:01:37.426Z"`)
8
+ })
9
+
10
+ test('falls back to whole seconds when the millisecond timestamp is absent', () => {
11
+ expect(Value.blockToIso({ timestamp: '0x6a7b5571' })).toMatchInlineSnapshot(
12
+ `"2026-08-11T17:01:37.000Z"`,
13
+ )
14
+ })
15
+
16
+ test('reads a viem-formatted header', () => {
17
+ expect(
18
+ Value.blockToIso({ timestamp: 1_786_467_697n, timestampMillis: '0x19ff1c5c312' }),
19
+ ).toMatchInlineSnapshot(`"2026-08-11T17:01:37.426Z"`)
20
+ })
21
+
22
+ test('returns undefined without a usable timestamp', () => {
23
+ expect(Value.blockToIso(undefined)).toMatchInlineSnapshot(`undefined`)
24
+ expect(Value.blockToIso({ timestamp: null })).toMatchInlineSnapshot(`undefined`)
25
+ })
26
+ })
27
+
3
28
  describe('tokenAmount', () => {
4
29
  test('renders a self-contained token quantity', () => {
5
30
  expect(
@@ -111,6 +111,35 @@ export function hexSecondsToIso(value: unknown): string | undefined {
111
111
  return seconds === undefined ? undefined : new Date(seconds * 1000).toISOString()
112
112
  }
113
113
 
114
+ /**
115
+ * Read a block's timestamp as epoch milliseconds, preferring Tempo's
116
+ * `timestampMillis` over the second-granularity `timestamp`. Accepts hex
117
+ * quantities (raw RPC) and bigint/number (viem-formatted).
118
+ */
119
+ function blockToMillis(block: unknown): number | undefined {
120
+ if (typeof block !== 'object' || block === null) return undefined
121
+ const { timestamp, timestampMillis } = block as {
122
+ timestamp?: unknown
123
+ timestampMillis?: unknown
124
+ }
125
+ const millis = quantity(timestampMillis)
126
+ if (millis !== undefined) return millis
127
+ const seconds = quantity(timestamp)
128
+ return seconds === undefined ? undefined : seconds * 1_000
129
+ }
130
+
131
+ /** Convert a block's timestamp to an ISO 8601 datetime string, preserving milliseconds. */
132
+ export function blockToIso(block: unknown): string | undefined {
133
+ const millis = blockToMillis(block)
134
+ return millis === undefined ? undefined : new Date(millis).toISOString()
135
+ }
136
+
137
+ /** Narrows a quantity that may arrive hex-encoded (raw RPC) or decoded (viem). */
138
+ function quantity(value: unknown): number | undefined {
139
+ const number = toNumber(value)
140
+ return number === undefined || !Number.isFinite(number) ? undefined : number
141
+ }
142
+
114
143
  /**
115
144
  * Coerce a TIDX timestamp column (Date / epoch seconds / epoch ms / ISO string)
116
145
  * to an ISO 8601 datetime string. Returns undefined when the value cannot be
@@ -220,6 +220,10 @@ describe('resolveRpc', () => {
220
220
  })
221
221
 
222
222
  describe('getClient', () => {
223
+ test('disables CCIP-Read', () => {
224
+ expect(Viem.getClient({ chainId: 4217 }).ccipRead).toBe(false)
225
+ })
226
+
223
227
  test('disables multicall for Zones', () => {
224
228
  const l1Client = Viem.getClient({ chainId: 4217 })
225
229
  const zoneClient = Viem.getClient({ chainId: zone.id, zone })
@@ -6,7 +6,8 @@ import type { TempoActions } from 'viem/tempo'
6
6
  import { tempoMainnet, tempoTestnet } from 'viem/tempo/chains'
7
7
  import type * as z from 'zod/mini'
8
8
 
9
- import * as Auth from './Auth.js'
9
+ import type * as Auth from './Auth.js'
10
+ import * as Credential from './Credential.js'
10
11
  import type * as Schema from './Schema.js'
11
12
 
12
13
  /** Known Tempo chains keyed by id, for metadata lookups. */
@@ -79,7 +80,7 @@ export function resolveRpc(
79
80
  context: resolveRpc.Context,
80
81
  ): resolveRpc.Resolved {
81
82
  const config = typeof rpc === 'function' ? rpc(context) : rpc
82
- const auth = Auth.resolveCredential({ chainId: context.chainId, value: config?.auth })
83
+ const auth = Credential.resolveCredential({ chainId: context.chainId, value: config?.auth })
83
84
  const publicZoneUrl = context.zone
84
85
  ? (resolveUrl(config?.publicZoneUrl, context.chainId) ?? context.zone.rpcUrls.default.http[0])
85
86
  : undefined
@@ -165,6 +166,7 @@ export function getClient(options: getClient.Options): getClient.ReturnType {
165
166
  // configure a deployed multicall3, can still serve the aggregated call.
166
167
  // Zones disable multicall entirely and issue concurrent direct `eth_call`s instead.
167
168
  batch: { multicall: zone ? false : { deployless: true } },
169
+ ccipRead: false,
168
170
  chain: { ...tempoMainnet, id: chainId },
169
171
  transport: http(rpcUrl, fetchOptions ? { fetchOptions } : {}),
170
172
  })
@@ -250,7 +252,7 @@ export declare namespace getClient {
250
252
  type Rpc =
251
253
  | {
252
254
  /** Shared or per-chain RPC credentials. Values containing `:` use Basic auth; others use Bearer auth. */
253
- auth?: Auth.ChainCredential | undefined
255
+ auth?: Credential.ChainCredential | undefined
254
256
  /** Resolves the public Zone RPC URL used with caller-provided authorization. */
255
257
  publicZoneUrl?: UrlResolver | undefined
256
258
  /** Headers sent to Zone RPC upstreams. */
@@ -260,7 +262,7 @@ export declare namespace getClient {
260
262
  }
261
263
  | ((context: resolveRpc.Context) => {
262
264
  /** Shared or per-chain RPC credentials, or undefined for none. */
263
- auth?: Auth.ChainCredential | undefined
265
+ auth?: Credential.ChainCredential | undefined
264
266
  /** Public Zone RPC URL used with caller-provided authorization. */
265
267
  publicZoneUrl?: string | undefined
266
268
  /** Headers sent to Zone RPC upstreams. */
@@ -0,0 +1,89 @@
1
+ import type { Hex } from 'ox'
2
+
3
+ import * as WebhookTransfer from './WebhookTransfer.js'
4
+
5
+ const token: Hex.Hex = '0x20c0000000000000000000000000000000000001'
6
+ const sender: Hex.Hex = `0x${'aa'.repeat(20)}`
7
+ const recipient: Hex.Hex = `0x${'bb'.repeat(20)}`
8
+ const other: Hex.Hex = `0x${'cc'.repeat(20)}`
9
+ const transactionHash = `0x${'ef'.repeat(32)}`
10
+ const transferTopic = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
11
+ const timestamp = '2026-01-01T00:00:00.000Z'
12
+
13
+ const addressTopic = (address: string) => `0x${'0'.repeat(24)}${address.slice(2)}`
14
+
15
+ function transferLog(overrides: Record<string, unknown> = {}) {
16
+ return {
17
+ address: token,
18
+ blockNumber: '0x7b',
19
+ data: `0x${(10n ** 18n).toString(16).padStart(64, '0')}`,
20
+ logIndex: '0x4',
21
+ topics: [transferTopic, addressTopic(sender), addressTopic(recipient)],
22
+ transactionHash,
23
+ transactionIndex: '0x0',
24
+ ...overrides,
25
+ } as never
26
+ }
27
+
28
+ describe('decode', () => {
29
+ test('decodes a strict TIP-20 Transfer log', () => {
30
+ expect(WebhookTransfer.decode(transferLog(), timestamp)).toMatchInlineSnapshot(`
31
+ {
32
+ "address": "0x20c0000000000000000000000000000000000001",
33
+ "amount": "1000000000000000000",
34
+ "blockNumber": 123,
35
+ "logIndex": 4,
36
+ "recipient": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
37
+ "sender": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
38
+ "timestamp": "2026-01-01T00:00:00.000Z",
39
+ "transactionHash": "0xefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefef",
40
+ }
41
+ `)
42
+ })
43
+
44
+ test('rejects non-TIP-20 addresses and malformed payloads', () => {
45
+ const cases: readonly Record<string, unknown>[] = [
46
+ // Outside the deterministic TIP-20 address range.
47
+ { address: other },
48
+ // Missing recipient topic.
49
+ { topics: [transferTopic, addressTopic(sender)] },
50
+ // Wrong event selector.
51
+ { topics: [`0x${'12'.repeat(32)}`, addressTopic(sender), addressTopic(recipient)] },
52
+ // Sender topic without the address left-padding.
53
+ { topics: [transferTopic, `0x${'ff'.repeat(32)}`, addressTopic(recipient)] },
54
+ // Amount payload is not one word.
55
+ { data: '0x01' },
56
+ // Pending-log positions cannot form a cursor.
57
+ { blockNumber: null },
58
+ { logIndex: null },
59
+ ]
60
+ for (const overrides of cases)
61
+ expect(
62
+ WebhookTransfer.decode(transferLog(overrides), timestamp),
63
+ JSON.stringify(overrides),
64
+ ).toBeUndefined()
65
+ })
66
+ })
67
+
68
+ describe('matches', () => {
69
+ const data = WebhookTransfer.decode(transferLog(), timestamp)!
70
+
71
+ test('AND-combines side, either-side address, and token filters', () => {
72
+ const cases: readonly [WebhookTransfer.Filters, boolean][] = [
73
+ [{}, true],
74
+ [{ sender }, true],
75
+ [{ sender: recipient }, false],
76
+ [{ recipient }, true],
77
+ [{ recipient: sender }, false],
78
+ [{ address: sender }, true],
79
+ [{ address: recipient }, true],
80
+ [{ address: other }, false],
81
+ [{ token }, true],
82
+ [{ token: '0x20c0000000000000000000000000000000000002' }, false],
83
+ [{ recipient, sender, token }, true],
84
+ [{ recipient: sender, sender, token }, false],
85
+ ]
86
+ for (const [filters, expected] of cases)
87
+ expect(WebhookTransfer.matches(data, filters), JSON.stringify(filters)).toBe(expected)
88
+ })
89
+ })
@@ -18,9 +18,11 @@ const scanChunkBlocks = 8
18
18
  const event = parseAbiItem(Transfers.signature)
19
19
  const topic = encodeEventTopics({ abi: [event] })[0]
20
20
 
21
- type Filters = z.output<typeof Transfers.schema.Predicates>
21
+ /** Parsed transfer filter predicates. */
22
+ export type Filters = z.output<typeof Transfers.schema.Predicates>
22
23
 
23
- type EventData = {
24
+ /** One decoded TIP-20 Transfer row, as the read endpoint returns it. */
25
+ export type EventData = {
24
26
  address: string
25
27
  amount: string
26
28
  blockNumber: number
@@ -303,7 +305,7 @@ function isTip20Address(address: string): boolean {
303
305
  }
304
306
 
305
307
  /** Decodes one strict TIP-20 Transfer wire payload. */
306
- function decode(log: RpcLog, timestamp: string): EventData | undefined {
308
+ export function decode(log: RpcLog, timestamp: string): EventData | undefined {
307
309
  if (!isTip20Address(log.address)) return undefined
308
310
  if (
309
311
  log.topics.length !== 3 ||
@@ -343,7 +345,7 @@ function decode(log: RpcLog, timestamp: string): EventData | undefined {
343
345
  }
344
346
 
345
347
  /** Re-checks every transfer filter after RPC topic pushdown. */
346
- function matches(data: EventData, filters: Filters): boolean {
348
+ export function matches(data: EventData, filters: Filters): boolean {
347
349
  if (filters.address && data.sender !== filters.address && data.recipient !== filters.address)
348
350
  return false
349
351
  if (filters.recipient && data.recipient !== filters.recipient) return false
@@ -389,13 +391,13 @@ async function scanCohort(options: {
389
391
  items: [],
390
392
  state,
391
393
  }))
392
- const timestamps = new Map<number, Promise<string>>()
394
+ const timestamps = new Map<number, Promise<string | undefined>>()
393
395
  const timestamp = (blockNumber: number) => {
394
396
  const existing = timestamps.get(blockNumber)
395
397
  if (existing) return existing
396
398
  const pending = client
397
399
  .getBlock({ blockNumber: BigInt(blockNumber), includeTransactions: false })
398
- .then((block) => new Date(Number(block.timestamp) * 1_000).toISOString())
400
+ .then((block) => Value.blockToIso(block))
399
401
  timestamps.set(blockNumber, pending)
400
402
  return pending
401
403
  }
@@ -412,7 +414,9 @@ async function scanCohort(options: {
412
414
  const blockNumber = Value.hexToNumber(log.blockNumber)
413
415
  const logIndex = Value.hexToNumber(log.logIndex)
414
416
  if (blockNumber === undefined || logIndex === undefined) continue
415
- const data = decode(log, await timestamp(blockNumber))
417
+ const blockTimestamp = await timestamp(blockNumber)
418
+ if (blockTimestamp === undefined) continue
419
+ const data = decode(log, blockTimestamp)
416
420
  if (!data) continue
417
421
  for (const accumulator of accumulators) {
418
422
  if (!canMatch(accumulator, blockNumber, logIndex)) continue