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
@@ -2,6 +2,7 @@ import { Hex } from 'ox'
2
2
 
3
3
  import * as Db from '../../db/Db.js'
4
4
  import * as FundingDepositAddresses from '../../db/tables/fundingDepositAddresses.js'
5
+ import * as FundingDepositRequestObservations from '../../db/tables/fundingDepositRequestObservations.js'
5
6
  import * as FundingDeposits from '../../db/tables/fundingDeposits.js'
6
7
  import type * as Metrics from '../../Metrics.js'
7
8
  import * as Value from '../Value.js'
@@ -12,7 +13,14 @@ import * as Subsidy from './Subsidy.js'
12
13
  const defaultBatchSize = 25
13
14
  const defaultLeaseMs = 2 * 60_000
14
15
  const defaultPollIntervalMs = 60_000
16
+ const chainProviderFollowUpDelaySeconds = 1
17
+ const chainProviderMaximumFollowUpAttempts = 30
18
+ const followUpDelaySeconds = { delivery: 1, provider: 5, settlement: 2 } as const satisfies Record<
19
+ Provider.Webhook.receive.FollowUp['kind'],
20
+ number
21
+ >
15
22
  const maximumPages = 100
23
+ const maximumFollowUpAttempts = 6
16
24
 
17
25
  /** Verified token transfer returned by an authoritative chain reader. */
