tapimo 0.7.2 → 0.8.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 (294) 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 +20434 -19484
  11. package/dist/App.d.ts.map +1 -1
  12. package/dist/App.js +62 -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 +168 -168
  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 +10305 -12785
  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 +24 -24
  35. package/dist/apps/data/routes/addresses.d.ts +288 -288
  36. package/dist/apps/data/routes/balances.d.ts +6 -6
  37. package/dist/apps/data/routes/blocks.d.ts +39 -39
  38. package/dist/apps/data/routes/coingecko.d.ts +170 -170
  39. package/dist/apps/data/routes/earn.d.ts +436 -6
  40. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  41. package/dist/apps/data/routes/earn.js +631 -4
  42. package/dist/apps/data/routes/earn.js.map +1 -1
  43. package/dist/apps/data/routes/exchanges.d.ts +10 -10
  44. package/dist/apps/data/routes/fee-amm.d.ts +2 -2
  45. package/dist/apps/data/routes/indexer.d.ts +6 -6
  46. package/dist/apps/data/routes/indexer.js +4 -1
  47. package/dist/apps/data/routes/indexer.js.map +1 -1
  48. package/dist/apps/data/routes/receipts.d.ts +235 -235
  49. package/dist/apps/data/routes/rpc.d.ts +1 -1
  50. package/dist/apps/data/routes/rpc.js +4 -1
  51. package/dist/apps/data/routes/rpc.js.map +1 -1
  52. package/dist/apps/data/routes/tokenlist.d.ts +36 -36
  53. package/dist/apps/data/routes/tokens.d.ts +6 -6
  54. package/dist/apps/data/routes/transactions.d.ts +2 -2
  55. package/dist/apps/data/routes/transfers.d.ts +1 -1
  56. package/dist/apps/data/routes/valuation.d.ts +1 -1
  57. package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
  58. package/dist/apps/data/routes/webhooks.d.ts +9 -9
  59. package/dist/apps/data/routes/zones.d.ts +14 -14
  60. package/dist/apps/funding/Access.d.ts +16 -0
  61. package/dist/apps/funding/Access.d.ts.map +1 -0
  62. package/dist/apps/funding/Access.js +22 -0
  63. package/dist/apps/funding/Access.js.map +1 -0
  64. package/dist/apps/{data/routes/funding.d.ts → funding/App.d.ts} +1717 -3035
  65. package/dist/apps/funding/App.d.ts.map +1 -0
  66. package/dist/apps/funding/App.js +46 -0
  67. package/dist/apps/funding/App.js.map +1 -0
  68. package/dist/apps/funding/Inventory.d.ts +5 -0
  69. package/dist/apps/funding/Inventory.d.ts.map +1 -0
  70. package/dist/apps/funding/Inventory.js +25 -0
  71. package/dist/apps/funding/Inventory.js.map +1 -0
  72. package/dist/apps/funding/ProviderAttempts.d.ts +5 -0
  73. package/dist/apps/funding/ProviderAttempts.d.ts.map +1 -0
  74. package/dist/apps/funding/ProviderAttempts.js +24 -0
  75. package/dist/apps/funding/ProviderAttempts.js.map +1 -0
  76. package/dist/apps/funding/index.d.ts +2 -0
  77. package/dist/apps/funding/index.d.ts.map +1 -0
  78. package/dist/apps/funding/index.js +2 -0
  79. package/dist/apps/funding/index.js.map +1 -0
  80. package/dist/apps/funding/routes/chains.d.ts +169 -0
  81. package/dist/apps/funding/routes/chains.d.ts.map +1 -0
  82. package/dist/apps/funding/routes/chains.js +80 -0
  83. package/dist/apps/funding/routes/chains.js.map +1 -0
  84. package/dist/apps/funding/routes/deposit-addresses.d.ts +2388 -0
  85. package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -0
  86. package/dist/apps/funding/routes/deposit-addresses.js +860 -0
  87. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -0
  88. package/dist/apps/funding/routes/providers.d.ts +91 -0
  89. package/dist/apps/funding/routes/providers.d.ts.map +1 -0
  90. package/dist/apps/funding/routes/providers.js +56 -0
  91. package/dist/apps/funding/routes/providers.js.map +1 -0
  92. package/dist/apps/funding/routes/quotes.d.ts +426 -0
  93. package/dist/apps/funding/routes/quotes.d.ts.map +1 -0
  94. package/dist/apps/funding/routes/quotes.js +188 -0
  95. package/dist/apps/funding/routes/quotes.js.map +1 -0
  96. package/dist/apps/funding/routes/transfers.d.ts +1999 -0
  97. package/dist/apps/funding/routes/transfers.d.ts.map +1 -0
  98. package/dist/apps/funding/routes/transfers.js +614 -0
  99. package/dist/apps/funding/routes/transfers.js.map +1 -0
  100. package/dist/apps/index.d.ts +1 -0
  101. package/dist/apps/index.d.ts.map +1 -1
  102. package/dist/apps/index.js +1 -0
  103. package/dist/apps/index.js.map +1 -1
  104. package/dist/apps/management/App.d.ts +430 -165
  105. package/dist/apps/management/App.d.ts.map +1 -1
  106. package/dist/apps/management/routes/api-keys.d.ts +298 -21
  107. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  108. package/dist/apps/management/routes/api-keys.js +75 -1
  109. package/dist/apps/management/routes/api-keys.js.map +1 -1
  110. package/dist/apps/management/routes/billing.d.ts +6 -6
  111. package/dist/apps/management/routes/faucet.d.ts +35 -35
  112. package/dist/apps/management/routes/invitations.d.ts +48 -48
  113. package/dist/apps/management/routes/invite-links.d.ts +38 -38
  114. package/dist/apps/management/routes/members.d.ts +69 -69
  115. package/dist/apps/management/routes/orgs.d.ts +37 -37
  116. package/dist/apps/management/routes/projects.d.ts +25 -25
  117. package/dist/apps/management/routes/scopes.d.ts +1 -1
  118. package/dist/apps/management/routes/usage.d.ts +72 -72
  119. package/dist/apps/management/routes/verified-token-requests.d.ts +18 -18
  120. package/dist/apps/management/routes/webhooks.d.ts +128 -128
  121. package/dist/apps/mcp.d.ts +2 -3
  122. package/dist/apps/mcp.d.ts.map +1 -1
  123. package/dist/apps/mcp.js +16 -14
  124. package/dist/apps/mcp.js.map +1 -1
  125. package/dist/apps/metadata.d.ts +4 -0
  126. package/dist/apps/metadata.d.ts.map +1 -1
  127. package/dist/apps/metadata.js +10 -0
  128. package/dist/apps/metadata.js.map +1 -1
  129. package/dist/apps/mpp/App.d.ts +2 -2
  130. package/dist/cloudflare.d.ts +50 -0
  131. package/dist/cloudflare.d.ts.map +1 -1
  132. package/dist/cloudflare.js +178 -0
  133. package/dist/cloudflare.js.map +1 -1
  134. package/dist/db/Db.d.ts +2 -0
  135. package/dist/db/Db.d.ts.map +1 -1
  136. package/dist/db/Db.js +37 -0
  137. package/dist/db/Db.js.map +1 -1
  138. package/dist/db/Schema.d.ts +12 -0
  139. package/dist/db/Schema.d.ts.map +1 -1
  140. package/dist/db/Schema.js +20 -0
  141. package/dist/db/Schema.js.map +1 -1
  142. package/dist/db/tables/fundingDepositAddresses.d.ts +13 -0
  143. package/dist/db/tables/fundingDepositAddresses.d.ts.map +1 -1
  144. package/dist/db/tables/fundingDepositAddresses.js +12 -0
  145. package/dist/db/tables/fundingDepositAddresses.js.map +1 -1
  146. package/dist/db/tables/fundingDepositRequestObservations.d.ts +45 -0
  147. package/dist/db/tables/fundingDepositRequestObservations.d.ts.map +1 -0
  148. package/dist/db/tables/fundingDepositRequestObservations.js +46 -0
  149. package/dist/db/tables/fundingDepositRequestObservations.js.map +1 -0
  150. package/dist/db/tables/fundingDeposits.d.ts +57 -1
  151. package/dist/db/tables/fundingDeposits.d.ts.map +1 -1
  152. package/dist/db/tables/fundingDeposits.js +82 -0
  153. package/dist/db/tables/fundingDeposits.js.map +1 -1
  154. package/dist/internal/Auth.d.ts +11 -11
  155. package/dist/internal/Auth.d.ts.map +1 -1
  156. package/dist/internal/Auth.js +41 -15
  157. package/dist/internal/Auth.js.map +1 -1
  158. package/dist/internal/Cache.js +1 -1
  159. package/dist/internal/Cache.js.map +1 -1
  160. package/dist/internal/EdgeCache.d.ts +2 -2
  161. package/dist/internal/EdgeCache.d.ts.map +1 -1
  162. package/dist/internal/EdgeCache.js +10 -4
  163. package/dist/internal/EdgeCache.js.map +1 -1
  164. package/dist/internal/IpAllowlist.d.ts +15 -0
  165. package/dist/internal/IpAllowlist.d.ts.map +1 -0
  166. package/dist/internal/IpAllowlist.js +27 -0
  167. package/dist/internal/IpAllowlist.js.map +1 -0
  168. package/dist/internal/OpenApi.d.ts +1 -1
  169. package/dist/internal/OpenApi.js +1 -1
  170. package/dist/internal/OpenApi.js.map +1 -1
  171. package/dist/internal/Tidx.d.ts +6 -0
  172. package/dist/internal/Tidx.d.ts.map +1 -1
  173. package/dist/internal/Tidx.js +13 -3
  174. package/dist/internal/Tidx.js.map +1 -1
  175. package/dist/internal/Viem.d.ts +8 -0
  176. package/dist/internal/Viem.d.ts.map +1 -1
  177. package/dist/internal/Viem.js +12 -5
  178. package/dist/internal/Viem.js.map +1 -1
  179. package/dist/internal/funding/Deposit.d.ts +42 -0
  180. package/dist/internal/funding/Deposit.d.ts.map +1 -1
  181. package/dist/internal/funding/Deposit.js +37 -5
  182. package/dist/internal/funding/Deposit.js.map +1 -1
  183. package/dist/internal/funding/Evidence.d.ts +1 -5
  184. package/dist/internal/funding/Evidence.d.ts.map +1 -1
  185. package/dist/internal/funding/Evidence.js +4 -3
  186. package/dist/internal/funding/Evidence.js.map +1 -1
  187. package/dist/internal/funding/Provider.d.ts +102 -0
  188. package/dist/internal/funding/Provider.d.ts.map +1 -1
  189. package/dist/internal/funding/Provider.js +5 -0
  190. package/dist/internal/funding/Provider.js.map +1 -1
  191. package/dist/internal/funding/Reconciliation.d.ts +16 -2
  192. package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
  193. package/dist/internal/funding/Reconciliation.js +257 -48
  194. package/dist/internal/funding/Reconciliation.js.map +1 -1
  195. package/dist/internal/funding/SourceObservation.d.ts +108 -0
  196. package/dist/internal/funding/SourceObservation.d.ts.map +1 -0
  197. package/dist/internal/funding/SourceObservation.js +143 -0
  198. package/dist/internal/funding/SourceObservation.js.map +1 -0
  199. package/dist/internal/funding/Subsidy.d.ts +2 -2
  200. package/dist/internal/funding/Subsidy.d.ts.map +1 -1
  201. package/dist/internal/funding/Subsidy.js +1 -1
  202. package/dist/internal/funding/Subsidy.js.map +1 -1
  203. package/dist/internal/funding/index.d.ts +1 -0
  204. package/dist/internal/funding/index.d.ts.map +1 -1
  205. package/dist/internal/funding/index.js +1 -0
  206. package/dist/internal/funding/index.js.map +1 -1
  207. package/dist/internal/funding/providers/relay.d.ts +43 -1
  208. package/dist/internal/funding/providers/relay.d.ts.map +1 -1
  209. package/dist/internal/funding/providers/relay.js +434 -27
  210. package/dist/internal/funding/providers/relay.js.map +1 -1
  211. package/package.json +3 -3
  212. package/src/ApiKey.test.ts +35 -0
  213. package/src/ApiKey.ts +27 -0
  214. package/src/ApiKeys.test.ts +29 -0
  215. package/src/ApiKeys.ts +11 -3
  216. package/src/App.test.ts +23 -5
  217. package/src/App.ts +76 -5
  218. package/src/Client.test-d.ts +171 -19
  219. package/src/admin/apps/api-keys.test.ts +25 -1
  220. package/src/admin/apps/api-keys.ts +6 -2
  221. package/src/admin/ui.generated.ts +67 -67
  222. package/src/analytics/Analytics.ts +4 -0
  223. package/src/apps/composition.test.ts +100 -2
  224. package/src/apps/data/App.test.ts +0 -12
  225. package/src/apps/data/App.ts +4 -8
  226. package/src/apps/data/routes/earn.test.ts +283 -0
  227. package/src/apps/data/routes/earn.ts +800 -3
  228. package/src/apps/data/routes/indexer.ts +2 -1
  229. package/src/apps/data/routes/rpc.ts +2 -0
  230. package/src/apps/data/routes/tokens.test.ts +6 -8
  231. package/src/apps/funding/Access.ts +26 -0
  232. package/src/apps/funding/App.test.ts +78 -0
  233. package/src/apps/funding/App.ts +63 -0
  234. package/src/apps/funding/Inventory.ts +30 -0
  235. package/src/apps/funding/ProviderAttempts.ts +26 -0
  236. package/src/apps/funding/index.ts +1 -0
  237. package/src/apps/funding/routes/chains.test.ts +78 -0
  238. package/src/apps/funding/routes/chains.ts +101 -0
  239. package/src/apps/funding/routes/deposit-addresses.test.ts +1628 -0
  240. package/src/apps/funding/routes/deposit-addresses.ts +1015 -0
  241. package/src/apps/funding/routes/providers.test.ts +165 -0
  242. package/src/apps/funding/routes/providers.ts +73 -0
  243. package/src/apps/funding/routes/quotes.test.ts +511 -0
  244. package/src/apps/funding/routes/quotes.ts +205 -0
  245. package/src/apps/funding/routes/transfers.test.ts +1255 -0
  246. package/src/apps/funding/routes/transfers.ts +704 -0
  247. package/src/apps/index.ts +1 -0
  248. package/src/apps/management/routes/api-keys.test.ts +98 -0
  249. package/src/apps/management/routes/api-keys.ts +96 -1
  250. package/src/apps/mcp.test.ts +17 -2
  251. package/src/apps/mcp.ts +18 -13
  252. package/src/apps/metadata.ts +12 -0
  253. package/src/apps/mpp/internal/Policy.test.ts +7 -1
  254. package/src/cloudflare.ts +279 -0
  255. package/src/db/Db.ts +62 -0
  256. package/src/db/Schema.ts +24 -0
  257. package/src/db/tables/fundingDepositAddresses.ts +28 -0
  258. package/src/db/tables/fundingDepositRequestObservations.test.ts +106 -0
  259. package/src/db/tables/fundingDepositRequestObservations.ts +99 -0
  260. package/src/db/tables/fundingDeposits.test.ts +138 -0
  261. package/src/db/tables/fundingDeposits.ts +145 -1
  262. package/src/internal/Auth.test.ts +177 -5
  263. package/src/internal/Auth.ts +48 -18
  264. package/src/internal/Cache.test.ts +37 -3
  265. package/src/internal/Cache.ts +1 -1
  266. package/src/internal/EdgeCache.test.ts +7 -7
  267. package/src/internal/EdgeCache.ts +13 -4
  268. package/src/internal/IpAllowlist.test.ts +52 -0
  269. package/src/internal/IpAllowlist.ts +39 -0
  270. package/src/internal/Log.test.ts +7 -1
  271. package/src/internal/OpenApi.ts +1 -1
  272. package/src/internal/Tidx.test.ts +28 -0
  273. package/src/internal/Tidx.ts +19 -3
  274. package/src/internal/Viem.test.ts +28 -0
  275. package/src/internal/Viem.ts +21 -5
  276. package/src/internal/funding/Deposit.ts +74 -5
  277. package/src/internal/funding/Evidence.test.ts +2 -2
  278. package/src/internal/funding/Evidence.ts +4 -9
  279. package/src/internal/funding/Provider.ts +115 -0
  280. package/src/internal/funding/Reconciliation.local.test.ts +7 -0
  281. package/src/internal/funding/Reconciliation.test.ts +1052 -35
  282. package/src/internal/funding/Reconciliation.ts +328 -56
  283. package/src/internal/funding/SourceObservation.test.ts +301 -0
  284. package/src/internal/funding/SourceObservation.ts +267 -0
  285. package/src/internal/funding/Subsidy.ts +2 -2
  286. package/src/internal/funding/index.test.ts +1 -0
  287. package/src/internal/funding/index.ts +1 -0
  288. package/src/internal/funding/providers/relay.test.ts +94 -4
  289. package/src/internal/funding/providers/relay.ts +528 -27
  290. package/dist/apps/data/routes/funding.d.ts.map +0 -1
  291. package/dist/apps/data/routes/funding.js +0 -1557
  292. package/dist/apps/data/routes/funding.js.map +0 -1
  293. package/src/apps/data/routes/funding.test.ts +0 -2994
  294. 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,12 @@ 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 followUpDelaySeconds = { delivery: 1, provider: 5, settlement: 2 } as const satisfies Record<
