tapimo 0.10.1 → 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 (144) hide show
  1. package/dist/App.d.ts +1776 -1543
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/Webhooks.d.ts +1 -1
  4. package/dist/Webhooks.d.ts.map +1 -1
  5. package/dist/Webhooks.js +1 -1
  6. package/dist/Webhooks.js.map +1 -1
  7. package/dist/admin/apps/api-keys.d.ts +4 -4
  8. package/dist/analytics/Analytics.d.ts.map +1 -1
  9. package/dist/analytics/Analytics.js +206 -0
  10. package/dist/analytics/Analytics.js.map +1 -1
  11. package/dist/analytics/tables/requestEvents.d.ts +4 -0
  12. package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
  13. package/dist/analytics/tables/requestEvents.js +2 -0
  14. package/dist/analytics/tables/requestEvents.js.map +1 -1
  15. package/dist/apps/data/App.d.ts.map +1 -1
  16. package/dist/apps/data/App.js +2 -0
  17. package/dist/apps/data/App.js.map +1 -1
  18. package/dist/apps/data/routes/addresses.d.ts +340 -340
  19. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  20. package/dist/apps/data/routes/balances.js +2 -6
  21. package/dist/apps/data/routes/balances.js.map +1 -1
  22. package/dist/apps/data/routes/coingecko.d.ts +176 -176
  23. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  24. package/dist/apps/data/routes/earn.js +63 -34
  25. package/dist/apps/data/routes/earn.js.map +1 -1
  26. package/dist/apps/data/routes/exchanges.d.ts +4 -0
  27. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  28. package/dist/apps/data/routes/exchanges.js +115 -77
  29. package/dist/apps/data/routes/exchanges.js.map +1 -1
  30. package/dist/apps/data/routes/indexer.d.ts +34 -34
  31. package/dist/apps/data/routes/receipts.d.ts +4 -3
  32. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  33. package/dist/apps/data/routes/receipts.js +15 -14
  34. package/dist/apps/data/routes/receipts.js.map +1 -1
  35. package/dist/apps/data/routes/tokens.d.ts +1 -1
  36. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  37. package/dist/apps/data/routes/tokens.js +4 -3
  38. package/dist/apps/data/routes/tokens.js.map +1 -1
  39. package/dist/apps/data/routes/transactions.d.ts +281 -281
  40. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  41. package/dist/apps/data/routes/transfers.js +25 -7
  42. package/dist/apps/data/routes/transfers.js.map +1 -1
  43. package/dist/apps/data/routes/webhooks.d.ts +380 -364
  44. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  45. package/dist/apps/data/routes/webhooks.js +16 -9
  46. package/dist/apps/data/routes/webhooks.js.map +1 -1
  47. package/dist/apps/data/routes/zones.d.ts +31 -31
  48. package/dist/apps/funding/App.d.ts +233 -0
  49. package/dist/apps/funding/App.d.ts.map +1 -1
  50. package/dist/apps/funding/routes/deposit-addresses.d.ts +330 -0
  51. package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
  52. package/dist/apps/funding/routes/deposit-addresses.js +207 -11
  53. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
  54. package/dist/apps/funding/routes/providers.d.ts +13 -13
  55. package/dist/apps/management/routes/faucet.d.ts +32 -32
  56. package/dist/apps/management/routes/members.d.ts +79 -79
  57. package/dist/apps/management/routes/usage.d.ts +80 -80
  58. package/dist/db/Db.d.ts.map +1 -1
  59. package/dist/db/Db.js +32 -0
  60. package/dist/db/Db.js.map +1 -1
  61. package/dist/db/tables/fundingDepositAddresses.d.ts +20 -0
  62. package/dist/db/tables/fundingDepositAddresses.d.ts.map +1 -1
  63. package/dist/db/tables/fundingDepositAddresses.js +17 -0
  64. package/dist/db/tables/fundingDepositAddresses.js.map +1 -1
  65. package/dist/db/tables/fundingDeposits.d.ts +41 -0
  66. package/dist/db/tables/fundingDeposits.d.ts.map +1 -1
  67. package/dist/db/tables/fundingDeposits.js +69 -0
  68. package/dist/db/tables/fundingDeposits.js.map +1 -1
  69. package/dist/internal/Auth.d.ts +0 -26
  70. package/dist/internal/Auth.d.ts.map +1 -1
  71. package/dist/internal/Auth.js +0 -33
  72. package/dist/internal/Auth.js.map +1 -1
  73. package/dist/internal/Credential.d.ts +27 -0
  74. package/dist/internal/Credential.d.ts.map +1 -0
  75. package/dist/internal/Credential.js +34 -0
  76. package/dist/internal/Credential.js.map +1 -0
  77. package/dist/internal/EarnRates.js +11 -7
  78. package/dist/internal/EarnRates.js.map +1 -1
  79. package/dist/internal/Log.d.ts +8 -0
  80. package/dist/internal/Log.d.ts.map +1 -1
  81. package/dist/internal/Log.js +4 -0
  82. package/dist/internal/Log.js.map +1 -1
  83. package/dist/internal/MetricSink.d.ts +24 -0
  84. package/dist/internal/MetricSink.d.ts.map +1 -1
  85. package/dist/internal/MetricSink.js +25 -0
  86. package/dist/internal/MetricSink.js.map +1 -1
  87. package/dist/internal/Tidx.d.ts +4 -3
  88. package/dist/internal/Tidx.d.ts.map +1 -1
  89. package/dist/internal/Tidx.js +2 -2
  90. package/dist/internal/Tidx.js.map +1 -1
  91. package/dist/internal/Viem.d.ts +4 -3
  92. package/dist/internal/Viem.d.ts.map +1 -1
  93. package/dist/internal/Viem.js +3 -2
  94. package/dist/internal/Viem.js.map +1 -1
  95. package/dist/internal/Webhooks.d.ts +2 -0
  96. package/dist/internal/Webhooks.d.ts.map +1 -1
  97. package/dist/internal/Webhooks.js +10 -3
  98. package/dist/internal/Webhooks.js.map +1 -1
  99. package/dist/internal/index.d.ts +1 -0
  100. package/dist/internal/index.d.ts.map +1 -1
  101. package/dist/internal/index.js +1 -0
  102. package/dist/internal/index.js.map +1 -1
  103. package/package.json +1 -1
  104. package/src/App.test.ts +30 -0
  105. package/src/Client.test-d.ts +33 -1
  106. package/src/Webhooks.ts +1 -1
  107. package/src/analytics/Analytics.test.ts +34 -0
  108. package/src/analytics/Analytics.ts +206 -0
  109. package/src/analytics/tables/requestEvents.ts +6 -0
  110. package/src/apps/composition.test.ts +1 -0
  111. package/src/apps/data/App.ts +2 -0
  112. package/src/apps/data/routes/balances.ts +2 -6
  113. package/src/apps/data/routes/earn.ts +64 -34
  114. package/src/apps/data/routes/exchanges.test.ts +8 -3
  115. package/src/apps/data/routes/exchanges.ts +134 -82
  116. package/src/apps/data/routes/receipts.ts +16 -14
  117. package/src/apps/data/routes/tokens.ts +3 -2
  118. package/src/apps/data/routes/transactions.bench.ts +8 -0
  119. package/src/apps/data/routes/transfers.ts +36 -10
  120. package/src/apps/data/routes/webhooks.ts +31 -10
  121. package/src/apps/funding/routes/deposit-addresses.test.ts +216 -2
  122. package/src/apps/funding/routes/deposit-addresses.ts +254 -18
  123. package/src/apps/funding/routes/transfers.test.ts +17 -2
  124. package/src/db/Db.ts +36 -0
  125. package/src/db/tables/fundingDepositAddresses.test.ts +28 -0
  126. package/src/db/tables/fundingDepositAddresses.ts +39 -0
  127. package/src/db/tables/fundingDeposits.test.ts +82 -0
  128. package/src/db/tables/fundingDeposits.ts +130 -0
  129. package/src/internal/Auth.test.ts +0 -48
  130. package/src/internal/Auth.ts +0 -59
  131. package/src/internal/Credential.test.ts +51 -0
  132. package/src/internal/Credential.ts +58 -0
  133. package/src/internal/EarnRates.ts +13 -7
  134. package/src/internal/Log.test.ts +3 -1
  135. package/src/internal/Log.ts +14 -0
  136. package/src/internal/MetricSink.test.ts +50 -1
  137. package/src/internal/MetricSink.ts +52 -0
  138. package/src/internal/Tidx.ts +5 -4
  139. package/src/internal/Viem.test.ts +4 -0
  140. package/src/internal/Viem.ts +6 -4
  141. package/src/internal/Webhooks.test.ts +9 -0
  142. package/src/internal/Webhooks.ts +10 -3
  143. package/src/internal/index.test.ts +1 -0
  144. package/src/internal/index.ts +1 -0
