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
@@ -1,6 +1,7 @@
1
1
  import { Hex } from 'ox';
2
2
  import * as Db from '../../db/Db.js';
3
3
  import * as FundingDepositAddresses from '../../db/tables/fundingDepositAddresses.js';
4
+ import * as FundingDepositRequestObservations from '../../db/tables/fundingDepositRequestObservations.js';
4
5
  import * as FundingDeposits from '../../db/tables/fundingDeposits.js';
5
6
  import * as Value from '../Value.js';
6
7
  import * as Deposit from './Deposit.js';
@@ -9,23 +10,36 @@ import * as Subsidy from './Subsidy.js';
9
10
  const defaultBatchSize = 25;
10
11
  const defaultLeaseMs = 2 * 60_000;
11
12
  const defaultPollIntervalMs = 60_000;
13
+ const chainProviderFollowUpDelaySeconds = 1;
14
+ const chainProviderMaximumFollowUpAttempts = 30;
15
+ const followUpDelaySeconds = { delivery: 1, provider: 5, settlement: 2 };
12
16
  const maximumPages = 100;
17
+ const maximumFollowUpAttempts = 6;
13
18
  /** Creates a reusable-address reconciliation tracker. */
14
19
  export function createTracker(options) {
15
20
  const now = options.now ?? (() => new Date());
16
21
  const batchSize = options.batchSize ?? defaultBatchSize;
17
22
  const leaseMs = options.leaseMs ?? defaultLeaseMs;
18
23
  const pollIntervalMs = options.pollIntervalMs ?? defaultPollIntervalMs;
19
- const record = (outcome, providerId) => {
24
+ const record = (outcome, providerId, trigger) => {
20
25
  options.metrics?.count('funding_deposit_reconciliation_count', 1, {
21
26
  outcome,
22
27
  provider: providerId,
28
+ trigger,
23
29
  });
24
30
  };
25
31
  return {
26
32
  async reconcile(message) {
27
33
  const database = Db.get(options.db);
28
34
  const startedAt = now();
35
+ // Legacy poll messages carry a lease version; legacy webhook messages do not.
36
+ const trigger = (() => {
37
+ if (message.trigger)
38
+ return message.trigger;
39
+ if (message.pollLeaseVersion !== undefined)
40
+ return 'poll';
41
+ return 'webhook';
42
+ })();
29
43
  const claimed = await claim(database, message, {
30
44
  leaseUntil: new Date(startedAt.getTime() + leaseMs).toISOString(),
31
45
  now: startedAt.toISOString(),
@@ -35,7 +49,7 @@ export function createTracker(options) {
35
49
  const provider = options.providers.find((candidate) => candidate.id === claimed.providerId);
36
50
  if (!provider || !Provider.canListDepositAddressRequests(provider)) {
37
51
  await fail(database, claimed, startedAt, pollIntervalMs);
38
- record('provider_unavailable', claimed.providerId);
52
+ record('provider_unavailable', claimed.providerId, trigger);
39
53
  return { type: 'provider_unavailable' };
40
54
  }
41
55
  const heartbeat = maintainLease(database, claimed, { leaseMs, now });
@@ -46,16 +60,38 @@ export function createTracker(options) {
46
60
  requestId: message.requestId,
47
61
  signal: options.signal,
48
62
  });
63
+ if (trigger === 'poll')
64
+ await FundingDepositRequestObservations.observePoll(database, {
65
+ depositAddressId: claimed.id,
66
+ observedAt: now().toISOString(),
67
+ providerRequestIds: requests.requests.map((request) => request.id),
68
+ });
69
+ const webhookUpdateObserved = (() => {
70
+ const timing = message.webhookTiming;
71
+ if (!timing)
72
+ return true;
73
+ if (!message.requestId)
74
+ return false;
75
+ // Relay can send a webhook before its requests API indexes the same update.
76
+ return requests.requests.some((request) => request.id === message.requestId && request.updatedAt >= timing.providerUpdatedAt);
77
+ })();
49
78
  heartbeat.assert();
50
79
  let created = 0;
51
80
  let updated = 0;
52
- const pendingRequestIds = new Set(requests.pendingRequestIds);
81
+ const pendingRequestIds = new Set([
82
+ ...requests.pendingRequestIds,
83
+ ...(message.requestId ? [message.requestId] : []),
84
+ ]);
85
+ let followUpKind;
53
86
  for (const request of requests.requests) {
54
87
  const result = await reconcileRequest(database, {
55
88
  address: claimed,
56
89
  metrics: options.metrics,
90
+ provider,
57
91
  request,
92
+ signal: options.signal,
58
93
  subsidies: options.subsidies,
94
+ trigger,
59
95
  verifyTransfers: options.verifyTransfers,
60
96
  });
61
97
  heartbeat.assert();
@@ -63,9 +99,31 @@ export function createTracker(options) {
63
99
  updated += result.updated;
64
100
  if (result.terminal)
65
101
  pendingRequestIds.delete(request.id);
66
- else
102
+ else {
67
103
  pendingRequestIds.add(request.id);
104
+ followUpKind = result.followUp ?? followUpKind ?? 'provider';
105
+ }
68
106
  }
107
+ const unattributed = await reconcileUnattributed(database, {
108
+ address: claimed,
109
+ metrics: options.metrics,
110
+ provider,
111
+ signal: options.signal,
112
+ subsidies: options.subsidies,
113
+ verifyTransfers: options.verifyTransfers,
114
+ });
115
+ heartbeat.assert();
116
+ updated += unattributed.updatedIds.size;
117
+ if (!unattributed.terminal)
118
+ followUpKind = unattributed.followUp ?? followUpKind ?? 'provider';
119
+ const awaitsProvider = await FundingDeposits.hasUnattributed(database, claimed.id);
120
+ if (message.requestId && message.webhookTiming && !webhookUpdateObserved)
121
+ pendingRequestIds.add(message.requestId);
122
+ if (created > 0)
123
+ options.metrics?.count('funding_deposit_detection_count', created, {
124
+ provider: claimed.providerId,
125
+ trigger,
126
+ });
69
127
  const leaseFailure = await heartbeat.stop();
70
128
  if (leaseFailure !== undefined)
71
129
  throw leaseFailure;
@@ -76,20 +134,56 @@ export function createTracker(options) {
76
134
  nextPollAt: new Date(completedAt.getTime() + pollIntervalMs).toISOString(),
77
135
  pollLeaseVersion: claimed.pollLeaseVersion,
78
136
  providerRequestIds: claimed.providerRequestIds,
79
- providerState: reconciliationState(claimed.providerState, requests, [
80
- ...pendingRequestIds,
81
- ]),
137
+ providerState: reconciliationState(claimed.providerState, {
138
+ pendingRequestIds: [...pendingRequestIds],
139
+ requests,
140
+ ...(message.webhookTiming && message.requestId && webhookUpdateObserved
141
+ ? {
142
+ webhookTiming: {
143
+ ...message.webhookTiming,
144
+ reconciliationCompletedAt: completedAt.toISOString(),
145
+ requestId: message.requestId,
146
+ },
147
+ }
148
+ : {}),
149
+ }),
82
150
  });
83
151
  if (!stored)
84
152
  return { type: 'stale' };
85
- record('completed', claimed.providerId);
86
- options.metrics?.histogram('funding_deposit_reconciliation_duration_ms', completedAt.getTime() - startedAt.getTime(), { provider: claimed.providerId });
153
+ if (pendingRequestIds.size > 0 || awaitsProvider) {
154
+ const followUp = nextFollowUp(message, followUpKind ?? 'provider');
155
+ if (followUp)
156
+ await options
157
+ .dispatch({
158
+ addressId: claimed.id,
159
+ followUp: followUp.state,
160
+ ...(message.requestId ? { requestId: message.requestId } : {}),
161
+ trigger,
162
+ type: 'funding:deposit-address:reconcile',
163
+ ...(message.webhookTiming && !webhookUpdateObserved
164
+ ? { webhookTiming: message.webhookTiming }
165
+ : {}),
166
+ }, { delaySeconds: followUp.delaySeconds })
167
+ .catch((cause) => {
168
+ record('follow_up_enqueue_failed', claimed.providerId, trigger);
169
+ throw cause;
170
+ });
171
+ record(followUp ? 'follow_up_enqueued' : 'follow_up_exhausted', claimed.providerId, trigger);
172
+ }
173
+ if (message.webhookTiming && webhookUpdateObserved)
174
+ recordWebhookLatency(options.metrics, {
175
+ completedAt,
176
+ providerId: claimed.providerId,
177
+ timing: message.webhookTiming,
178
+ });
179
+ record('completed', claimed.providerId, trigger);
180
+ options.metrics?.histogram('funding_deposit_reconciliation_duration_ms', completedAt.getTime() - startedAt.getTime(), { provider: claimed.providerId, trigger });
87
181
  return { created, type: 'completed', updated };
88
182
  }
89
183
  catch (cause) {
90
184
  const leaseFailure = await heartbeat.stop();
91
185
  await fail(database, claimed, now(), pollIntervalMs);
92
- record('failed', claimed.providerId);
186
+ record('failed', claimed.providerId, trigger);
93
187
  throw leaseFailure ?? cause;
94
188
  }
95
189
  finally {
@@ -118,14 +212,15 @@ export function createTracker(options) {
118
212
  await options.dispatch({
119
213
  addressId: address.id,
120
214
  pollLeaseVersion: address.pollLeaseVersion,
215
+ trigger: 'poll',
121
216
  type: 'funding:deposit-address:reconcile',
122
217
  });
123
- record('enqueued', provider.id);
218
+ record('enqueued', provider.id, 'poll');
124
219
  }
125
220
  catch {
126
221
  failed += 1;
127
222
  await fail(database, address, startedAt, pollIntervalMs);
128
- record('enqueue_failed', provider.id);
223
+ record('enqueue_failed', provider.id, 'poll');
129
224
  }
130
225
  }
131
226
  if (claimed >= batchSize)
@@ -264,54 +359,170 @@ function readHighWater(state) {
264
359
  : {}),
265
360
  };
266
361
  }
267
- function reconciliationState(state, requests, pendingRequestIds) {
362
+ function reconciliationState(state, options) {
268
363
  const current = state && typeof state === 'object' ? state : {};
364
+ const reconciliation = (() => {
365
+ const value = current['reconciliation'];
366
+ if (value && typeof value === 'object')
367
+ return value;
368
+ return {};
369
+ })();
269
370
  return {
270
371
  ...current,
271
372
  reconciliation: {
272
- pendingRequestIds,
273
- requestIds: requests.highWaterRequestIds,
274
- ...(requests.highWaterUpdatedAt ? { updatedAt: requests.highWaterUpdatedAt } : {}),
373
+ ...reconciliation,
374
+ pendingRequestIds: options.pendingRequestIds,
375
+ requestIds: options.requests.highWaterRequestIds,
376
+ ...(options.requests.highWaterUpdatedAt
377
+ ? { updatedAt: options.requests.highWaterUpdatedAt }
378
+ : {}),
379
+ ...(options.webhookTiming ? { webhookTiming: options.webhookTiming } : {}),
275
380
  },
276
381
  };
277
382
  }
383
+ async function observeProviderDelivery(options) {
384
+ const { verified } = options;
385
+ const [entry] = verified;
386
+ if (!entry ||
387
+ verified.length !== 1 ||
388
+ options.address.deliveryStrategy !== 'provider' ||
389
+ options.address.providerOutputToken.tokenKey !==
390
+ options.address.snapshot.destinationToken.tokenKey ||
391
+ (options.request?.status === 'success' &&
392
+ options.request.destinationTransactionHashes.length > 0) ||
393
+ !options.provider ||
394
+ !Provider.canObserveDepositAddressRequest(options.provider))
395
+ return undefined;
396
+ const result = await options.provider
397
+ .observeDepositAddressRequest({
398
+ address: options.address.address,
399
+ destinationChain: options.address.snapshot.destinationChain,
400
+ destinationToken: options.address.providerOutputToken,
401
+ providerState: entry.deposit.providerState ?? undefined,
402
+ recipient: options.address.recipient,
403
+ refundAddress: options.address.refundAddress,
404
+ ...(options.request ? { request: options.request } : {}),
405
+ source: entry.source,
406
+ sourceChain: options.address.snapshot.sourceChain,
407
+ sourceToken: options.address.snapshot.sourceToken,
408
+ }, options.signal ?? new AbortController().signal)
409
+ .catch((cause) => {
410
+ if (options.signal?.aborted)
411
+ throw cause;
412
+ return undefined;
413
+ });
414
+ options.metrics?.count('funding_deposit_delivery_observation_count', 1, {
415
+ outcome: result?.type ?? 'failed',
416
+ provider: options.address.providerId,
417
+ });
418
+ return result;
419
+ }
420
+ async function reconcileUnattributed(db, options) {
421
+ const deposits = await FundingDeposits.listUnattributed(db, options.address.id);
422
+ if (deposits.length === 0)
423
+ return { terminal: true, updatedIds: new Set() };
424
+ const transfersByTransaction = new Map(await Promise.all(unique(deposits.map((deposit) => deposit.sourceTransactionHash)).map(async (transactionHash) => [
425
+ transactionHash,
426
+ await options.verifyTransfers({
427
+ chain: options.address.snapshot.sourceChain,
428
+ recipient: options.address.address,
429
+ token: options.address.snapshot.sourceToken,
430
+ transactionHash,
431
+ }),
432
+ ])));
433
+ let followUp;
434
+ let terminal = true;
435
+ const updatedIds = new Set();
436
+ for (const deposit of deposits) {
437
+ const source = transfersByTransaction
438
+ .get(deposit.sourceTransactionHash)
439
+ ?.find((transfer) => transfer.transferIndex === deposit.sourceTransferIndex);
440
+ if (!source) {
441
+ terminal = false;
442
+ continue;
443
+ }
444
+ const delivery = await reconcileDelivery(db, {
445
+ ...options,
446
+ verified: [
447
+ {
448
+ deposit,
449
+ required: destinationBaseUnits(BigInt(source.amount), options.address.snapshot.sourceToken.decimals, options.address.snapshot.destinationToken.decimals),
450
+ source,
451
+ sourceAmount: BigInt(source.amount),
452
+ },
453
+ ],
454
+ });
455
+ if (delivery.followUp === 'delivery' || !followUp)
456
+ followUp = delivery.followUp;
457
+ if (!delivery.terminal)
458
+ terminal = false;
459
+ for (const id of delivery.updatedIds)
460
+ updatedIds.add(id);
461
+ }
462
+ return { ...(followUp ? { followUp } : {}), terminal, updatedIds };
463
+ }
278
464
  /** Reconciles one provider request against verified chain evidence. */
279
465
  export async function reconcileRequest(db, options) {
466
+ const trigger = options.trigger ?? 'poll';
467
+ const observation = await FundingDepositRequestObservations.get(db, {
468
+ depositAddressId: options.address.id,
469
+ providerRequestId: options.request.id,
470
+ });
471
+ if (observation)
472
+ await FundingDeposits.recordRequestObservation(db, {
473
+ depositAddressId: observation.depositAddressId,
474
+ ...(observation.pollObservedAt ? { pollObservedAt: observation.pollObservedAt } : {}),
475
+ providerRequestId: observation.providerRequestId,
476
+ ...(observation.webhookReceivedAt
477
+ ? { webhookReceivedAt: observation.webhookReceivedAt }
478
+ : {}),
479
+ });
280
480
  let created = 0;
281
481
  const observed = [];
282
482
  // Persist provider observations first so transient chain failures do not hide acknowledged deposits.
283
483
  for (const transactionHash of unique(options.request.sourceTransactionHashes)) {
284
- let deposits = await FundingDeposits.listByProviderObservation(db, {
484
+ const deposits = await FundingDeposits.withSourceTransaction(db, {
285
485
  depositAddressId: options.address.id,
286
- providerRequestId: options.request.id,
287
- sourceTransactionHash: transactionHash,
288
- });
289
- if (deposits.length === 0)
290
- deposits = await FundingDeposits.listBySourceTransaction(db, {
291
- depositAddressId: options.address.id,
292
- sourceTransactionHash: transactionHash,
293
- });
294
- if (deposits.length === 0) {
295
- deposits = [
296
- await Deposit.create(db, {
297
- now: new Date(options.request.createdAt),
486
+ fn: async (tx) => {
487
+ let records = await FundingDeposits.listByProviderObservation(tx, {
488
+ depositAddressId: options.address.id,
298
489
  providerRequestId: options.request.id,
299
- snapshot: {
300
- depositAddressId: options.address.id,
301
- destinationChain: options.address.snapshot.destinationChain,
302
- destinationToken: options.address.snapshot.destinationToken,
303
- provider: options.address.snapshot.provider,
304
- recipient: options.address.recipient,
305
- refundAddress: options.address.refundAddress,
306
- sourceChain: options.address.snapshot.sourceChain,
307
- sourceToken: options.address.snapshot.sourceToken,
308
- sourceTransactionHashes: [transactionHash],
309
- },
310
490
  sourceTransactionHash: transactionHash,
311
- }),
312
- ];
313
- created += 1;
314
- }
491
+ });
492
+ if (records.length === 0)
493
+ records = await FundingDeposits.listBySourceTransaction(tx, {
494
+ depositAddressId: options.address.id,
495
+ sourceTransactionHash: transactionHash,
496
+ });
497
+ if (records.length > 0)
498
+ return records;
499
+ created += 1;
500
+ return [
501
+ await Deposit.create(tx, {
502
+ detectionTrigger: trigger,
503
+ now: new Date(options.request.createdAt),
504
+ ...(observation?.pollObservedAt ? { pollObservedAt: observation.pollObservedAt } : {}),
505
+ providerRequestId: options.request.id,
506
+ snapshot: {
507
+ depositAddressId: options.address.id,
508
+ destinationChain: options.address.snapshot.destinationChain,
509
+ destinationToken: options.address.snapshot.destinationToken,
510
+ provider: options.address.snapshot.provider,
511
+ recipient: options.address.recipient,
512
+ refundAddress: options.address.refundAddress,
513
+ sourceChain: options.address.snapshot.sourceChain,
514
+ sourceToken: options.address.snapshot.sourceToken,
515
+ sourceTransactionHashes: [transactionHash],
516
+ },
517
+ sourceTransactionHash: transactionHash,
518
+ ...(observation?.webhookReceivedAt
519
+ ? { webhookReceivedAt: observation.webhookReceivedAt }
520
+ : {}),
521
+ }),
522
+ ];
523
+ },
524
+ sourceTransactionHash: transactionHash,
525
+ });
315
526
  observed.push({ deposits, transactionHash, transfers: [] });
316
527
  }
317
528
  const sources = await Promise.all(observed.map(async (source) => ({
@@ -324,7 +535,6 @@ export async function reconcileRequest(db, options) {
324
535
  }),
325
536
  })));
326
537
  const sourceEvidenceComplete = sources.length > 0 && sources.every((source) => source.transfers.length > 0);
327
- let terminal = sourceEvidenceComplete;
328
538
  const updatedIds = new Set();
329
539
  const verified = [];
330
540
  for (const source of sources) {
@@ -347,7 +557,9 @@ export async function reconcileRequest(db, options) {
347
557
  : record.providerTransferIndex === providerTransferIndex);
348
558
  if (!deposit) {
349
559
  deposit = await Deposit.create(db, {
560
+ detectionTrigger: trigger,
350
561
  now: new Date(options.request.createdAt),
562
+ ...(observation?.pollObservedAt ? { pollObservedAt: observation.pollObservedAt } : {}),
351
563
  providerRequestId: options.request.id,
352
564
  providerTransferIndex,
353
565
  snapshot: {
@@ -366,15 +578,22 @@ export async function reconcileRequest(db, options) {
366
578
  },
367
579
  sourceTransactionHash: transfer.transactionHash,
368
580
  sourceTransferIndex: transfer.transferIndex,
581
+ ...(observation?.webhookReceivedAt
582
+ ? { webhookReceivedAt: observation.webhookReceivedAt }
583
+ : {}),
369
584
  });
370
585
  source.deposits.push(deposit);
371
586
  created += 1;
372
587
  }
373
- if (!Deposit.isTerminal(deposit.status)) {
588
+ if (!Deposit.isTerminal(deposit.status) ||
589
+ deposit.providerRequestId === null ||
590
+ !deposit.providerRequestIds.includes(options.request.id)) {
374
591
  const result = await Deposit.transition(db, {
375
592
  expectedVersion: deposit.version,
376
593
  id: deposit.id,
594
+ providerRequestId: options.request.id,
377
595
  providerRequestIds: unique([...deposit.providerRequestIds, options.request.id]),
596
+ providerTransferIndex,
378
597
  snapshot: {
379
598
  ...deposit.snapshot,
380
599
  ...(options.address.snapshot.subsidize ? { destinationAmountRequired } : {}),
@@ -389,27 +608,54 @@ export async function reconcileRequest(db, options) {
389
608
  updatedIds.add(deposit.id);
390
609
  }
391
610
  }
392
- verified.push({ deposit, required, sourceAmount: BigInt(transfer.amount) });
611
+ verified.push({ deposit, required, source: transfer, sourceAmount: BigInt(transfer.amount) });
393
612
  }
394
613
  }
395
614
  if (verified.length === 0)
396
615
  return { created, terminal: false, updated: updatedIds.size };
397
616
  if (!sourceEvidenceComplete)
398
617
  return { created, terminal: false, updated: updatedIds.size };
399
- const destinationEvidence = await Promise.all(options.request.destinationTransactionHashes.map((transactionHash) => options.verifyTransfers({
618
+ const delivery = await reconcileDelivery(db, { ...options, verified });
619
+ for (const id of delivery.updatedIds)
620
+ updatedIds.add(id);
621
+ return {
622
+ created,
623
+ ...(delivery.followUp ? { followUp: delivery.followUp } : {}),
624
+ terminal: delivery.terminal,
625
+ updated: updatedIds.size,
626
+ };
627
+ }
628
+ async function reconcileDelivery(db, options) {
629
+ let followUp;
630
+ let terminal = true;
631
+ const updatedIds = new Set();
632
+ if (options.verified.every((entry) => Deposit.isTerminal(entry.deposit.status)))
633
+ return { terminal, updatedIds };
634
+ const deliveryObservation = await observeProviderDelivery(options);
635
+ if (deliveryObservation?.type === 'pending')
636
+ followUp = 'delivery';
637
+ const providerTransactionHashes = options.request?.destinationTransactionHashes ?? [];
638
+ const destinationTransactionHashes = unique([
639
+ ...providerTransactionHashes,
640
+ ...(deliveryObservation?.type === 'verified'
641
+ ? deliveryObservation.destinationTransactionHashes
642
+ : []),
643
+ ]);
644
+ const destinationEvidence = await Promise.all(destinationTransactionHashes.map((transactionHash) => options.verifyTransfers({
400
645
  chain: options.address.snapshot.destinationChain,
401
646
  recipient: options.address.recipient,
402
647
  token: options.address.providerOutputToken,
403
648
  transactionHash,
404
649
  })));
405
650
  const destinationTransfers = destinationEvidence.flat();
406
- const providerEvidenceComplete = options.request.status === 'success' &&
651
+ const providerEvidenceComplete = (options.request?.status === 'success' || deliveryObservation?.type === 'verified') &&
652
+ destinationEvidence.length > 0 &&
407
653
  destinationEvidence.every((transfers) => transfers.length > 0);
408
654
  const providerOutput = destinationTransfers.reduce((total, transfer) => total + BigInt(transfer.amount), 0n);
409
- const totalRequired = verified.reduce((total, entry) => total + entry.required, 0n);
655
+ const totalRequired = options.verified.reduce((total, entry) => total + entry.required, 0n);
410
656
  let allocated = 0n;
411
657
  let cumulativeRequired = 0n;
412
- for (const entry of verified) {
658
+ for (const entry of options.verified) {
413
659
  const { required, sourceAmount } = entry;
414
660
  cumulativeRequired += required;
415
661
  const nextAllocated = totalRequired === 0n ? 0n : (providerOutput * cumulativeRequired) / totalRequired;
@@ -435,6 +681,8 @@ export async function reconcileRequest(db, options) {
435
681
  });
436
682
  if (settlement.status === 'bridging' || settlement.status === 'settling')
437
683
  terminal = false;
684
+ if (settlement.status === 'settling')
685
+ followUp = 'settlement';
438
686
  const destinationAmount = Value.tokenAmount({
439
687
  baseUnits: delivered + settlement.delivered,
440
688
  currency: options.address.snapshot.destinationToken.currency,
@@ -452,10 +700,21 @@ export async function reconcileRequest(db, options) {
452
700
  }),
453
701
  }
454
702
  : {}),
455
- providerRequestIds: unique([...deposit.providerRequestIds, options.request.id]),
703
+ providerRequestIds: options.request
704
+ ? unique([...deposit.providerRequestIds, options.request.id])
705
+ : deposit.providerRequestIds,
706
+ ...(deliveryObservation?.providerState
707
+ ? {
708
+ providerState: {
709
+ ...deposit.providerState,
710
+ ...deliveryObservation.providerState,
711
+ },
712
+ }
713
+ : {}),
456
714
  providerTransactionHashes: unique([
457
715
  ...deposit.providerTransactionHashes,
458
- ...options.request.destinationTransactionHashes,
716
+ ...providerTransactionHashes,
717
+ ...(deliveryObservation?.providerTransactionHashes ?? []),
459
718
  ]),
460
719
  ...(settlement.settlementTransaction
461
720
  ? { settlementTransaction: settlement.settlementTransaction }
@@ -497,7 +756,7 @@ export async function reconcileRequest(db, options) {
497
756
  }
498
757
  }
499
758
  }
500
- return { created, terminal, updated: updatedIds.size };
759
+ return { ...(followUp ? { followUp } : {}), terminal, updatedIds };
501
760
  }
502
761
  async function reconcileSubsidy(options) {
503
762
  const empty = { delivered: 0n, destinationTransactionHashes: [] };
@@ -665,6 +924,41 @@ function destinationBaseUnits(amount, sourceDecimals, destinationDecimals) {
665
924
  function unique(values) {
666
925
  return [...new Set(values)];
667
926
  }
927
+ function nextFollowUp(message, kind) {
928
+ const attempt = message.followUp?.kind === kind ? message.followUp.attempt + 1 : 0;
929
+ const chainProvider = kind === 'provider' && message.trigger === 'chain';
930
+ const maximumAttempts = chainProvider
931
+ ? chainProviderMaximumFollowUpAttempts
932
+ : maximumFollowUpAttempts;
933
+ if (attempt >= maximumAttempts)
934
+ return undefined;
935
+ return {
936
+ delaySeconds: chainProvider ? chainProviderFollowUpDelaySeconds : followUpDelaySeconds[kind],
937
+ state: { attempt, kind },
938
+ };
939
+ }
940
+ function recordWebhookLatency(metrics, options) {
941
+ if (!metrics)
942
+ return;
943
+ const completedAt = options.completedAt.getTime();
944
+ const enqueuedAt = Date.parse(options.timing.enqueuedAt);
945
+ const providerUpdatedAt = Date.parse(options.timing.providerUpdatedAt);
946
+ const receivedAt = Date.parse(options.timing.receivedAt);
947
+ const sentAt = Date.parse(options.timing.sentAt);
948
+ const segments = [
949
+ { from: providerUpdatedAt, segment: 'provider_to_sent', to: sentAt },
950
+ { from: sentAt, segment: 'sent_to_received', to: receivedAt },
951
+ { from: receivedAt, segment: 'received_to_enqueued', to: enqueuedAt },
952
+ { from: enqueuedAt, segment: 'enqueued_to_reconciled', to: completedAt },
953
+ { from: providerUpdatedAt, segment: 'provider_to_reconciled', to: completedAt },
954
+ ];
955
+ for (const segment of segments)
956
+ metrics.histogram('funding_deposit_webhook_latency_ms', Math.max(0, segment.to - segment.from), {
957
+ provider: options.providerId,
958
+ segment: segment.segment,
959
+ status: options.timing.providerStatus,
960
+ });
961
+ }
668
962
  /** Provider pagination exceeded the bounded reconciliation limit. */
669
963
  export class PaginationLimitError extends Error {
670
964
  name = 'Funding.Reconciliation.PaginationLimitError';