tapimo 0.7.2 → 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 (300) hide show
  1. package/README.md +10 -7
  2. package/dist/ApiKey.d.ts +3 -0
  3. package/dist/ApiKey.d.ts.map +1 -1
  4. package/dist/ApiKey.js +10 -0
  5. package/dist/ApiKey.js.map +1 -1
  6. package/dist/ApiKeys.d.ts +7 -3
  7. package/dist/ApiKeys.d.ts.map +1 -1
  8. package/dist/ApiKeys.js +9 -2
  9. package/dist/ApiKeys.js.map +1 -1
  10. package/dist/App.d.ts +20117 -19183
  11. package/dist/App.d.ts.map +1 -1
  12. package/dist/App.js +61 -4
  13. package/dist/App.js.map +1 -1
  14. package/dist/admin/App.d.ts +84 -74
  15. package/dist/admin/App.d.ts.map +1 -1
  16. package/dist/admin/apps/api-keys.d.ts +14 -1
  17. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  18. package/dist/admin/apps/api-keys.js +6 -2
  19. package/dist/admin/apps/api-keys.js.map +1 -1
  20. package/dist/admin/apps/organizations.d.ts +2 -0
  21. package/dist/admin/apps/organizations.d.ts.map +1 -1
  22. package/dist/admin/apps/verified-tokens.d.ts +11 -11
  23. package/dist/admin/ui.generated.d.ts +1 -1
  24. package/dist/admin/ui.generated.js +67 -67
  25. package/dist/admin/ui.generated.js.map +1 -1
  26. package/dist/analytics/Analytics.d.ts +2 -0
  27. package/dist/analytics/Analytics.d.ts.map +1 -1
  28. package/dist/analytics/Analytics.js +3 -0
  29. package/dist/analytics/Analytics.js.map +1 -1
  30. package/dist/apps/data/App.d.ts +11696 -13937
  31. package/dist/apps/data/App.d.ts.map +1 -1
  32. package/dist/apps/data/App.js +4 -8
  33. package/dist/apps/data/App.js.map +1 -1
  34. package/dist/apps/data/routes/activities.d.ts +271 -271
  35. package/dist/apps/data/routes/addresses.d.ts +398 -167
  36. package/dist/apps/data/routes/addresses.d.ts.map +1 -1
  37. package/dist/apps/data/routes/balances.d.ts +278 -8
  38. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  39. package/dist/apps/data/routes/balances.js +114 -2
  40. package/dist/apps/data/routes/balances.js.map +1 -1
  41. package/dist/apps/data/routes/blocks.d.ts +123 -123
  42. package/dist/apps/data/routes/coingecko.d.ts +170 -170
  43. package/dist/apps/data/routes/earn.d.ts +498 -6
  44. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  45. package/dist/apps/data/routes/earn.js +876 -4
  46. package/dist/apps/data/routes/earn.js.map +1 -1
  47. package/dist/apps/data/routes/exchanges.d.ts +10 -10
  48. package/dist/apps/data/routes/fee-amm.d.ts +2 -2
  49. package/dist/apps/data/routes/indexer.d.ts +6 -6
  50. package/dist/apps/data/routes/indexer.js +4 -1
  51. package/dist/apps/data/routes/indexer.js.map +1 -1
  52. package/dist/apps/data/routes/receipts.d.ts +235 -235
  53. package/dist/apps/data/routes/rpc.d.ts +31 -31
  54. package/dist/apps/data/routes/rpc.js +4 -1
  55. package/dist/apps/data/routes/rpc.js.map +1 -1
  56. package/dist/apps/data/routes/tokenlist.d.ts +36 -36
  57. package/dist/apps/data/routes/tokens.d.ts +449 -449
  58. package/dist/apps/data/routes/transactions.d.ts +2 -2
  59. package/dist/apps/data/routes/transfers.d.ts +81 -81
  60. package/dist/apps/data/routes/valuation.d.ts +1 -1
  61. package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
  62. package/dist/apps/data/routes/webhooks.d.ts +9 -9
  63. package/dist/apps/data/routes/zones.d.ts +14 -14
  64. package/dist/apps/funding/Access.d.ts +16 -0
  65. package/dist/apps/funding/Access.d.ts.map +1 -0
  66. package/dist/apps/funding/Access.js +22 -0
  67. package/dist/apps/funding/Access.js.map +1 -0
  68. package/dist/apps/{data/routes/funding.d.ts → funding/App.d.ts} +1640 -3217
  69. package/dist/apps/funding/App.d.ts.map +1 -0
  70. package/dist/apps/funding/App.js +61 -0
  71. package/dist/apps/funding/App.js.map +1 -0
  72. package/dist/apps/funding/Inventory.d.ts +5 -0
  73. package/dist/apps/funding/Inventory.d.ts.map +1 -0
  74. package/dist/apps/funding/Inventory.js +25 -0
  75. package/dist/apps/funding/Inventory.js.map +1 -0
  76. package/dist/apps/funding/ProviderAttempts.d.ts +5 -0
  77. package/dist/apps/funding/ProviderAttempts.d.ts.map +1 -0
  78. package/dist/apps/funding/ProviderAttempts.js +24 -0
  79. package/dist/apps/funding/ProviderAttempts.js.map +1 -0
  80. package/dist/apps/funding/index.d.ts +2 -0
  81. package/dist/apps/funding/index.d.ts.map +1 -0
  82. package/dist/apps/funding/index.js +2 -0
  83. package/dist/apps/funding/index.js.map +1 -0
  84. package/dist/apps/funding/routes/chains.d.ts +169 -0
  85. package/dist/apps/funding/routes/chains.d.ts.map +1 -0
  86. package/dist/apps/funding/routes/chains.js +80 -0
  87. package/dist/apps/funding/routes/chains.js.map +1 -0
  88. package/dist/apps/funding/routes/deposit-addresses.d.ts +1997 -0
  89. package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -0
  90. package/dist/apps/funding/routes/deposit-addresses.js +778 -0
  91. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -0
  92. package/dist/apps/funding/routes/providers.d.ts +91 -0
  93. package/dist/apps/funding/routes/providers.d.ts.map +1 -0
  94. package/dist/apps/funding/routes/providers.js +56 -0
  95. package/dist/apps/funding/routes/providers.js.map +1 -0
  96. package/dist/apps/funding/routes/quotes.d.ts +426 -0
  97. package/dist/apps/funding/routes/quotes.d.ts.map +1 -0
  98. package/dist/apps/funding/routes/quotes.js +188 -0
  99. package/dist/apps/funding/routes/quotes.js.map +1 -0
  100. package/dist/apps/funding/routes/transfers.d.ts +1999 -0
  101. package/dist/apps/funding/routes/transfers.d.ts.map +1 -0
  102. package/dist/apps/funding/routes/transfers.js +614 -0
  103. package/dist/apps/funding/routes/transfers.js.map +1 -0
  104. package/dist/apps/index.d.ts +1 -0
  105. package/dist/apps/index.d.ts.map +1 -1
  106. package/dist/apps/index.js +1 -0
  107. package/dist/apps/index.js.map +1 -1
  108. package/dist/apps/management/App.d.ts +430 -165
  109. package/dist/apps/management/App.d.ts.map +1 -1
  110. package/dist/apps/management/routes/api-keys.d.ts +298 -21
  111. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  112. package/dist/apps/management/routes/api-keys.js +75 -1
  113. package/dist/apps/management/routes/api-keys.js.map +1 -1
  114. package/dist/apps/management/routes/billing.d.ts +6 -6
  115. package/dist/apps/management/routes/faucet.d.ts +35 -35
  116. package/dist/apps/management/routes/invitations.d.ts +122 -122
  117. package/dist/apps/management/routes/invite-links.d.ts +38 -38
  118. package/dist/apps/management/routes/members.d.ts +69 -69
  119. package/dist/apps/management/routes/orgs.d.ts +99 -99
  120. package/dist/apps/management/routes/projects.d.ts +25 -25
  121. package/dist/apps/management/routes/scopes.d.ts +1 -1
  122. package/dist/apps/management/routes/usage.d.ts +72 -72
  123. package/dist/apps/management/routes/verified-token-requests.d.ts +77 -77
  124. package/dist/apps/management/routes/webhooks.d.ts +128 -128
  125. package/dist/apps/mcp.d.ts +2 -3
  126. package/dist/apps/mcp.d.ts.map +1 -1
  127. package/dist/apps/mcp.js +16 -14
  128. package/dist/apps/mcp.js.map +1 -1
  129. package/dist/apps/metadata.d.ts +4 -0
  130. package/dist/apps/metadata.d.ts.map +1 -1
  131. package/dist/apps/metadata.js +10 -0
  132. package/dist/apps/metadata.js.map +1 -1
  133. package/dist/apps/mpp/App.d.ts +44 -44
  134. package/dist/cloudflare.d.ts +50 -0
  135. package/dist/cloudflare.d.ts.map +1 -1
  136. package/dist/cloudflare.js +178 -0
  137. package/dist/cloudflare.js.map +1 -1
  138. package/dist/db/Db.d.ts +2 -0
  139. package/dist/db/Db.d.ts.map +1 -1
  140. package/dist/db/Db.js +45 -0
  141. package/dist/db/Db.js.map +1 -1
  142. package/dist/db/Schema.d.ts +12 -0
  143. package/dist/db/Schema.d.ts.map +1 -1
  144. package/dist/db/Schema.js +20 -0
  145. package/dist/db/Schema.js.map +1 -1
  146. package/dist/db/tables/fundingDepositAddresses.d.ts +13 -0
  147. package/dist/db/tables/fundingDepositAddresses.d.ts.map +1 -1
  148. package/dist/db/tables/fundingDepositAddresses.js +12 -0
  149. package/dist/db/tables/fundingDepositAddresses.js.map +1 -1
  150. package/dist/db/tables/fundingDepositRequestObservations.d.ts +45 -0
  151. package/dist/db/tables/fundingDepositRequestObservations.d.ts.map +1 -0
  152. package/dist/db/tables/fundingDepositRequestObservations.js +46 -0
  153. package/dist/db/tables/fundingDepositRequestObservations.js.map +1 -0
  154. package/dist/db/tables/fundingDeposits.d.ts +74 -1
  155. package/dist/db/tables/fundingDeposits.d.ts.map +1 -1
  156. package/dist/db/tables/fundingDeposits.js +126 -0
  157. package/dist/db/tables/fundingDeposits.js.map +1 -1
  158. package/dist/internal/Auth.d.ts +15 -11
  159. package/dist/internal/Auth.d.ts.map +1 -1
  160. package/dist/internal/Auth.js +52 -17
  161. package/dist/internal/Auth.js.map +1 -1
  162. package/dist/internal/Cache.js +1 -1
  163. package/dist/internal/Cache.js.map +1 -1
  164. package/dist/internal/EdgeCache.d.ts +2 -2
  165. package/dist/internal/EdgeCache.d.ts.map +1 -1
  166. package/dist/internal/EdgeCache.js +10 -4
  167. package/dist/internal/EdgeCache.js.map +1 -1
  168. package/dist/internal/IpAllowlist.d.ts +15 -0
  169. package/dist/internal/IpAllowlist.d.ts.map +1 -0
  170. package/dist/internal/IpAllowlist.js +27 -0
  171. package/dist/internal/IpAllowlist.js.map +1 -0
  172. package/dist/internal/OpenApi.d.ts +1 -1
  173. package/dist/internal/OpenApi.js +1 -1
  174. package/dist/internal/OpenApi.js.map +1 -1
  175. package/dist/internal/Tidx.d.ts +6 -0
  176. package/dist/internal/Tidx.d.ts.map +1 -1
  177. package/dist/internal/Tidx.js +13 -3
  178. package/dist/internal/Tidx.js.map +1 -1
  179. package/dist/internal/Viem.d.ts +8 -0
  180. package/dist/internal/Viem.d.ts.map +1 -1
  181. package/dist/internal/Viem.js +12 -5
  182. package/dist/internal/Viem.js.map +1 -1
  183. package/dist/internal/funding/Deposit.d.ts +42 -0
  184. package/dist/internal/funding/Deposit.d.ts.map +1 -1
  185. package/dist/internal/funding/Deposit.js +37 -5
  186. package/dist/internal/funding/Deposit.js.map +1 -1
  187. package/dist/internal/funding/Evidence.d.ts +1 -5
  188. package/dist/internal/funding/Evidence.d.ts.map +1 -1
  189. package/dist/internal/funding/Evidence.js +4 -3
  190. package/dist/internal/funding/Evidence.js.map +1 -1
  191. package/dist/internal/funding/Provider.d.ts +102 -0
  192. package/dist/internal/funding/Provider.d.ts.map +1 -1
  193. package/dist/internal/funding/Provider.js +5 -0
  194. package/dist/internal/funding/Provider.js.map +1 -1
  195. package/dist/internal/funding/Reconciliation.d.ts +16 -2
  196. package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
  197. package/dist/internal/funding/Reconciliation.js +348 -54
  198. package/dist/internal/funding/Reconciliation.js.map +1 -1
  199. package/dist/internal/funding/SourceObservation.d.ts +108 -0
  200. package/dist/internal/funding/SourceObservation.d.ts.map +1 -0
  201. package/dist/internal/funding/SourceObservation.js +143 -0
  202. package/dist/internal/funding/SourceObservation.js.map +1 -0
  203. package/dist/internal/funding/Subsidy.d.ts +2 -2
  204. package/dist/internal/funding/Subsidy.d.ts.map +1 -1
  205. package/dist/internal/funding/Subsidy.js +1 -1
  206. package/dist/internal/funding/Subsidy.js.map +1 -1
  207. package/dist/internal/funding/index.d.ts +1 -0
  208. package/dist/internal/funding/index.d.ts.map +1 -1
  209. package/dist/internal/funding/index.js +1 -0
  210. package/dist/internal/funding/index.js.map +1 -1
  211. package/dist/internal/funding/providers/relay.d.ts +43 -1
  212. package/dist/internal/funding/providers/relay.d.ts.map +1 -1
  213. package/dist/internal/funding/providers/relay.js +438 -27
  214. package/dist/internal/funding/providers/relay.js.map +1 -1
  215. package/package.json +3 -3
  216. package/src/ApiKey.test.ts +35 -0
  217. package/src/ApiKey.ts +27 -0
  218. package/src/ApiKeys.test.ts +29 -0
  219. package/src/ApiKeys.ts +11 -3
  220. package/src/App.test.ts +55 -5
  221. package/src/App.ts +79 -5
  222. package/src/Client.test-d.ts +203 -28
  223. package/src/admin/apps/api-keys.test.ts +25 -1
  224. package/src/admin/apps/api-keys.ts +6 -2
  225. package/src/admin/ui.generated.ts +67 -67
  226. package/src/analytics/Analytics.ts +4 -0
  227. package/src/apps/composition.test.ts +130 -3
  228. package/src/apps/data/App.test.ts +0 -12
  229. package/src/apps/data/App.ts +4 -8
  230. package/src/apps/data/routes/balances.test.ts +42 -0
  231. package/src/apps/data/routes/balances.ts +218 -85
  232. package/src/apps/data/routes/earn.test.ts +363 -0
  233. package/src/apps/data/routes/earn.ts +1141 -3
  234. package/src/apps/data/routes/indexer.ts +2 -1
  235. package/src/apps/data/routes/rpc.ts +2 -0
  236. package/src/apps/data/routes/tokens.test.ts +6 -8
  237. package/src/apps/funding/Access.ts +26 -0
  238. package/src/apps/funding/App.test.ts +78 -0
  239. package/src/apps/funding/App.ts +78 -0
  240. package/src/apps/funding/Inventory.ts +30 -0
  241. package/src/apps/funding/ProviderAttempts.ts +26 -0
  242. package/src/apps/funding/index.ts +1 -0
  243. package/src/apps/funding/routes/chains.test.ts +78 -0
  244. package/src/apps/funding/routes/chains.ts +101 -0
  245. package/src/apps/funding/routes/deposit-addresses.test.ts +1649 -0
  246. package/src/apps/funding/routes/deposit-addresses.ts +932 -0
  247. package/src/apps/funding/routes/providers.test.ts +165 -0
  248. package/src/apps/funding/routes/providers.ts +73 -0
  249. package/src/apps/funding/routes/quotes.test.ts +511 -0
  250. package/src/apps/funding/routes/quotes.ts +205 -0
  251. package/src/apps/funding/routes/transfers.test.ts +1236 -0
  252. package/src/apps/funding/routes/transfers.ts +704 -0
  253. package/src/apps/index.ts +1 -0
  254. package/src/apps/management/routes/api-keys.test.ts +98 -0
  255. package/src/apps/management/routes/api-keys.ts +96 -1
  256. package/src/apps/mcp.test.ts +17 -2
  257. package/src/apps/mcp.ts +18 -13
  258. package/src/apps/metadata.ts +12 -0
  259. package/src/apps/mpp/internal/Policy.test.ts +7 -1
  260. package/src/cloudflare.ts +279 -0
  261. package/src/db/Db.ts +71 -0
  262. package/src/db/Schema.ts +24 -0
  263. package/src/db/tables/fundingDepositAddresses.ts +28 -0
  264. package/src/db/tables/fundingDepositRequestObservations.test.ts +106 -0
  265. package/src/db/tables/fundingDepositRequestObservations.ts +99 -0
  266. package/src/db/tables/fundingDeposits.test.ts +239 -0
  267. package/src/db/tables/fundingDeposits.ts +214 -1
  268. package/src/internal/Auth.test.ts +205 -5
  269. package/src/internal/Auth.ts +63 -19
  270. package/src/internal/Cache.test.ts +37 -3
  271. package/src/internal/Cache.ts +1 -1
  272. package/src/internal/EdgeCache.test.ts +7 -7
  273. package/src/internal/EdgeCache.ts +13 -4
  274. package/src/internal/IpAllowlist.test.ts +52 -0
  275. package/src/internal/IpAllowlist.ts +39 -0
  276. package/src/internal/Log.test.ts +7 -1
  277. package/src/internal/OpenApi.ts +1 -1
  278. package/src/internal/Tidx.test.ts +28 -0
  279. package/src/internal/Tidx.ts +19 -3
  280. package/src/internal/Viem.test.ts +28 -0
  281. package/src/internal/Viem.ts +21 -5
  282. package/src/internal/funding/Deposit.ts +74 -5
  283. package/src/internal/funding/Evidence.test.ts +2 -2
  284. package/src/internal/funding/Evidence.ts +4 -9
  285. package/src/internal/funding/Provider.ts +115 -0
  286. package/src/internal/funding/Reconciliation.local.test.ts +7 -0
  287. package/src/internal/funding/Reconciliation.test.ts +1364 -109
  288. package/src/internal/funding/Reconciliation.ts +467 -88
  289. package/src/internal/funding/SourceObservation.test.ts +301 -0
  290. package/src/internal/funding/SourceObservation.ts +267 -0
  291. package/src/internal/funding/Subsidy.ts +2 -2
  292. package/src/internal/funding/index.test.ts +1 -0
  293. package/src/internal/funding/index.ts +1 -0
  294. package/src/internal/funding/providers/relay.test.ts +94 -4
  295. package/src/internal/funding/providers/relay.ts +533 -27
  296. package/dist/apps/data/routes/funding.d.ts.map +0 -1
  297. package/dist/apps/data/routes/funding.js +0 -1557
  298. package/dist/apps/data/routes/funding.js.map +0 -1
  299. package/src/apps/data/routes/funding.test.ts +0 -2994
  300. package/src/apps/data/routes/funding.ts +0 -1821