@@ -976,9 +976,10 @@ function countFilters(options: countReceipts.Options): string[] {
976
976
 
977
977
  /**
978
978
  * Capped total count of receipts matching the same filters as the page. The
979
- * address scope reuses the page's `UNION` workaround (`receipts` indexes `from`
980
- * but not `to`, so the `to` side resolves hashes via the `txs` index), counting
981
- * the deduped keyset so self-receipts (`from = to`) aren't double-counted.
979
+ * address scope reuses the page's `UNION` workaround. Fee-payer filters make a
980
+ * direct `receipts.to` predicate selective; otherwise the `to` side resolves
981
+ * hashes via the `txs` index. The deduped keyset avoids double-counting
982
+ * self-receipts (`from = to`).
982
983
  * `totalCountCapped: true` is a lower bound — at least {@link Schema.countCap} matches.
983
984
  */
984
985
  export function countReceipts(
@@ -995,10 +996,11 @@ export function countReceipts(
995
996
  let query: string
996
997
  if (address !== undefined) {
997
998
  const fromWhere = [`"from" = '${address}'`, ...filters].join(' AND ')
998
- const toWhere = [
999
- `tx_hash IN (SELECT hash FROM txs WHERE "to" = '${address}')`,
1000
- ...filters,
1001
- ].join(' AND ')
999
+ const toPredicate =
1000
+ options.feePayer !== undefined
1001
+ ? `"to" = '${address}'`
1002
+ : `tx_hash IN (SELECT hash FROM txs WHERE "to" = '${address}')`
1003
+ const toWhere = [toPredicate, ...filters].join(' AND ')
1002
1004
  query = `
1003
1005
  SELECT count(*) AS total
1004
1006
  FROM (
@@ -1069,13 +1071,13 @@ export async function embedReceipts(
1069
1071
  const { chainId, include, rows } = options
1070
1072
  const found = new Map<string, humanizeReceipt.Output>()
1071
1073
 
1072
- // Only mined rows can have a receipt; their block numbers also feed the
1073
- // block bound the `logs` lookup requires.
1074
- const blocks: number[] = []
1074
+ // Only mined rows can have a receipt; their block numbers also constrain the
1075
+ // `logs` lookup to the page's exact blocks.
1076
+ const blocks = new Set<number>()
1075
1077
  const hashes = new Set<string>()
1076
1078
  for (const row of rows) {
1077
1079
  if (row.blockNumber === null) continue
1078
- blocks.push(row.blockNumber)
1080
+ blocks.add(row.blockNumber)
1079
1081
  hashes.add(row.hash.toLowerCase())
1080
1082
  }
1081
1083
  if (hashes.size === 0) return new Map()
@@ -1086,8 +1088,8 @@ export async function embedReceipts(
1086
1088
  // Raw-table queries (no signature-decoded CTE), so the inline SQL is cast to
1087
1089
  // `string`. The receipts lookup needs no block bound (`receipts.tx_hash` is
1088
1090
  // indexed — see the `sideQuery` comment in `getReceipts`); the logs lookup
1089
- // REQUIRES one: `logs` is sorted by block position, so a bare `tx_hash IN`
1090
- // scans to genesis (see `blockBound` in `transfers.ts`).
1091
+ // uses the page's exact blocks so sparse transaction pages do not scan every
1092
+ // block between their oldest and newest rows.
1091
1093
  const [receipts_result, logs_result] = await Timing.time(c, 'receipts_embed', () =>
1092
1094
  Promise.all([
1093
1095
  options.receipts
@@ -1100,7 +1102,7 @@ export async function embedReceipts(
1100
1102
  tidx.fetch({
1101
1103
  chainId,
1102
1104
  query:
1103
- `SELECT tx_hash, block_num, tx_idx, log_idx, address, selector, topic1, topic2, topic3, data, block_timestamp FROM logs WHERE block_num BETWEEN ${Math.min(...blocks)} AND ${Math.max(...blocks)} AND tx_hash IN (${inList}) ORDER BY block_num ASC, log_idx ASC` as string,
1105
+ `SELECT tx_hash, block_num, tx_idx, log_idx, address, selector, topic1, topic2, topic3, data, block_timestamp FROM logs WHERE block_num IN (${[...blocks].join(', ')}) AND tx_hash IN (${inList}) ORDER BY block_num ASC, log_idx ASC` as string,
1104
1106
  }),
1105
1107
  ]),
1106
1108
  )
@@ -1228,12 +1228,13 @@ export async function resolveTokens(
1228
1228
  const logos = new Map(logoByAddress)
1229
1229
  const metadataMap = new Map(metadataByAddress)
1230
1230
  const onchainLogos = new Map(
1231
- includeLogoUri
1231
+ includeLogoUri && options.metadata !== undefined
1232
1232
  ? await Timing.time(c, 'token_logo_uri', () =>
1233
1233
  Promise.all(
1234
1234
  addresses
1235
1235
  .filter(
1236
1236
  (address) =>
1237
+ options.metadata?.has(address) === true &&
1237
1238
  !logos.get(address) &&
1238
1239
  !snapshot?.byAddress.get(address)?.logoUri &&
1239
1240
  !metadataMap.get(address)?.logoUri,
@@ -1321,7 +1322,7 @@ function getTokenLogoUri(c: Context<App.Environment>, options: getTokenMetadata.
1321
1322
 
1322
1323
  /**
1323
1324
  * Resolves a token's on-chain metadata (`name`/`symbol`/`decimals`/`currency`/
1324
- * `totalSupply`) via a single `token.getMetadata` RPC, memoized under the shared
1325
+ * `logoUri`/`totalSupply`) via a single `token.getMetadata` RPC, memoized under the shared
1325
1326
  * `token:v1:{chainId}:{address}:metadata` key. Exported so feed enrichers (e.g.
1326
1327
  * `activities`) reuse the same cache entry as the token resource rather than
1327
1328
  * fetching metadata through a parallel path.
@@ -30,6 +30,14 @@ QueryBench.bench({
30
30
  },
31
31
  })
32
32
 
33
+ QueryBench.bench({
34
+ name: 'getTransactions?address&include=receipt',
35
+ path: {
36
+ 4217: `/v1/transactions?address=${address[4217]}&include=receipt`,
37
+ 42431: `/v1/transactions?address=${address[42431]}&include=receipt`,
38
+ },
39
+ })
40
+
33
41
  QueryBench.bench({
34
42
  name: 'getTransaction',
35
43
  path: {
@@ -437,10 +437,10 @@ export function transfers(options: transfers.Options = {}) {
437
437
  transactionHash: row.transactionHash,
438
438
  }))
439
439
 
440
- // Memo/attribution are opt-in: one `TransferWithMemo` lookup bounded
441
- // to the page's transactions resolves both — the decoded text `memo`,
442
- // and `attribution` when the memo is a known MPP service fingerprint.
443
- if (options.include.includes('memo') && data.length > 0) {
440
+ const includeCrossToken = options.include.includes('crossToken')
441
+ const includeMemo = options.include.includes('memo')
442
+ // Attach memos before cross-token folding changes the matching fields.
443
+ if (includeMemo && includeCrossToken && data.length > 0) {
444
444
  const memoByRow = await memoMap(c, { chainId, rows: data })
445
445
  data = data.map((transfer) => {
446
446
  const entry = memoByRow.get(
@@ -469,7 +469,7 @@ export function transfers(options: transfers.Options = {}) {
469
469
  // own event token is the delivered token), so a `token=` filter matches
470
470
  // cross-token rows by their destination token, and each transfer still
471
471
  // appears exactly once across pages.
472
- if (options.include.includes('crossToken') && data.length > 0) {
472
+ if (includeCrossToken && data.length > 0) {
473
473
  const { fold, suppress } = await crossTokenMap(c, { chainId, rows: data })
474
474
  data = data.flatMap((transfer) => {
475
475
  const key = eventKey(transfer.transactionHash, transfer.logIndex)
@@ -502,11 +502,17 @@ export function transfers(options: transfers.Options = {}) {
502
502
  ]),
503
503
  ),
504
504
  )
505
- const tokensByAddress = new Map<
506
- string,
507
- Awaited<ReturnType<typeof Tokens.getTokenMetadata>>
508
- >()
509
- await Timing.time(c, 'tokens', async () => {
505
+ // Memo and token metadata depend only on the page rows, so resolve
506
+ // them concurrently when cross-token folding is not requested.
507
+ const memoPromise =
508
+ includeMemo && !includeCrossToken && data.length > 0
509
+ ? memoMap(c, { chainId, rows: data })
510
+ : undefined
511
+ const tokensPromise = Timing.time(c, 'tokens', async () => {
512
+ const tokensByAddress = new Map<
513
+ string,
514
+ Awaited<ReturnType<typeof Tokens.getTokenMetadata>>
515
+ >()
510
516
  for (let index = 0; index < uniqueTokens.length; index += tokenReadChunkSize) {
511
517
  const group = uniqueTokens.slice(index, index + tokenReadChunkSize)
512
518
  const entries = await Promise.all(
@@ -536,7 +542,27 @@ export function transfers(options: transfers.Options = {}) {
536
542
  )
537
543
  for (const entry of entries) tokensByAddress.set(...entry)
538
544
  }
545
+ return tokensByAddress
539
546
  })
547
+ const [memoByRow, tokensByAddress] = await Promise.all([memoPromise, tokensPromise])
548
+ if (memoByRow)
549
+ data = data.map((transfer) => {
550
+ const entry = memoByRow.get(
551
+ transferKey(
552
+ transfer.transactionHash,
553
+ transfer.sender,
554
+ transfer.recipient,
555
+ transfer.sourceToken.amount,
556
+ transfer.sourceToken.address,
557
+ ),
558
+ )
559
+ if (!entry) return transfer
560
+ return {
561
+ ...transfer,
562
+ ...(entry.attribution ? { attribution: entry.attribution } : {}),
563
+ ...(entry.memo ? { memo: entry.memo } : {}),
564
+ }
565
+ })
540
566
 
541
567
  // Value each leg via its curated display currency, in the requested
542
568
  // denomination.
@@ -2276,16 +2276,11 @@ export async function poll(options: poll.Options): Promise<poll.Result> {
2276
2276
  // Token metadata is best-effort for webhooks: transfers use it for display,
2277
2277
  // while transactions expose fee-token references without blocking delivery.
2278
2278
  const enrichStartedAt = Date.now()
2279
- const data =
2280
- eventType === 'token:transfer'
2281
- ? await withTokenMetadata(item.data, { cache: cache_metadata, chainId, client })
2282
- : eventType === 'transaction:included'
2283
- ? await withFeeTokenMetadata(item.data, {
2284
- cache: cache_metadata,
2285
- chainId,
2286
- client,
2287
- })
2288
- : item.data
2279
+ const data = await enrichEventData(eventType, item.data, {
2280
+ cache: cache_metadata,
2281
+ chainId,
2282
+ client,
2283
+ })
2289
2284
  durations.enrich += Date.now() - enrichStartedAt
2290
2285
  observed.add('enrich')
2291
2286
  options.signal?.throwIfAborted()
@@ -3074,6 +3069,32 @@ function decodeScanLog(log: ScanLog, event: AbiEvent): ScanLog {
3074
3069
  }
3075
3070
  }
3076
3071
 
3072
+ /**
3073
+ * Attaches best-effort token metadata to one matched row before delivery.
3074
+ * Transfers gain display metadata; transactions resolve their fee token.
3075
+ */
3076
+ export async function enrichEventData(
3077
+ eventType: EventType,
3078
+ data: unknown,
3079
+ options: enrichEventData.Options,
3080
+ ): Promise<unknown> {
3081
+ if (eventType === 'token:transfer') return withTokenMetadata(data, options)
3082
+ if (eventType === 'transaction:included') return withFeeTokenMetadata(data, options)
3083
+ return data
3084
+ }
3085
+
3086
+ export declare namespace enrichEventData {
3087
+ /** Metadata lookup dependencies. */
3088
+ type Options = {
3089
+ /** Memoization store for token metadata (5 minute TTL). */
3090
+ cache: Store.Store
3091
+ /** Chain the row was produced on. */
3092
+ chainId: number
3093
+ /** Client for `token.getMetadata` reads. */
3094
+ client: Viem.getClient.ReturnType
3095
+ }
3096
+ }
3097
+
3077
3098
  /** Scans contract logs for one subscription, then re-checks every filter in memory. */
3078
3099
  async function scanLogs(options: {
3079
3100
  batchSize: number
@@ -966,6 +966,67 @@ describe.skipIf(!relayApiKey)('POST /funding/deposit-addresses (relay)', () => {
966
966
  })
967
967
  })
968
968
 
969
+ describe('GET /funding/deposit-addresses', () => {
970
+ test('pages organization addresses and narrows project-attributed keys', async () => {
971
+ const app = transfersApp()
972
+ const first = await seedDepositAddress({ orgId: fundingAddressListReader.orgId })
973
+ const second = await seedDepositAddress({ orgId: fundingAddressListReader.orgId })
974
+ const third = await seedDepositAddress({ orgId: fundingAddressListReader.orgId })
975
+ const project = await seedDepositAddress({
976
+ orgId: fundingAddressListReader.orgId,
977
+ projectId: fundingAddressListProjectReader.projectId,
978
+ })
979
+ const sibling = await seedDepositAddress({
980
+ orgId: fundingAddressListReader.orgId,
981
+ projectId: 'prj_funding_address_list_sibling',
982
+ })
983
+ const sixth = await seedDepositAddress({ orgId: fundingAddressListReader.orgId })
984
+ await seedDepositAddress({ orgId: fundingForeign.orgId })
985
+ await seedDepositAddress({
986
+ environment: 'sandbox',
987
+ orgId: fundingAddressListReader.orgId,
988
+ })
989
+
990
+ const path = '/v1/funding/deposit-addresses'
991
+ expect((await app.request(path)).status).toBe(401)
992
+ expect((await app.request(path, as(TestApp.key))).status).toBe(403)
993
+
994
+ const response = await app.request(`${path}?limit=5`, as(fundingAddressListReader))
995
+ expect(response.status).toBe(200)
996
+ expect({
997
+ cacheControl: response.headers.get('cache-control'),
998
+ etag: response.headers.get('etag'),
999
+ vary: response.headers.get('vary'),
1000
+ }).toEqual({
1001
+ cacheControl: 'no-store',
1002
+ etag: null,
1003
+ vary: 'Accept-Encoding, Authorization, Tempo-API-Key, X-API-Key',
1004
+ })
1005
+ const body = await TestApp.json(response, Funding.schema.listFundingDepositAddresses.Response)
1006
+ expect(body.data.map((address) => address.id)).toEqual([
1007
+ sixth.id,
1008
+ sibling.id,
1009
+ project.id,
1010
+ third.id,
1011
+ second.id,
1012
+ ])
1013
+ expect(body.nextCursor).toBe(Cursor.encode([second.createdAt, second.id]))
1014
+
1015
+ const next = await TestApp.json(
1016
+ await app.request(`${path}?limit=5&cursor=${body.nextCursor}`, as(fundingAddressListReader)),
1017
+ Funding.schema.listFundingDepositAddresses.Response,
1018
+ )
1019
+ expect(next.data.map((address) => address.id)).toEqual([first.id])
1020
+ expect(next.nextCursor).toBeNull()
1021
+
1022
+ const projectBody = await TestApp.json(
1023
+ await app.request(path, as(fundingAddressListProjectReader)),
1024
+ Funding.schema.listFundingDepositAddresses.Response,
1025
+ )
1026
+ expect(projectBody.data.map((address) => address.id)).toEqual([project.id])
1027
+ })
1028
+ })
1029
+
969
1030
  describe('GET /funding/deposit-addresses/:id', () => {
970
1031
  test('requires funding:read and returns the owned address', async () => {
971
1032
  const app = transfersApp()
@@ -1329,6 +1390,63 @@ describe('GET /funding/deposits?depositAddress', () => {
1329
1390
  })
1330
1391
  })
1331
1392
 
1393
+ describe('GET /funding/deposits', () => {
1394
+ test('pages organization deposits and narrows project-attributed keys', async () => {
1395
+ const app = transfersApp()
1396
+ const firstAddress = await seedDepositAddress({ orgId: fundingDepositListReader.orgId })
1397
+ const secondAddress = await seedDepositAddress({ orgId: fundingDepositListReader.orgId })
1398
+ const thirdAddress = await seedDepositAddress({ orgId: fundingDepositListReader.orgId })
1399
+ const projectAddress = await seedDepositAddress({
1400
+ orgId: fundingDepositListReader.orgId,
1401
+ projectId: fundingDepositListProjectReader.projectId,
1402
+ })
1403
+ const siblingAddress = await seedDepositAddress({
1404
+ orgId: fundingDepositListReader.orgId,
1405
+ projectId: 'prj_funding_deposit_list_sibling',
1406
+ })
1407
+ const sixthAddress = await seedDepositAddress({ orgId: fundingDepositListReader.orgId })
1408
+ const first = await seedDeposit(firstAddress)
1409
+ const second = await seedDeposit(secondAddress)
1410
+ const third = await seedDeposit(thirdAddress)
1411
+ const project = await seedDeposit(projectAddress)
1412
+ const sibling = await seedDeposit(siblingAddress)
1413
+ const sixth = await seedDeposit(sixthAddress)
1414
+ const foreignAddress = await seedDepositAddress({ orgId: fundingForeign.orgId })
1415
+ await seedDeposit(foreignAddress)
1416
+ const sandboxAddress = await seedDepositAddress({
1417
+ environment: 'sandbox',
1418
+ orgId: fundingDepositListReader.orgId,
1419
+ })
1420
+ await seedDeposit(sandboxAddress)
1421
+
1422
+ const path = '/v1/funding/deposits'
1423
+ const response = await app.request(`${path}?limit=5`, as(fundingDepositListReader))
1424
+ expect(response.status).toBe(200)
1425
+ const body = await TestApp.json(response, Funding.schema.listFundingDeposits.Response)
1426
+ expect(body.data.map((deposit) => deposit.id)).toEqual([
1427
+ sixth.id,
1428
+ sibling.id,
1429
+ project.id,
1430
+ third.id,
1431
+ second.id,
1432
+ ])
1433
+ expect(body.nextCursor).toBe(Cursor.encode([second.createdAt, second.id]))
1434
+
1435
+ const next = await TestApp.json(
1436
+ await app.request(`${path}?limit=5&cursor=${body.nextCursor}`, as(fundingDepositListReader)),
1437
+ Funding.schema.listFundingDeposits.Response,
1438
+ )
1439
+ expect(next.data.map((deposit) => deposit.id)).toEqual([first.id])
1440
+ expect(next.nextCursor).toBeNull()
1441
+
1442
+ const projectBody = await TestApp.json(
1443
+ await app.request(path, as(fundingDepositListProjectReader)),
1444
+ Funding.schema.listFundingDeposits.Response,
1445
+ )
1446
+ expect(projectBody.data.map((deposit) => deposit.id)).toEqual([project.id])
1447
+ })
1448
+ })
1449
+
1332
1450
  describe('GET /funding/deposits?recipient', () => {
1333
1451
  test('requires funding:read and pages recipient deposits across addresses', async () => {
1334
1452
  const app = transfersApp()
@@ -1336,7 +1454,6 @@ describe('GET /funding/deposits?recipient', () => {
1336
1454
  const path = `/v1/funding/deposits?recipient=${recipient.toLowerCase()}`
1337
1455
  expect((await app.request(path)).status).toBe(401)
1338
1456
  expect((await app.request(path, as(TestApp.key))).status).toBe(403)
1339
- expect((await app.request('/v1/funding/deposits', as(fundingReader))).status).toBe(400)
1340
1457
  expect(
1341
1458
  (await app.request('/v1/funding/deposits?depositAddress=', as(fundingReader))).status,
1342
1459
  ).toBe(400)
@@ -1347,7 +1464,7 @@ describe('GET /funding/deposits?recipient', () => {
1347
1464
  as(fundingReader),
1348
1465
  )
1349
1466
  ).status,
1350
- ).toBe(400)
1467
+ ).toBe(200)
1351
1468
 
1352
1469
  const firstAddress = await seedDepositAddress({
1353
1470
  snapshot: TestFunding.depositAddressSnapshot({
@@ -1438,6 +1555,61 @@ describe('GET /funding/deposits?recipient', () => {
1438
1555
  })
1439
1556
  })
1440
1557
 
1558
+ describe('GET /funding/deposits?filters', () => {
1559
+ test('AND-combines canonical aliases and includes the matching count', async () => {
1560
+ const app = transfersApp()
1561
+ const snapshot = TestFunding.depositAddressSnapshot({
1562
+ address: 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE',
1563
+ })
1564
+ const address = await seedDepositAddress({
1565
+ orgId: fundingDepositFilterReader.orgId,
1566
+ snapshot,
1567
+ })
1568
+ const deposit = await seedDeposit(address)
1569
+ const unrelatedAddress = await seedDepositAddress({
1570
+ orgId: fundingDepositFilterReader.orgId,
1571
+ snapshot: TestFunding.depositAddressSnapshot({
1572
+ provider: { id: 'relay_unrelated', name: 'Relay' },
1573
+ recipient: `0x${'22'.repeat(20)}`,
1574
+ }),
1575
+ })
1576
+ await seedDeposit(unrelatedAddress)
1577
+ const query = new URLSearchParams({
1578
+ depositAddress: snapshot.address,
1579
+ destinationToken: 'usdt0',
1580
+ include: 'totalCount',
1581
+ provider: 'relay',
1582
+ recipient: snapshot.recipient.toLowerCase(),
1583
+ sourceChain: 'tron',
1584
+ sourceToken: 'usdt',
1585
+ status: 'detected',
1586
+ })
1587
+
1588
+ const body = await TestApp.json(
1589
+ await app.request(`/v1/funding/deposits?${query}`, as(fundingDepositFilterReader)),
1590
+ Funding.schema.listFundingDeposits.Response,
1591
+ )
1592
+ expect(body).toEqual({
1593
+ data: [FundingDeposit.toPublic(deposit)],
1594
+ meta: { totalCount: 1, totalCountCapped: false },
1595
+ nextCursor: null,
1596
+ })
1597
+
1598
+ const empty = await TestApp.json(
1599
+ await app.request(
1600
+ '/v1/funding/deposits?sourceToken=unknown&include=totalCount',
1601
+ as(fundingDepositFilterReader),
1602
+ ),
1603
+ Funding.schema.listFundingDeposits.Response,
1604
+ )
1605
+ expect(empty).toEqual({
1606
+ data: [],
1607
+ meta: { totalCount: 0, totalCountCapped: false },
1608
+ nextCursor: null,
1609
+ })
1610
+ })
1611
+ })
1612
+
1441
1613
  describe('GET /funding/deposits/:id', () => {
1442
1614
  test('returns the owned deposit without private reconciliation state', async () => {
1443
1615
  const app = transfersApp()
@@ -1544,6 +1716,43 @@ const fundingReader = {
1544
1716
  token: 'secret_funding_reader',
1545
1717
  } satisfies TestApp.kvStore.Key
1546
1718
 
1719
+ const fundingAddressListReader = {
1720
+ id: 'key_funding_address_list',
1721
+ orgId: 'org_funding_address_list',
1722
+ scopes: ['funding:read'],
1723
+ token: 'secret_funding_address_list',
1724
+ } satisfies TestApp.kvStore.Key
1725
+
1726
+ const fundingAddressListProjectReader = {
1727
+ id: 'key_funding_address_list_project',
1728
+ orgId: fundingAddressListReader.orgId,
1729
+ projectId: 'prj_funding_address_list',
1730
+ scopes: ['funding:read'],
1731
+ token: 'secret_funding_address_list_project',
1732
+ } satisfies TestApp.kvStore.Key
1733
+
1734
+ const fundingDepositListReader = {
1735
+ id: 'key_funding_deposit_list',
1736
+ orgId: 'org_funding_deposit_list',
1737
+ scopes: ['funding:read'],
1738
+ token: 'secret_funding_deposit_list',
1739
+ } satisfies TestApp.kvStore.Key
1740
+
1741
+ const fundingDepositFilterReader = {
1742
+ id: 'key_funding_deposit_filter',
1743
+ orgId: 'org_funding_deposit_filter',
1744
+ scopes: ['funding:read'],
1745
+ token: 'secret_funding_deposit_filter',
1746
+ } satisfies TestApp.kvStore.Key
1747
+
1748
+ const fundingDepositListProjectReader = {
1749
+ id: 'key_funding_deposit_list_project',
1750
+ orgId: fundingDepositListReader.orgId,
1751
+ projectId: 'prj_funding_deposit_list',
1752
+ scopes: ['funding:read'],
1753
+ token: 'secret_funding_deposit_list_project',
1754
+ } satisfies TestApp.kvStore.Key
1755
+
1547
1756
  const fundingProjectReader = {
1548
1757
  id: 'key_funding_project',
1549
1758
  orgId: 'org_funding',
@@ -1589,6 +1798,11 @@ function transfersApp(options: TestApp.create.Options = {}) {
1589
1798
  auth: {
1590
1799
  keys: [
1591
1800
  TestApp.key,
1801
+ fundingAddressListProjectReader,
1802
+ fundingAddressListReader,
1803
+ fundingDepositFilterReader,
1804
+ fundingDepositListProjectReader,
1805
+ fundingDepositListReader,
1592
1806
  fundingForeign,
1593
1807
  fundingForeignWriter,
1594
1808
  fundingProjectReader,