tapimo 0.8.0 → 0.9.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 (80) hide show
  1. package/dist/App.d.ts +2095 -2111
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +0 -1
  4. package/dist/App.js.map +1 -1
  5. package/dist/admin/apps/verified-tokens.d.ts +166 -166
  6. package/dist/apps/data/App.d.ts +4084 -3845
  7. package/dist/apps/data/App.d.ts.map +1 -1
  8. package/dist/apps/data/routes/activities.d.ts +270 -270
  9. package/dist/apps/data/routes/addresses.d.ts +248 -17
  10. package/dist/apps/data/routes/addresses.d.ts.map +1 -1
  11. package/dist/apps/data/routes/balances.d.ts +272 -2
  12. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  13. package/dist/apps/data/routes/balances.js +114 -2
  14. package/dist/apps/data/routes/balances.js.map +1 -1
  15. package/dist/apps/data/routes/blocks.d.ts +123 -123
  16. package/dist/apps/data/routes/earn.d.ts +65 -3
  17. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  18. package/dist/apps/data/routes/earn.js +259 -14
  19. package/dist/apps/data/routes/earn.js.map +1 -1
  20. package/dist/apps/data/routes/rpc.d.ts +31 -31
  21. package/dist/apps/data/routes/tokens.d.ts +454 -454
  22. package/dist/apps/data/routes/transfers.d.ts +81 -81
  23. package/dist/apps/funding/App.d.ts +40 -299
  24. package/dist/apps/funding/App.d.ts.map +1 -1
  25. package/dist/apps/funding/App.js +20 -5
  26. package/dist/apps/funding/App.js.map +1 -1
  27. package/dist/apps/funding/routes/deposit-addresses.d.ts +43 -434
  28. package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
  29. package/dist/apps/funding/routes/deposit-addresses.js +21 -103
  30. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
  31. package/dist/apps/funding/routes/quotes.d.ts +89 -89
  32. package/dist/apps/funding/routes/transfers.js +5 -5
  33. package/dist/apps/funding/routes/transfers.js.map +1 -1
  34. package/dist/apps/management/routes/invitations.d.ts +134 -134
  35. package/dist/apps/management/routes/orgs.d.ts +98 -98
  36. package/dist/apps/management/routes/verified-token-requests.d.ts +83 -83
  37. package/dist/apps/mpp/App.d.ts +44 -44
  38. package/dist/db/Db.d.ts.map +1 -1
  39. package/dist/db/Db.js +8 -0
  40. package/dist/db/Db.js.map +1 -1
  41. package/dist/db/tables/fundingDeposits.d.ts +18 -1
  42. package/dist/db/tables/fundingDeposits.d.ts.map +1 -1
  43. package/dist/db/tables/fundingDeposits.js +46 -2
  44. package/dist/db/tables/fundingDeposits.js.map +1 -1
  45. package/dist/internal/Auth.d.ts +4 -0
  46. package/dist/internal/Auth.d.ts.map +1 -1
  47. package/dist/internal/Auth.js +11 -2
  48. package/dist/internal/Auth.js.map +1 -1
  49. package/dist/internal/funding/Provider.d.ts +6 -6
  50. package/dist/internal/funding/Provider.d.ts.map +1 -1
  51. package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
  52. package/dist/internal/funding/Reconciliation.js +101 -16
  53. package/dist/internal/funding/Reconciliation.js.map +1 -1
  54. package/dist/internal/funding/providers/relay.d.ts.map +1 -1
  55. package/dist/internal/funding/providers/relay.js +6 -2
  56. package/dist/internal/funding/providers/relay.js.map +1 -1
  57. package/package.json +1 -1
  58. package/src/App.test.ts +32 -0
  59. package/src/App.ts +4 -1
  60. package/src/Client.test-d.ts +56 -33
  61. package/src/apps/composition.test.ts +39 -10
  62. package/src/apps/data/routes/balances.test.ts +42 -0
  63. package/src/apps/data/routes/balances.ts +218 -85
  64. package/src/apps/data/routes/earn.test.ts +89 -9
  65. package/src/apps/data/routes/earn.ts +355 -14
  66. package/src/apps/funding/App.ts +20 -5
  67. package/src/apps/funding/routes/deposit-addresses.test.ts +43 -22
  68. package/src/apps/funding/routes/deposit-addresses.ts +34 -117
  69. package/src/apps/funding/routes/transfers.test.ts +6 -25
  70. package/src/apps/funding/routes/transfers.ts +5 -5
  71. package/src/db/Db.ts +9 -0
  72. package/src/db/tables/fundingDeposits.test.ts +101 -0
  73. package/src/db/tables/fundingDeposits.ts +71 -2
  74. package/src/internal/Auth.test.ts +28 -0
  75. package/src/internal/Auth.ts +15 -1
  76. package/src/internal/funding/Provider.ts +4 -4
  77. package/src/internal/funding/Reconciliation.test.ts +292 -54
  78. package/src/internal/funding/Reconciliation.ts +160 -53
  79. package/src/internal/funding/SourceObservation.test.ts +2 -2
  80. package/src/internal/funding/providers/relay.ts +7 -2
