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
package/src/cloudflare.ts CHANGED
@@ -4,6 +4,9 @@
4
4
  // @ts-ignore
5
5
  import { DurableObject as CloudflareDurableObject } from 'cloudflare:workers'
6
6
 
7
+ import * as Db from './db/Db.js'
8
+ import * as Funding from './internal/funding/index.js'
9
+ import * as Metrics from './Metrics.js'
7
10
  import * as Store from './internal/Store.js'
8
11
 
9
12
  type State = ConstructorParameters<typeof CloudflareDurableObject>[0]
@@ -68,3 +71,279 @@ export class DurableObject extends CloudflareDurableObject {
68
71
  return this.#store.swap!(key, expected, next, options)
69
72
  }
70
73
  }
74
+
75
+ const fundingSourceAlarmIntervalMs = 500
76
+ const fundingSourceRetryIntervalMs = 2_000
77
+
78
+ type FundingSourceConfiguration = {
79
+ chainId: string
80
+ rpcUrls: readonly string[]
81
+ tokenAddress: string
82
+ }
83
+
84
+ type FundingSourceRoute = Funding.SourceObservation.Route
85
+
86
+ type FundingSourceRegisterOptions = FundingSourceConfiguration & {
87
+ route: FundingSourceRoute
88
+ }
89
+
90
+ type FundingSourceSyncOptions = FundingSourceConfiguration & {
91
+ routes: readonly FundingSourceRoute[]
92
+ }
93
+
94
+ type FundingSourceConfigurationRow = {
95
+ value: string
96
+ }
97
+
98
+ type FundingSourceRouteRow = {
99
+ address: string
100
+ id: string
101
+ }
102
+
103
+ type FundingSourceStateRow = {
104
+ value: string
105
+ }
106
+
107
+ /** Cloudflare Durable Object that detects EVM funding deposits before provider indexing. */
108
+ export class FundingSourceObserver<
109
+ environment extends FundingSourceObserver.Environment = FundingSourceObserver.Environment,
110
+ > extends CloudflareDurableObject {
111
+ /** Durable Object storage and alarm context. */
112
+ protected context: State
113
+ /** Cloudflare bindings used by the observer. */
114
+ protected environment: environment
115
+
116
+ constructor(ctx: State, env: environment) {
117
+ super(ctx, env as never)
118
+ this.context = ctx
119
+ this.environment = env
120
+ void ctx.blockConcurrencyWhile(async () => {
121
+ ctx.storage.sql.exec(
122
+ 'CREATE TABLE IF NOT EXISTS configuration (id INTEGER PRIMARY KEY CHECK (id = 1), value TEXT NOT NULL)',
123
+ )
124
+ ctx.storage.sql.exec('CREATE TABLE IF NOT EXISTS backfills (id TEXT PRIMARY KEY)')
125
+ ctx.storage.sql.exec(
126
+ 'CREATE TABLE IF NOT EXISTS routes (id TEXT PRIMARY KEY, address TEXT NOT NULL UNIQUE)',
127
+ )
128
+ ctx.storage.sql.exec(
129
+ 'CREATE TABLE IF NOT EXISTS state (key TEXT PRIMARY KEY, value TEXT NOT NULL)',
130
+ )
131
+ })
132
+ }
133
+
134
+ /** Registers one newly created deposit address and starts source observation. */
135
+ async register(options: FundingSourceRegisterOptions): Promise<void> {
136
+ const configuration = serializeFundingSourceConfiguration(options)
137
+ const route = this.context.storage.sql
138
+ .exec<FundingSourceRouteRow>('SELECT id, address FROM routes WHERE id = ?', options.route.id)
139
+ .toArray()[0]
140
+ this.context.storage.transactionSync(() => {
141
+ this.context.storage.sql.exec(
142
+ 'INSERT OR REPLACE INTO configuration (id, value) VALUES (1, ?)',
143
+ configuration,
144
+ )
145
+ this.context.storage.sql.exec(
146
+ 'INSERT OR REPLACE INTO routes (id, address) VALUES (?, ?)',
147
+ options.route.id,
148
+ options.route.address.toLowerCase(),
149
+ )
150
+ if (!route || route.address.toLowerCase() !== options.route.address.toLowerCase())
151
+ this.context.storage.sql.exec(
152
+ 'INSERT OR IGNORE INTO backfills (id) VALUES (?)',
153
+ options.route.id,
154
+ )
155
+ })
156
+ await this.#ensureAlarm()
157
+ }
158
+
159
+ /** Replaces the active routes from the database and repairs alarm scheduling. */
160
+ async sync(options: FundingSourceSyncOptions): Promise<void> {
161
+ const configuration = serializeFundingSourceConfiguration(options)
162
+ const existing = new Map(
163
+ this.context.storage.sql
164
+ .exec<FundingSourceRouteRow>('SELECT id, address FROM routes ORDER BY id')
165
+ .toArray()
166
+ .map((route: FundingSourceRouteRow) => [route.id, route.address.toLowerCase()] as const),
167
+ )
168
+ const changed = options.routes.filter(
169
+ (route) => existing.get(route.id) !== route.address.toLowerCase(),
170
+ )
171
+ this.context.storage.transactionSync(() => {
172
+ this.context.storage.sql.exec(
173
+ 'INSERT OR REPLACE INTO configuration (id, value) VALUES (1, ?)',
174
+ configuration,
175
+ )
176
+ this.context.storage.sql.exec('DELETE FROM routes')
177
+ for (const route of options.routes)
178
+ this.context.storage.sql.exec(
179
+ 'INSERT INTO routes (id, address) VALUES (?, ?)',
180
+ route.id,
181
+ route.address.toLowerCase(),
182
+ )
183
+ this.context.storage.sql.exec('DELETE FROM backfills WHERE id NOT IN (SELECT id FROM routes)')
184
+ for (const route of changed)
185
+ this.context.storage.sql.exec('INSERT OR IGNORE INTO backfills (id) VALUES (?)', route.id)
186
+ })
187
+ if (options.routes.length === 0) {
188
+ await this.context.storage.deleteAlarm()
189
+ return
190
+ }
191
+ await this.#ensureAlarm()
192
+ }
193
+
194
+ /** Scans confirmed source blocks and persists newly observed deposits. */
195
+ async alarm(): Promise<void> {
196
+ const metrics = this.createMetrics()
197
+ const configuration = this.#readConfiguration()
198
+ const routes: FundingSourceRouteRow[] = this.context.storage.sql
199
+ .exec<FundingSourceRouteRow>('SELECT id, address FROM routes ORDER BY id')
200
+ .toArray()
201
+ if (!configuration || routes.length === 0) return
202
+
203
+ try {
204
+ const cursor = this.context.storage.sql
205
+ .exec<FundingSourceStateRow>("SELECT value FROM state WHERE key = 'cursor'")
206
+ .toArray()[0]
207
+ const result = await Funding.SourceObservation.scan({
208
+ chainId: configuration.chainId,
209
+ ...(cursor ? { cursor: BigInt(cursor.value) } : {}),
210
+ routes,
211
+ rpcUrls: configuration.rpcUrls,
212
+ tokenAddress: configuration.tokenAddress,
213
+ })
214
+ const backfills = this.context.storage.sql
215
+ .exec<FundingSourceRouteRow>(
216
+ 'SELECT routes.id, routes.address FROM routes INNER JOIN backfills ON backfills.id = routes.id ORDER BY routes.id',
217
+ )
218
+ .toArray()
219
+ // Backfill new routes independently so the main cursor retains any older shard backlog.
220
+ const backfill = await (async () => {
221
+ if (!cursor || backfills.length === 0) return undefined
222
+ return Funding.SourceObservation.scan({
223
+ chainId: configuration.chainId,
224
+ routes: backfills,
225
+ rpcUrls: configuration.rpcUrls,
226
+ tokenAddress: configuration.tokenAddress,
227
+ })
228
+ })()
229
+ const database = Db.get(() =>
230
+ Db.postgres({ connectionString: this.environment.HYPERDRIVE.connectionString }),
231
+ )
232
+ const routesById = new Map(routes.map((route) => [route.id, route] as const))
233
+ const observations = new Map(
234
+ [...result.observations, ...(backfill?.observations ?? [])].map((observation) => [
235
+ `${observation.addressId}:${observation.transactionHash.toLowerCase()}:${observation.transferIndex}`,
236
+ observation,
237
+ ]),
238
+ ).values()
239
+ for (const observation of observations) {
240
+ const route = routesById.get(observation.addressId)
241
+ if (!route) continue
242
+ const persisted = await Funding.SourceObservation.observe(database, {
243
+ ...observation,
244
+ recipient: route.address,
245
+ })
246
+ if (
247
+ persisted.type === 'ignored' ||
248
+ persisted.record.providerRequestId !== null ||
249
+ Funding.Deposit.isTerminal(persisted.record.status)
250
+ )
251
+ continue
252
+ await this.environment.FUNDING_RECONCILIATION_QUEUE.send({
253
+ addressId: observation.addressId,
254
+ trigger: 'chain',
255
+ type: 'funding:deposit-address:reconcile',
256
+ })
257
+ metrics.count('funding_source_observer_deposit_count', 1, {
258
+ chain: configuration.chainId,
259
+ outcome: persisted.type,
260
+ })
261
+ }
262
+ this.context.storage.transactionSync(() => {
263
+ this.context.storage.sql.exec(
264
+ "INSERT OR REPLACE INTO state (key, value) VALUES ('cursor', ?)",
265
+ result.cursor.toString(),
266
+ )
267
+ for (const route of backfills)
268
+ this.context.storage.sql.exec('DELETE FROM backfills WHERE id = ?', route.id)
269
+ })
270
+ metrics.count('funding_source_observer_scan_count', 1, {
271
+ chain: configuration.chainId,
272
+ outcome: 'completed',
273
+ })
274
+ metrics.gauge(
275
+ 'funding_source_observer_block_lag',
276
+ Math.max(0, Number(result.head - result.cursor)),
277
+ { chain: configuration.chainId },
278
+ )
279
+ await this.context.storage.setAlarm(
280
+ result.cursor < result.head ? Date.now() : Date.now() + fundingSourceAlarmIntervalMs,
281
+ )
282
+ } catch (cause) {
283
+ metrics.count('funding_source_observer_scan_count', 1, {
284
+ chain: configuration.chainId,
285
+ outcome: 'failed',
286
+ })
287
+ this.capture(cause)
288
+ await this.context.storage.setAlarm(Date.now() + fundingSourceRetryIntervalMs)
289
+ } finally {
290
+ metrics.flush()
291
+ }
292
+ }
293
+
294
+ /** Reports one source-observer failure. */
295
+ protected capture(cause: unknown): void {
296
+ console.error(cause)
297
+ }
298
+
299
+ /** Creates the operational metric sink used by one alarm invocation. */
300
+ protected createMetrics(): Metrics.Metrics {
301
+ return Metrics.cloudflare({
302
+ enabled: Boolean(this.environment.ENVIRONMENT),
303
+ environment: this.environment.ENVIRONMENT ?? 'development',
304
+ service: 'tapimo',
305
+ })
306
+ }
307
+
308
+ #configuration(): string | undefined {
309
+ return this.context.storage.sql
310
+ .exec<FundingSourceConfigurationRow>('SELECT value FROM configuration WHERE id = 1')
311
+ .toArray()[0]?.value
312
+ }
313
+
314
+ async #ensureAlarm(): Promise<void> {
315
+ if ((await this.context.storage.getAlarm()) === null)
316
+ await this.context.storage.setAlarm(Date.now())
317
+ }
318
+
319
+ #readConfiguration(): FundingSourceConfiguration | undefined {
320
+ const value = this.#configuration()
321
+ return value ? (JSON.parse(value) as FundingSourceConfiguration) : undefined
322
+ }
323
+ }
324
+
325
+ export declare namespace FundingSourceObserver {
326
+ /** Cloudflare bindings and deployment metadata used by the observer. */
327
+ type Environment = {
328
+ /** Deployment environment included in operational metrics. */
329
+ ENVIRONMENT?: string | undefined
330
+ /** Queue that receives deposit-address reconciliation work. */
331
+ FUNDING_RECONCILIATION_QUEUE: {
332
+ /** Enqueues one chain-detected address for reconciliation. */
333
+ send(message: Funding.Provider.Webhook.receive.Dispatchable): Promise<unknown>
334
+ }
335
+ /** Authoritative Postgres connection exposed through Hyperdrive. */
336
+ HYPERDRIVE: {
337
+ /** PostgreSQL connection string supplied by the Hyperdrive binding. */
338
+ connectionString: string
339
+ }
340
+ }
341
+ }
342
+
343
+ function serializeFundingSourceConfiguration(options: FundingSourceConfiguration): string {
344
+ return JSON.stringify({
345
+ chainId: options.chainId,
346
+ rpcUrls: options.rpcUrls,
347
+ tokenAddress: options.tokenAddress.toLowerCase(),
348
+ } satisfies FundingSourceConfiguration)
349
+ }
package/src/db/Db.ts CHANGED
@@ -17,6 +17,7 @@ import type * as EarnVaults from './tables/earnVaults.js'
17
17
  import type * as EnabledBillingSources from './tables/enabledBillingSources.js'
