tapimo 0.9.0 → 0.10.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 (198) hide show
  1. package/dist/App.d.ts +4887 -5074
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/Webhooks.d.ts +21 -20
  4. package/dist/Webhooks.d.ts.map +1 -1
  5. package/dist/Webhooks.js +66 -23
  6. package/dist/Webhooks.js.map +1 -1
  7. package/dist/admin/apps/verified-tokens.d.ts +168 -168
  8. package/dist/apps/Zones.d.ts +34 -22
  9. package/dist/apps/Zones.d.ts.map +1 -1
  10. package/dist/apps/Zones.js +9 -8
  11. package/dist/apps/Zones.js.map +1 -1
  12. package/dist/apps/data/App.d.ts +5198 -5099
  13. package/dist/apps/data/App.d.ts.map +1 -1
  14. package/dist/apps/data/FxOracle.d.ts +4 -5
  15. package/dist/apps/data/FxOracle.d.ts.map +1 -1
  16. package/dist/apps/data/FxOracle.js +19 -4
  17. package/dist/apps/data/FxOracle.js.map +1 -1
  18. package/dist/apps/data/routes/activities.d.ts +270 -270
  19. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  20. package/dist/apps/data/routes/activities.js +32 -27
  21. package/dist/apps/data/routes/activities.js.map +1 -1
  22. package/dist/apps/data/routes/addresses.d.ts +52 -52
  23. package/dist/apps/data/routes/balances.d.ts +36 -36
  24. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  25. package/dist/apps/data/routes/balances.js +135 -81
  26. package/dist/apps/data/routes/balances.js.map +1 -1
  27. package/dist/apps/data/routes/blocks.d.ts +125 -125
  28. package/dist/apps/data/routes/coingecko.d.ts +54 -54
  29. package/dist/apps/data/routes/earn.d.ts +512 -58
  30. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  31. package/dist/apps/data/routes/earn.js +368 -44
  32. package/dist/apps/data/routes/earn.js.map +1 -1
  33. package/dist/apps/data/routes/exchanges.d.ts +120 -120
  34. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  35. package/dist/apps/data/routes/exchanges.js +114 -118
  36. package/dist/apps/data/routes/exchanges.js.map +1 -1
  37. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  38. package/dist/apps/data/routes/receipts.js +68 -47
  39. package/dist/apps/data/routes/receipts.js.map +1 -1
  40. package/dist/apps/data/routes/rpc.d.ts +31 -31
  41. package/dist/apps/data/routes/tokens.d.ts +461 -459
  42. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  43. package/dist/apps/data/routes/transactions.d.ts +59 -51
  44. package/dist/apps/data/routes/transactions.d.ts.map +1 -1
  45. package/dist/apps/data/routes/transactions.js +25 -23
  46. package/dist/apps/data/routes/transactions.js.map +1 -1
  47. package/dist/apps/data/routes/transfers.d.ts +108 -86
  48. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  49. package/dist/apps/data/routes/transfers.js +618 -105
  50. package/dist/apps/data/routes/transfers.js.map +1 -1
  51. package/dist/apps/data/routes/valuation.d.ts +21 -21
  52. package/dist/apps/data/routes/valuation.d.ts.map +1 -1
  53. package/dist/apps/data/routes/valuation.js +2 -2
  54. package/dist/apps/data/routes/valuation.js.map +1 -1
  55. package/dist/apps/data/routes/verified-tokens.d.ts +37 -37
  56. package/dist/apps/data/routes/webhooks.d.ts +102 -505
  57. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  58. package/dist/apps/data/routes/webhooks.js +484 -342
  59. package/dist/apps/data/routes/webhooks.js.map +1 -1
  60. package/dist/apps/data/routes/zones.d.ts +12 -12
  61. package/dist/apps/data/routes/zones.d.ts.map +1 -1
  62. package/dist/apps/data/routes/zones.js +3 -1
  63. package/dist/apps/data/routes/zones.js.map +1 -1
  64. package/dist/apps/funding/App.d.ts +96 -96
  65. package/dist/apps/funding/routes/deposit-addresses.d.ts +52 -52
  66. package/dist/apps/funding/routes/quotes.d.ts +89 -89
  67. package/dist/apps/funding/routes/transfers.d.ts +4 -4
  68. package/dist/apps/management/App.d.ts +429 -727
  69. package/dist/apps/management/App.d.ts.map +1 -1
  70. package/dist/apps/management/routes/api-keys.d.ts +24 -24
  71. package/dist/apps/management/routes/billing.d.ts +72 -72
  72. package/dist/apps/management/routes/faucet.d.ts +6 -6
  73. package/dist/apps/management/routes/invitations.d.ts +134 -134
  74. package/dist/apps/management/routes/invite-links.d.ts +38 -38
  75. package/dist/apps/management/routes/me.d.ts +6 -6
  76. package/dist/apps/management/routes/members.d.ts +30 -30
  77. package/dist/apps/management/routes/orgs.d.ts +98 -98
  78. package/dist/apps/management/routes/projects.d.ts +20 -20
  79. package/dist/apps/management/routes/usage.d.ts +24 -24
  80. package/dist/apps/management/routes/verified-token-requests.d.ts +83 -83
  81. package/dist/apps/management/routes/webhooks.d.ts +158 -573
  82. package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
  83. package/dist/apps/mpp/App.d.ts +56 -44
  84. package/dist/apps/mpp/App.d.ts.map +1 -1
  85. package/dist/apps/mpp/Mpp.d.ts +3 -0
  86. package/dist/apps/mpp/Mpp.d.ts.map +1 -1
  87. package/dist/apps/mpp/Mpp.js +3 -0
  88. package/dist/apps/mpp/Mpp.js.map +1 -1
  89. package/dist/apps/mpp/internal/Adapter.d.ts.map +1 -1
  90. package/dist/apps/mpp/internal/Adapter.js +9 -2
  91. package/dist/apps/mpp/internal/Adapter.js.map +1 -1
  92. package/dist/db/Db.d.ts +2 -2
  93. package/dist/db/Db.d.ts.map +1 -1
  94. package/dist/db/Db.js +17 -15
  95. package/dist/db/Db.js.map +1 -1
  96. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  97. package/dist/db/tables/sponsoredTransactions.js +4 -1
  98. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  99. package/dist/db/tables/webhookSubscriptions.d.ts +56 -3
  100. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  101. package/dist/db/tables/webhookSubscriptions.js +76 -3
  102. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  103. package/dist/internal/MetricSink.d.ts +112 -3
  104. package/dist/internal/MetricSink.d.ts.map +1 -1
  105. package/dist/internal/MetricSink.js +116 -3
  106. package/dist/internal/MetricSink.js.map +1 -1
  107. package/dist/internal/Schema.js +1 -1
  108. package/dist/internal/Schema.js.map +1 -1
  109. package/dist/internal/Tidx.d.ts +1 -3
  110. package/dist/internal/Tidx.d.ts.map +1 -1
  111. package/dist/internal/Tidx.js +1 -3
  112. package/dist/internal/Tidx.js.map +1 -1
  113. package/dist/internal/Viem.d.ts.map +1 -1
  114. package/dist/internal/Viem.js +2 -1
  115. package/dist/internal/Viem.js.map +1 -1
  116. package/dist/internal/WebhookDestination.d.ts +2 -0
  117. package/dist/internal/WebhookDestination.d.ts.map +1 -1
  118. package/dist/internal/WebhookDestination.js +11 -0
  119. package/dist/internal/WebhookDestination.js.map +1 -1
  120. package/dist/internal/WebhookTransfer.d.ts +55 -0
  121. package/dist/internal/WebhookTransfer.d.ts.map +1 -0
  122. package/dist/internal/WebhookTransfer.js +296 -0
  123. package/dist/internal/WebhookTransfer.js.map +1 -0
  124. package/dist/internal/Webhooks.d.ts +37 -0
  125. package/dist/internal/Webhooks.d.ts.map +1 -1
  126. package/dist/internal/Webhooks.js +87 -13
  127. package/dist/internal/Webhooks.js.map +1 -1
  128. package/dist/internal/funding/Provider.d.ts +4 -0
  129. package/dist/internal/funding/Provider.d.ts.map +1 -1
  130. package/dist/internal/funding/Provider.js +11 -7
  131. package/dist/internal/funding/Provider.js.map +1 -1
  132. package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
  133. package/dist/internal/funding/Reconciliation.js +128 -0
  134. package/dist/internal/funding/Reconciliation.js.map +1 -1
  135. package/dist/internal/funding/providers/relay.d.ts +3 -0
  136. package/dist/internal/funding/providers/relay.d.ts.map +1 -1
  137. package/dist/internal/funding/providers/relay.js +12 -1
  138. package/dist/internal/funding/providers/relay.js.map +1 -1
  139. package/package.json +3 -3
  140. package/src/Client.test-d.ts +60 -5
  141. package/src/Webhooks.bench.ts +714 -0
  142. package/src/Webhooks.memory.bench.ts +250 -0
  143. package/src/Webhooks.test.ts +77 -6
  144. package/src/Webhooks.ts +83 -29
  145. package/src/apps/Zones.test.ts +5 -4
  146. package/src/apps/Zones.ts +9 -8
  147. package/src/apps/data/FxOracle.test.ts +18 -0
  148. package/src/apps/data/FxOracle.ts +28 -5
  149. package/src/apps/data/routes/activities.test.ts +19 -0
  150. package/src/apps/data/routes/activities.ts +34 -30
  151. package/src/apps/data/routes/balances.test.ts +119 -10
  152. package/src/apps/data/routes/balances.ts +154 -95
  153. package/src/apps/data/routes/blocks.test.ts +12 -6
  154. package/src/apps/data/routes/earn.test.ts +197 -0
  155. package/src/apps/data/routes/earn.ts +486 -44
  156. package/src/apps/data/routes/exchanges.test.ts +7 -6
  157. package/src/apps/data/routes/exchanges.ts +124 -117
  158. package/src/apps/data/routes/receipts.test.ts +24 -7
  159. package/src/apps/data/routes/receipts.ts +67 -50
  160. package/src/apps/data/routes/tokens.test.ts +51 -0
  161. package/src/apps/data/routes/transactions.test.ts +42 -0
  162. package/src/apps/data/routes/transactions.ts +44 -39
  163. package/src/apps/data/routes/transfers.test.ts +402 -24
  164. package/src/apps/data/routes/transfers.ts +708 -101
  165. package/src/apps/data/routes/valuation.test.ts +26 -0
  166. package/src/apps/data/routes/valuation.ts +6 -2
  167. package/src/apps/data/routes/webhooks.test.ts +414 -130
  168. package/src/apps/data/routes/webhooks.ts +603 -419
  169. package/src/apps/data/routes/zones.ts +6 -1
  170. package/src/apps/funding/routes/quotes.test.ts +1 -1
  171. package/src/apps/mpp/Mpp.test.ts +13 -1
  172. package/src/apps/mpp/Mpp.ts +8 -0
  173. package/src/apps/mpp/internal/Adapter.ts +10 -2
  174. package/src/db/Db.test.ts +40 -6
  175. package/src/db/Db.ts +33 -19
  176. package/src/db/tables/sponsoredTransactions.ts +9 -2
  177. package/src/db/tables/webhookSubscriptions.ts +116 -4
  178. package/src/internal/EarnVaults.test.ts +2 -2
  179. package/src/internal/MetricSink.test.ts +518 -0
  180. package/src/internal/MetricSink.ts +266 -9
  181. package/src/internal/Schema.ts +1 -1
  182. package/src/internal/Tidx.test.ts +4 -4
  183. package/src/internal/Tidx.ts +1 -3
  184. package/src/internal/Viem.test.ts +8 -0
  185. package/src/internal/Viem.ts +2 -1
  186. package/src/internal/WebhookDestination.test.ts +20 -0
  187. package/src/internal/WebhookDestination.ts +10 -0
  188. package/src/internal/WebhookTransfer.ts +426 -0
  189. package/src/internal/Webhooks.test.ts +137 -0
  190. package/src/internal/Webhooks.ts +131 -17
  191. package/src/internal/funding/Provider.test.ts +47 -7
  192. package/src/internal/funding/Provider.ts +12 -7
  193. package/src/internal/funding/Reconciliation.local.test.ts +3 -1
  194. package/src/internal/funding/Reconciliation.test.ts +1240 -0
  195. package/src/internal/funding/Reconciliation.ts +136 -0
  196. package/src/internal/funding/SourceObservation.test.ts +1 -0
  197. package/src/internal/funding/providers/relay.test.ts +1 -4
  198. package/src/internal/funding/providers/relay.ts +16 -1