17
+ Provider.Webhook.receive.FollowUp['kind'],
18
+ number
19
+ >
15
20
  const maximumPages = 100
21
+ const maximumFollowUpAttempts = 6
16
22
 
17
23
  /** Verified token transfer returned by an authoritative chain reader. */
18
24
  export type TransferEvidence = {
@@ -52,10 +58,11 @@ export function createTracker(options: createTracker.Options): createTracker.Tra
52
58
  const leaseMs = options.leaseMs ?? defaultLeaseMs
53
59
  const pollIntervalMs = options.pollIntervalMs ?? defaultPollIntervalMs
54
60
 
55
- const record = (outcome: string, providerId: string) => {
61
+ const record = (outcome: string, providerId: string, trigger: Deposit.DetectionTrigger) => {
56
62
  options.metrics?.count('funding_deposit_reconciliation_count', 1, {
57
63
  outcome,
58
64
  provider: providerId,
65
+ trigger,
59
66
  })
60
67
  }
61
68
 
@@ -63,6 +70,12 @@ export function createTracker(options: createTracker.Options): createTracker.Tra
63
70
  async reconcile(message) {
64
71
  const database = Db.get(options.db)
65
72
  const startedAt = now()
73
+ // Legacy poll messages carry a lease version; legacy webhook messages do not.
74
+ const trigger = (() => {
75
+ if (message.trigger) return message.trigger
76
+ if (message.pollLeaseVersion !== undefined) return 'poll'
77
+ return 'webhook'
78
+ })()
66
79
  const claimed = await claim(database, message, {
67
80
  leaseUntil: new Date(startedAt.getTime() + leaseMs).toISOString(),
68
81
  now: startedAt.toISOString(),
@@ -72,7 +85,7 @@ export function createTracker(options: createTracker.Options): createTracker.Tra
72
85
  const provider = options.providers.find((candidate) => candidate.id === claimed.providerId)
73
86
  if (!provider || !Provider.canListDepositAddressRequests(provider)) {
74
87
  await fail(database, claimed, startedAt, pollIntervalMs)
75
- record('provider_unavailable', claimed.providerId)
88
+ record('provider_unavailable', claimed.providerId, trigger)
76
89
  return { type: 'provider_unavailable' }
77
90
  }
78
91
 
@@ -84,24 +97,58 @@ export function createTracker(options: createTracker.Options): createTracker.Tra
84
97
  requestId: message.requestId,
85
98
  signal: options.signal,
86
99
  })
100
+ if (trigger === 'poll')
101
+ await FundingDepositRequestObservations.observePoll(database, {
102
+ depositAddressId: claimed.id,
103
+ observedAt: now().toISOString(),
104
+ providerRequestIds: requests.requests.map((request) => request.id),
105
+ })
106
+ const webhookUpdateObserved = (() => {
107
+ const timing = message.webhookTiming
108
+ if (!timing) return true
109
+ if (!message.requestId) return false
110
+ // Relay can send a webhook before its requests API indexes the same update.
111
+ return requests.requests.some(
112
+ (request) =>
113
+ request.id === message.requestId && request.updatedAt >= timing.providerUpdatedAt,
114
+ )
115
+ })()
87
116
  heartbeat.assert()
88
117
  let created = 0
89
118
  let updated = 0
90
- const pendingRequestIds = new Set(requests.pendingRequestIds)
119
+ const pendingRequestIds = new Set([
120
+ ...requests.pendingRequestIds,
121
+ ...(message.requestId ? [message.requestId] : []),
122
+ ])
123
+ let followUpKind: Provider.Webhook.receive.FollowUp['kind'] | undefined
91
124
  for (const request of requests.requests) {
92
125
  const result = await reconcileRequest(database, {
93
126
  address: claimed,
94
127
  metrics: options.metrics,
128
+ provider,
95
129
  request,
130
+ signal: options.signal,
96
131
  subsidies: options.subsidies,
132
+ trigger,
97
133
  verifyTransfers: options.verifyTransfers,
98
134
  })
99
135
  heartbeat.assert()
100
136
  created += result.created
101
137
  updated += result.updated
102
138
  if (result.terminal) pendingRequestIds.delete(request.id)
103
- else pendingRequestIds.add(request.id)
139
+ else {
140
+ pendingRequestIds.add(request.id)
141
+ followUpKind = result.followUp ?? followUpKind ?? 'provider'
142
+ }
104
143
  }
144
+ const awaitsProvider = await FundingDeposits.hasUnattributed(database, claimed.id)
145
+ if (message.requestId && message.webhookTiming && !webhookUpdateObserved)
146
+ pendingRequestIds.add(message.requestId)
147
+ if (created > 0)
148
+ options.metrics?.count('funding_deposit_detection_count', created, {
149
+ provider: claimed.providerId,
150
+ trigger,
151
+ })
105
152
 
106
153
  const leaseFailure = await heartbeat.stop()
107
154
  if (leaseFailure !== undefined) throw leaseFailure
@@ -112,22 +159,65 @@ export function createTracker(options: createTracker.Options): createTracker.Tra
112
159
  nextPollAt: new Date(completedAt.getTime() + pollIntervalMs).toISOString(),
113
160
  pollLeaseVersion: claimed.pollLeaseVersion,
114
161
  providerRequestIds: claimed.providerRequestIds,
115
- providerState: reconciliationState(claimed.providerState, requests, [
116
- ...pendingRequestIds,
117
- ]),
162
+ providerState: reconciliationState(claimed.providerState, {
163
+ pendingRequestIds: [...pendingRequestIds],
164
+ requests,
165
+ ...(message.webhookTiming && message.requestId && webhookUpdateObserved
166
+ ? {
167
+ webhookTiming: {
168
+ ...message.webhookTiming,
169
+ reconciliationCompletedAt: completedAt.toISOString(),
170
+ requestId: message.requestId,
171
+ },
172
+ }
173
+ : {}),
174
+ }),
118
175
  })
119
176
  if (!stored) return { type: 'stale' }
120
- record('completed', claimed.providerId)
177
+ if (pendingRequestIds.size > 0 || awaitsProvider) {
178
+ const followUp = nextFollowUp(message, followUpKind ?? 'provider')
179
+ if (followUp)
180
+ await options
181
+ .dispatch(
182
+ {
183
+ addressId: claimed.id,
184
+ followUp: followUp.state,
185
+ ...(message.requestId ? { requestId: message.requestId } : {}),
186
+ trigger,
187
+ type: 'funding:deposit-address:reconcile',
188
+ ...(message.webhookTiming && !webhookUpdateObserved
189
+ ? { webhookTiming: message.webhookTiming }
190
+ : {}),
191
+ },
192
+ { delaySeconds: followUp.delaySeconds },
193
+ )
194
+ .catch((cause) => {
195
+ record('follow_up_enqueue_failed', claimed.providerId, trigger)
196
+ throw cause
197
+ })
198
+ record(
199
+ followUp ? 'follow_up_enqueued' : 'follow_up_exhausted',
200
+ claimed.providerId,
201
+ trigger,
202
+ )
203
+ }
204
+ if (message.webhookTiming && webhookUpdateObserved)
205
+ recordWebhookLatency(options.metrics, {
206
+ completedAt,
207
+ providerId: claimed.providerId,
208
+ timing: message.webhookTiming,
209
+ })
210
+ record('completed', claimed.providerId, trigger)
121
211
  options.metrics?.histogram(
122
212
  'funding_deposit_reconciliation_duration_ms',
123
213
  completedAt.getTime() - startedAt.getTime(),
124
- { provider: claimed.providerId },
214
+ { provider: claimed.providerId, trigger },
125
215
  )
126
216
  return { created, type: 'completed', updated }
127
217
  } catch (cause) {
128
218
  const leaseFailure = await heartbeat.stop()
129
219
  await fail(database, claimed, now(), pollIntervalMs)
130
- record('failed', claimed.providerId)
220
+ record('failed', claimed.providerId, trigger)
131
221
  throw leaseFailure ?? cause
132
222
  } finally {
133
223
  options.metrics?.flush()
@@ -159,13 +249,14 @@ export function createTracker(options: createTracker.Options): createTracker.Tra
159
249
  await options.dispatch({
160
250
  addressId: address.id,
161
251
  pollLeaseVersion: address.pollLeaseVersion,
252
+ trigger: 'poll',
162
253
  type: 'funding:deposit-address:reconcile',
163
254
  })
164
- record('enqueued', provider.id)
255
+ record('enqueued', provider.id, 'poll')
165
256
  } catch {
166
257
  failed += 1
167
258
  await fail(database, address, startedAt, pollIntervalMs)
168
- record('enqueue_failed', provider.id)
259
+ record('enqueue_failed', provider.id, 'poll')
169
260
  }