18
18
  import type * as FundingCatalog from './tables/fundingCatalog.js'
19
19
  import type * as FundingDepositAddresses from './tables/fundingDepositAddresses.js'
20
+ import type * as FundingDepositRequestObservations from './tables/fundingDepositRequestObservations.js'
20
21
  import type * as FundingDeposits from './tables/fundingDeposits.js'
21
22
  import type * as FundingIdempotency from './tables/fundingIdempotency.js'
22
23
  import type * as FundingTransferEvents from './tables/fundingTransferEvents.js'
@@ -50,6 +51,7 @@ export type Database = {
50
51
  funding_chain_tokens: FundingCatalog.ChainTokenTable
51
52
  funding_chains: FundingCatalog.ChainTable
52
53
  funding_deposit_addresses: FundingDepositAddresses.Table
54
+ funding_deposit_request_observations: FundingDepositRequestObservations.Table
53
55
  funding_deposits: FundingDeposits.Table
54
56
  funding_idempotency_requests: FundingIdempotency.Table
55
57
  funding_routes: FundingCatalog.RouteTable
@@ -1209,6 +1211,66 @@ export const migrations: readonly Migration[] = [
1209
1211
  $$`,
1210
1212
  ),
1211
1213
  },
1214
+ {
1215
+ name: '0108_funding_deposits_recipient_index',
1216
+ // The expression indexes existing snapshots without a deployment-order-sensitive column backfill.
1217
+ up: (db) =>
1218
+ raw(
1219
+ db,
1220
+ sql`CREATE INDEX IF NOT EXISTS funding_deposits_recipient_idx ON funding_deposits (org_id, environment, lower(snapshot->>'recipient'), created_at, id)`,
1221
+ ),
1222
+ },
1223
+ {
1224
+ name: '0109_funding_deposits_detection_trigger',
1225
+ up: (db) =>
1226
+ raw(
1227
+ db,
1228
+ sql`ALTER TABLE funding_deposits ADD COLUMN IF NOT EXISTS detection_trigger text CHECK (detection_trigger IN ('manual', 'poll', 'webhook'))`,
1229
+ ),
1230
+ },
1231
+ {
1232
+ name: '0110_funding_deposit_request_observations',
1233
+ up: (db) =>
1234
+ db.schema
1235
+ .createTable('fundingDepositRequestObservations')
1236
+ .ifNotExists()
1237
+ .addColumn('depositAddressId', 'text', (c) =>
1238
+ c.notNull().references('fundingDepositAddresses.id').onDelete('cascade'),
1239
+ )
1240
+ .addColumn('pollObservedAt', 'text')
1241
+ .addColumn('providerRequestId', 'text', (c) => c.notNull())
1242
+ .addColumn('webhookReceivedAt', 'text')
1243
+ .addPrimaryKeyConstraint('funding_deposit_request_observations_pkey', [
1244
+ 'depositAddressId',
1245
+ 'providerRequestId',
1246
+ ]),
1247
+ },
1248
+ {
1249
+ name: '0111_funding_deposit_observation_timestamps',
1250
+ up: (db) =>
1251
+ raw(
1252
+ db,
1253
+ sql`ALTER TABLE funding_deposits
1254
+ ADD COLUMN IF NOT EXISTS poll_observed_at text,
1255
+ ADD COLUMN IF NOT EXISTS webhook_received_at text`,
1256
+ ),
1257
+ },
1258
+ {
1259
+ name: '0112_funding_deposits_source_observation_index',
1260
+ up: (db) =>
1261
+ raw(
1262
+ db,
1263
+ sql`CREATE UNIQUE INDEX IF NOT EXISTS funding_deposits_source_observation_idx ON funding_deposits (deposit_address_id, source_transaction_hash, source_transfer_index)`,
1264
+ ),
1265
+ },
1266
+ {
1267
+ name: '0113_funding_deposits_chain_detection_trigger',
1268
+ up: (db) =>
1269
+ raw(
1270
+ db,
1271
+ sql`ALTER TABLE funding_deposits DROP CONSTRAINT IF EXISTS funding_deposits_detection_trigger_check, ADD CONSTRAINT funding_deposits_detection_trigger_check CHECK (detection_trigger IN ('chain', 'manual', 'poll', 'webhook'))`,
1272
+ ),
1273
+ },
1212
1274
  ]
1213
1275
 
1214
1276
  /**
package/src/db/Schema.ts CHANGED
@@ -517,6 +517,21 @@ export const FundingDepositAddress = z.object({
517
517
  /** Columns of the `funding_deposit_addresses` table. */
518
518
  export type FundingDepositAddress = z.output<typeof FundingDepositAddress>
519
519
 
520
+ /** First Tempo observations of one provider request through polling and webhooks. */
521
+ export const FundingDepositRequestObservation = z.object({
522
+ depositAddressId: z.string().check(z.describe('Funding deposit address id (`fda_…`).')),
523
+ pollObservedAt: z
524
+ .nullable(z.string())
525
+ .check(z.describe('When polling first observed the provider request, or null.')),
526
+ providerRequestId: z.string().check(z.describe('Provider request identifier.')),
527
+ webhookReceivedAt: z
528
+ .nullable(z.string())
529
+ .check(z.describe('When Tempo first received an authenticated webhook, or null.')),
530
+ })
531
+
532
+ /** Columns of the `funding_deposit_request_observations` table. */
533
+ export type FundingDepositRequestObservation = z.output<typeof FundingDepositRequestObservation>
534
+
520
535
  /**
521
536
  * One provider-observed deposit. Public route and evidence live in `snapshot`;
522
537
  * reconciliation and settlement state is private.
@@ -524,11 +539,17 @@ export type FundingDepositAddress = z.output<typeof FundingDepositAddress>
524
539
  export const FundingDeposit = z.object({
525
540
  createdAt: z.string().check(z.describe('When the deposit was detected (ISO 8601).')),
526
541
  depositAddressId: z.string().check(z.describe('Funding deposit address id (`fda_…`).')),
542
+ detectionTrigger: z
543
+ .nullable(z.custom<funding_Deposit.DetectionTrigger>())
544
+ .check(z.describe('Mechanism that first detected the deposit, or null for legacy rows.')),
527
545
  environment: z
528
546
  .enum(['production', 'sandbox'])
529
547
  .check(z.describe('Key environment inherited from the deposit address.')),
530
548
  id: z.string().check(z.describe('Funding deposit id (`fdp_…`).')),
531
549
  orgId: z.string().check(z.describe('Owning organization id (`org_…`).')),
550
+ pollObservedAt: z
551
+ .nullable(z.string())
552
+ .check(z.describe('When polling first observed the provider request, or null.')),
532
553
  projectId: z.nullable(z.string()).check(z.describe('Attributed project id (`prj_…`), or null.')),
533
554
  providerOutputAmount: z
534
555
  .nullable(z.custom<z.output<typeof internal_Schema.TokenAmount>>())
@@ -580,6 +601,9 @@ export const FundingDeposit = z.object({
580
601
  tempoGasPaid: z.nullable(z.string()).check(z.describe('Tempo gas paid for settlement, or null.')),
581
602
  updatedAt: z.string().check(z.describe('When the deposit last materially changed (ISO 8601).')),
582
603
  version: z.number().check(z.int(), z.describe('Material version used to guard updates.')),
604
+ webhookReceivedAt: z
605
+ .nullable(z.string())
606
+ .check(z.describe('When Tempo first received an authenticated provider webhook, or null.')),
583
607
  })
584
608
 
585
609
  /** Columns of the `funding_deposits` table. */
@@ -168,6 +168,34 @@ export function getMatching(db: Db.Db, match: Match): Promise<Record | undefined
168
168
  return query.executeTakeFirst()
169
169
  }
170
170
 
171
+ /** Lists active provider addresses for one source-chain token. */
172
+ export function listActiveSources(
173
+ db: Db.Db,
174
+ options: listActiveSources.Options,
175
+ ): Promise<readonly Record[]> {
176
+ return db.kysely
177
+ .selectFrom('funding_deposit_addresses')
178
+ .selectAll()
179
+ .where('providerId', '=', options.providerId)
180
+ .where('sourceChainId', '=', options.sourceChainId)
181
+ .where('sourceTokenKey', '=', options.sourceTokenKey)
182
+ .where('status', '=', 'active')
183
+ .orderBy('id', 'asc')
184
+ .execute()
185
+ }
186
+
187
+ export declare namespace listActiveSources {
188
+ /** Provider and source asset identifying one observer shard. */
189
+ type Options = {
190
+ /** Funding provider that owns the deposit addresses. */
191
+ providerId: string
192
+ /** Source chain CAIP-2 id. */
193
+ sourceChainId: string
194
+ /** Source token key. */
195
+ sourceTokenKey: string
196
+ }
197
+ }
198
+
171
199
  /** Claims a bounded batch of active addresses due for provider reconciliation. */
172
200
  export function claimDue(db: Db.Db, options: claimDue.Options): Promise<readonly Record[]> {
173
201
  return db.transaction(async (tx) => {
@@ -0,0 +1,106 @@
1
+ import * as DepositAddress from '../../internal/funding/DepositAddress.js'
2
+ import * as TestApp from '../../../test/App.js'
3
+ import * as TestFunding from '../../../test/Funding.js'
4
+ import * as FundingDepositRequestObservations from './fundingDepositRequestObservations.js'
5
+
6
+ const db = TestApp.database()
7
+
8
+ let addressCounter = 0
9
+ function seedAddress() {
10
+ const index = addressCounter++
11
+ return DepositAddress.create(db, {
12
+ apiKeyId: 'key_request_observations',
13
+ deliveryStrategy: 'provider',
14
+ environment: 'production',
15
+ now: new Date(Date.UTC(2026, 0, 1, 0, 0, index)),
16
+ orgId: 'org_request_observations',
17
+ providerOutputToken: TestFunding.providerOutputToken(),
18
+ snapshot: TestFunding.depositAddressSnapshot({
19
+ provider: { id: `relay_observation_${index}`, name: 'Relay' },
20
+ recipient: `0x${index.toString(16).padStart(40, '0')}`,
21
+ }),
22
+ })
23
+ }
24
+
25
+ describe('get', () => {
26
+ test('returns no observation before a request is seen', async () => {
27
+ const address = await seedAddress()
28
+ expect(
29
+ await FundingDepositRequestObservations.get(db, {
30
+ depositAddressId: address.id,
31
+ providerRequestId: 'relay_unseen',
32
+ }),
33
+ ).toMatchInlineSnapshot(`undefined`)
34
+ })
35
+ })
36
+
37
+ describe('observePoll', () => {
38
+ test('retains the first poll timestamp for each request', async () => {
39
+ const address = await seedAddress()
40
+ await FundingDepositRequestObservations.observePoll(db, {
41
+ depositAddressId: address.id,
42
+ observedAt: '2026-01-01T00:00:10.000Z',
43
+ providerRequestIds: ['relay_poll', 'relay_poll'],
44
+ })
45
+ await FundingDepositRequestObservations.observePoll(db, {
46
+ depositAddressId: address.id,
47
+ observedAt: '2026-01-01T00:00:09.000Z',
48
+ providerRequestIds: ['relay_poll'],
49
+ })
50
+
51
+ expect(
52
+ await FundingDepositRequestObservations.get(db, {
53
+ depositAddressId: address.id,
54
+ providerRequestId: 'relay_poll',
55
+ }),
56
+ ).toMatchInlineSnapshot(
57
+ { depositAddressId: expect.any(String) },
58
+ `
59
+ {
60
+ "depositAddressId": Any<String>,
61
+ "pollObservedAt": "2026-01-01T00:00:09.000Z",
62
+ "providerRequestId": "relay_poll",
63
+ "webhookReceivedAt": null,
64
+ }
65
+ `,
66
+ )
67
+ })
68
+ })
69
+
70
+ describe('observeWebhook', () => {
71
+ test('retains both first-observation timestamps regardless of arrival order', async () => {
72
+ const address = await seedAddress()
73
+ await FundingDepositRequestObservations.observeWebhook(db, {
74
+ depositAddressId: address.id,
75
+ providerRequestId: 'relay_webhook',
76
+ receivedAt: '2026-01-01T00:00:08.000Z',
77
+ })
78
+ await FundingDepositRequestObservations.observePoll(db, {
79
+ depositAddressId: address.id,
80
+ observedAt: '2026-01-01T00:00:07.000Z',
81
+ providerRequestIds: ['relay_webhook'],
82
+ })
83
+ await FundingDepositRequestObservations.observeWebhook(db, {
84
+ depositAddressId: address.id,
85
+ providerRequestId: 'relay_webhook',
86
+ receivedAt: '2026-01-01T00:00:06.000Z',
87
+ })
88
+
89
+ expect(
90
+ await FundingDepositRequestObservations.get(db, {
91
+ depositAddressId: address.id,
92
+ providerRequestId: 'relay_webhook',
93
+ }),
94
+ ).toMatchInlineSnapshot(
95
+ { depositAddressId: expect.any(String) },
96
+ `
97
+ {
98
+ "depositAddressId": Any<String>,
99
+ "pollObservedAt": "2026-01-01T00:00:07.000Z",
100
+ "providerRequestId": "relay_webhook",
101
+ "webhookReceivedAt": "2026-01-01T00:00:06.000Z",
102
+ }
103
+ `,
104
+ )
105
+ })
106
+ })
@@ -0,0 +1,99 @@
1
+ import { sql, type Selectable } from 'kysely'
2
+
3
+ import type * as Db from '../Db.js'
4
+ import type * as db_Schema from '../Schema.js'
5
+
6
+ /** Columns of the `funding_deposit_request_observations` table. */
7
+ export type Table = db_Schema.FundingDepositRequestObservation
8
+
9
+ /** First Tempo observations of one provider request. */
10
+ export type Record = Selectable<Table>
11
+
12
+ /** Gets observation timestamps for one provider request. */
13
+ export function get(db: Db.Db, options: get.Options): Promise<Record | undefined> {
14
+ return db.kysely
15
+ .selectFrom('funding_deposit_request_observations')
16
+ .selectAll()
17
+ .where('depositAddressId', '=', options.depositAddressId)
18
+ .where('providerRequestId', '=', options.providerRequestId)
19
+ .executeTakeFirst()
20
+ }
21
+
22
+ export declare namespace get {
23
+ /** Provider request identity scoped to one reusable address. */
24
+ type Options = {
25
+ /** Funding deposit address id (`fda_…`). */
26
+ depositAddressId: string
27
+ /** Provider request identifier. */
28
+ providerRequestId: string
29
+ }
30
+ }
31
+
32
+ /** Records when polling first observed each provider request. */
33
+ export async function observePoll(db: Db.Db, options: observePoll.Options): Promise<Record[]> {
34
+ const providerRequestIds = [...new Set(options.providerRequestIds)]
35
+ if (providerRequestIds.length === 0) return []
36
+ return db.kysely
37
+ .insertInto('funding_deposit_request_observations')
38
+ .values(
39
+ providerRequestIds.map((providerRequestId) => ({
40
+ depositAddressId: options.depositAddressId,
41
+ pollObservedAt: options.observedAt,
42
+ providerRequestId,
43
+ webhookReceivedAt: null,
44
+ })),
45
+ )
46
+ .onConflict((oc) =>
47
+ oc.columns(['depositAddressId', 'providerRequestId']).doUpdateSet({
48
+ pollObservedAt: sql<string>`LEAST(COALESCE(funding_deposit_request_observations.poll_observed_at, excluded.poll_observed_at), excluded.poll_observed_at)`,
49
+ }),
50
+ )
51
+ .returningAll()
52
+ .execute()
53
+ }
54
+
55
+ export declare namespace observePoll {
56
+ /** Provider requests observed in one successful poll response. */
57
+ type Options = {
58
+ /** Funding deposit address id (`fda_…`). */
59
+ depositAddressId: string
60
+ /** When Tempo observed the provider requests. */
61
+ observedAt: string
62
+ /** Provider request identifiers returned by the poll. */
63
+ providerRequestIds: readonly string[]
64
+ }
65
+ }
66
+
67
+ /** Records when Tempo first received an authenticated webhook for a provider request. */
68
+ export function observeWebhook(
69
+ db: Db.Db,
70
+ options: observeWebhook.Options,
71
+ ): Promise<Record | undefined> {
72
+ return db.kysely
73
+ .insertInto('funding_deposit_request_observations')
74
+ .values({
75
+ depositAddressId: options.depositAddressId,
76
+ pollObservedAt: null,
77
+ providerRequestId: options.providerRequestId,
78
+ webhookReceivedAt: options.receivedAt,
79
+ })
80
+ .onConflict((oc) =>
81
+ oc.columns(['depositAddressId', 'providerRequestId']).doUpdateSet({
82
+ webhookReceivedAt: sql<string>`LEAST(COALESCE(funding_deposit_request_observations.webhook_received_at, excluded.webhook_received_at), excluded.webhook_received_at)`,
83
+ }),
84
+ )
85
+ .returningAll()
86
+ .executeTakeFirst()
87
+ }
88
+
89
+ export declare namespace observeWebhook {
90
+ /** Authenticated webhook observation for one provider request. */
91
+ type Options = {
92
+ /** Funding deposit address id (`fda_…`). */
93
+ depositAddressId: string
94
+ /** Provider request identifier. */
95
+ providerRequestId: string
96
+ /** When Tempo received the webhook. */
97
+ receivedAt: string
98
+ }
99
+ }