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
@@ -126,6 +126,27 @@ export namespace schema {
126
126
  export const Response = DepositAddress
127
127
  }
128
128
 
129
+ /** Schemas for the listFundingDepositAddresses operation. */
130
+ export namespace listFundingDepositAddresses {
131
+ /** Query parameters for listing funding deposit addresses. */
132
+ export const Query = z
133
+ .object({
134
+ cursor: Schema.Cursor,
135
+ limit: Schema.Limit,
136
+ })
137
+ .check(z.describe('Paging parameters for listing funding deposit addresses.'))
138
+
139
+ /** One page of reusable funding deposit addresses. */
140
+ export const Response = z
141
+ .object({
142
+ data: z
143
+ .array(DepositAddress)
144
+ .check(z.describe('Funding deposit addresses on this page, newest first.')),
145
+ nextCursor: Schema.NextCursor,
146
+ })
147
+ .check(z.describe('A page of reusable funding deposit addresses.'))
148
+ }
149
+
129
150
  /** Schemas for the reconcileFundingDepositAddress operation. */
130
151
  export namespace reconcileFundingDepositAddress {
131
152
  /** Path parameters for reconciling a funding deposit address. */
@@ -144,32 +165,49 @@ export namespace schema {
144
165
  z.meta({ examples: ['TJRabPrwbZy45sbavfcjinPJC18kjpRTv8'] }),
145
166
  ),
146
167
  cursor: Schema.Cursor,
168
+ destinationToken: z
169
+ .optional(z.string().check(z.minLength(1)))
170
+ .check(
171
+ z.describe('Destination token symbol, contract address, or token key.'),
172
+ z.meta({ examples: ['usdt0'] }),
173
+ ),
174
+ include: Schema.totalCountInclude,
147
175
  limit: Schema.Limit,
176
+ provider: z
177
+ .optional(z.string().check(z.minLength(1)))
178
+ .check(z.describe('Funding provider id.'), z.meta({ examples: ['relay'] })),
148
179
  recipient: z
149
180
  .optional(Schema.Address)
150
181
  .check(
151
182
  z.describe('Tempo account that received the deposits.'),
152
183
  z.meta({ examples: [`0x${'11'.repeat(20)}`] }),
153
184
  ),
185
+ sourceChain: z
186
+ .optional(z.string().check(z.minLength(1)))
187
+ .check(
188
+ z.describe('Source chain CAIP-2 id, slug, or alias.'),
189
+ z.meta({ examples: ['tron'] }),
190
+ ),
191
+ sourceToken: z
192
+ .optional(z.string().check(z.minLength(1)))
193
+ .check(
194
+ z.describe('Source token symbol, contract address, or token key.'),
195
+ z.meta({ examples: ['usdt'] }),
196
+ ),
197
+ status: z.optional(FundingDeposit.schema.Status),
154
198
  })
155
- .check(
156
- z.refine(
157
- (query) => (query.depositAddress === undefined) !== (query.recipient === undefined),
158
- {
159
- error: 'Pass exactly one of `depositAddress` or `recipient`.',
160
- path: ['depositAddress'],
161
- },
162
- ),
163
- z.describe('Exactly one deposit filter and optional paging parameters.'),
164
- )
199
+ .check(z.describe('Optional deposit filters and paging parameters.'))
165
200
 
166
201
  /** One page of detected deposits. */
167
202
  export const Response = z
168
203
  .object({
169
204
  data: z.array(Deposit).check(z.describe('Detected deposits on this page, newest first.')),
205
+ meta: z
206
+ .optional(Schema.CountMeta)
207
+ .check(z.describe('Extra response metadata requested with `include`.')),
170
208
  nextCursor: Schema.NextCursor,
171
209
  })
172
- .check(z.describe('A page of funding deposits matching the selected filter.'))
210
+ .check(z.describe('A page of visible funding deposits.'))
173
211
  }
174
212
 
175
213
  /** Schemas for the getFundingDeposit operation. */