@@ -77,6 +77,180 @@ 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
+
181
+ describe('listByRecipient', () => {
182
+ test('pages recipient deposits across addresses and hides foreign ownership', async () => {
183
+ const recipient = '0xABcdEFABcdEFabcdEfAbCdefabcdeFABcDEFabCD'
184
+ const firstAddress = await seedAddress({
185
+ snapshot: TestFunding.depositAddressSnapshot({
186
+ provider: { id: 'relay_recipient_first', name: 'Relay' },
187
+ recipient,
188
+ }),
189
+ })
190
+ const secondAddress = await seedAddress({
191
+ snapshot: TestFunding.depositAddressSnapshot({
192
+ provider: { id: 'relay_recipient_second', name: 'Relay' },
193
+ recipient,
194
+ subsidize: true,
195
+ }),
196
+ })
197
+ const first = await seedDeposit(firstAddress)
198
+ const second = await seedDeposit(secondAddress)
199
+ const third = await seedDeposit(firstAddress)
200
+ const unrelatedAddress = await seedAddress()
201
+ await seedDeposit(unrelatedAddress)
202
+ const foreignAddress = await seedAddress({
203
+ orgId: 'org_other',
204
+ snapshot: TestFunding.depositAddressSnapshot({
205
+ provider: { id: 'relay_recipient_foreign', name: 'Relay' },
206
+ recipient,
207
+ }),
208
+ })
209
+ await seedDeposit(foreignAddress)
210
+
211
+ const page = await FundingDeposits.listByRecipient(db, {
212
+ limit: 2,
213
+ owner,
214
+ recipient: recipient.toLowerCase(),
215
+ })
216
+ expect(page.map((row) => row.id)).toEqual([third.id, second.id])
217
+ const rest = await FundingDeposits.listByRecipient(db, {
218
+ cursor: { createdAt: second.createdAt, id: second.id },
219
+ limit: 2,
220
+ owner,
221
+ recipient: recipient.toLowerCase(),
222
+ })
223
+ expect(rest.map((row) => row.id)).toEqual([first.id])
224
+ })
225
+
226
+ test('narrows project-attributed owners to their project', async () => {
227
+ const recipient = `0x${'ab'.repeat(20)}`
228
+ const firstAddress = await seedAddress({
229
+ projectId: 'prj_deposit',
230
+ snapshot: TestFunding.depositAddressSnapshot({
231
+ provider: { id: 'relay_recipient_project', name: 'Relay' },
232
+ recipient,
233
+ }),
234
+ })
235
+ const secondAddress = await seedAddress({
236
+ projectId: 'prj_other',
237
+ snapshot: TestFunding.depositAddressSnapshot({
238
+ provider: { id: 'relay_recipient_other_project', name: 'Relay' },
239
+ recipient,
240
+ }),
241
+ })
242
+ const first = await seedDeposit(firstAddress)
243
+ await seedDeposit(secondAddress)
244
+
245
+ const rows = await FundingDeposits.listByRecipient(db, {
246
+ limit: 10,
247
+ owner: { ...owner, projectId: 'prj_deposit' },
248
+ recipient,
249
+ })
250
+ expect(rows.map((row) => row.id)).toEqual([first.id])
251
+ })
252
+ })
253
+
80
254
  describe('getOwned', () => {
81
255
  test('returns owned deposits and hides foreign ownership', async () => {
82
256
  const address = await seedAddress()
@@ -227,6 +401,71 @@ describe('update', () => {
227
401
  })
228
402
  })
229
403
 
404
+ describe('recordRequestObservation', () => {
405
+ test('attaches the first poll and webhook timestamps without changing material state', async () => {
406
+ const address = await seedAddress()
407
+ const record = await seedDeposit(address, { providerRequestId: 'relay_observed' })
408
+ await FundingDeposits.recordRequestObservation(db, {
409
+ depositAddressId: address.id,
410
+ pollObservedAt: '2026-01-02T00:00:02.000Z',
411
+ providerRequestId: 'relay_observed',
412
+ webhookReceivedAt: '2026-01-02T00:00:03.000Z',
413
+ })
414
+ await FundingDeposits.recordRequestObservation(db, {
415
+ depositAddressId: address.id,
416
+ pollObservedAt: '2026-01-02T00:00:01.000Z',
417
+ providerRequestId: 'relay_observed',
418
+ webhookReceivedAt: '2026-01-02T00:00:04.000Z',
419
+ })
420
+
421
+ expect(await FundingDeposits.get(db, record.id)).toMatchInlineSnapshot(
422
+ {
423
+ createdAt: expect.any(String),
424
+ depositAddressId: expect.any(String),
425
+ id: expect.any(String),
426
+ providerOutputToken: expect.any(Object),
427
+ snapshot: expect.any(Object),
428
+ updatedAt: expect.any(String),
429
+ },
430
+ `
431
+ {
432
+ "createdAt": Any<String>,
433
+ "depositAddressId": Any<String>,
434
+ "detectionTrigger": null,
435
+ "environment": "production",
436
+ "id": Any<String>,
437
+ "orgId": "org_deposits",
438
+ "pollObservedAt": "2026-01-02T00:00:01.000Z",
439
+ "projectId": null,
440
+ "providerOutputAmount": null,
441
+ "providerOutputToken": Any<Object>,
442
+ "providerRequestId": "relay_observed",
443
+ "providerRequestIds": [
444
+ "relay_observed",
445
+ ],
446
+ "providerState": null,
447
+ "providerTransactionHashes": [],
448
+ "providerTransferIndex": 0,
449
+ "retryState": null,
450
+ "settlementTransaction": null,
451
+ "settlementTransactionHash": null,
452
+ "snapshot": Any<Object>,
453
+ "sourceChainId": "tron:0x2b6653dc",
454
+ "sourceTransactionHash": "0000000000000000000000000000000000000000000000000000000000000011",
455
+ "sourceTransferIndex": 0,
456
+ "status": "detected",
457
+ "statusReason": null,
458
+ "subsidyAmount": null,
459
+ "tempoGasPaid": null,
460
+ "updatedAt": Any<String>,
461
+ "version": 1,
462
+ "webhookReceivedAt": "2026-01-02T00:00:03.000Z",
463
+ }
464
+ `,
465
+ )
466
+ })
467
+ })
468
+
230
469
  describe('toPublic', () => {
231
470
  test('omits private reconciliation and settlement state', async () => {
232
471
  const address = await seedAddress()
@@ -1,4 +1,4 @@
1
- import type { JSONColumnType, Selectable } from 'kysely'
1
+ import { sql, type JSONColumnType, type Selectable } from 'kysely'
2
2
 
3
3
  import type * as Db from '../Db.js'
4
4
  import type * as db_Schema from '../Schema.js'
@@ -40,6 +40,63 @@ export function insert(db: Db.Db, record: Record): Promise<Record> {
40
40
  .executeTakeFirstOrThrow()
41
41
  }
42
42
 
43
+ /** Inserts a chain-observed source transfer or returns its existing deposit. */
44
+ export async function insertOrGetSource(
45
+ db: Db.Db,
46
+ record: Record & { sourceTransferIndex: number },
47
+ ): Promise<insertOrGetSource.Result> {
48
+ const inserted = await db.kysely
49
+ .insertInto('funding_deposits')
50
+ .values({
51
+ ...record,
52
+ providerRequestIds: JSON.stringify(record.providerRequestIds),
53
+ providerTransactionHashes: JSON.stringify(record.providerTransactionHashes),
54
+ })
55
+ .onConflict((oc) =>
56
+ oc.columns(['depositAddressId', 'sourceTransactionHash', 'sourceTransferIndex']).doNothing(),
57
+ )
58
+ .returningAll()
59
+ .executeTakeFirst()
60
+ if (inserted) return { record: inserted, type: 'created' }
61
+ const existing = await db.kysely
62
+ .selectFrom('funding_deposits')
63
+ .selectAll()
64
+ .where('depositAddressId', '=', record.depositAddressId)
65
+ .where('sourceTransactionHash', '=', record.sourceTransactionHash)
66
+ .where('sourceTransferIndex', '=', record.sourceTransferIndex)
67
+ .executeTakeFirstOrThrow()
68
+ return { record: existing, type: 'existing' }
69
+ }
70
+
71
+ export declare namespace insertOrGetSource {
72
+ /** Result of idempotently persisting one verified source transfer. */
73
+ type Result = { record: Record; type: 'created' | 'existing' }
74
+ }
75
+
76
+ /** Serializes source attribution for one address and transaction. */
77
+ export function withSourceTransaction<const result>(
78
+ db: Db.Db,
79
+ options: withSourceTransaction.Options<result>,
80
+ ): Promise<result> {
81
+ return db.transaction(async (tx) => {
82
+ const key = `${options.depositAddressId}:${options.sourceTransactionHash.toLowerCase()}`
83
+ await sql`SELECT pg_advisory_xact_lock(hashtextextended(${key}, 0))`.execute(tx.kysely)
84
+ return options.fn(tx)
85
+ })
86
+ }
87
+
88
+ export declare namespace withSourceTransaction {
89
+ /** Source identity and transaction-scoped work protected by its advisory lock. */
90
+ type Options<result> = {
91
+ /** Funding deposit address that received the transfer. */
92
+ depositAddressId: string
93
+ /** Database work serialized for the source transaction. */
94
+ fn: (db: Db.Db) => Promise<result>
95
+ /** Source transaction whose deposit attribution must not race. */
96
+ sourceTransactionHash: string
97
+ }
98
+ }
99
+
43
100
  /** Gets deposits created from one provider-observed source transaction. */
44
101
  export function listByProviderObservation(
45
102
  db: Db.Db,
@@ -67,6 +124,45 @@ export declare namespace listByProviderObservation {
67
124
  }
68
125
  }
69
126
 
127
+ /** Attaches first-observation timestamps to every deposit from one provider request. */
128
+ export function recordRequestObservation(
129
+ db: Db.Db,
130
+ options: recordRequestObservation.Options,
131
+ ): Promise<Record[]> {
132
+ return db.kysely
133
+ .updateTable('funding_deposits')
134
+ .set({
135
+ ...(options.pollObservedAt
136
+ ? {
137
+ pollObservedAt: sql<string>`LEAST(COALESCE(poll_observed_at, ${options.pollObservedAt}), ${options.pollObservedAt})`,
138
+ }
139
+ : {}),
140
+ ...(options.webhookReceivedAt
141
+ ? {
142
+ webhookReceivedAt: sql<string>`LEAST(COALESCE(webhook_received_at, ${options.webhookReceivedAt}), ${options.webhookReceivedAt})`,
143
+ }
144
+ : {}),
145
+ })
146
+ .where('depositAddressId', '=', options.depositAddressId)
147
+ .where('providerRequestId', '=', options.providerRequestId)
148
+ .returningAll()
149
+ .execute()
150
+ }
151
+
152
+ export declare namespace recordRequestObservation {
153
+ /** First observations associated with one provider request. */
154
+ type Options = {
155
+ /** Funding deposit address id (`fda_…`). */
156
+ depositAddressId: string
157
+ /** When polling first observed the provider request. */
158
+ pollObservedAt?: string | undefined
159
+ /** Provider request identifier. */
160
+ providerRequestId: string
161
+ /** When Tempo first received an authenticated provider webhook. */
162
+ webhookReceivedAt?: string | undefined
163
+ }
164
+ }
165
+
70
166
  /** Gets deposits associated with one address and source transaction. */
71
167
  export function listBySourceTransaction(
72
168
  db: Db.Db,
@@ -91,6 +187,32 @@ export declare namespace listBySourceTransaction {
91
187
  }
92
188
  }
93
189
 
190
+ /** Lists nonterminal source transfers that await provider attribution. */
191
+ export function listUnattributed(db: Db.Db, depositAddressId: string): Promise<Record[]> {
192
+ return db.kysely
193
+ .selectFrom('funding_deposits')
194
+ .selectAll()
195
+ .where('depositAddressId', '=', depositAddressId)
196
+ .where('providerRequestId', 'is', null)
197
+ .where('status', 'not in', ['completed', 'refunded'])
198
+ .orderBy('createdAt', 'asc')
199
+ .orderBy('id', 'asc')
200
+ .execute()
201
+ }
202
+
203
+ /** Returns whether an address has a source transfer awaiting provider attribution. */
204
+ export async function hasUnattributed(db: Db.Db, depositAddressId: string): Promise<boolean> {
205
+ return Boolean(
206
+ await db.kysely
207
+ .selectFrom('funding_deposits')
208
+ .select('id')
209
+ .where('depositAddressId', '=', depositAddressId)
210
+ .where('providerRequestId', 'is', null)
211
+ .limit(1)
212
+ .executeTakeFirst(),
213
+ )
214
+ }
215
+
94
216
  /** Gets a funding deposit without ownership scoping. */
95
217
  export function get(db: Db.Db, id: string): Promise<Record | undefined> {
96
218
  return db.kysely
@@ -155,6 +277,97 @@ export declare namespace listByAddress {
155
277
  }
156
278
  }
157
279
 
280
+ /** Lists deposits for one reusable source-chain address newest-first. */
281
+ export function listByDepositAddress(
282
+ db: Db.Db,
283
+ options: listByDepositAddress.Options,
284
+ ): Promise<Record[]> {
285
+ const { cursor, depositAddress, limit, owner } = options
286
+ const address = /^0x[0-9a-fA-F]{40}$/.test(depositAddress)
287
+ ? depositAddress.toLowerCase()
288
+ : depositAddress
289
+ let query = db.kysely
290
+ .selectFrom('funding_deposits')
291
+ .innerJoin(
292
+ 'funding_deposit_addresses',
293
+ 'funding_deposit_addresses.id',
294
+ 'funding_deposits.depositAddressId',
295
+ )
296
+ .selectAll('funding_deposits')
297
+ .where('funding_deposit_addresses.address', '=', address)
298
+ .where('funding_deposit_addresses.environment', '=', owner.environment)
299
+ .where('funding_deposit_addresses.orgId', '=', owner.orgId)
300
+ .where('funding_deposits.environment', '=', owner.environment)
301
+ .where('funding_deposits.orgId', '=', owner.orgId)
302
+ if (owner.projectId !== undefined)
303
+ query = query
304
+ .where('funding_deposit_addresses.projectId', '=', owner.projectId)
305
+ .where('funding_deposits.projectId', '=', owner.projectId)
306
+ if (cursor)
307
+ query = query.where((eb) =>
308
+ eb.or([
309
+ eb('funding_deposits.createdAt', '<', cursor.createdAt),
310
+ eb.and([
311
+ eb('funding_deposits.createdAt', '=', cursor.createdAt),
312
+ eb('funding_deposits.id', '<', cursor.id),
313
+ ]),
314
+ ]),
315
+ )
316
+ return query
317
+ .orderBy('funding_deposits.createdAt', 'desc')
318
+ .orderBy('funding_deposits.id', 'desc')
319
+ .limit(limit)
320
+ .execute()
321
+ }
322
+
323
+ export declare namespace listByDepositAddress {
324
+ /** Deposit address filter, ownership, and paging options. */
325
+ type Options = {
326
+ /** Exclusive lower bound from the previous page. */
327
+ cursor?: listByAddress.Cursor | undefined
328
+ /** Reusable source-chain address that received the funds. */
329
+ depositAddress: string
330
+ /** Maximum rows to return. */
331
+ limit: number
332
+ /** Ownership filter derived from the authenticated API key. */
333
+ owner: Owner
334
+ }
335
+ }
336
+
337
+ /** Lists one recipient's owner-visible deposits newest-first. */
338
+ export function listByRecipient(db: Db.Db, options: listByRecipient.Options): Promise<Record[]> {
339
+ const { cursor, limit, owner, recipient } = options
340
+ let query = db.kysely
341
+ .selectFrom('funding_deposits')
342
+ .selectAll()
343
+ .where('environment', '=', owner.environment)
344
+ .where('orgId', '=', owner.orgId)
345
+ .where(sql<boolean>`lower(snapshot->>'recipient') = ${recipient.toLowerCase()}`)
346
+ if (owner.projectId !== undefined) query = query.where('projectId', '=', owner.projectId)
347
+ if (cursor)
348
+ query = query.where((eb) =>
349
+ eb.or([
350
+ eb('createdAt', '<', cursor.createdAt),
351
+ eb.and([eb('createdAt', '=', cursor.createdAt), eb('id', '<', cursor.id)]),
352
+ ]),
353
+ )
354
+ return query.orderBy('createdAt', 'desc').orderBy('id', 'desc').limit(limit).execute()
355
+ }
356
+
357
+ export declare namespace listByRecipient {
358
+ /** Recipient filter, ownership, and paging options. */
359
+ type Options = {
360
+ /** Exclusive lower bound from the previous page. */
361
+ cursor?: listByAddress.Cursor | undefined
362
+ /** Maximum rows to return. */
363
+ limit: number
364
+ /** Ownership filter derived from the authenticated API key. */
365
+ owner: Owner
366
+ /** Tempo account that receives completed deposits. */
367
+ recipient: string
368
+ }
369
+ }
370
+
158
371
  /** Applies a version-guarded deposit update. */
159
372
  export function update(db: Db.Db, options: update.Options): Promise<Record | undefined> {
160
373
  return db.kysely