@@ -1152,10 +1152,16 @@ describe('POST /funding/deposit-addresses/:id/reconcile', () => {
1152
1152
  })
1153
1153
  })
1154
1154
 
1155
- describe('GET /funding/deposit-addresses/:id/deposits', () => {
1155
+ describe('GET /funding/deposits?depositAddress', () => {
1156
1156
  test('pages deposits newest-first under an owned address', async () => {
1157
1157
  const app = transfersApp()
1158
- const address = await seedDepositAddress()
1158
+ const address = await seedDepositAddress({
1159
+ snapshot: TestFunding.depositAddressSnapshot({
1160
+ address: 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE',
1161
+ provider: { id: 'relay_list_deposit_address', name: 'Relay' },
1162
+ recipient: `0x${'41'.repeat(20)}`,
1163
+ }),
1164
+ })
1159
1165
  const first = await seedDeposit(address)
1160
1166
  const second = await seedDeposit(address)
1161
1167
  const third = await seedDeposit(address)
@@ -1164,7 +1170,7 @@ describe('GET /funding/deposit-addresses/:id/deposits', () => {
1164
1170
  const sixth = await seedDeposit(address)
1165
1171
 
1166
1172
  const response = await app.request(
1167
- `/v1/funding/deposit-addresses/${address.id}/deposits?limit=5`,
1173
+ `/v1/funding/deposits?depositAddress=${address.address}&limit=5`,
1168
1174
  as(fundingReader),
1169
1175
  )
1170
1176
  expect(response.status).toBe(200)
@@ -1177,10 +1183,7 @@ describe('GET /funding/deposit-addresses/:id/deposits', () => {
1177
1183
  "etag": null,
1178
1184
  }
1179
1185
  `)
1180
- const body = await TestApp.json(
1181
- response,
1182
- Funding.schema.listFundingDepositAddressDeposits.Response,
1183
- )
1186
+ const body = await TestApp.json(response, Funding.schema.listFundingDeposits.Response)
1184
1187
  expect(body.data.map((deposit) => deposit.id)).toEqual([
1185
1188
  sixth.id,
1186
1189
  fifth.id,
@@ -1192,10 +1195,10 @@ describe('GET /funding/deposit-addresses/:id/deposits', () => {
1192
1195
 
1193
1196
  const next = await TestApp.json(
1194
1197
  await app.request(
1195
- `/v1/funding/deposit-addresses/${address.id}/deposits?limit=5&cursor=${body.nextCursor}`,
1198
+ `/v1/funding/deposits?depositAddress=${address.address}&limit=5&cursor=${body.nextCursor}`,
1196
1199
  as(fundingReader),
1197
1200
  ),
1198
- Funding.schema.listFundingDepositAddressDeposits.Response,
1201
+ Funding.schema.listFundingDeposits.Response,
1199
1202
  )
1200
1203
  expect(next.data.map((deposit) => deposit.id)).toEqual([first.id])
1201
1204
  expect(next.nextCursor).toBeNull()
@@ -1205,6 +1208,7 @@ describe('GET /funding/deposit-addresses/:id/deposits', () => {
1205
1208
  const app = transfersApp()
1206
1209
  const address = await seedDepositAddress({
1207
1210
  snapshot: TestFunding.depositAddressSnapshot({
1211
+ address: 'TF17BgPaZYbz8oxbjhriubPDsA7ArKoLX3',
1208
1212
  provider: { id: 'relay_observed', name: 'Relay' },
1209
1213
  recipient: `0x${'31'.repeat(20)}`,
1210
1214
  }),
@@ -1228,14 +1232,11 @@ describe('GET /funding/deposit-addresses/:id/deposits', () => {
1228
1232
  })
1229
1233
 
1230
1234
  const response = await app.request(
1231
- `/v1/funding/deposit-addresses/${address.id}/deposits`,
1235
+ `/v1/funding/deposits?depositAddress=${address.address}`,
1232
1236
  as(fundingReader),
1233
1237
  )
1234
1238
  expect(response.status).toBe(200)
1235
- const body = await TestApp.json(
1236
- response,
1237
- Funding.schema.listFundingDepositAddressDeposits.Response,
1238
- )
1239
+ const body = await TestApp.json(response, Funding.schema.listFundingDeposits.Response)
1239
1240
  expect(body).toMatchInlineSnapshot(
1240
1241
  {
1241
1242
  data: [
@@ -1305,21 +1306,30 @@ describe('GET /funding/deposit-addresses/:id/deposits', () => {
1305
1306
  )
1306
1307
  })
1307
1308
 
1308
- test('returns the parent not-found error before listing deposits', async () => {
1309
+ test('hides a foreign deposit address', async () => {
1309
1310
  const app = transfersApp()
1310
- const address = await seedDepositAddress({ orgId: fundingForeign.orgId })
1311
+ const address = await seedDepositAddress({
1312
+ orgId: fundingForeign.orgId,
1313
+ snapshot: TestFunding.depositAddressSnapshot({
1314
+ address: 'TWd4WrZ9wn84f5x1hZhL4DHvk738ns5jwb',
1315
+ provider: { id: 'relay_foreign_deposit_address', name: 'Relay' },
1316
+ recipient: `0x${'42'.repeat(20)}`,
1317
+ }),
1318
+ })
1319
+ await seedDeposit(address)
1311
1320
  const response = await app.request(
1312
- `/v1/funding/deposit-addresses/${address.id}/deposits`,
1321
+ `/v1/funding/deposits?depositAddress=${address.address}`,
1313
1322
  as(fundingReader),
1314
1323
  )
1315
- expect(response.status).toBe(404)
1316
- expect(((await response.json()) as { error: { code: string } }).error.code).toBe(
1317
- 'funding_deposit_address_not_found',
1318
- )
1324
+ expect(response.status).toBe(200)
1325
+ expect(await TestApp.json(response, Funding.schema.listFundingDeposits.Response)).toEqual({
1326
+ data: [],
1327
+ nextCursor: null,
1328
+ })
1319
1329
  })
1320
1330
  })
1321
1331
 
1322
- describe('GET /funding/deposits', () => {
1332
+ describe('GET /funding/deposits?recipient', () => {
1323
1333
  test('requires funding:read and pages recipient deposits across addresses', async () => {
1324
1334
  const app = transfersApp()
1325
1335
  const recipient = '0xABcdEFABcdEFabcdEfAbCdefabcdeFABcDEFabCD'
@@ -1327,6 +1337,17 @@ describe('GET /funding/deposits', () => {
1327
1337
  expect((await app.request(path)).status).toBe(401)
1328
1338
  expect((await app.request(path, as(TestApp.key))).status).toBe(403)
1329
1339
  expect((await app.request('/v1/funding/deposits', as(fundingReader))).status).toBe(400)
1340
+ expect(
1341
+ (await app.request('/v1/funding/deposits?depositAddress=', as(fundingReader))).status,
1342
+ ).toBe(400)
1343
+ expect(
1344
+ (
1345
+ await app.request(
1346
+ `${path}&depositAddress=TJRabPrwbZy45sbavfcjinPJC18kjpRTv8`,
1347
+ as(fundingReader),
1348
+ )
1349
+ ).status,
1350
+ ).toBe(400)
1330
1351
 
1331
1352
  const firstAddress = await seedDepositAddress({
1332
1353
  snapshot: TestFunding.depositAddressSnapshot({
@@ -132,35 +132,36 @@ export namespace schema {
132
132
  export const Params = getFundingDepositAddress.Params
133
133
  }
134
134
 
135
- /** Schemas for the listFundingDepositAddressDeposits operation. */
136
- export namespace listFundingDepositAddressDeposits {
137
- /** Path parameters for listing one address's deposits. */
138
- export const Params = getFundingDepositAddress.Params
139
-
140
- /** Query parameters for listing one address's deposits. */
141
- export const Query = z
142
- .object({ cursor: Schema.Cursor, limit: Schema.Limit })
143
- .check(z.describe('Paging parameters for one funding deposit address.'))
144
-
145
- /** One page of detected deposits. */
146
- export const Response = z
147
- .object({
148
- data: z.array(Deposit).check(z.describe('Detected deposits on this page, newest first.')),
149
- nextCursor: Schema.NextCursor,
150
- })
151
- .check(z.describe('A page of deposits detected for one funding deposit address.'))
152
- }
153
-
154
135
  /** Schemas for the listFundingDeposits operation. */
155
136
  export namespace listFundingDeposits {
156
- /** Query parameters for listing one account's funding deposits. */
137
+ /** Query parameters for listing funding deposits. */
157
138
  export const Query = z
158
139
  .object({
140
+ depositAddress: z
141
+ .optional(z.string().check(z.minLength(1)))
142
+ .check(
143
+ z.describe('Reusable source-chain deposit address that received the funds.'),
144
+ z.meta({ examples: ['TJRabPrwbZy45sbavfcjinPJC18kjpRTv8'] }),
145
+ ),
159
146
  cursor: Schema.Cursor,
160
147
  limit: Schema.Limit,
161
- recipient: Schema.Address.check(z.describe('Tempo account that received the deposits.')),
148
+ recipient: z
149
+ .optional(Schema.Address)
150
+ .check(
151
+ z.describe('Tempo account that received the deposits.'),
152
+ z.meta({ examples: [`0x${'11'.repeat(20)}`] }),
153
+ ),
162
154
  })
163
- .check(z.describe('Recipient filter and paging parameters for funding deposits.'))
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
+ )
164
165
 
165
166
  /** One page of detected deposits. */
166
167
  export const Response = z
@@ -168,7 +169,7 @@ export namespace schema {
168
169
  data: z.array(Deposit).check(z.describe('Detected deposits on this page, newest first.')),
169
170
  nextCursor: Schema.NextCursor,
170
171
  })
171
- .check(z.describe('A page of deposits received by one Tempo account.'))
172
+ .check(z.describe('A page of funding deposits matching the selected filter.'))
172
173
  }
173
174
 
174
175
  /** Schemas for the getFundingDeposit operation. */
@@ -273,11 +274,6 @@ const fundingDepositExample = {
273
274
  updatedAt: '2026-08-04T00:01:00.000Z',
274
275
  } satisfies z.output<typeof schema.Deposit>
275
276
 
276
- const fundingDepositAddressDepositsExample = {
277
- data: [fundingDepositExample],
278
- nextCursor: null,
279
- } satisfies z.output<typeof schema.listFundingDepositAddressDeposits.Response>
280
-
281
277
  const fundingDepositsExample = {
282
278
  data: [fundingDepositExample],
283
279
  nextCursor: null,
@@ -650,88 +646,6 @@ export function depositAddresses(options: depositAddresses.Options = {}) {
650
646
  }
651
647
  },
652
648
  )
653
- .get(
654
- '/v1/funding/deposit-addresses/:id{fda_[A-Za-z0-9_-]+}/deposits',
655
- Auth.policy({ apiKey: { scopes: ['funding:read'] } }),
656
- OpenApi.validate('param', schema.listFundingDepositAddressDeposits.Params, {
657
- code: 'param_invalid',
658
- message: 'Check the path parameters and try again.',
659
- }),
660
- OpenApi.validate('query', schema.listFundingDepositAddressDeposits.Query, {
661
- code: 'query_invalid',
662
- message: 'Check the query parameters and try again.',
663
- }),
664
- OpenApi.describeRoute({
665
- description: 'Lists deposits detected for one funding deposit address, newest first.',
666
- operationId: 'listFundingDepositAddressDeposits',
667
- responses: OpenApi.responses({
668
- errors: {
669
- 400: { codes: ['param_invalid', 'query_invalid'] },
670
- 404: {
671
- codes: ['funding_deposit_address_not_found'],
672
- description: 'No visible funding deposit address exists for the identifier.',
673
- },
674
- },
675
- success: {
676
- description: 'A page of deposits detected for the address.',
677
- example: fundingDepositAddressDepositsExample,
678
- schema: schema.listFundingDepositAddressDeposits.Response,
679
- },
680
- }),
681
- summary: 'List deposits by deposit address',
682
- tags: ['Deposit Addresses'],
683
- }),
684
- async (c) => {
685
- if (Auth.narrowAccess) return Auth.accessError(c)
686
- if (OpenApi.narrowValidation)
687
- return OpenApi.validationError(c, {
688
- code: 'query_invalid',
689
- message: 'Check the request parameters and try again.',
690
- })
691
-
692
- const owner = Access.owner(c)
693
- if (!owner) return Access.unauthorized(c)
694
-
695
- const db = Db.get(c.get('db'))
696
- const { id } = c.req.valid('param')
697
- const { cursor, limit } = c.req.valid('query')
698
- const decoded = cursor ? Cursor.decode(cursor, ['timestamp', 'id']) : undefined
699
- try {
700
- const address = await FundingDepositAddresses.getOwned(db, owner, id)
701
- if (!address)
702
- return Response.error(c, {
703
- code: 'funding_deposit_address_not_found',
704
- message: 'Funding deposit address not found',
705
- status: 404,
706
- })
707
-
708
- const rows = await FundingDeposits.listByAddress(db, owner, id, {
709
- ...(decoded
710
- ? { cursor: { createdAt: String(decoded[0]), id: String(decoded[1]) } }
711
- : {}),
712
- limit: limit + 1,
713
- })
714
- const page = Cursor.paginate({
715
- key: (record) => [record.createdAt, record.id],
716
- limit,
717
- rows,
718
- })
719
- const data = page.rows.map(FundingDeposit.toPublic)
720
-
721
- c.header('Cache-Control', Cache.policies.noStore)
722
- c.header('Vary', 'Accept-Encoding, Authorization, Tempo-API-Key, X-API-Key')
723
- return c.json(
724
- Response.validated(schema.listFundingDepositAddressDeposits.Response, {
725
- data,
726
- nextCursor: page.nextCursor,
727
- }),
728
- 200,
729
- )
730
- } catch (cause) {
731
- return Response.upstream(c, cause)
732
- }
733
- },
734
- )
735
649
  .get(
736
650
  '/v1/funding/deposit-addresses/:id{fda_[A-Za-z0-9_-]+}',
737
651
  Auth.policy({ apiKey: { scopes: ['funding:read'] } }),
@@ -868,19 +782,19 @@ export function depositAddresses(options: depositAddresses.Options = {}) {
868
782
  message: 'Check the query parameters and try again.',
869
783
  }),
870
784
  OpenApi.describeRoute({
871
- description: 'Lists deposits received by one Tempo account, newest first.',
785
+ description: 'Lists deposits by deposit address or Tempo recipient, newest first.',
872
786
  operationId: 'listFundingDeposits',
873
787
  responses: OpenApi.responses({
874
788
  errors: {
875
789
  400: { codes: ['query_invalid'] },
876
790
  },
877
791
  success: {
878
- description: 'A page of funding deposits received by the account.',
792
+ description: 'A page of funding deposits matching the selected filter.',
879
793
  example: fundingDepositsExample,
880
794
  schema: schema.listFundingDeposits.Response,
881
795
  },
882
796
  }),
883
- summary: 'List deposits by recipient',
797
+ summary: 'List deposits',
884
798
  tags: ['Deposit Addresses'],
885
799
  }),
886
800
  async (c) => {
@@ -895,17 +809,20 @@ export function depositAddresses(options: depositAddresses.Options = {}) {
895
809
  if (!owner) return Access.unauthorized(c)
896
810
 
897
811
  const db = Db.get(c.get('db'))
898
- const { cursor, limit, recipient } = c.req.valid('query')
812
+ const { cursor, depositAddress, limit, recipient } = c.req.valid('query')
899
813
  const decoded = cursor ? Cursor.decode(cursor, ['timestamp', 'id']) : undefined
900
814
  try {
901
- const rows = await FundingDeposits.listByRecipient(db, {
815
+ const paging = {
902
816
  ...(decoded
903
817
  ? { cursor: { createdAt: String(decoded[0]), id: String(decoded[1]) } }
904
818
  : {}),
905
819
  limit: limit + 1,
906
820
  owner,
907
- recipient,
908
- })
821
+ }
822
+ const rows =
823
+ depositAddress === undefined
824
+ ? await FundingDeposits.listByRecipient(db, { ...paging, recipient: recipient! })
825
+ : await FundingDeposits.listByDepositAddress(db, { ...paging, depositAddress })
909
826
  const page = Cursor.paginate({
910
827
  key: (record) => [record.createdAt, record.id],
911
828
  limit,
@@ -830,9 +830,6 @@ describe('funding transfers OpenAPI', () => {
830
830
  !path.endsWith('/deposits') &&
831
831
  !path.endsWith('/reconcile'),
832
832
  )!
833
- const depositAddressDepositsPath = Object.keys(spec.paths).find(
834
- (path) => path.includes('/v1/funding/deposit-addresses/{') && path.endsWith('/deposits'),
835
- )!
836
833
  const depositPath = Object.keys(spec.paths).find((path) =>
837
834
  path.includes('/v1/funding/deposits/{'),
838
835
  )!
@@ -847,7 +844,6 @@ describe('funding transfers OpenAPI', () => {
847
844
  deposit: summarize(spec.paths[depositPath]!.get!),
848
845
  depositAddress: summarize(spec.paths[depositAddressPath]!.get!),
849
846
  depositAddressCreate: summarize(spec.paths['/v1/funding/deposit-addresses']!.post!),
850
- depositAddressDeposits: summarize(spec.paths[depositAddressDepositsPath]!.get!),
851
847
  depositAddressReconcile: summarize(spec.paths[reconcilePath]!.post!),
852
848
  deposits: summarize(spec.paths['/v1/funding/deposits']!.get!),
853
849
  detail: summarize(spec.paths[detailPath]!.get!),
@@ -896,17 +892,13 @@ describe('funding transfers OpenAPI', () => {
896
892
  "description": "Creates or returns a reusable deposit address for funding a Tempo account.",
897
893
  "summary": "Create deposit address",
898
894
  },
899
- "depositAddressDeposits": {
900
- "description": "Lists deposits detected for one funding deposit address, newest first.",
901
- "summary": "List deposits by deposit address",
902
- },
903
895
  "depositAddressReconcile": {
904
896
  "description": "Queues an immediate provider reconciliation for one reusable funding deposit address.",
905
897
  "summary": "Reconcile deposit address",
906
898
  },
907
899
  "deposits": {
908
- "description": "Lists deposits received by one Tempo account, newest first.",
909
- "summary": "List deposits by recipient",
900
+ "description": "Lists deposits by deposit address or Tempo recipient, newest first.",
901
+ "summary": "List deposits",
910
902
  },
911
903
  "detail": {
912
904
  "description": "Returns a transfer by ID.",
@@ -934,7 +926,6 @@ describe('funding transfers OpenAPI', () => {
934
926
  },
935
927
  },
936
928
  "sidebar": [
937
- "List transfers",
938
929
  "Get quotes",
939
930
  "Get chains",
940
931
  "Get providers",
@@ -947,8 +938,7 @@ describe('funding transfers OpenAPI', () => {
947
938
  "Get deposit address",
948
939
  "Get transfer",
949
940
  "Get deposit",
950
- "List deposits by deposit address",
951
- "List deposits by recipient",
941
+ "List deposits",
952
942
  ],
953
943
  }
954
944
  `)
@@ -1100,8 +1090,9 @@ describe('funding transfers OpenAPI', () => {
1100
1090
  const operation = spec.paths['/v1/funding/transfers']!.get
1101
1091
  const fields = Object.keys(operation.responses['200']!.content['application/json']!.example.data[0]!) // prettier-ignore
1102
1092
  expect(fields).toEqual([...fields].sort())
1103
- for (const parameter of operation.parameters ?? [])
1104
- expect(parameter.example ?? parameter.schema?.examples, parameter.name).toBeDefined()
1093
+ for (const route of [operation, spec.paths['/v1/funding/deposits']!.get])
1094
+ for (const parameter of route.parameters ?? [])
1095
+ expect(parameter.example ?? parameter.schema?.examples, parameter.name).toBeDefined()
1105
1096
  })
1106
1097
 
1107
1098
  test('publishes operation ids and required scopes', async () => {
@@ -1128,9 +1119,6 @@ describe('funding transfers OpenAPI', () => {
1128
1119
  !path.endsWith('/deposits') &&
1129
1120
  !path.endsWith('/reconcile'),
1130
1121
  )
1131
- const depositAddressDepositsPath = Object.keys(spec.paths).find(
1132
- (path) => path.includes('/v1/funding/deposit-addresses/{') && path.endsWith('/deposits'),
1133
- )
1134
1122
  const depositPath = Object.keys(spec.paths).find((path) =>
1135
1123
  path.includes('/v1/funding/deposits/{'),
1136
1124
  )
@@ -1141,11 +1129,6 @@ describe('funding transfers OpenAPI', () => {
1141
1129
  depositAddressCreate: spec.paths['/v1/funding/deposit-addresses']?.post?.operationId,
1142
1130
  depositAddressCreateScopes:
1143
1131
  spec.paths['/v1/funding/deposit-addresses']?.post?.['x-required-scopes'],
1144
- depositAddressDeposits:
1145
- depositAddressDepositsPath && spec.paths[depositAddressDepositsPath]?.get.operationId,
1146
- depositAddressDepositsScopes:
1147
- depositAddressDepositsPath &&
1148
- spec.paths[depositAddressDepositsPath]?.get['x-required-scopes'],
1149
1132
  depositAddressReconcile: reconcilePath && spec.paths[reconcilePath]?.post?.operationId,
1150
1133
  depositAddressReconcileScopes:
1151
1134
  reconcilePath && spec.paths[reconcilePath]?.post?.['x-required-scopes'],
@@ -1170,8 +1153,6 @@ describe('funding transfers OpenAPI', () => {
1170
1153
  depositAddress: 'getFundingDepositAddress',
1171
1154
  depositAddressCreate: 'createFundingDepositAddress',
1172
1155
  depositAddressCreateScopes: ['funding:write'],
1173
- depositAddressDeposits: 'listFundingDepositAddressDeposits',
1174
- depositAddressDepositsScopes: ['funding:read'],
1175
1156
  depositAddressReconcile: 'reconcileFundingDepositAddress',
1176
1157
  depositAddressReconcileScopes: ['funding:write'],
1177
1158
  depositAddressScopes: ['funding:read'],
@@ -290,7 +290,7 @@ export function transfers() {
290
290
  },
291
291
  }),
292
292
  summary: 'List transfers',
293
- tags: ['Transfers'],
293
+ tags: ['Funding Transfers'],
294
294
  }),
295
295
  fundingEtag,
296
296
  async (c) => {
@@ -373,7 +373,7 @@ export function transfers() {
373
373
  },
374
374
  }),
375
375
  summary: 'Get transfer',
376
- tags: ['Transfers'],
376
+ tags: ['Funding Transfers'],
377
377
  }),
378
378
  async (c) => {
379
379
  if (Auth.narrowAccess) return Auth.accessError(c)
@@ -448,7 +448,7 @@ export function transfers() {
448
448
  },
449
449
  }),
450
450
  summary: 'Create transfer',
451
- tags: ['Transfers'],
451
+ tags: ['Funding Transfers'],
452
452
  }),
453
453
  async (c) => {
454
454
  if (Auth.narrowAccess) return Auth.accessError(c)
@@ -637,7 +637,7 @@ export function transfers() {
637
637
  operationId: 'createFundingTransferVault',
638
638
  responses: unimplementedFundingTransferResponses,
639
639
  summary: 'Create transfer into vault',
640
- tags: ['Transfers'],
640
+ tags: ['Funding Transfers'],
641
641
  }),
642
642
  (c) => {
643
643
  if (Auth.narrowAccess) return Auth.accessError(c)
@@ -671,7 +671,7 @@ export function transfers() {
671
671
  operationId: 'createFundingTransferZone',
672
672
  responses: unimplementedFundingTransferResponses,
673
673
  summary: 'Create transfer into zone',
674
- tags: ['Transfers'],
674
+ tags: ['Funding Transfers'],
675
675
  }),
676
676
  (c) => {
677
677
  if (Auth.narrowAccess) return Auth.accessError(c)
package/src/db/Db.ts CHANGED
@@ -1271,6 +1271,15 @@ export const migrations: readonly Migration[] = [
1271
1271
  sql`ALTER TABLE funding_deposits DROP CONSTRAINT IF EXISTS funding_deposits_detection_trigger_check, ADD CONSTRAINT funding_deposits_detection_trigger_check CHECK (detection_trigger IN ('chain', 'manual', 'poll', 'webhook'))`,
1272
1272
  ),
1273
1273
  },
1274
+ {
1275
+ name: '0114_funding_deposit_addresses_owner_address_index',
1276
+ up: (db) =>
1277
+ db.schema
1278
+ .createIndex('funding_deposit_addresses_owner_address_idx')
1279
+ .ifNotExists()
1280
+ .on('fundingDepositAddresses')
1281
+ .columns(['orgId', 'environment', 'address', 'projectId']),
1282
+ },
1274
1283
  ]
1275
1284
 
1276
1285
  /**
@@ -77,6 +77,107 @@ describe('listByAddress', () => {
77
77
  })
78
78
  })
79
79
 
80
+ describe('listByDepositAddress', () => {
81
+ const seedAddressDeposit = (address: Awaited<ReturnType<typeof seedAddress>>, index: number) => {
82
+ const sourceTransactionHash = `fe${index.toString(16).padStart(62, '0')}`
83
+ return Deposit.create(db, {
84
+ now: new Date(Date.UTC(2026, 1, 2, 0, 0, index)),
85
+ providerRequestId: `relay_deposit_address_request_${index}`,
86
+ snapshot: TestFunding.depositSnapshot(address.id, {
87
+ provider: address.snapshot.provider,
88
+ sourceTransactionHashes: [sourceTransactionHash],
89
+ }),
90
+ sourceTransactionHash,
91
+ sourceTransferIndex: 0,
92
+ })
93
+ }
94
+
95
+ test('pages one reusable address across provider records newest-first', async () => {
96
+ const depositAddress = 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE'
97
+ const firstAddress = await seedAddress({
98
+ snapshot: TestFunding.depositAddressSnapshot({
99
+ address: depositAddress,
100
+ provider: { id: 'relay_deposit_address_first', name: 'Relay' },
101
+ }),
102
+ })
103
+ const secondAddress = await seedAddress({
104
+ snapshot: TestFunding.depositAddressSnapshot({
105
+ address: depositAddress,
106
+ provider: { id: 'relay_deposit_address_second', name: 'Relay' },
107
+ subsidize: true,
108
+ }),
109
+ })
110
+ const first = await seedAddressDeposit(firstAddress, 1)
111
+ const second = await seedAddressDeposit(secondAddress, 2)
112
+ const third = await seedAddressDeposit(firstAddress, 3)
113
+ const unrelatedAddress = await seedAddress({
114
+ snapshot: TestFunding.depositAddressSnapshot({
115
+ address: 'TF17BgPaZYbz8oxbjhriubPDsA7ArKoLX3',
116
+ provider: { id: 'relay_deposit_address_unrelated', name: 'Relay' },
117
+ recipient: `0x${'fe'.repeat(20)}`,
118
+ }),
119
+ })
120
+ await seedAddressDeposit(unrelatedAddress, 4)
121
+
122
+ const page = await FundingDeposits.listByDepositAddress(db, {
123
+ depositAddress,
124
+ limit: 2,
125
+ owner,
126
+ })
127
+ expect(page.map((row) => row.id)).toEqual([third.id, second.id])
128
+ const rest = await FundingDeposits.listByDepositAddress(db, {
129
+ cursor: { createdAt: second.createdAt, id: second.id },
130
+ depositAddress,
131
+ limit: 2,
132
+ owner,
133
+ })
134
+ expect(rest.map((row) => row.id)).toEqual([first.id])
135
+ })
136
+
137
+ test('narrows project-attributed owners to their project', async () => {
138
+ const depositAddress = 'TWd4WrZ9wn84f5x1hZhL4DHvk738ns5jwb'
139
+ const address = await seedAddress({
140
+ projectId: 'prj_deposit',
141
+ snapshot: TestFunding.depositAddressSnapshot({
142
+ address: depositAddress,
143
+ provider: { id: 'relay_deposit_address_project', name: 'Relay' },
144
+ }),
145
+ })
146
+ await seedAddressDeposit(address, 5)
147
+
148
+ expect(
149
+ await FundingDeposits.listByDepositAddress(db, {
150
+ depositAddress,
151
+ limit: 10,
152
+ owner: { ...owner, projectId: 'prj_other' },
153
+ }),
154
+ ).toEqual([])
155
+ })
156
+
157
+ test('normalizes EVM address casing', async () => {
158
+ const depositAddress = `0x${'Ab'.repeat(20)}`
159
+ const source = TestFunding.transferSnapshot()
160
+ const address = await seedAddress({
161
+ snapshot: TestFunding.depositAddressSnapshot({
162
+ address: depositAddress,
163
+ provider: { id: 'relay_deposit_address_evm', name: 'Relay' },
164
+ recipient: `0x${'fd'.repeat(20)}`,
165
+ refundAddress: depositAddress,
166
+ sourceChain: source.sourceChain,
167
+ sourceToken: source.sourceToken,
168
+ }),
169
+ })
170
+ const record = await seedAddressDeposit(address, 6)
171
+
172
+ const rows = await FundingDeposits.listByDepositAddress(db, {
173
+ depositAddress,
174
+ limit: 10,
175
+ owner,
176
+ })
177
+ expect(rows.map((row) => row.id)).toEqual([record.id])
178
+ })
179
+ })
180
+
80
181
  describe('listByRecipient', () => {
81
182
  test('pages recipient deposits across addresses and hides foreign ownership', async () => {
82
183
  const recipient = '0xABcdEFABcdEFabcdEfAbCdefabcdeFABcDEFabCD'