170
261
  }
171
262
  if (claimed >= batchSize) break
@@ -184,7 +275,10 @@ export declare namespace createTracker {
184
275
  /** Authoritative database or per-invocation database factory. */
185
276
  db: Db.Source
186
277
  /** Durable reconciliation queue transport. */
187
- dispatch: (message: Provider.Webhook.receive.Dispatchable) => Promise<void>
278
+ dispatch: (
279
+ message: Provider.Webhook.receive.Dispatchable,
280
+ options?: createTracker.DispatchOptions,
281
+ ) => Promise<void>
188
282
  /** Duration of one reconciliation lease. */
189
283
  leaseMs?: number | undefined
190
284
  /** Optional bounded operational metrics sink. */
@@ -203,6 +297,12 @@ export declare namespace createTracker {
203
297
  verifyTransfers: VerifyTransfers
204
298
  }
205
299
 
300
+ /** Delivery options for one reconciliation queue message. */
301
+ type DispatchOptions = {
302
+ /** Delay before the queue makes the message available. */
303
+ delaySeconds: number
304
+ }
305
+
206
306
  /** Result of reconciling one queue message. */
207
307
  type ReconcileResult =
208
308
  | { type: 'busy' | 'provider_unavailable' | 'stale' }
@@ -211,7 +311,7 @@ export declare namespace createTracker {
211
311
  /** Scheduled and queue-driven reusable-address tracker. */
212
312
  type Tracker = {
213
313
  /** Reconciles one address from provider discovery and verified chain evidence. */
214
- reconcile(message: Provider.Webhook.receive.Dispatchable): Promise<ReconcileResult>
314
+ reconcile(message: Provider.Webhook.receive.Queued): Promise<ReconcileResult>
215
315
  /** Claims and enqueues one bounded batch of due reusable addresses. */
216
316
  tick(): Promise<{ claimed: number; failed: number }>
217
317
  }
@@ -222,11 +322,7 @@ type ClaimOptions = {
222
322
  now: string
223
323
  }
224
324
 
225
- async function claim(
226
- db: Db.Db,
227
- message: Provider.Webhook.receive.Dispatchable,
228
- options: ClaimOptions,
229
- ) {
325
+ async function claim(db: Db.Db, message: Provider.Webhook.receive.Queued, options: ClaimOptions) {
230
326
  if (message.pollLeaseVersion === undefined)
231
327
  return FundingDepositAddresses.claim(db, {
232
328
  id: message.addressId,
@@ -421,18 +517,42 @@ function readHighWater(state: FundingDepositAddresses.Record['providerState']):
421
517
  }
422
518
  }
423
519
 
520
+ type ReconciledWebhookTiming = Provider.Webhook.receive.WebhookTiming & {
521
+ /** When Tempo completed the webhook-triggered reconciliation. */
522
+ reconciliationCompletedAt: string
523
+ /** Provider request matched to the observed update. */
524
+ requestId: string
525
+ }
526
+
527
+ type ReconciliationStateOptions = {
528
+ /** Provider requests that still require reconciliation. */
529
+ pendingRequestIds: readonly string[]
530
+ /** Provider requests and high-water state observed in this pass. */
531
+ requests: Requests
532
+ /** Completed webhook timing retained for diagnostics. */
533
+ webhookTiming?: ReconciledWebhookTiming | undefined
534
+ }
535
+
424
536
  function reconciliationState(
425
537
  state: FundingDepositAddresses.Record['providerState'],
426
- requests: Requests,
427
- pendingRequestIds: readonly string[],
538
+ options: ReconciliationStateOptions,
428
539
  ) {
429
540
  const current = state && typeof state === 'object' ? state : {}
541
+ const reconciliation = (() => {
542
+ const value = current['reconciliation']
543
+ if (value && typeof value === 'object') return value
544
+ return {}
545
+ })()
430
546
  return {
431
547
  ...current,
432
548
  reconciliation: {
433
- pendingRequestIds,
434
- requestIds: requests.highWaterRequestIds,
435
- ...(requests.highWaterUpdatedAt ? { updatedAt: requests.highWaterUpdatedAt } : {}),
549
+ ...reconciliation,
550
+ pendingRequestIds: options.pendingRequestIds,
551
+ requestIds: options.requests.highWaterRequestIds,
552
+ ...(options.requests.highWaterUpdatedAt
553
+ ? { updatedAt: options.requests.highWaterUpdatedAt }
554
+ : {}),
555
+ ...(options.webhookTiming ? { webhookTiming: options.webhookTiming } : {}),
436
556
  },
437
557
  }
438
558
  }
@@ -446,49 +566,120 @@ type ObservedSourceTransaction = {
446
566
  type VerifiedDeposit = {
447
567
  deposit: FundingDeposits.Record
448
568
  required: bigint
569
+ source: TransferEvidence
449
570
  sourceAmount: bigint
450
571
  }
451
572
 
573
+ async function observeProviderDelivery(
574
+ options: reconcileRequest.Options,
575
+ verified: readonly VerifiedDeposit[],
576
+ ) {
577
+ const [entry] = verified
578
+ if (
579
+ !entry ||
580
+ verified.length !== 1 ||
581
+ options.address.deliveryStrategy !== 'provider' ||
582
+ options.address.providerOutputToken.tokenKey !==
583
+ options.address.snapshot.destinationToken.tokenKey ||
584
+ (options.request.status === 'success' &&
585
+ options.request.destinationTransactionHashes.length > 0) ||
586
+ !options.provider ||
587
+ !Provider.canObserveDepositAddressRequest(options.provider)
588
+ )
589
+ return undefined
590
+
591
+ const result = await options.provider
592
+ .observeDepositAddressRequest(
593
+ {
594
+ address: options.address.address,
595
+ destinationChain: options.address.snapshot.destinationChain,
596
+ destinationToken: options.address.providerOutputToken,
597
+ providerState: entry.deposit.providerState ?? undefined,
598
+ recipient: options.address.recipient,
599
+ refundAddress: options.address.refundAddress,
600
+ request: options.request,
601
+ source: entry.source,
602
+ sourceChain: options.address.snapshot.sourceChain,
603
+ sourceToken: options.address.snapshot.sourceToken,
604
+ },
605
+ options.signal ?? new AbortController().signal,
606
+ )
607
+ .catch((cause) => {
608
+ if (options.signal?.aborted) throw cause
609
+ return undefined
610
+ })
611
+ options.metrics?.count('funding_deposit_delivery_observation_count', 1, {
612
+ outcome: result?.type ?? 'failed',
613
+ provider: options.address.providerId,
614
+ })
615
+ return result
616
+ }
617
+
452
618
  /** Reconciles one provider request against verified chain evidence. */
453
619
  export async function reconcileRequest(
454
620
  db: Db.Db,
455
621
  options: reconcileRequest.Options,
456
622
  ): Promise<reconcileRequest.ReturnType> {
623
+ const trigger = options.trigger ?? 'poll'
624
+ const observation = await FundingDepositRequestObservations.get(db, {
625
+ depositAddressId: options.address.id,
626
+ providerRequestId: options.request.id,
627
+ })
628
+ if (observation)
629
+ await FundingDeposits.recordRequestObservation(db, {
630
+ depositAddressId: observation.depositAddressId,
631
+ ...(observation.pollObservedAt ? { pollObservedAt: observation.pollObservedAt } : {}),
632
+ providerRequestId: observation.providerRequestId,
633
+ ...(observation.webhookReceivedAt
634
+ ? { webhookReceivedAt: observation.webhookReceivedAt }
635
+ : {}),
636
+ })
457
637
  let created = 0
638
+ let followUp: reconcileRequest.ReturnType['followUp']
458
639
  const observed: ObservedSourceTransaction[] = []
459
640
  // Persist provider observations first so transient chain failures do not hide acknowledged deposits.
460
641
  for (const transactionHash of unique(options.request.sourceTransactionHashes)) {
461
- let deposits = await FundingDeposits.listByProviderObservation(db, {
642
+ const deposits = await FundingDeposits.withSourceTransaction(db, {
462
643
  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),
644
+ fn: async (tx): Promise<FundingDeposits.Record[]> => {
645
+ let records = await FundingDeposits.listByProviderObservation(tx, {
646
+ depositAddressId: options.address.id,
475
647
  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
648
  sourceTransactionHash: transactionHash,
488
- }),
489
- ]
490
- created += 1
491
- }
649
+ })
650
+ if (records.length === 0)
651
+ records = await FundingDeposits.listBySourceTransaction(tx, {
652
+ depositAddressId: options.address.id,
653
+ sourceTransactionHash: transactionHash,
654
+ })
655
+ if (records.length > 0) return records
656
+ created += 1
657
+ return [
658
+ await Deposit.create(tx, {
659
+ detectionTrigger: trigger,
660
+ now: new Date(options.request.createdAt),
661
+ ...(observation?.pollObservedAt ? { pollObservedAt: observation.pollObservedAt } : {}),
662
+ providerRequestId: options.request.id,
663
+ snapshot: {
664
+ depositAddressId: options.address.id,
665
+ destinationChain: options.address.snapshot.destinationChain,
666
+ destinationToken: options.address.snapshot.destinationToken,
667
+ provider: options.address.snapshot.provider,
668
+ recipient: options.address.recipient,
669
+ refundAddress: options.address.refundAddress,
670
+ sourceChain: options.address.snapshot.sourceChain,
671
+ sourceToken: options.address.snapshot.sourceToken,
672
+ sourceTransactionHashes: [transactionHash],
673
+ },
674
+ sourceTransactionHash: transactionHash,
675
+ ...(observation?.webhookReceivedAt
676
+ ? { webhookReceivedAt: observation.webhookReceivedAt }
677
+ : {}),
678
+ }),
679
+ ]
680
+ },
681
+ sourceTransactionHash: transactionHash,
682
+ })
492
683
  observed.push({ deposits, transactionHash, transfers: [] })
493
684
  }
494
685
 
@@ -539,7 +730,9 @@ export async function reconcileRequest(
539
730
  )
540
731
  if (!deposit) {
541
732
  deposit = await Deposit.create(db, {
733
+ detectionTrigger: trigger,
542
734
  now: new Date(options.request.createdAt),
735
+ ...(observation?.pollObservedAt ? { pollObservedAt: observation.pollObservedAt } : {}),
543
736
  providerRequestId: options.request.id,
544
737
  providerTransferIndex,
545
738
  snapshot: {
@@ -558,6 +751,9 @@ export async function reconcileRequest(
558
751
  },
559
752
  sourceTransactionHash: transfer.transactionHash,
560
753
  sourceTransferIndex: transfer.transferIndex,
754
+ ...(observation?.webhookReceivedAt
755
+ ? { webhookReceivedAt: observation.webhookReceivedAt }
756
+ : {}),
561
757
  })
562
758
  source.deposits.push(deposit)
563
759
  created += 1
@@ -566,7 +762,9 @@ export async function reconcileRequest(
566
762
  const result = await Deposit.transition(db, {
567
763
  expectedVersion: deposit.version,
568
764
  id: deposit.id,
765
+ providerRequestId: options.request.id,
569
766
  providerRequestIds: unique([...deposit.providerRequestIds, options.request.id]),
767
+ providerTransferIndex,
570
768
  snapshot: {
571
769
  ...deposit.snapshot,
572
770
  ...(options.address.snapshot.subsidize ? { destinationAmountRequired } : {}),
@@ -581,14 +779,22 @@ export async function reconcileRequest(
581
779
  updatedIds.add(deposit.id)
582
780
  }
583
781
  }
584
- verified.push({ deposit, required, sourceAmount: BigInt(transfer.amount) })
782
+ verified.push({ deposit, required, source: transfer, sourceAmount: BigInt(transfer.amount) })
585
783
  }
586
784
  }
587
785
  if (verified.length === 0) return { created, terminal: false, updated: updatedIds.size }
588
786
  if (!sourceEvidenceComplete) return { created, terminal: false, updated: updatedIds.size }
589
787
 
788
+ const deliveryObservation = await observeProviderDelivery(options, verified)
789
+ if (deliveryObservation?.type === 'pending') followUp = 'delivery'
790
+ const destinationTransactionHashes = unique([
791
+ ...options.request.destinationTransactionHashes,
792
+ ...(deliveryObservation?.type === 'verified'
793
+ ? deliveryObservation.destinationTransactionHashes
794
+ : []),
795
+ ])
590
796
  const destinationEvidence = await Promise.all(
591
- options.request.destinationTransactionHashes.map((transactionHash) =>
797
+ destinationTransactionHashes.map((transactionHash) =>
592
798
  options.verifyTransfers({
593
799
  chain: options.address.snapshot.destinationChain,
594
800
  recipient: options.address.recipient,
@@ -599,7 +805,8 @@ export async function reconcileRequest(
599
805
  )
600
806
  const destinationTransfers = destinationEvidence.flat()
601
807
  const providerEvidenceComplete =
602
- options.request.status === 'success' &&
808
+ (options.request.status === 'success' || deliveryObservation?.type === 'verified') &&
809
+ destinationEvidence.length > 0 &&
603
810
  destinationEvidence.every((transfers) => transfers.length > 0)
604
811
  const providerOutput = destinationTransfers.reduce(
605
812
  (total, transfer) => total + BigInt(transfer.amount),
@@ -635,6 +842,7 @@ export async function reconcileRequest(
635
842
  provider: options.address.providerId,
636
843
  })
637
844
  if (settlement.status === 'bridging' || settlement.status === 'settling') terminal = false
845
+ if (settlement.status === 'settling') followUp = 'settlement'
638
846
  const destinationAmount = Value.tokenAmount({
639
847
  baseUnits: delivered + settlement.delivered,
640
848
  currency: options.address.snapshot.destinationToken.currency,
@@ -653,9 +861,18 @@ export async function reconcileRequest(
653
861
  }
654
862
  : {}),
655
863
  providerRequestIds: unique([...deposit.providerRequestIds, options.request.id]),
864
+ ...(deliveryObservation?.providerState
865
+ ? {
866
+ providerState: {
867
+ ...deposit.providerState,
868
+ ...deliveryObservation.providerState,
869
+ },
870
+ }
871
+ : {}),
656
872
  providerTransactionHashes: unique([
657
873
  ...deposit.providerTransactionHashes,
658
874
  ...options.request.destinationTransactionHashes,
875
+ ...(deliveryObservation?.providerTransactionHashes ?? []),
659
876
  ]),
660
877
  ...(settlement.settlementTransaction
661
878
  ? { settlementTransaction: settlement.settlementTransaction }
@@ -697,7 +914,7 @@ export async function reconcileRequest(
697
914
  }
698
915
  }
699
916
  }
700
- return { created, terminal, updated: updatedIds.size }
917
+ return { created, ...(followUp ? { followUp } : {}), terminal, updated: updatedIds.size }
701
918
  }
702
919
 
703
920
  export declare namespace reconcileRequest {
@@ -707,10 +924,16 @@ export declare namespace reconcileRequest {
707
924
  address: FundingDepositAddresses.Record
708
925
  /** Optional bounded operational metrics sink. */
709
926
  metrics?: Metrics.Metrics | undefined
927
+ /** Provider integration that discovered the request. */
928
+ provider?: Provider.Provider | undefined
710
929
  /** Provider request containing discovered chain transactions. */
711
930
  request: Provider.listDepositAddressRequests.ReturnType['requests'][number]
931
+ /** Abort signal forwarded to provider delivery observation. */
932
+ signal?: AbortSignal | undefined
712
933
  /** Optional pathUSD-funded subsidy settler. */
713
934
  subsidies?: Subsidy.Settler | undefined
935
+ /** Mechanism that requested reconciliation. */
936
+ trigger?: Deposit.DetectionTrigger | undefined
714
937
  /** Authoritative source and destination transfer verifier. */
715
938
  verifyTransfers: VerifyTransfers
716
939
  }
@@ -719,6 +942,8 @@ export declare namespace reconcileRequest {
719
942
  type ReturnType = {
720
943
  /** Number of newly detected source transfers. */
721
944
  created: number
945
+ /** Work that needs a bounded delayed follow-up. */
946
+ followUp?: 'delivery' | 'settlement' | undefined
722
947
  /** Whether every deposit from this request reached a terminal state. */
723
948
  terminal: boolean
724
949
  /** Number of existing or new deposits materially changed. */
@@ -919,6 +1144,53 @@ function unique(values: readonly string[]) {
919
1144
  return [...new Set(values)]
920
1145
  }
921
1146
 
1147
+ function nextFollowUp(
1148
+ message: Provider.Webhook.receive.Queued,
1149
+ kind: Provider.Webhook.receive.FollowUp['kind'],
1150
+ ) {
1151
+ const attempt = message.followUp?.kind === kind ? message.followUp.attempt + 1 : 0
1152
+ if (attempt >= maximumFollowUpAttempts) return undefined
1153
+ return { delaySeconds: followUpDelaySeconds[kind], state: { attempt, kind } }
1154
+ }
1155
+
1156
+ type RecordWebhookLatencyOptions = {
1157
+ /** When Tempo completed the webhook-triggered reconciliation. */
1158
+ completedAt: Date
1159
+ /** Funding provider that sent the webhook. */
1160
+ providerId: string
1161
+ /** Authenticated timestamps carried from webhook receipt. */
1162
+ timing: Provider.Webhook.receive.WebhookTiming
1163
+ }
1164
+
1165
+ function recordWebhookLatency(
1166
+ metrics: Metrics.Metrics | undefined,
1167
+ options: RecordWebhookLatencyOptions,
1168
+ ) {
1169
+ if (!metrics) return
1170
+ const completedAt = options.completedAt.getTime()
1171
+ const enqueuedAt = Date.parse(options.timing.enqueuedAt)
1172
+ const providerUpdatedAt = Date.parse(options.timing.providerUpdatedAt)
1173
+ const receivedAt = Date.parse(options.timing.receivedAt)
1174
+ const sentAt = Date.parse(options.timing.sentAt)
1175
+ const segments = [
1176
+ { from: providerUpdatedAt, segment: 'provider_to_sent', to: sentAt },
1177
+ { from: sentAt, segment: 'sent_to_received', to: receivedAt },
1178
+ { from: receivedAt, segment: 'received_to_enqueued', to: enqueuedAt },
1179
+ { from: enqueuedAt, segment: 'enqueued_to_reconciled', to: completedAt },
1180
+ { from: providerUpdatedAt, segment: 'provider_to_reconciled', to: completedAt },
1181
+ ] as const
1182
+ for (const segment of segments)
1183
+ metrics.histogram(
1184
+ 'funding_deposit_webhook_latency_ms',
1185
+ Math.max(0, segment.to - segment.from),
1186
+ {
1187
+ provider: options.providerId,
1188
+ segment: segment.segment,
1189
+ status: options.timing.providerStatus,
1190
+ },
1191
+ )
1192
+ }
1193
+
922
1194
  /** Provider pagination exceeded the bounded reconciliation limit. */
923
1195
  export class PaginationLimitError extends Error {
924
1196
  override name = 'Funding.Reconciliation.PaginationLimitError'