18
26
  export type TransferEvidence = {
@@ -52,10 +60,11 @@ export function createTracker(options: createTracker.Options): createTracker.Tra
52
60
  const leaseMs = options.leaseMs ?? defaultLeaseMs
53
61
  const pollIntervalMs = options.pollIntervalMs ?? defaultPollIntervalMs
54
62
 
55
- const record = (outcome: string, providerId: string) => {
63
+ const record = (outcome: string, providerId: string, trigger: Deposit.DetectionTrigger) => {
56
64
  options.metrics?.count('funding_deposit_reconciliation_count', 1, {
57
65
  outcome,
58
66
  provider: providerId,
67
+ trigger,
59
68
  })
60
69
  }
61
70
 
@@ -63,6 +72,12 @@ export function createTracker(options: createTracker.Options): createTracker.Tra
63
72
  async reconcile(message) {
64
73
  const database = Db.get(options.db)
65
74
  const startedAt = now()
75
+ // Legacy poll messages carry a lease version; legacy webhook messages do not.
76
+ const trigger = (() => {
77
+ if (message.trigger) return message.trigger
78
+ if (message.pollLeaseVersion !== undefined) return 'poll'
79
+ return 'webhook'
80
+ })()
66
81
  const claimed = await claim(database, message, {
67
82
  leaseUntil: new Date(startedAt.getTime() + leaseMs).toISOString(),
68
83
  now: startedAt.toISOString(),
@@ -72,7 +87,7 @@ export function createTracker(options: createTracker.Options): createTracker.Tra
72
87
  const provider = options.providers.find((candidate) => candidate.id === claimed.providerId)
73
88
  if (!provider || !Provider.canListDepositAddressRequests(provider)) {
74
89
  await fail(database, claimed, startedAt, pollIntervalMs)
75
- record('provider_unavailable', claimed.providerId)
90
+ record('provider_unavailable', claimed.providerId, trigger)
76
91
  return { type: 'provider_unavailable' }
77
92
  }
78
93
 
@@ -84,24 +99,70 @@ export function createTracker(options: createTracker.Options): createTracker.Tra
84
99
  requestId: message.requestId,
85
100
  signal: options.signal,
86
101
  })
102
+ if (trigger === 'poll')
103
+ await FundingDepositRequestObservations.observePoll(database, {
104
+ depositAddressId: claimed.id,
105
+ observedAt: now().toISOString(),
106
+ providerRequestIds: requests.requests.map((request) => request.id),
107
+ })
108
+ const webhookUpdateObserved = (() => {
109
+ const timing = message.webhookTiming
110
+ if (!timing) return true
111
+ if (!message.requestId) return false
112
+ // Relay can send a webhook before its requests API indexes the same update.
113
+ return requests.requests.some(
114
+ (request) =>
115
+ request.id === message.requestId && request.updatedAt >= timing.providerUpdatedAt,
116
+ )
117
+ })()
87
118
  heartbeat.assert()
88
119
  let created = 0
89
120
  let updated = 0
90
- const pendingRequestIds = new Set(requests.pendingRequestIds)
121
+ const pendingRequestIds = new Set([
122
+ ...requests.pendingRequestIds,
123
+ ...(message.requestId ? [message.requestId] : []),
124
+ ])
125
+ let followUpKind: Provider.Webhook.receive.FollowUp['kind'] | undefined
91
126
  for (const request of requests.requests) {
92
127
  const result = await reconcileRequest(database, {
93
128
  address: claimed,
94
129
  metrics: options.metrics,
130
+ provider,
95
131
  request,
132
+ signal: options.signal,
96
133
  subsidies: options.subsidies,
134
+ trigger,
97
135
  verifyTransfers: options.verifyTransfers,
98
136
  })
99
137
  heartbeat.assert()
100
138
  created += result.created
101
139
  updated += result.updated
102
140
  if (result.terminal) pendingRequestIds.delete(request.id)
103
- else pendingRequestIds.add(request.id)
141
+ else {
142
+ pendingRequestIds.add(request.id)
143
+ followUpKind = result.followUp ?? followUpKind ?? 'provider'
144
+ }
104
145
  }
146
+ const unattributed = await reconcileUnattributed(database, {
147
+ address: claimed,
148
+ metrics: options.metrics,
149
+ provider,
150
+ signal: options.signal,
151
+ subsidies: options.subsidies,
152
+ verifyTransfers: options.verifyTransfers,
153
+ })
154
+ heartbeat.assert()
155
+ updated += unattributed.updatedIds.size
156
+ if (!unattributed.terminal)
157
+ followUpKind = unattributed.followUp ?? followUpKind ?? 'provider'
158
+ const awaitsProvider = await FundingDeposits.hasUnattributed(database, claimed.id)
159
+ if (message.requestId && message.webhookTiming && !webhookUpdateObserved)
160
+ pendingRequestIds.add(message.requestId)
161
+ if (created > 0)
162
+ options.metrics?.count('funding_deposit_detection_count', created, {
163
+ provider: claimed.providerId,
164
+ trigger,
165
+ })
105
166
 
106
167
  const leaseFailure = await heartbeat.stop()
107
168
  if (leaseFailure !== undefined) throw leaseFailure
@@ -112,22 +173,65 @@ export function createTracker(options: createTracker.Options): createTracker.Tra
112
173
  nextPollAt: new Date(completedAt.getTime() + pollIntervalMs).toISOString(),
113
174
  pollLeaseVersion: claimed.pollLeaseVersion,
114
175
  providerRequestIds: claimed.providerRequestIds,
115
- providerState: reconciliationState(claimed.providerState, requests, [
116
- ...pendingRequestIds,
117
- ]),
176
+ providerState: reconciliationState(claimed.providerState, {
177
+ pendingRequestIds: [...pendingRequestIds],
178
+ requests,
179
+ ...(message.webhookTiming && message.requestId && webhookUpdateObserved
180
+ ? {
181
+ webhookTiming: {
182
+ ...message.webhookTiming,
183
+ reconciliationCompletedAt: completedAt.toISOString(),
184
+ requestId: message.requestId,
185
+ },
186
+ }
187
+ : {}),
188
+ }),
118
189
  })
119
190
  if (!stored) return { type: 'stale' }
120
- record('completed', claimed.providerId)
191
+ if (pendingRequestIds.size > 0 || awaitsProvider) {
192
+ const followUp = nextFollowUp(message, followUpKind ?? 'provider')
193
+ if (followUp)
194
+ await options
195
+ .dispatch(
196
+ {
197
+ addressId: claimed.id,
198
+ followUp: followUp.state,
199
+ ...(message.requestId ? { requestId: message.requestId } : {}),
200
+ trigger,
201
+ type: 'funding:deposit-address:reconcile',
202
+ ...(message.webhookTiming && !webhookUpdateObserved
203
+ ? { webhookTiming: message.webhookTiming }
204
+ : {}),
205
+ },
206
+ { delaySeconds: followUp.delaySeconds },
207
+ )
208
+ .catch((cause) => {
209
+ record('follow_up_enqueue_failed', claimed.providerId, trigger)
210
+ throw cause
211
+ })
212
+ record(
213
+ followUp ? 'follow_up_enqueued' : 'follow_up_exhausted',
214
+ claimed.providerId,
215
+ trigger,
216
+ )
217
+ }
218
+ if (message.webhookTiming && webhookUpdateObserved)
219
+ recordWebhookLatency(options.metrics, {
220
+ completedAt,
221
+ providerId: claimed.providerId,
222
+ timing: message.webhookTiming,
223
+ })
224
+ record('completed', claimed.providerId, trigger)
121
225
  options.metrics?.histogram(
122
226
  'funding_deposit_reconciliation_duration_ms',
123
227
  completedAt.getTime() - startedAt.getTime(),
124
- { provider: claimed.providerId },
228
+ { provider: claimed.providerId, trigger },
125
229
  )
126
230
  return { created, type: 'completed', updated }
127
231
  } catch (cause) {
128
232
  const leaseFailure = await heartbeat.stop()
129
233
  await fail(database, claimed, now(), pollIntervalMs)
130
- record('failed', claimed.providerId)
234
+ record('failed', claimed.providerId, trigger)
131
235
  throw leaseFailure ?? cause
132
236
  } finally {
133
237
  options.metrics?.flush()
@@ -159,13 +263,14 @@ export function createTracker(options: createTracker.Options): createTracker.Tra
159
263
  await options.dispatch({
160
264
  addressId: address.id,
161
265
  pollLeaseVersion: address.pollLeaseVersion,
266
+ trigger: 'poll',
162
267
  type: 'funding:deposit-address:reconcile',
163
268
  })
164
- record('enqueued', provider.id)
269
+ record('enqueued', provider.id, 'poll')
165
270
  } catch {
166
271
  failed += 1
167
272
  await fail(database, address, startedAt, pollIntervalMs)
168
- record('enqueue_failed', provider.id)
273
+ record('enqueue_failed', provider.id, 'poll')
169
274
  }