@@ -276,9 +314,114 @@ const fundingDepositExample = {
276
314
 
277
315
  const fundingDepositsExample = {
278
316
  data: [fundingDepositExample],
317
+ meta: { totalCount: 1, totalCountCapped: false },
279
318
  nextCursor: null,
280
319
  } satisfies z.output<typeof schema.listFundingDeposits.Response>
281
320
 
321
+ const fundingDepositAddressesExample = {
322
+ data: [fundingDepositAddressExample],
323
+ nextCursor: null,
324
+ } satisfies z.output<typeof schema.listFundingDepositAddresses.Response>
325
+
326
+ function resolveDepositFilters(options: resolveDepositFilters.Options): FundingDeposits.Filters {
327
+ const { catalog, parameters } = options
328
+ return {
329
+ ...(parameters.depositAddress === undefined
330
+ ? {}
331
+ : { depositAddress: parameters.depositAddress }),
332
+ ...(parameters.destinationToken === undefined
333
+ ? {}
334
+ : {
335
+ destinationTokenKeys: resolveTokenKeys({
336
+ catalog,
337
+ input: parameters.destinationToken,
338
+ side: 'destination',
339
+ }),
340
+ }),
341
+ ...(parameters.provider === undefined
342
+ ? {}
343
+ : { providerId: parameters.provider.trim().toLowerCase() }),
344
+ ...(parameters.recipient === undefined ? {} : { recipient: parameters.recipient }),
345
+ ...(parameters.sourceChain === undefined
346
+ ? {}
347
+ : { sourceChainIds: resolveChainIds(catalog, parameters.sourceChain) }),
348
+ ...(parameters.sourceToken === undefined
349
+ ? {}
350
+ : {
351
+ sourceTokenKeys: resolveTokenKeys({
352
+ catalog,
353
+ input: parameters.sourceToken,
354
+ side: 'source',
355
+ }),
356
+ }),
357
+ ...(parameters.status === undefined ? {} : { status: parameters.status }),
358
+ }
359
+ }
360
+
361
+ declare namespace resolveDepositFilters {
362
+ type Options = {
363
+ catalog?: FundingCatalog.Snapshot | undefined
364
+ parameters: z.output<typeof schema.listFundingDeposits.Query>
365
+ }
366
+ }
367
+
368
+ function resolveChainIds(catalog: FundingCatalog.Snapshot | undefined, input: string): string[] {
369
+ const trimmed = input.trim()
370
+ const chain = catalog?.chainsByKey.get(trimmed.toLowerCase())
371
+ if (chain) return [chain.id]
372
+ return /^[a-z][a-z0-9-]*:.+$/i.test(trimmed) ? [trimmed] : []
373
+ }
374
+
375
+ function resolveTokenKeys(options: resolveTokenKeys.Options): string[] {
376
+ const trimmed = options.input.trim()
377
+ const normalized = trimmed.toLowerCase()
378
+ const keys =
379
+ options.catalog?.routes
380
+ .map((entry) => entry.route[options.side])
381
+ .filter((token) => matchesToken(token, trimmed, normalized))
382
+ .map(tokenKey) ?? []
383
+ if (keys.length) return [...new Set(keys)]
384
+ if (!/^[a-z][a-z0-9-]*:.+\/(?:erc20|trc20|token):.+$/i.test(trimmed)) return []
385
+ return [/\/erc20:/i.test(trimmed) ? normalized : trimmed]
386
+ }
387
+
388
+ declare namespace resolveTokenKeys {
389
+ type Options = {
390
+ catalog?: FundingCatalog.Snapshot | undefined
391
+ input: string
392
+ side: 'destination' | 'source'
393
+ }
394
+ }
395
+
396
+ type ResolvedToken = FundingCatalog.Snapshot['routes'][number]['route']['source']
397
+
398
+ function matchesToken(token: ResolvedToken, trimmed: string, normalized: string): boolean {
399
+ return (
400
+ token.slug === normalized ||
401
+ token.symbol.toLowerCase() === normalized ||
402
+ matchesTokenIdentifier(token, token.address, trimmed, normalized) ||
403
+ matchesTokenIdentifier(token, tokenKey(token), trimmed, normalized)
404
+ )
405
+ }
406
+
407
+ function matchesTokenIdentifier(
408
+ token: ResolvedToken,
409
+ candidate: string,
410
+ trimmed: string,
411
+ normalized: string,
412
+ ): boolean {
413
+ return token.chain.kind === 'evm' ? candidate.toLowerCase() === normalized : candidate === trimmed
414
+ }
415
+
416
+ function tokenKey(token: ResolvedToken): string {
417
+ const namespace = (() => {
418
+ if (token.chain.kind === 'solana') return 'token'
419
+ if (token.chain.kind === 'tron') return 'trc20'
420
+ return 'erc20'
421
+ })()
422
+ return `${token.chain.id}/${namespace}:${token.address}`
423
+ }
424
+
282
425
  const reconcileFundingDepositAddressResponses = (() => {
283
426
  const responses = OpenApi.responses({
284
427
  errors: {
@@ -646,6 +789,72 @@ export function depositAddresses(options: depositAddresses.Options = {}) {
646
789
  }
647
790
  },
648
791
  )
792
+ .get(
793
+ '/v1/funding/deposit-addresses',
794
+ Auth.policy({ apiKey: { scopes: ['funding:read'] } }),
795
+ OpenApi.validate('query', schema.listFundingDepositAddresses.Query, {
796
+ code: 'query_invalid',
797
+ message: 'Check the query parameters and try again.',
798
+ }),
799
+ OpenApi.describeRoute({
800
+ description: 'Lists reusable funding deposit addresses, newest first.',
801
+ operationId: 'listFundingDepositAddresses',
802
+ responses: OpenApi.responses({
803
+ errors: {
804
+ 400: { codes: ['query_invalid'] },
805
+ },
806
+ success: {
807
+ description: 'A page of reusable funding deposit addresses.',
808
+ example: fundingDepositAddressesExample,
809
+ schema: schema.listFundingDepositAddresses.Response,
810
+ },
811
+ }),
812
+ summary: 'List deposit addresses',
813
+ tags: ['Deposit Addresses'],
814
+ }),
815
+ async (c) => {
816
+ if (Auth.narrowAccess) return Auth.accessError(c)
817
+ if (OpenApi.narrowValidation)
818
+ return OpenApi.validationError(c, {
819
+ code: 'query_invalid',
820
+ message: 'Check the query parameters and try again.',
821
+ })
822
+
823
+ const owner = Access.owner(c)
824
+ if (!owner) return Access.unauthorized(c)
825
+
826
+ const db = Db.get(c.get('db'))
827
+ const { cursor, limit } = c.req.valid('query')
828
+ const decoded = cursor ? Cursor.decode(cursor, ['timestamp', 'id']) : undefined
829
+ try {
830
+ const rows = await FundingDepositAddresses.list(db, {
831
+ ...(decoded
832
+ ? { cursor: { createdAt: String(decoded[0]), id: String(decoded[1]) } }
833
+ : {}),
834
+ limit: limit + 1,
835
+ owner,
836
+ })
837
+ const page = Cursor.paginate({
838
+ key: (record) => [record.createdAt, record.id],
839
+ limit,
840
+ rows,
841
+ })
842
+ const data = page.rows.map(FundingDepositAddress.toPublic)
843
+
844
+ c.header('Cache-Control', Cache.policies.noStore)
845
+ c.header('Vary', 'Accept-Encoding, Authorization, Tempo-API-Key, X-API-Key')
846
+ return c.json(
847
+ Response.validated(schema.listFundingDepositAddresses.Response, {
848
+ data,
849
+ nextCursor: page.nextCursor,
850
+ }),
851
+ 200,
852
+ )
853
+ } catch (cause) {
854
+ return Response.upstream(c, cause)
855
+ }
856
+ },
857
+ )
649
858
  .get(
650
859
  '/v1/funding/deposit-addresses/:id{fda_[A-Za-z0-9_-]+}',
651
860
  Auth.policy({ apiKey: { scopes: ['funding:read'] } }),
@@ -782,14 +991,14 @@ export function depositAddresses(options: depositAddresses.Options = {}) {
782
991
  message: 'Check the query parameters and try again.',
783
992
  }),
784
993
  OpenApi.describeRoute({
785
- description: 'Lists deposits by deposit address or Tempo recipient, newest first.',
994
+ description: 'Lists visible deposits, optionally filtered, newest first.',
786
995
  operationId: 'listFundingDeposits',
787
996
  responses: OpenApi.responses({
788
997
  errors: {
789
998
  400: { codes: ['query_invalid'] },
790
999
  },
791
1000
  success: {
792
- description: 'A page of funding deposits matching the selected filter.',
1001
+ description: 'A page of visible funding deposits.',
793
1002
  example: fundingDepositsExample,
794
1003
  schema: schema.listFundingDeposits.Response,
795
1004
  },
@@ -809,32 +1018,59 @@ export function depositAddresses(options: depositAddresses.Options = {}) {
809
1018
  if (!owner) return Access.unauthorized(c)
810
1019
 
811
1020
  const db = Db.get(c.get('db'))
812
- const { cursor, depositAddress, limit, recipient } = c.req.valid('query')
1021
+ const parameters = c.req.valid('query')
1022
+ const { cursor, include, limit } = parameters
813
1023
  const decoded = cursor ? Cursor.decode(cursor, ['timestamp', 'id']) : undefined
814
1024
  try {
1025
+ const catalog =
1026
+ parameters.destinationToken !== undefined ||
1027
+ parameters.sourceChain !== undefined ||
1028
+ parameters.sourceToken !== undefined
1029
+ ? await FundingCatalog.snapshot(c)
1030
+ : undefined
1031
+ const filters = resolveDepositFilters({ catalog, parameters })
1032
+ // Counts are best-effort and share every ownership and filter predicate
1033
+ // with the page query. A failure omits `meta` rather than failing the page.
1034
+ const countPromise = include.includes('totalCount')
1035
+ ? FundingDeposits.count(db, {
1036
+ ...filters,
1037
+ cap: Schema.countCap,
1038
+ owner,
1039
+ }).catch(() => {
1040
+ c.set('fundingDepositCountFailed', true)
1041
+ return undefined
1042
+ })
1043
+ : undefined
815
1044
  const paging = {
1045
+ ...filters,
816
1046
  ...(decoded
817
1047
  ? { cursor: { createdAt: String(decoded[0]), id: String(decoded[1]) } }
818
1048
  : {}),
819
1049
  limit: limit + 1,
820
1050
  owner,
821
1051
  }
822
- const rows =
823
- depositAddress === undefined
824
- ? await FundingDeposits.listByRecipient(db, { ...paging, recipient: recipient! })
825
- : await FundingDeposits.listByDepositAddress(db, { ...paging, depositAddress })
1052
+ const rows = await FundingDeposits.list(db, paging)
826
1053
  const page = Cursor.paginate({
827
1054
  key: (record) => [record.createdAt, record.id],
828
1055
  limit,
829
1056
  rows,
830
1057
  })
831
1058
  const data = page.rows.map(FundingDeposit.toPublic)
1059
+ const count = await countPromise
832
1060
 
833
1061
  c.header('Cache-Control', Cache.policies.noStore)
834
1062
  c.header('Vary', 'Accept-Encoding, Authorization, Tempo-API-Key')
835
1063
  return c.json(
836
1064
  Response.validated(schema.listFundingDeposits.Response, {
837
1065
  data,
1066
+ ...(count === undefined
1067
+ ? {}
1068
+ : {
1069
+ meta: {
1070
+ totalCount: Math.min(count, Schema.countCap),
1071
+ totalCountCapped: count > Schema.countCap,
1072
+ },
1073
+ }),
838
1074
  nextCursor: page.nextCursor,
839
1075
  }),
840
1076
  200,
@@ -841,6 +841,7 @@ describe('funding transfers OpenAPI', () => {
841
841
  deposit: summarize(spec.paths[depositPath]!.get!),
842
842
  depositAddress: summarize(spec.paths[depositAddressPath]!.get!),
843
843
  depositAddressCreate: summarize(spec.paths['/v1/funding/deposit-addresses']!.post!),
844
+ depositAddressList: summarize(spec.paths['/v1/funding/deposit-addresses']!.get!),
844
845
  depositAddressReconcile: summarize(spec.paths[reconcilePath]!.post!),
845
846
  deposits: summarize(spec.paths['/v1/funding/deposits']!.get!),
846
847
  detail: summarize(spec.paths[detailPath]!.get!),
@@ -889,12 +890,16 @@ describe('funding transfers OpenAPI', () => {
889
890
  "description": "Creates or returns a reusable deposit address for funding a Tempo account.",
890
891
  "summary": "Create deposit address",
891
892
  },
893
+ "depositAddressList": {
894
+ "description": "Lists reusable funding deposit addresses, newest first.",
895
+ "summary": "List deposit addresses",
896
+ },
892
897
  "depositAddressReconcile": {
893
898
  "description": "Queues an immediate provider reconciliation for one reusable funding deposit address.",
894
899
  "summary": "Reconcile deposit address",
895
900
  },
896
901
  "deposits": {
897
- "description": "Lists deposits by deposit address or Tempo recipient, newest first.",
902
+ "description": "Lists visible deposits, optionally filtered, newest first.",
898
903
  "summary": "List deposits",
899
904
  },
900
905
  "detail": {
@@ -926,6 +931,7 @@ describe('funding transfers OpenAPI', () => {
926
931
  "Get quotes",
927
932
  "Get chains",
928
933
  "Get providers",
934
+ "List deposit addresses",
929
935
  "Create deposit address",
930
936
  "List transfers",
931
937
  "Create transfer",
@@ -1087,7 +1093,11 @@ describe('funding transfers OpenAPI', () => {
1087
1093
  const operation = spec.paths['/v1/funding/transfers']!.get
1088
1094
  const fields = Object.keys(operation.responses['200']!.content['application/json']!.example.data[0]!) // prettier-ignore
1089
1095
  expect(fields).toEqual([...fields].sort())
1090
- for (const route of [operation, spec.paths['/v1/funding/deposits']!.get])
1096
+ for (const route of [
1097
+ operation,
1098
+ spec.paths['/v1/funding/deposit-addresses']!.get,
1099
+ spec.paths['/v1/funding/deposits']!.get,
1100
+ ])
1091
1101
  for (const parameter of route.parameters ?? [])
1092
1102
  expect(parameter.example ?? parameter.schema?.examples, parameter.name).toBeDefined()
1093
1103
  })
@@ -1126,6 +1136,9 @@ describe('funding transfers OpenAPI', () => {
1126
1136
  depositAddressCreate: spec.paths['/v1/funding/deposit-addresses']?.post?.operationId,
1127
1137
  depositAddressCreateScopes:
1128
1138
  spec.paths['/v1/funding/deposit-addresses']?.post?.['x-required-scopes'],
1139
+ depositAddressList: spec.paths['/v1/funding/deposit-addresses']?.get.operationId,
1140
+ depositAddressListScopes:
1141
+ spec.paths['/v1/funding/deposit-addresses']?.get['x-required-scopes'],
1129
1142
  depositAddressReconcile: reconcilePath && spec.paths[reconcilePath]?.post?.operationId,
1130
1143
  depositAddressReconcileScopes:
1131
1144
  reconcilePath && spec.paths[reconcilePath]?.post?.['x-required-scopes'],
@@ -1150,6 +1163,8 @@ describe('funding transfers OpenAPI', () => {
1150
1163
  depositAddress: 'getFundingDepositAddress',
1151
1164
  depositAddressCreate: 'createFundingDepositAddress',
1152
1165
  depositAddressCreateScopes: ['funding:write'],
1166
+ depositAddressList: 'listFundingDepositAddresses',
1167
+ depositAddressListScopes: ['funding:read'],
1153
1168
  depositAddressReconcile: 'reconcileFundingDepositAddress',
1154
1169
  depositAddressReconcileScopes: ['funding:write'],
1155
1170
  depositAddressScopes: ['funding:read'],
package/src/db/Db.ts CHANGED
@@ -1362,6 +1362,42 @@ export const migrations: readonly Migration[] = [
1362
1362
  sql`DELETE FROM webhook_queue_events WHERE status = 'pending' AND attempting_at IS NULL AND expires_at <= to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')`,
1363
1363
  ),
1364
1364
  },
1365
+ {
1366
+ name: '0123_funding_deposit_addresses_owner_list_index',
1367
+ up: (db) =>
1368
+ db.schema
1369
+ .createIndex('funding_deposit_addresses_owner_list_idx')
1370
+ .ifNotExists()
1371
+ .on('fundingDepositAddresses')
1372
+ .columns(['orgId', 'environment', 'createdAt', 'id']),
1373
+ },
1374
+ {
1375
+ name: '0124_funding_deposit_addresses_project_list_index',
1376
+ up: (db) =>
1377
+ db.schema
1378
+ .createIndex('funding_deposit_addresses_project_list_idx')
1379
+ .ifNotExists()
1380
+ .on('fundingDepositAddresses')
1381
+ .columns(['orgId', 'environment', 'projectId', 'createdAt', 'id']),
1382
+ },
1383
+ {
1384
+ name: '0125_funding_deposits_owner_list_index',
1385
+ up: (db) =>
1386
+ db.schema
1387
+ .createIndex('funding_deposits_owner_list_idx')
1388
+ .ifNotExists()
1389
+ .on('fundingDeposits')
1390
+ .columns(['orgId', 'environment', 'createdAt', 'id']),
1391
+ },
1392
+ {
1393
+ name: '0126_funding_deposits_project_list_index',
1394
+ up: (db) =>
1395
+ db.schema
1396
+ .createIndex('funding_deposits_project_list_idx')
1397
+ .ifNotExists()
1398
+ .on('fundingDeposits')
1399
+ .columns(['orgId', 'environment', 'projectId', 'createdAt', 'id']),
1400
+ },
1365
1401
  ]
1366
1402
 
1367
1403
  /**
@@ -578,3 +578,31 @@ describe('renewPoll', () => {
578
578
  `)
579
579
  })
580
580
  })
581
+
582
+ describe('list', () => {
583
+ test('pages organization addresses and narrows project-attributed owners', async () => {
584
+ const listOwner = { environment: owner.environment, orgId: 'org_address_list' } as const
585
+ const first = await seed({ orgId: listOwner.orgId })
586
+ const project = await seed({ orgId: listOwner.orgId, projectId: 'prj_address_list' })
587
+ const sibling = await seed({ orgId: listOwner.orgId, projectId: 'prj_address_sibling' })
588
+ await seed({ orgId: 'org_address_list_foreign' })
589
+ await seed({ environment: 'sandbox', orgId: listOwner.orgId })
590
+
591
+ const page = await FundingDepositAddresses.list(db, { limit: 2, owner: listOwner })
592
+ expect(page.map((record) => record.id)).toEqual([sibling.id, project.id])
593
+ const rest = await FundingDepositAddresses.list(db, {
594
+ cursor: { createdAt: project.createdAt, id: project.id },
595
+ limit: 2,
596
+ owner: listOwner,
597
+ })
598
+ expect(rest.map((record) => record.id)).toEqual([first.id])
599
+ expect(
600
+ (
601
+ await FundingDepositAddresses.list(db, {
602
+ limit: 10,
603
+ owner: { ...listOwner, projectId: 'prj_address_list' },
604
+ })
605
+ ).map((record) => record.id),
606
+ ).toEqual([project.id])
607
+ })
608
+ })
@@ -148,6 +148,45 @@ export function getOwned(db: Db.Db, owner: Owner, id: string): Promise<Record |
148
148
  return query.executeTakeFirst()
149
149
  }
150
150
 
151
+ /** Lists owner-visible funding deposit addresses newest-first. */
152
+ export function list(db: Db.Db, options: list.Options): Promise<Record[]> {
153
+ const { cursor, limit, owner } = options
154
+ let query = db.kysely
155
+ .selectFrom('funding_deposit_addresses')
156
+ .selectAll()
157
+ .where('environment', '=', owner.environment)
158
+ .where('orgId', '=', owner.orgId)
159
+ if (owner.projectId !== undefined) query = query.where('projectId', '=', owner.projectId)
160
+ if (cursor)
161
+ query = query.where((eb) =>
162
+ eb.or([
163
+ eb('createdAt', '<', cursor.createdAt),
164
+ eb.and([eb('createdAt', '=', cursor.createdAt), eb('id', '<', cursor.id)]),
165
+ ]),
166
+ )
167
+ return query.orderBy('createdAt', 'desc').orderBy('id', 'desc').limit(limit).execute()
168
+ }
169
+
170
+ export declare namespace list {
171
+ /** Ownership and paging options for funding deposit addresses. */
172
+ type Options = {
173
+ /** Exclusive lower bound from the previous page. */
174
+ cursor?: Cursor | undefined
175
+ /** Maximum rows to return. */
176
+ limit: number
177
+ /** Ownership filter derived from the authenticated API key. */
178
+ owner: Owner
179
+ }
180
+
181
+ /** Cursor fields for the last address returned by the previous page. */
182
+ type Cursor = {
183
+ /** Deposit address creation time. */
184
+ createdAt: string
185
+ /** Deposit address id, used as a deterministic tie-breaker. */
186
+ id: string
187
+ }
188
+ }
189
+
151
190
  /** Gets the reusable address that matches one owner and route identity. */
152
191
  export function getMatching(db: Db.Db, match: Match): Promise<Record | undefined> {
153
192
  let query = db.kysely
@@ -498,3 +498,85 @@ describe('toPublic', () => {
498
498
  })
499
499
  })
500
500
  })
501
+
502
+ describe('list', () => {
503
+ test('pages organization deposits and narrows project-attributed owners', async () => {
504
+ const listOwner = { environment: owner.environment, orgId: 'org_deposit_list' } as const
505
+ const firstAddress = await seedAddress({ orgId: listOwner.orgId })
506
+ const projectAddress = await seedAddress({
507
+ orgId: listOwner.orgId,
508
+ projectId: 'prj_deposit_list',
509
+ })
510
+ const siblingAddress = await seedAddress({
511
+ orgId: listOwner.orgId,
512
+ projectId: 'prj_deposit_sibling',
513
+ })
514
+ const first = await seedDeposit(firstAddress)
515
+ const project = await seedDeposit(projectAddress)
516
+ const sibling = await seedDeposit(siblingAddress)
517
+ const foreignAddress = await seedAddress({ orgId: 'org_deposit_list_foreign' })
518
+ await seedDeposit(foreignAddress)
519
+ const sandboxAddress = await seedAddress({
520
+ environment: 'sandbox',
521
+ orgId: listOwner.orgId,
522
+ })
523
+ await seedDeposit(sandboxAddress)
524
+
525
+ const page = await FundingDeposits.list(db, { limit: 2, owner: listOwner })
526
+ expect(page.map((record) => record.id)).toEqual([sibling.id, project.id])
527
+ const rest = await FundingDeposits.list(db, {
528
+ cursor: { createdAt: project.createdAt, id: project.id },
529
+ limit: 2,
530
+ owner: listOwner,
531
+ })
532
+ expect(rest.map((record) => record.id)).toEqual([first.id])
533
+ expect(
534
+ (
535
+ await FundingDeposits.list(db, {
536
+ limit: 10,
537
+ owner: { ...listOwner, projectId: 'prj_deposit_list' },
538
+ })
539
+ ).map((record) => record.id),
540
+ ).toEqual([project.id])
541
+ })
542
+
543
+ test('AND-combines filters and counts through the same ownership scope', async () => {
544
+ const listOwner = { environment: owner.environment, orgId: 'org_deposit_filters' } as const
545
+ const recipient = `0x${'ab'.repeat(20)}`
546
+ const address = await seedAddress({
547
+ orgId: listOwner.orgId,
548
+ snapshot: TestFunding.depositAddressSnapshot({
549
+ provider: { id: 'relay_filters', name: 'Relay' },
550
+ recipient,
551
+ }),
552
+ })
553
+ const match = await seedDeposit(address)
554
+ const unrelatedAddress = await seedAddress({ orgId: listOwner.orgId })
555
+ await seedDeposit(unrelatedAddress)
556
+ const filters = {
557
+ depositAddress: address.address,
558
+ destinationTokenKeys: [address.destinationTokenKey],
559
+ providerId: address.providerId,
560
+ recipient: recipient.toLowerCase(),
561
+ sourceChainIds: [address.sourceChainId],
562
+ sourceTokenKeys: [address.sourceTokenKey],
563
+ status: 'detected',
564
+ } as const
565
+
566
+ expect(
567
+ (await FundingDeposits.list(db, { ...filters, limit: 10, owner: listOwner })).map(
568
+ (record) => record.id,
569
+ ),
570
+ ).toEqual([match.id])
571
+ expect(await FundingDeposits.count(db, { ...filters, cap: 10, owner: listOwner })).toBe(1)
572
+ expect(await FundingDeposits.count(db, { ...filters, cap: 0, owner: listOwner })).toBe(1)
573
+ expect(
574
+ await FundingDeposits.count(db, {
575
+ ...filters,
576
+ destinationTokenKeys: [],
577
+ cap: 10,
578
+ owner: listOwner,
579
+ }),
580
+ ).toBe(0)
581
+ })
582
+ })