@@ -8,6 +8,7 @@ import { Addresses } from 'viem/tempo'
8
8
 
9
9
  import * as TestApp from '../../../../test/App.js'
10
10
  import * as Runtime from '../../../../test/runtime.js'
11
+ import * as Tidx from '../../../internal/Tidx.js'
11
12
  import * as Transfers from './transfers.js'
12
13
 
13
14
  // Deterministic EUR-based rate set so conversion math is exact in assertions.
@@ -79,6 +80,35 @@ describe('GET /transfers', () => {
79
80
  },
80
81
  )
81
82
 
83
+ test.runIf(Runtime.get().mode === 'testnet')(
84
+ 'returns an empty page for an address with only non-TIP-20 events',
85
+ async () => {
86
+ const client = TestApp.client({
87
+ tidx: { auth: process.env.TIDX_AUTH, baseUrl: Tidx.url[4217] },
88
+ })
89
+ const response = await client.v1.transfers.$get(
90
+ {
91
+ query: {
92
+ address: '0x9ef044cb9b80230b60a46b4ed3958672419e668f',
93
+ chainId: '4217',
94
+ limit: '10',
95
+ page: '1',
96
+ },
97
+ },
98
+ TestApp.auth,
99
+ )
100
+ const body = await TestApp.json(response, Transfers.schema.getTransfers.Response)
101
+
102
+ expect(response.status).toBe(200)
103
+ expect(body).toMatchInlineSnapshot(`
104
+ {
105
+ "data": [],
106
+ "nextCursor": null,
107
+ }
108
+ `)
109
+ },
110
+ )
111
+
82
112
  test('degrades unavailable verification data to null valuations', async () => {
83
113
  const db = () =>
84
114
  Db.postgres({
@@ -87,7 +117,13 @@ describe('GET /transfers', () => {
87
117
  const client = TestApp.client({ db, fx: { oracle: fixed } })
88
118
 
89
119
  const response = await client.v1.transfers.$get(
90
- { query: { limit: '5', token: TestApp.tokenWithHolders } },
120
+ {
121
+ query: {
122
+ limit: '5',
123
+ token: TestApp.tokenWithHolders,
124
+ 'valuation.currency': 'USD',
125
+ },
126
+ },
91
127
  TestApp.auth,
92
128
  )
93
129
  const body = await TestApp.json(response, Transfers.schema.getTransfers.Response)
@@ -383,6 +419,87 @@ describe('GET /transfers', () => {
383
419
  expect(byRecipient.data.every((entry) => entry.recipient === recipient)).toBe(true)
384
420
  })
385
421
 
422
+ test('paginates recipient scans through the real indexer', async () => {
423
+ const runtime = Runtime.get()
424
+ const recipient = Schema.Address.parse(
425
+ runtime.mode === 'testnet'
426
+ ? '0xabb0023644c54a20d0fe92becfa764dacaf59ba9'
427
+ : runtime.fixtures!.fundedAccounts[0]!,
428
+ )
429
+ const tidx = Tidx.getClient({
430
+ chainId: runtime.chainId,
431
+ tidx:
432
+ runtime.mode === 'localnet'
433
+ ? { baseUrl: runtime.tidxUrl }
434
+ : { auth: process.env.TIDX_AUTH, baseUrl: runtime.tidxUrl },
435
+ })
436
+ const bounds = runtime.mode === 'testnet' ? { toBlock: 27_608_730 } : {}
437
+ const reference = await Transfers.scan(
438
+ { tidx },
439
+ { chainId: runtime.chainId, limit: 2, order: 'desc', recipient, ...bounds },
440
+ )
441
+ const first = await Transfers.scan(
442
+ { tidx },
443
+ { chainId: runtime.chainId, limit: 1, order: 'desc', recipient, ...bounds },
444
+ )
445
+
446
+ expect(reference.items).toHaveLength(2)
447
+ expect(first.items).toHaveLength(1)
448
+ expect(first.nextCursor).not.toBeNull()
449
+ if (!first.nextCursor) return
450
+
451
+ const [cursor, positional] = await Promise.all([
452
+ Transfers.scan(
453
+ { tidx },
454
+ {
455
+ chainId: runtime.chainId,
456
+ cursor: first.nextCursor,
457
+ limit: 1,
458
+ order: 'desc',
459
+ recipient,
460
+ ...bounds,
461
+ },
462
+ ),
463
+ Transfers.scan(
464
+ { tidx },
465
+ { chainId: runtime.chainId, limit: 1, order: 'desc', page: 2, recipient, ...bounds },
466
+ ),
467
+ ])
468
+ const ids = (page: Transfers.scan.Page) =>
469
+ page.items.map(({ data }) => `${data.transactionHash}-${data.logIndex}`)
470
+ const [sameAddress, senderAddress] = await Promise.all([
471
+ Transfers.scan(
472
+ { tidx },
473
+ {
474
+ chainId: runtime.chainId,
475
+ eitherSide: recipient,
476
+ limit: 2,
477
+ order: 'desc',
478
+ recipient,
479
+ ...bounds,
480
+ },
481
+ ),
482
+ Transfers.scan(
483
+ { tidx },
484
+ {
485
+ chainId: runtime.chainId,
486
+ eitherSide: first.items[0]!.data.sender,
487
+ limit: 1,
488
+ order: 'desc',
489
+ recipient,
490
+ ...bounds,
491
+ },
492
+ ),
493
+ ])
494
+
495
+ expect(ids(first)).toEqual(ids(reference).slice(0, 1))
496
+ expect(ids(cursor)).toEqual(ids(reference).slice(1, 2))
497
+ expect(ids(positional)).toEqual(ids(reference).slice(1, 2))
498
+ expect(ids(sameAddress)).toEqual(ids(reference))
499
+ expect(ids(senderAddress)).toEqual(ids(first))
500
+ expect([...reference.items, ...cursor.items, ...positional.items].every(({ data }) => data.recipient === recipient && data.address.startsWith('0x20c'))).toBe(true) // prettier-ignore
501
+ })
502
+
386
503
  test('rejects mixing `address` with `sender` or `recipient`', async () => {
387
504
  const client = TestApp.client()
388
505
  const response = await client.v1.transfers.$get(
@@ -640,7 +757,7 @@ describe('GET /transfers', () => {
640
757
  )
641
758
 
642
759
  test.skipIf(Runtime.get().mode !== 'localnet')(
643
- 'counts unscoped cross-token transfers',
760
+ 'counts address-scoped cross-token transfers',
644
761
  async () => {
645
762
  const crossToken = Runtime.get().fixtures?.crossTokenTransfer
646
763
  expect(crossToken).toBeDefined()
@@ -652,7 +769,7 @@ describe('GET /transfers', () => {
652
769
  query: {
653
770
  address: crossToken.sender,
654
771
  include: 'crossToken,totalCount',
655
- limit: '50',
772
+ limit: '200',
656
773
  },
657
774
  },
658
775
  TestApp.auth,
@@ -661,9 +778,72 @@ describe('GET /transfers', () => {
661
778
 
662
779
  expect(response.status).toBe(200)
663
780
  expect(response.headers.get('server-timing')?.includes('transfers_count;dur=')).toBe(true)
664
- expect(body.data.some((entry) => entry.transactionHash === crossToken.hash)).toBe(true)
665
- expect(body.meta).toBeDefined()
666
- expect(body.meta!.totalCount).toBeGreaterThanOrEqual(body.data.length)
781
+ expect(body.data.filter((entry) => entry.transactionHash === crossToken.hash)).toHaveLength(1)
782
+ expect(body.nextCursor).toBeNull()
783
+ expect(body.meta).toMatchObject({
784
+ totalCount: body.data.length,
785
+ totalCountCapped: false,
786
+ })
787
+ },
788
+ )
789
+
790
+ test.skipIf(Runtime.get().mode !== 'localnet')(
791
+ 'applies address page offsets after merging both sides',
792
+ async () => {
793
+ const address = Runtime.get().fixtures?.crossTokenTransfer?.sender
794
+ expect(address).toBeDefined()
795
+ if (!address) return
796
+
797
+ const client = TestApp.client()
798
+ const [referenceResponse, firstResponse, secondResponse] = await Promise.all([
799
+ client.v1.transfers.$get({ query: { address, limit: '10' } }, TestApp.auth),
800
+ client.v1.transfers.$get({ query: { address, limit: '5' } }, TestApp.auth),
801
+ client.v1.transfers.$get({ query: { address, limit: '5', page: '2' } }, TestApp.auth),
802
+ ])
803
+ const reference = await TestApp.json(
804
+ referenceResponse,
805
+ Transfers.schema.getTransfers.Response,
806
+ )
807
+ const first = await TestApp.json(firstResponse, Transfers.schema.getTransfers.Response)
808
+ const second = await TestApp.json(secondResponse, Transfers.schema.getTransfers.Response)
809
+
810
+ expect([referenceResponse.status, firstResponse.status, secondResponse.status]).toEqual([
811
+ 200, 200, 200,
812
+ ])
813
+ expect(reference.data).toHaveLength(10)
814
+ expect(first.data.map(({ id }) => id)).toEqual(reference.data.slice(0, 5).map(({ id }) => id))
815
+ expect(second.data.map(({ id }) => id)).toEqual(
816
+ reference.data.slice(5, 10).map(({ id }) => id),
817
+ )
818
+ },
819
+ )
820
+
821
+ test.skipIf(Runtime.get().mode !== 'localnet')(
822
+ 'counts address-scoped plumbing from complete cross-token legs',
823
+ async () => {
824
+ const crossToken = Runtime.get().fixtures?.crossTokenTransfer
825
+ expect(crossToken).toBeDefined()
826
+ if (!crossToken) return
827
+
828
+ const client = TestApp.client()
829
+ const response = await client.v1.transfers.$get(
830
+ {
831
+ query: {
832
+ address: Addresses.stablecoinDex,
833
+ include: 'crossToken,totalCount',
834
+ limit: '50',
835
+ },
836
+ },
837
+ TestApp.auth,
838
+ )
839
+ const body = await TestApp.json(response, Transfers.schema.getTransfers.Response)
840
+
841
+ expect(response.status).toBe(200)
842
+ expect(body.data.some((entry) => entry.transactionHash === crossToken.hash)).toBe(false)
843
+ expect(body.meta).toMatchObject({
844
+ totalCount: body.data.length,
845
+ totalCountCapped: false,
846
+ })
667
847
  },
668
848
  )
669
849
 
@@ -694,12 +874,11 @@ describe('GET /transfers', () => {
694
874
  },
695
875
  )
696
876
 
697
- test('values verified transfer legs in the default denomination', async () => {
877
+ test('omits transfer valuations unless a denomination is requested', async () => {
698
878
  const db = TestApp.database()
699
879
  await TestApp.verifiedSeed(db, Runtime.get().chainId)
700
880
  const client = TestApp.client({ db })
701
881
 
702
- // pathUSD is curated USD, so its legs value at identity with no rates.
703
882
  const response = await client.v1.transfers.$get(
704
883
  { query: { limit: '5', token: TestApp.tokenWithHolders } },
705
884
  TestApp.auth,
@@ -708,12 +887,9 @@ describe('GET /transfers', () => {
708
887
 
709
888
  expect(response.status).toBe(200)
710
889
  expect(body.data.length).toBeGreaterThan(0)
711
- for (const entry of body.data)
712
- expect(entry.sourceAmount.valuation).toEqual({
713
- amount: core_Value.format(BigInt(entry.sourceAmount.baseUnits), 6),
714
- currency: 'USD',
715
- })
890
+ for (const entry of body.data) expect('valuation' in entry.sourceAmount).toBe(false)
716
891
  expect(body.meta?.valuation).toBeUndefined()
892
+ expect(response.headers.get('server-timing')).not.toContain('valuation_rates;dur=')
717
893
  })
718
894
 
719
895
  test('values transfer legs in a requested denomination', async () => {
@@ -774,7 +950,7 @@ describe('GET /transfers', () => {
774
950
  (address) => address.toLowerCase() !== TestApp.tokenWithHolders,
775
951
  )!
776
952
  const response = await client.v1.transfers.$get(
777
- { query: { limit: '5', token } },
953
+ { query: { limit: '5', token, 'valuation.currency': 'USD' } },
778
954
  TestApp.auth,
779
955
  )
780
956
  const body = await TestApp.json(response, Transfers.schema.getTransfers.Response)
@@ -803,6 +979,96 @@ describe('GET /transfers', () => {
803
979
  }
804
980
  `)
805
981
  })
982
+
983
+ test('bounds an unscoped transfer page by timestamp', async () => {
984
+ const client = TestApp.client()
985
+ const runtime = Runtime.get()
986
+ const tidx = Tidx.getClient({
987
+ chainId: runtime.chainId,
988
+ tidx:
989
+ runtime.mode === 'localnet'
990
+ ? { baseUrl: runtime.tidxUrl }
991
+ : { auth: process.env.TIDX_AUTH, baseUrl: runtime.tidxUrl },
992
+ })
993
+ const latest = await Transfers.scan(
994
+ { tidx },
995
+ { chainId: runtime.chainId, limit: 1, order: 'desc' },
996
+ )
997
+ const latestTransfer = latest.items[0]?.data
998
+ if (latestTransfer === undefined) throw new Error('expected an indexed transfer')
999
+
1000
+ const block = await tidx.fetch({
1001
+ chainId: runtime.chainId,
1002
+ query: `SELECT timestamp FROM blocks WHERE num = ${latestTransfer.blockNumber} LIMIT 1`,
1003
+ })
1004
+ const seconds = block.rows[0]?.['timestamp']
1005
+ if (typeof seconds !== 'number') throw new Error('expected an indexed block timestamp')
1006
+ const timestamp = new Date(seconds * 1_000).toISOString()
1007
+ const response = await client.v1.transfers.$get(
1008
+ {
1009
+ query: {
1010
+ limit: '5',
1011
+ order: 'desc',
1012
+ 'timestamp.from': timestamp,
1013
+ 'timestamp.to': timestamp,
1014
+ },
1015
+ },
1016
+ TestApp.auth,
1017
+ )
1018
+ const page = await TestApp.json(response, Transfers.schema.getTransfers.Response)
1019
+
1020
+ expect(response.status).toBe(200)
1021
+ expect(page.data.length).toBeGreaterThan(0)
1022
+ expect(page.data.every((item) => Date.parse(item.timestamp) === Date.parse(timestamp))).toBe(
1023
+ true,
1024
+ )
1025
+
1026
+ const fractionalResponse = await client.v1.transfers.$get(
1027
+ {
1028
+ query: {
1029
+ limit: '5',
1030
+ order: 'desc',
1031
+ 'timestamp.from': new Date(seconds * 1_000 + 500).toISOString(),
1032
+ 'timestamp.to': new Date(seconds * 1_000 + 999).toISOString(),
1033
+ },
1034
+ },
1035
+ TestApp.auth,
1036
+ )
1037
+ const fractional = await TestApp.json(
1038
+ fractionalResponse,
1039
+ Transfers.schema.getTransfers.Response,
1040
+ )
1041
+
1042
+ expect(fractionalResponse.status).toBe(200)
1043
+ expect(fractional.data).toEqual([])
1044
+
1045
+ const emptyResponse = await client.v1.transfers.$get(
1046
+ {
1047
+ query: {
1048
+ limit: '5',
1049
+ order: 'desc',
1050
+ 'timestamp.to': '1970-01-01T00:00:00Z',
1051
+ },
1052
+ },
1053
+ TestApp.auth,
1054
+ )
1055
+ const empty = await TestApp.json(emptyResponse, Transfers.schema.getTransfers.Response)
1056
+
1057
+ expect(emptyResponse.status).toBe(200)
1058
+ expect(empty.data).toEqual([])
1059
+ })
1060
+
1061
+ test('keeps the largest positional window within the indexer row limit', async () => {
1062
+ const client = TestApp.client()
1063
+ const response = await client.v1.transfers.$get(
1064
+ { query: { limit: '200', order: 'desc', page: '50' } },
1065
+ TestApp.auth,
1066
+ )
1067
+ const page = await TestApp.json(response, Transfers.schema.getTransfers.Response)
1068
+
1069
+ expect(response.status).toBe(200)
1070
+ expect(page.data.length).toBeLessThanOrEqual(200)
1071
+ })
806
1072
  })
807
1073
 
808
1074
  describe('collate', () => {
@@ -816,7 +1082,11 @@ describe('collate', () => {
816
1082
  test('passes a lone engine-paged stream through untouched', () => {
817
1083
  const rows = [row(40, 4), row(20, 2)]
818
1084
  expect(
819
- Transfers.collate([{ rows, sample: false }], { limit: 3, offset: 0, order: 'desc' }),
1085
+ Transfers.collate([{ capped: false, rows, sample: false }], {
1086
+ limit: 3,
1087
+ offset: 0,
1088
+ order: 'desc',
1089
+ }),
820
1090
  ).toEqual({
821
1091
  capped: false,
822
1092
  rows,
@@ -827,8 +1097,8 @@ describe('collate', () => {
827
1097
  const shared = row(30, 3)
828
1098
  const { capped, rows } = Transfers.collate(
829
1099
  [
830
- { rows: [row(40, 4), shared], sample: false },
831
- { rows: [{ ...shared }, row(10, 1)], sample: false },
1100
+ { capped: false, rows: [row(40, 4), shared], sample: false },
1101
+ { capped: false, rows: [{ ...shared }, row(10, 1)], sample: false },
832
1102
  ],
833
1103
  { limit: 2, offset: 1, order: 'desc' },
834
1104
  )
@@ -837,21 +1107,92 @@ describe('collate', () => {
837
1107
  })
838
1108
 
839
1109
  test('orders and pages a below-cap fallback sample', () => {
840
- const { capped, rows } = Transfers.collate([{ rows: [row(10, 1), row(30, 3)], sample: true }], {
841
- limit: 2,
842
- offset: 0,
843
- order: 'desc',
844
- })
1110
+ const { capped, rows } = Transfers.collate(
1111
+ [{ capped: false, rows: [row(10, 1), row(30, 3)], sample: true }],
1112
+ { limit: 2, offset: 0, order: 'desc' },
1113
+ )
845
1114
  expect(capped).toBe(false)
846
1115
  expect(rows.map((entry) => entry['block_num'])).toEqual([30, 10])
847
1116
  })
848
1117
 
1118
+ test('preserves the cap marker on an ordered stream', () => {
1119
+ const rows = [row(30, 3), row(10, 1)]
1120
+ expect(
1121
+ Transfers.collate([{ capped: true, rows, sample: false }], {
1122
+ limit: 2,
1123
+ offset: 0,
1124
+ order: 'desc',
1125
+ }),
1126
+ ).toEqual({ capped: true, rows })
1127
+ })
1128
+
1129
+ test('preserves resumable progress on a lone ordered stream', () => {
1130
+ const rows = [row(30, 3), row(10, 1)]
1131
+ const resume = { offset: 4, position: [10, 1] as const }
1132
+ expect(
1133
+ Transfers.collate([{ capped: true, resume, rows, sample: false }], {
1134
+ limit: 2,
1135
+ offset: 0,
1136
+ order: 'desc',
1137
+ }),
1138
+ ).toEqual({ capped: true, resume, rows })
1139
+ })
1140
+
1141
+ test('preserves common resumable progress across split ordered streams', () => {
1142
+ const result = Transfers.collate(
1143
+ [
1144
+ {
1145
+ capped: true,
1146
+ resume: { offset: 0, position: [20, 2] },
1147
+ rows: [row(50, 5), row(30, 3)],
1148
+ sample: false,
1149
+ },
1150
+ {
1151
+ capped: true,
1152
+ resume: { offset: 0, position: [25, 2] },
1153
+ rows: [row(45, 4), row(25, 2)],
1154
+ sample: false,
1155
+ },
1156
+ ],
1157
+ { limit: 5, offset: 1, order: 'desc' },
1158
+ )
1159
+ expect(result).toEqual({
1160
+ capped: true,
1161
+ resume: { offset: 0, position: [25, 2] },
1162
+ rows: [row(45, 4), row(30, 3), row(25, 2)],
1163
+ })
1164
+ })
1165
+
1166
+ test('returns a complete split window before the common resume point', () => {
1167
+ const result = Transfers.collate(
1168
+ [
1169
+ {
1170
+ capped: true,
1171
+ resume: { offset: 0, position: [20, 2] },
1172
+ rows: [row(50, 5), row(30, 3)],
1173
+ sample: false,
1174
+ },
1175
+ {
1176
+ capped: true,
1177
+ resume: { offset: 0, position: [25, 2] },
1178
+ rows: [row(45, 4), row(25, 2)],
1179
+ sample: false,
1180
+ },
1181
+ ],
1182
+ { limit: 3, offset: 0, order: 'desc' },
1183
+ )
1184
+ expect(result).toEqual({
1185
+ capped: false,
1186
+ rows: [row(50, 5), row(45, 4), row(30, 3)],
1187
+ })
1188
+ })
1189
+
849
1190
  test('flags a fallback sample that hit the cap', () => {
850
1191
  const sample = Array.from({ length: Schema.countCap }, (_, index) => row(index + 1, index))
851
1192
  const { capped, rows } = Transfers.collate(
852
1193
  [
853
- { rows: [row(40_000, 20_000)], sample: false },
854
- { rows: sample, sample: true },
1194
+ { capped: false, rows: [row(40_000, 20_000)], sample: false },
1195
+ { capped: true, rows: sample, sample: true },
855
1196
  ],
856
1197
  { limit: 3, offset: 0, order: 'desc' },
857
1198
  )
@@ -893,6 +1234,43 @@ describe('foldCount', () => {
893
1234
  })
894
1235
  })
895
1236
 
1237
+ test('classifies filtered plumbing rows from complete transaction legs', () => {
1238
+ const txHash = `0x${'dd'.repeat(32)}`
1239
+ const rows = [
1240
+ leg(txHash, 0, swapper, Addresses.stablecoinDex, sourceToken),
1241
+ leg(txHash, 1, Addresses.stablecoinDex, swapper, destinationToken),
1242
+ leg(txHash, 2, swapper, recipient, destinationToken),
1243
+ leg(txHash, 3, swapper, Addresses.feeManager, sourceToken),
1244
+ ]
1245
+ expect(
1246
+ Transfers.foldCount(rows.slice(0, 2), { capped: false, classificationRows: rows }),
1247
+ ).toEqual({
1248
+ totalCount: 0,
1249
+ totalCountCapped: false,
1250
+ })
1251
+ })
1252
+
1253
+ test('uses log order when classifying complete transaction legs', () => {
1254
+ const txHash = `0x${'dd'.repeat(32)}`
1255
+ const thirdToken = '0x20c0000000000000000000000000000000000003'
1256
+ const rows = [
1257
+ leg(txHash, 1, swapper, Addresses.stablecoinDex, sourceToken),
1258
+ leg(txHash, 4, Addresses.stablecoinDex, swapper, destinationToken),
1259
+ leg(txHash, 5, swapper, Addresses.stablecoinDex, destinationToken),
1260
+ leg(txHash, 7, Addresses.stablecoinDex, swapper, thirdToken),
1261
+ ]
1262
+ const delivery = leg(txHash, 8, swapper, recipient, thirdToken)
1263
+ expect(
1264
+ Transfers.foldCount(rows, {
1265
+ capped: false,
1266
+ classificationRows: [rows[2]!, rows[3]!, delivery, rows[0]!, rows[1]!],
1267
+ }),
1268
+ ).toEqual({
1269
+ totalCount: 4,
1270
+ totalCountCapped: false,
1271
+ })
1272
+ })
1273
+
896
1274
  test('keeps a cap-truncated sample conservative', () => {
897
1275
  const txHash = `0x${'dd'.repeat(32)}`
898
1276
  // A partial bundle: the DEX return leg fell outside the cap, defeating