170
275
  }
171
276
  if (claimed >= batchSize) break
@@ -184,7 +289,10 @@ export declare namespace createTracker {
184
289
  /** Authoritative database or per-invocation database factory. */
185
290
  db: Db.Source
186
291
  /** Durable reconciliation queue transport. */
187
- dispatch: (message: Provider.Webhook.receive.Dispatchable) => Promise<void>
292
+ dispatch: (
293
+ message: Provider.Webhook.receive.Dispatchable,
294
+ options?: createTracker.DispatchOptions,
295
+ ) => Promise<void>
188
296
  /** Duration of one reconciliation lease. */
189
297
  leaseMs?: number | undefined
190
298
  /** Optional bounded operational metrics sink. */
@@ -203,6 +311,12 @@ export declare namespace createTracker {
203
311
  verifyTransfers: VerifyTransfers
204
312
  }
205
313
 
314
+ /** Delivery options for one reconciliation queue message. */
315
+ type DispatchOptions = {
316
+ /** Delay before the queue makes the message available. */
317
+ delaySeconds: number
318
+ }
319
+
206
320
  /** Result of reconciling one queue message. */
207
321
  type ReconcileResult =
208
322
  | { type: 'busy' | 'provider_unavailable' | 'stale' }
@@ -211,7 +325,7 @@ export declare namespace createTracker {
211
325
  /** Scheduled and queue-driven reusable-address tracker. */
212
326
  type Tracker = {
213
327
  /** Reconciles one address from provider discovery and verified chain evidence. */
214
- reconcile(message: Provider.Webhook.receive.Dispatchable): Promise<ReconcileResult>
328
+ reconcile(message: Provider.Webhook.receive.Queued): Promise<ReconcileResult>
215
329
  /** Claims and enqueues one bounded batch of due reusable addresses. */
216
330
  tick(): Promise<{ claimed: number; failed: number }>
217
331
  }
@@ -222,11 +336,7 @@ type ClaimOptions = {
222
336
  now: string
223
337
  }
224
338
 
225
- async function claim(
226
- db: Db.Db,
227
- message: Provider.Webhook.receive.Dispatchable,
228
- options: ClaimOptions,
229
- ) {
339
+ async function claim(db: Db.Db, message: Provider.Webhook.receive.Queued, options: ClaimOptions) {
230
340
  if (message.pollLeaseVersion === undefined)
231
341
  return FundingDepositAddresses.claim(db, {
232
342
  id: message.addressId,
@@ -421,18 +531,42 @@ function readHighWater(state: FundingDepositAddresses.Record['providerState']):
421
531
  }
422
532
  }
423
533
 
534
+ type ReconciledWebhookTiming = Provider.Webhook.receive.WebhookTiming & {
535
+ /** When Tempo completed the webhook-triggered reconciliation. */
536
+ reconciliationCompletedAt: string
537
+ /** Provider request matched to the observed update. */
538
+ requestId: string
539
+ }
540
+
541
+ type ReconciliationStateOptions = {
542
+ /** Provider requests that still require reconciliation. */
543
+ pendingRequestIds: readonly string[]
544
+ /** Provider requests and high-water state observed in this pass. */
545
+ requests: Requests
546
+ /** Completed webhook timing retained for diagnostics. */
547
+ webhookTiming?: ReconciledWebhookTiming | undefined
548
+ }
549
+
424
550
  function reconciliationState(
425
551
  state: FundingDepositAddresses.Record['providerState'],
426
- requests: Requests,
427
- pendingRequestIds: readonly string[],
552
+ options: ReconciliationStateOptions,
428
553
  ) {
429
554
  const current = state && typeof state === 'object' ? state : {}
555
+ const reconciliation = (() => {
556
+ const value = current['reconciliation']
557
+ if (value && typeof value === 'object') return value
558
+ return {}
559
+ })()
430
560
  return {
431
561
  ...current,
432
562
  reconciliation: {
433
- pendingRequestIds,
434
- requestIds: requests.highWaterRequestIds,
435
- ...(requests.highWaterUpdatedAt ? { updatedAt: requests.highWaterUpdatedAt } : {}),
563
+ ...reconciliation,
564
+ pendingRequestIds: options.pendingRequestIds,
565
+ requestIds: options.requests.highWaterRequestIds,
566
+ ...(options.requests.highWaterUpdatedAt
567
+ ? { updatedAt: options.requests.highWaterUpdatedAt }
568
+ : {}),
569
+ ...(options.webhookTiming ? { webhookTiming: options.webhookTiming } : {}),
436
570
  },
437
571
  }
438
572
  }
@@ -446,49 +580,185 @@ type ObservedSourceTransaction = {
446
580
  type VerifiedDeposit = {
447
581
  deposit: FundingDeposits.Record
448
582
  required: bigint
583
+ source: TransferEvidence
449
584
  sourceAmount: bigint
450
585
  }
451
586
 
587
+ type ReconcileDeliveryOptions = {
588
+ address: FundingDepositAddresses.Record
589
+ metrics?: Metrics.Metrics | undefined
590
+ provider?: Provider.Provider | undefined
591
+ request?: Provider.listDepositAddressRequests.ReturnType['requests'][number] | undefined
592
+ signal?: AbortSignal | undefined
593
+ subsidies?: Subsidy.Settler | undefined
594
+ verified: readonly VerifiedDeposit[]
595
+ verifyTransfers: VerifyTransfers
596
+ }
597
+
598
+ async function observeProviderDelivery(options: ReconcileDeliveryOptions) {
599
+ const { verified } = options
600
+ const [entry] = verified
601
+ if (
602
+ !entry ||
603
+ verified.length !== 1 ||
604
+ options.address.deliveryStrategy !== 'provider' ||
605
+ options.address.providerOutputToken.tokenKey !==
606
+ options.address.snapshot.destinationToken.tokenKey ||
607
+ (options.request?.status === 'success' &&
608
+ options.request.destinationTransactionHashes.length > 0) ||
609
+ !options.provider ||
610
+ !Provider.canObserveDepositAddressRequest(options.provider)
611
+ )
612
+ return undefined
613
+
614
+ const result = await options.provider
615
+ .observeDepositAddressRequest(
616
+ {
617
+ address: options.address.address,
618
+ destinationChain: options.address.snapshot.destinationChain,
619
+ destinationToken: options.address.providerOutputToken,
620
+ providerState: entry.deposit.providerState ?? undefined,
621
+ recipient: options.address.recipient,
622
+ refundAddress: options.address.refundAddress,
623
+ ...(options.request ? { request: options.request } : {}),
624
+ source: entry.source,
625
+ sourceChain: options.address.snapshot.sourceChain,
626
+ sourceToken: options.address.snapshot.sourceToken,
627
+ },
628
+ options.signal ?? new AbortController().signal,
629
+ )
630
+ .catch((cause) => {
631
+ if (options.signal?.aborted) throw cause
632
+ return undefined
633
+ })
634
+ options.metrics?.count('funding_deposit_delivery_observation_count', 1, {
635
+ outcome: result?.type ?? 'failed',
636
+ provider: options.address.providerId,
637
+ })
638
+ return result
639
+ }
640
+
641
+ async function reconcileUnattributed(db: Db.Db, options: reconcileUnattributed.Options) {
642
+ const deposits = await FundingDeposits.listUnattributed(db, options.address.id)
643
+ if (deposits.length === 0) return { terminal: true, updatedIds: new Set<string>() }
644
+
645
+ const transfersByTransaction = new Map(
646
+ await Promise.all(
647
+ unique(deposits.map((deposit) => deposit.sourceTransactionHash)).map(
648
+ async (transactionHash) =>
649
+ [
650
+ transactionHash,
651
+ await options.verifyTransfers({
652
+ chain: options.address.snapshot.sourceChain,
653
+ recipient: options.address.address,
654
+ token: options.address.snapshot.sourceToken,
655
+ transactionHash,
656
+ }),
657
+ ] as const,
658
+ ),
659
+ ),
660
+ )
661
+ let followUp: reconcileRequest.ReturnType['followUp']
662
+ let terminal = true
663
+ const updatedIds = new Set<string>()
664
+ for (const deposit of deposits) {
665
+ const source = transfersByTransaction
666
+ .get(deposit.sourceTransactionHash)
667
+ ?.find((transfer) => transfer.transferIndex === deposit.sourceTransferIndex)
668
+ if (!source) {
669
+ terminal = false
670
+ continue
671
+ }
672
+ const delivery = await reconcileDelivery(db, {
673
+ ...options,
674
+ verified: [
675
+ {
676
+ deposit,
677
+ required: destinationBaseUnits(
678
+ BigInt(source.amount),
679
+ options.address.snapshot.sourceToken.decimals,
680
+ options.address.snapshot.destinationToken.decimals,
681
+ ),
682
+ source,
683
+ sourceAmount: BigInt(source.amount),
684
+ },
685
+ ],
686
+ })
687
+ if (delivery.followUp === 'delivery' || !followUp) followUp = delivery.followUp
688
+ if (!delivery.terminal) terminal = false
689
+ for (const id of delivery.updatedIds) updatedIds.add(id)
690
+ }
691
+ return { ...(followUp ? { followUp } : {}), terminal, updatedIds }
692
+ }
693
+
694
+ declare namespace reconcileUnattributed {
695
+ type Options = Omit<ReconcileDeliveryOptions, 'request' | 'verified'>
696
+ }
697
+
452
698
  /** Reconciles one provider request against verified chain evidence. */
453
699
  export async function reconcileRequest(
454
700
  db: Db.Db,
455
701
  options: reconcileRequest.Options,
456
702
  ): Promise<reconcileRequest.ReturnType> {
703
+ const trigger = options.trigger ?? 'poll'
704
+ const observation = await FundingDepositRequestObservations.get(db, {
705
+ depositAddressId: options.address.id,
706
+ providerRequestId: options.request.id,
707
+ })
708
+ if (observation)
709
+ await FundingDeposits.recordRequestObservation(db, {
710
+ depositAddressId: observation.depositAddressId,
711
+ ...(observation.pollObservedAt ? { pollObservedAt: observation.pollObservedAt } : {}),
712
+ providerRequestId: observation.providerRequestId,
713
+ ...(observation.webhookReceivedAt
714
+ ? { webhookReceivedAt: observation.webhookReceivedAt }
715
+ : {}),
716
+ })
457
717
  let created = 0
458
718
  const observed: ObservedSourceTransaction[] = []
459
719
  // Persist provider observations first so transient chain failures do not hide acknowledged deposits.
460
720
  for (const transactionHash of unique(options.request.sourceTransactionHashes)) {
461
- let deposits = await FundingDeposits.listByProviderObservation(db, {
721
+ const deposits = await FundingDeposits.withSourceTransaction(db, {
462
722
  depositAddressId: options.address.id,
463
- providerRequestId: options.request.id,
464
- sourceTransactionHash: transactionHash,
465
- })
466
- if (deposits.length === 0)
467
- deposits = await FundingDeposits.listBySourceTransaction(db, {
468
- depositAddressId: options.address.id,
469
- sourceTransactionHash: transactionHash,
470
- })
471
- if (deposits.length === 0) {
472
- deposits = [
473
- await Deposit.create(db, {
474
- now: new Date(options.request.createdAt),
723
+ fn: async (tx): Promise<FundingDeposits.Record[]> => {
724
+ let records = await FundingDeposits.listByProviderObservation(tx, {
725
+ depositAddressId: options.address.id,
475
726
  providerRequestId: options.request.id,
476
- snapshot: {
477
- depositAddressId: options.address.id,
478
- destinationChain: options.address.snapshot.destinationChain,
479
- destinationToken: options.address.snapshot.destinationToken,
480
- provider: options.address.snapshot.provider,
481
- recipient: options.address.recipient,
482
- refundAddress: options.address.refundAddress,
483
- sourceChain: options.address.snapshot.sourceChain,
484
- sourceToken: options.address.snapshot.sourceToken,
485
- sourceTransactionHashes: [transactionHash],
486
- },
487
727
  sourceTransactionHash: transactionHash,
488
- }),
489
- ]
490
- created += 1
491
- }
728
+ })
729
+ if (records.length === 0)
730
+ records = await FundingDeposits.listBySourceTransaction(tx, {
731
+ depositAddressId: options.address.id,
732
+ sourceTransactionHash: transactionHash,
733
+ })
734
+ if (records.length > 0) return records
735
+ created += 1
736
+ return [
737
+ await Deposit.create(tx, {
738
+ detectionTrigger: trigger,
739
+ now: new Date(options.request.createdAt),
740
+ ...(observation?.pollObservedAt ? { pollObservedAt: observation.pollObservedAt } : {}),
741
+ providerRequestId: options.request.id,
742
+ snapshot: {
743
+ depositAddressId: options.address.id,
744
+ destinationChain: options.address.snapshot.destinationChain,
745
+ destinationToken: options.address.snapshot.destinationToken,
746
+ provider: options.address.snapshot.provider,
747
+ recipient: options.address.recipient,
748
+ refundAddress: options.address.refundAddress,
749
+ sourceChain: options.address.snapshot.sourceChain,
750
+ sourceToken: options.address.snapshot.sourceToken,
751
+ sourceTransactionHashes: [transactionHash],
752
+ },
753
+ sourceTransactionHash: transactionHash,
754
+ ...(observation?.webhookReceivedAt
755
+ ? { webhookReceivedAt: observation.webhookReceivedAt }
756
+ : {}),
757
+ }),
758
+ ]
759
+ },
760
+ sourceTransactionHash: transactionHash,
761
+ })
492
762
  observed.push({ deposits, transactionHash, transfers: [] })
493
763
  }
494
764
 
@@ -508,7 +778,6 @@ export async function reconcileRequest(
508
778
 
509
779
  const sourceEvidenceComplete =
510
780
  sources.length > 0 && sources.every((source) => source.transfers.length > 0)
511
- let terminal = sourceEvidenceComplete
512
781
  const updatedIds = new Set<string>()
513
782
  const verified: VerifiedDeposit[] = []
514
783
  for (const source of sources) {
@@ -539,7 +808,9 @@ export async function reconcileRequest(
539
808
  )
540
809
  if (!deposit) {
541
810
  deposit = await Deposit.create(db, {
811
+ detectionTrigger: trigger,
542
812
  now: new Date(options.request.createdAt),
813
+ ...(observation?.pollObservedAt ? { pollObservedAt: observation.pollObservedAt } : {}),
543
814
  providerRequestId: options.request.id,
544
815
  providerTransferIndex,
545
816
  snapshot: {
@@ -558,15 +829,24 @@ export async function reconcileRequest(
558
829
  },
559
830
  sourceTransactionHash: transfer.transactionHash,
560
831
  sourceTransferIndex: transfer.transferIndex,
832
+ ...(observation?.webhookReceivedAt
833
+ ? { webhookReceivedAt: observation.webhookReceivedAt }
834
+ : {}),
561
835
  })
562
836
  source.deposits.push(deposit)
563
837
  created += 1
564
838
  }
565
- if (!Deposit.isTerminal(deposit.status)) {
839
+ if (
840
+ !Deposit.isTerminal(deposit.status) ||
841
+ deposit.providerRequestId === null ||
842
+ !deposit.providerRequestIds.includes(options.request.id)
843
+ ) {
566
844
  const result = await Deposit.transition(db, {
567
845
  expectedVersion: deposit.version,
568
846
  id: deposit.id,
847
+ providerRequestId: options.request.id,
569
848
  providerRequestIds: unique([...deposit.providerRequestIds, options.request.id]),
849
+ providerTransferIndex,
570
850
  snapshot: {
571
851
  ...deposit.snapshot,
572
852
  ...(options.address.snapshot.subsidize ? { destinationAmountRequired } : {}),
@@ -581,14 +861,72 @@ export async function reconcileRequest(
581
861
  updatedIds.add(deposit.id)
582
862
  }
583
863
  }
584
- verified.push({ deposit, required, sourceAmount: BigInt(transfer.amount) })
864
+ verified.push({ deposit, required, source: transfer, sourceAmount: BigInt(transfer.amount) })
585
865
  }
586
866
  }
587
867
  if (verified.length === 0) return { created, terminal: false, updated: updatedIds.size }
588
868
  if (!sourceEvidenceComplete) return { created, terminal: false, updated: updatedIds.size }
869
+ const delivery = await reconcileDelivery(db, { ...options, verified })
870
+ for (const id of delivery.updatedIds) updatedIds.add(id)
871
+ return {
872
+ created,
873
+ ...(delivery.followUp ? { followUp: delivery.followUp } : {}),
874
+ terminal: delivery.terminal,
875
+ updated: updatedIds.size,
876
+ }
877
+ }
589
878
 
879
+ export declare namespace reconcileRequest {
880
+ /** Provider request, address, and settlement dependencies. */
881
+ type Options = {
882
+ /** Reusable deposit address attributed by the provider. */
883
+ address: FundingDepositAddresses.Record
884
+ /** Optional bounded operational metrics sink. */
885
+ metrics?: Metrics.Metrics | undefined
886
+ /** Provider integration that discovered the request. */
887
+ provider?: Provider.Provider | undefined
888
+ /** Provider request containing discovered chain transactions. */
889
+ request: Provider.listDepositAddressRequests.ReturnType['requests'][number]
890
+ /** Abort signal forwarded to provider delivery observation. */
891
+ signal?: AbortSignal | undefined
892
+ /** Optional pathUSD-funded subsidy settler. */
893
+ subsidies?: Subsidy.Settler | undefined
894
+ /** Mechanism that requested reconciliation. */
895
+ trigger?: Deposit.DetectionTrigger | undefined
896
+ /** Authoritative source and destination transfer verifier. */
897
+ verifyTransfers: VerifyTransfers
898
+ }
899
+
900
+ /** Material changes produced by one provider request. */
901
+ type ReturnType = {
902
+ /** Number of newly detected source transfers. */
903
+ created: number
904
+ /** Work that needs a bounded delayed follow-up. */
905
+ followUp?: 'delivery' | 'settlement' | undefined
906
+ /** Whether every deposit from this request reached a terminal state. */
907
+ terminal: boolean
908
+ /** Number of existing or new deposits materially changed. */
909
+ updated: number
910
+ }
911
+ }
912
+
913
+ async function reconcileDelivery(db: Db.Db, options: ReconcileDeliveryOptions) {
914
+ let followUp: reconcileRequest.ReturnType['followUp']
915
+ let terminal = true
916
+ const updatedIds = new Set<string>()
917
+ if (options.verified.every((entry) => Deposit.isTerminal(entry.deposit.status)))
918
+ return { terminal, updatedIds }
919
+ const deliveryObservation = await observeProviderDelivery(options)
920
+ if (deliveryObservation?.type === 'pending') followUp = 'delivery'
921
+ const providerTransactionHashes = options.request?.destinationTransactionHashes ?? []
922
+ const destinationTransactionHashes = unique([
923
+ ...providerTransactionHashes,
924
+ ...(deliveryObservation?.type === 'verified'
925
+ ? deliveryObservation.destinationTransactionHashes
926
+ : []),
927
+ ])
590
928
  const destinationEvidence = await Promise.all(
591
- options.request.destinationTransactionHashes.map((transactionHash) =>
929
+ destinationTransactionHashes.map((transactionHash) =>
592
930
  options.verifyTransfers({
593
931
  chain: options.address.snapshot.destinationChain,
594
932
  recipient: options.address.recipient,
@@ -599,17 +937,18 @@ export async function reconcileRequest(
599
937
  )
600
938
  const destinationTransfers = destinationEvidence.flat()
601
939
  const providerEvidenceComplete =
602
- options.request.status === 'success' &&
940
+ (options.request?.status === 'success' || deliveryObservation?.type === 'verified') &&
941
+ destinationEvidence.length > 0 &&
603
942
  destinationEvidence.every((transfers) => transfers.length > 0)
604
943
  const providerOutput = destinationTransfers.reduce(
605
944
  (total, transfer) => total + BigInt(transfer.amount),
606
945
  0n,
607
946
  )
608
- const totalRequired = verified.reduce((total, entry) => total + entry.required, 0n)
947
+ const totalRequired = options.verified.reduce((total, entry) => total + entry.required, 0n)
609
948
  let allocated = 0n
610
949
  let cumulativeRequired = 0n
611
950
 
612
- for (const entry of verified) {
951
+ for (const entry of options.verified) {
613
952
  const { required, sourceAmount } = entry
614
953
  cumulativeRequired += required
615
954
  const nextAllocated =
@@ -635,6 +974,7 @@ export async function reconcileRequest(
635
974
  provider: options.address.providerId,
636
975
  })
637
976
  if (settlement.status === 'bridging' || settlement.status === 'settling') terminal = false
977
+ if (settlement.status === 'settling') followUp = 'settlement'
638
978
  const destinationAmount = Value.tokenAmount({
639
979
  baseUnits: delivered + settlement.delivered,
640
980
  currency: options.address.snapshot.destinationToken.currency,
@@ -652,10 +992,21 @@ export async function reconcileRequest(
652
992
  }),
653
993
  }
654
994
  : {}),
655
- providerRequestIds: unique([...deposit.providerRequestIds, options.request.id]),
995
+ providerRequestIds: options.request
996
+ ? unique([...deposit.providerRequestIds, options.request.id])
997
+ : deposit.providerRequestIds,
998
+ ...(deliveryObservation?.providerState
999
+ ? {
1000
+ providerState: {
1001
+ ...deposit.providerState,
1002
+ ...deliveryObservation.providerState,
1003
+ },
1004
+ }
1005
+ : {}),
656
1006
  providerTransactionHashes: unique([
657
1007
  ...deposit.providerTransactionHashes,
658
- ...options.request.destinationTransactionHashes,
1008
+ ...providerTransactionHashes,
1009
+ ...(deliveryObservation?.providerTransactionHashes ?? []),
659
1010
  ]),
660
1011
  ...(settlement.settlementTransaction
661
1012
  ? { settlementTransaction: settlement.settlementTransaction }
@@ -697,33 +1048,7 @@ export async function reconcileRequest(
697
1048
  }
698
1049
  }
699
1050
  }
700
- return { created, terminal, updated: updatedIds.size }
701
- }
702
-
703
- export declare namespace reconcileRequest {
704
- /** Provider request, address, and settlement dependencies. */
705
- type Options = {
706
- /** Reusable deposit address attributed by the provider. */
707
- address: FundingDepositAddresses.Record
708
- /** Optional bounded operational metrics sink. */
709
- metrics?: Metrics.Metrics | undefined
710
- /** Provider request containing discovered chain transactions. */
711
- request: Provider.listDepositAddressRequests.ReturnType['requests'][number]
712
- /** Optional pathUSD-funded subsidy settler. */
713
- subsidies?: Subsidy.Settler | undefined
714
- /** Authoritative source and destination transfer verifier. */
715
- verifyTransfers: VerifyTransfers
716
- }
717
-
718
- /** Material changes produced by one provider request. */
719
- type ReturnType = {
720
- /** Number of newly detected source transfers. */
721
- created: number
722
- /** Whether every deposit from this request reached a terminal state. */
723
- terminal: boolean
724
- /** Number of existing or new deposits materially changed. */
725
- updated: number
726
- }
1051
+ return { ...(followUp ? { followUp } : {}), terminal, updatedIds }
727
1052
  }
728
1053
 
729
1054
  type ReconcileSubsidyOptions = {
@@ -919,6 +1244,60 @@ function unique(values: readonly string[]) {
919
1244
  return [...new Set(values)]
920
1245
  }
921
1246
 
1247
+ function nextFollowUp(
1248
+ message: Provider.Webhook.receive.Queued,
1249
+ kind: Provider.Webhook.receive.FollowUp['kind'],
1250
+ ) {
1251
+ const attempt = message.followUp?.kind === kind ? message.followUp.attempt + 1 : 0
1252
+ const chainProvider = kind === 'provider' && message.trigger === 'chain'
1253
+ const maximumAttempts = chainProvider
1254
+ ? chainProviderMaximumFollowUpAttempts
1255
+ : maximumFollowUpAttempts
1256
+ if (attempt >= maximumAttempts) return undefined
1257
+ return {
1258
+ delaySeconds: chainProvider ? chainProviderFollowUpDelaySeconds : followUpDelaySeconds[kind],
1259
+ state: { attempt, kind },
1260
+ }
1261
+ }
1262
+
1263
+ type RecordWebhookLatencyOptions = {
1264
+ /** When Tempo completed the webhook-triggered reconciliation. */
1265
+ completedAt: Date
1266
+ /** Funding provider that sent the webhook. */
1267
+ providerId: string
1268
+ /** Authenticated timestamps carried from webhook receipt. */
1269
+ timing: Provider.Webhook.receive.WebhookTiming
1270
+ }
1271
+
1272
+ function recordWebhookLatency(
1273
+ metrics: Metrics.Metrics | undefined,
1274
+ options: RecordWebhookLatencyOptions,
1275
+ ) {
1276
+ if (!metrics) return
1277
+ const completedAt = options.completedAt.getTime()
1278
+ const enqueuedAt = Date.parse(options.timing.enqueuedAt)
1279
+ const providerUpdatedAt = Date.parse(options.timing.providerUpdatedAt)
1280
+ const receivedAt = Date.parse(options.timing.receivedAt)
1281
+ const sentAt = Date.parse(options.timing.sentAt)
1282
+ const segments = [
1283
+ { from: providerUpdatedAt, segment: 'provider_to_sent', to: sentAt },
1284
+ { from: sentAt, segment: 'sent_to_received', to: receivedAt },
1285
+ { from: receivedAt, segment: 'received_to_enqueued', to: enqueuedAt },
1286
+ { from: enqueuedAt, segment: 'enqueued_to_reconciled', to: completedAt },
1287
+ { from: providerUpdatedAt, segment: 'provider_to_reconciled', to: completedAt },
1288
+ ] as const
1289
+ for (const segment of segments)
1290
+ metrics.histogram(
1291
+ 'funding_deposit_webhook_latency_ms',
1292
+ Math.max(0, segment.to - segment.from),
1293
+ {
1294
+ provider: options.providerId,
1295
+ segment: segment.segment,
1296
+ status: options.timing.providerStatus,
1297
+ },
1298
+ )
1299
+ }
1300
+
922
1301
  /** Provider pagination exceeded the bounded reconciliation limit. */
923
1302
  export class PaginationLimitError extends Error {
924
1303
  override name = 'Funding.Reconciliation.PaginationLimitError'