tapimo 0.5.2 → 0.6.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 (299) hide show
  1. package/dist/App.d.ts +13987 -5755
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js.map +1 -1
  4. package/dist/admin/App.d.ts +910 -412
  5. package/dist/admin/App.d.ts.map +1 -1
  6. package/dist/admin/App.js +4 -0
  7. package/dist/admin/App.js.map +1 -1
  8. package/dist/admin/apps/early-access.d.ts +148 -0
  9. package/dist/admin/apps/early-access.d.ts.map +1 -0
  10. package/dist/admin/apps/early-access.js +133 -0
  11. package/dist/admin/apps/early-access.js.map +1 -0
  12. package/dist/admin/apps/verified-tokens.d.ts +643 -118
  13. package/dist/admin/apps/verified-tokens.d.ts.map +1 -1
  14. package/dist/admin/apps/verified-tokens.js +214 -1
  15. package/dist/admin/apps/verified-tokens.js.map +1 -1
  16. package/dist/admin/ui.generated.d.ts +1 -1
  17. package/dist/admin/ui.generated.d.ts.map +1 -1
  18. package/dist/admin/ui.generated.js +45 -33
  19. package/dist/admin/ui.generated.js.map +1 -1
  20. package/dist/apps/data/App.d.ts +3733 -2428
  21. package/dist/apps/data/App.d.ts.map +1 -1
  22. package/dist/apps/data/App.js +11 -6
  23. package/dist/apps/data/App.js.map +1 -1
  24. package/dist/apps/data/FundingQuoteProviders.d.ts +34 -1
  25. package/dist/apps/data/FundingQuoteProviders.d.ts.map +1 -1
  26. package/dist/apps/data/FundingQuoteProviders.js +193 -2
  27. package/dist/apps/data/FundingQuoteProviders.js.map +1 -1
  28. package/dist/apps/data/FxOracle.d.ts +71 -0
  29. package/dist/apps/data/FxOracle.d.ts.map +1 -0
  30. package/dist/apps/data/FxOracle.js +97 -0
  31. package/dist/apps/data/FxOracle.js.map +1 -0
  32. package/dist/apps/data/index.d.ts +1 -0
  33. package/dist/apps/data/index.d.ts.map +1 -1
  34. package/dist/apps/data/index.js +1 -0
  35. package/dist/apps/data/index.js.map +1 -1
  36. package/dist/apps/data/routes/activities.d.ts +3575 -1358
  37. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  38. package/dist/apps/data/routes/activities.js +230 -88
  39. package/dist/apps/data/routes/activities.js.map +1 -1
  40. package/dist/apps/data/routes/addresses.d.ts +963 -342
  41. package/dist/apps/data/routes/addresses.d.ts.map +1 -1
  42. package/dist/apps/data/routes/addresses.js +11 -9
  43. package/dist/apps/data/routes/addresses.js.map +1 -1
  44. package/dist/apps/data/routes/balances.d.ts +108 -57
  45. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  46. package/dist/apps/data/routes/balances.js +88 -45
  47. package/dist/apps/data/routes/balances.js.map +1 -1
  48. package/dist/apps/data/routes/coingecko.d.ts +169 -169
  49. package/dist/apps/data/routes/exchanges.d.ts +952 -201
  50. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  51. package/dist/apps/data/routes/exchanges.js +518 -76
  52. package/dist/apps/data/routes/exchanges.js.map +1 -1
  53. package/dist/apps/data/routes/fee-amm.d.ts +24 -24
  54. package/dist/apps/data/routes/funding.d.ts +8 -2
  55. package/dist/apps/data/routes/funding.d.ts.map +1 -1
  56. package/dist/apps/data/routes/funding.js +32 -6
  57. package/dist/apps/data/routes/funding.js.map +1 -1
  58. package/dist/apps/data/routes/indexer.d.ts +5 -5
  59. package/dist/apps/data/routes/receipts.d.ts +392 -315
  60. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  61. package/dist/apps/data/routes/receipts.js +152 -77
  62. package/dist/apps/data/routes/receipts.js.map +1 -1
  63. package/dist/apps/data/routes/tokens.d.ts +166 -173
  64. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  65. package/dist/apps/data/routes/tokens.js +14 -12
  66. package/dist/apps/data/routes/tokens.js.map +1 -1
  67. package/dist/apps/data/routes/transactions.d.ts +277 -536
  68. package/dist/apps/data/routes/transactions.d.ts.map +1 -1
  69. package/dist/apps/data/routes/transactions.js +96 -77
  70. package/dist/apps/data/routes/transactions.js.map +1 -1
  71. package/dist/apps/data/routes/transfers.d.ts +82 -17
  72. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  73. package/dist/apps/data/routes/transfers.js +70 -13
  74. package/dist/apps/data/routes/transfers.js.map +1 -1
  75. package/dist/apps/data/routes/valuation.d.ts +307 -0
  76. package/dist/apps/data/routes/valuation.d.ts.map +1 -0
  77. package/dist/apps/data/routes/valuation.js +292 -0
  78. package/dist/apps/data/routes/valuation.js.map +1 -0
  79. package/dist/apps/data/routes/verified-tokens.d.ts.map +1 -1
  80. package/dist/apps/data/routes/verified-tokens.js +7 -5
  81. package/dist/apps/data/routes/verified-tokens.js.map +1 -1
  82. package/dist/apps/data/routes/webhooks.d.ts +459 -479
  83. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  84. package/dist/apps/data/routes/webhooks.js +124 -25
  85. package/dist/apps/data/routes/webhooks.js.map +1 -1
  86. package/dist/apps/index.d.ts +1 -1
  87. package/dist/apps/index.d.ts.map +1 -1
  88. package/dist/apps/index.js +1 -1
  89. package/dist/apps/index.js.map +1 -1
  90. package/dist/apps/management/App.d.ts +8606 -1681
  91. package/dist/apps/management/App.d.ts.map +1 -1
  92. package/dist/apps/management/App.js +12 -1
  93. package/dist/apps/management/App.js.map +1 -1
  94. package/dist/apps/management/routes/api-keys.d.ts +23 -0
  95. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  96. package/dist/apps/management/routes/api-keys.js +5 -3
  97. package/dist/apps/management/routes/api-keys.js.map +1 -1
  98. package/dist/apps/management/routes/billing.d.ts +88 -48
  99. package/dist/apps/management/routes/billing.d.ts.map +1 -1
  100. package/dist/apps/management/routes/billing.js +10 -2
  101. package/dist/apps/management/routes/billing.js.map +1 -1
  102. package/dist/apps/management/routes/faucet.d.ts +218 -0
  103. package/dist/apps/management/routes/faucet.d.ts.map +1 -0
  104. package/dist/apps/management/routes/faucet.js +139 -0
  105. package/dist/apps/management/routes/faucet.js.map +1 -0
  106. package/dist/apps/management/routes/invitations.d.ts +121 -121
  107. package/dist/apps/management/routes/invitations.d.ts.map +1 -1
  108. package/dist/apps/management/routes/invitations.js +3 -0
  109. package/dist/apps/management/routes/invitations.js.map +1 -1
  110. package/dist/apps/management/routes/invite-links.d.ts +197 -168
  111. package/dist/apps/management/routes/invite-links.d.ts.map +1 -1
  112. package/dist/apps/management/routes/invite-links.js +26 -1
  113. package/dist/apps/management/routes/invite-links.js.map +1 -1
  114. package/dist/apps/management/routes/me.d.ts +2 -0
  115. package/dist/apps/management/routes/me.d.ts.map +1 -1
  116. package/dist/apps/management/routes/me.js +4 -0
  117. package/dist/apps/management/routes/me.js.map +1 -1
  118. package/dist/apps/management/routes/members.d.ts +49 -49
  119. package/dist/apps/management/routes/orgs.d.ts +91 -91
  120. package/dist/apps/management/routes/projects.d.ts +115 -115
  121. package/dist/apps/management/routes/usage.d.ts +35 -32
  122. package/dist/apps/management/routes/usage.d.ts.map +1 -1
  123. package/dist/apps/management/routes/usage.js +4 -0
  124. package/dist/apps/management/routes/usage.js.map +1 -1
  125. package/dist/apps/management/routes/verified-token-requests.d.ts +697 -0
  126. package/dist/apps/management/routes/verified-token-requests.d.ts.map +1 -0
  127. package/dist/apps/management/routes/verified-token-requests.js +271 -0
  128. package/dist/apps/management/routes/verified-token-requests.js.map +1 -0
  129. package/dist/apps/management/routes/webhooks.d.ts +6977 -0
  130. package/dist/apps/management/routes/webhooks.d.ts.map +1 -0
  131. package/dist/apps/management/routes/webhooks.js +573 -0
  132. package/dist/apps/management/routes/webhooks.js.map +1 -0
  133. package/dist/apps/mcp.d.ts +9 -2
  134. package/dist/apps/mcp.d.ts.map +1 -1
  135. package/dist/apps/mcp.js +9 -4
  136. package/dist/apps/mcp.js.map +1 -1
  137. package/dist/cli/Cli.d.ts.map +1 -1
  138. package/dist/cli/Cli.js +7 -2
  139. package/dist/cli/Cli.js.map +1 -1
  140. package/dist/db/Db.d.ts +4 -0
  141. package/dist/db/Db.d.ts.map +1 -1
  142. package/dist/db/Db.js +83 -0
  143. package/dist/db/Db.js.map +1 -1
  144. package/dist/db/Schema.d.ts +41 -0
  145. package/dist/db/Schema.d.ts.map +1 -1
  146. package/dist/db/Schema.js +45 -0
  147. package/dist/db/Schema.js.map +1 -1
  148. package/dist/db/tables/earlyAccess.d.ts +50 -0
  149. package/dist/db/tables/earlyAccess.d.ts.map +1 -0
  150. package/dist/db/tables/earlyAccess.js +63 -0
  151. package/dist/db/tables/earlyAccess.js.map +1 -0
  152. package/dist/db/tables/invitations.d.ts +12 -1
  153. package/dist/db/tables/invitations.d.ts.map +1 -1
  154. package/dist/db/tables/invitations.js +23 -0
  155. package/dist/db/tables/invitations.js.map +1 -1
  156. package/dist/db/tables/inviteLinks.d.ts +10 -2
  157. package/dist/db/tables/inviteLinks.d.ts.map +1 -1
  158. package/dist/db/tables/inviteLinks.js +10 -1
  159. package/dist/db/tables/inviteLinks.js.map +1 -1
  160. package/dist/db/tables/sponsoredTransactions.d.ts +2 -0
  161. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  162. package/dist/db/tables/sponsoredTransactions.js +2 -0
  163. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  164. package/dist/db/tables/verifiedTokenRequests.d.ts +94 -0
  165. package/dist/db/tables/verifiedTokenRequests.d.ts.map +1 -0
  166. package/dist/db/tables/verifiedTokenRequests.js +120 -0
  167. package/dist/db/tables/verifiedTokenRequests.js.map +1 -0
  168. package/dist/db/tables/webhookSubscriptions.d.ts +14 -0
  169. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  170. package/dist/db/tables/webhookSubscriptions.js +16 -0
  171. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  172. package/dist/internal/Auth.d.ts +43 -2
  173. package/dist/internal/Auth.d.ts.map +1 -1
  174. package/dist/internal/Auth.js +60 -45
  175. package/dist/internal/Auth.js.map +1 -1
  176. package/dist/internal/FundingQuotes.d.ts +69 -15
  177. package/dist/internal/FundingQuotes.d.ts.map +1 -1
  178. package/dist/internal/FundingQuotes.js +74 -25
  179. package/dist/internal/FundingQuotes.js.map +1 -1
  180. package/dist/internal/Log.d.ts +19 -1
  181. package/dist/internal/Log.d.ts.map +1 -1
  182. package/dist/internal/Log.js +2 -0
  183. package/dist/internal/Log.js.map +1 -1
  184. package/dist/internal/MetricSink.d.ts.map +1 -1
  185. package/dist/internal/MetricSink.js +9 -0
  186. package/dist/internal/MetricSink.js.map +1 -1
  187. package/dist/internal/OpenApi.d.ts +2 -2
  188. package/dist/internal/OpenApi.d.ts.map +1 -1
  189. package/dist/internal/OpenApi.js +3 -0
  190. package/dist/internal/OpenApi.js.map +1 -1
  191. package/dist/internal/Schema.d.ts +14 -5
  192. package/dist/internal/Schema.d.ts.map +1 -1
  193. package/dist/internal/Schema.js +23 -5
  194. package/dist/internal/Schema.js.map +1 -1
  195. package/dist/internal/Value.d.ts +18 -0
  196. package/dist/internal/Value.d.ts.map +1 -1
  197. package/dist/internal/Value.js +11 -1
  198. package/dist/internal/Value.js.map +1 -1
  199. package/dist/internal/VerifiedTokens.d.ts.map +1 -1
  200. package/dist/internal/VerifiedTokens.js +10 -1
  201. package/dist/internal/VerifiedTokens.js.map +1 -1
  202. package/dist/internal/Viem.d.ts +1 -1
  203. package/dist/internal/Webhooks.d.ts +24 -3
  204. package/dist/internal/Webhooks.d.ts.map +1 -1
  205. package/dist/internal/Webhooks.js +34 -8
  206. package/dist/internal/Webhooks.js.map +1 -1
  207. package/package.json +2 -2
  208. package/src/App.ts +9 -1
  209. package/src/Client.test-d.ts +638 -6
  210. package/src/admin/App.ts +4 -0
  211. package/src/admin/apps/early-access.test.ts +125 -0
  212. package/src/admin/apps/early-access.ts +175 -0
  213. package/src/admin/apps/verified-tokens.test.ts +201 -0
  214. package/src/admin/apps/verified-tokens.ts +532 -286
  215. package/src/admin/ui/plugin.ts +2 -1
  216. package/src/admin/ui/src/lib/early-access.ts +63 -0
  217. package/src/admin/ui/src/lib/verified-tokens.ts +79 -0
  218. package/src/admin/ui/src/routeTree.gen.ts +33 -2
  219. package/src/admin/ui/src/routes/__root.tsx +2 -1
  220. package/src/admin/ui/src/routes/early-access.tsx +157 -0
  221. package/src/admin/ui/src/routes/verified-tokens.tsx +230 -19
  222. package/src/admin/ui.generated.ts +48 -33
  223. package/src/apps/data/App.ts +22 -14
  224. package/src/apps/data/FundingQuoteProviders.test.ts +298 -0
  225. package/src/apps/data/FundingQuoteProviders.ts +257 -4
  226. package/src/apps/data/FxOracle.test.ts +118 -0
  227. package/src/apps/data/FxOracle.ts +153 -0
  228. package/src/apps/data/index.ts +1 -0
  229. package/src/apps/data/routes/activities.test.ts +173 -3
  230. package/src/apps/data/routes/activities.ts +403 -132
  231. package/src/apps/data/routes/addresses.ts +19 -9
  232. package/src/apps/data/routes/balances.test.ts +144 -11
  233. package/src/apps/data/routes/balances.ts +140 -51
  234. package/src/apps/data/routes/exchanges.test.ts +540 -9
  235. package/src/apps/data/routes/exchanges.ts +690 -103
  236. package/src/apps/data/routes/funding.test.ts +84 -0
  237. package/src/apps/data/routes/funding.ts +38 -6
  238. package/src/apps/data/routes/receipts.test.ts +125 -23
  239. package/src/apps/data/routes/receipts.ts +205 -100
  240. package/src/apps/data/routes/tokens.test.ts +43 -0
  241. package/src/apps/data/routes/tokens.ts +27 -12
  242. package/src/apps/data/routes/transactions.test.ts +72 -19
  243. package/src/apps/data/routes/transactions.ts +162 -92
  244. package/src/apps/data/routes/transfers.test.ts +146 -4
  245. package/src/apps/data/routes/transfers.ts +91 -13
  246. package/src/apps/data/routes/valuation.test.ts +422 -0
  247. package/src/apps/data/routes/valuation.ts +423 -0
  248. package/src/apps/data/routes/verified-tokens.ts +14 -2
  249. package/src/apps/data/routes/webhooks.test.ts +56 -8
  250. package/src/apps/data/routes/webhooks.ts +144 -27
  251. package/src/apps/index.ts +1 -1
  252. package/src/apps/management/App.ts +13 -0
  253. package/src/apps/management/routes/api-keys.test.ts +87 -0
  254. package/src/apps/management/routes/api-keys.ts +4 -3
  255. package/src/apps/management/routes/billing.test.ts +112 -1
  256. package/src/apps/management/routes/billing.ts +7 -2
  257. package/src/apps/management/routes/faucet.test.ts +79 -0
  258. package/src/apps/management/routes/faucet.ts +185 -0
  259. package/src/apps/management/routes/invitations.test.ts +172 -0
  260. package/src/apps/management/routes/invitations.ts +3 -0
  261. package/src/apps/management/routes/invite-links.test.ts +117 -0
  262. package/src/apps/management/routes/invite-links.ts +43 -1
  263. package/src/apps/management/routes/me.test.ts +140 -88
  264. package/src/apps/management/routes/me.ts +6 -0
  265. package/src/apps/management/routes/usage.test.ts +6 -0
  266. package/src/apps/management/routes/usage.ts +7 -0
  267. package/src/apps/management/routes/verified-token-requests.test.ts +377 -0
  268. package/src/apps/management/routes/verified-token-requests.ts +397 -0
  269. package/src/apps/management/routes/webhooks.test.ts +426 -0
  270. package/src/apps/management/routes/webhooks.ts +672 -0
  271. package/src/apps/mcp.test.ts +13 -0
  272. package/src/apps/mcp.ts +18 -5
  273. package/src/cli/Cli.test.ts +36 -0
  274. package/src/cli/Cli.ts +7 -2
  275. package/src/db/Db.test.ts +19 -0
  276. package/src/db/Db.ts +95 -0
  277. package/src/db/Schema.ts +53 -0
  278. package/src/db/tables/earlyAccess.ts +86 -0
  279. package/src/db/tables/invitations.ts +26 -1
  280. package/src/db/tables/inviteLinks.ts +22 -3
  281. package/src/db/tables/sponsoredTransactions.ts +4 -0
  282. package/src/db/tables/verifiedTokenRequests.test.ts +200 -0
  283. package/src/db/tables/verifiedTokenRequests.ts +182 -0
  284. package/src/db/tables/webhookSubscriptions.ts +31 -0
  285. package/src/internal/Auth.ts +73 -45
  286. package/src/internal/FundingQuotes.test.ts +143 -1
  287. package/src/internal/FundingQuotes.ts +95 -28
  288. package/src/internal/Log.test-d.ts +21 -0
  289. package/src/internal/Log.test.ts +50 -0
  290. package/src/internal/Log.ts +24 -1
  291. package/src/internal/MetricSink.test.ts +160 -0
  292. package/src/internal/MetricSink.ts +10 -0
  293. package/src/internal/OpenApi.ts +3 -0
  294. package/src/internal/Schema.ts +50 -5
  295. package/src/internal/Value.test.ts +20 -0
  296. package/src/internal/Value.ts +24 -1
  297. package/src/internal/VerifiedTokens.ts +9 -1
  298. package/src/internal/Webhooks.test.ts +140 -0
  299. package/src/internal/Webhooks.ts +53 -9
@@ -0,0 +1,160 @@
1
+ import * as Metrics from '../Metrics.js'
2
+ import type * as Log from './Log.js'
3
+ import * as MetricSink from './MetricSink.js'
4
+
5
+ type MetricEvent = {
6
+ name: string
7
+ tags: Metrics.Tags | undefined
8
+ type: 'count' | 'gauge' | 'histogram'
9
+ value: number
10
+ }
11
+
12
+ describe('requests', () => {
13
+ test('records bounded funding provider attempts without caller data', () => {
14
+ const events: MetricEvent[] = []
15
+ let flushes = 0
16
+ const metrics = Metrics.from({
17
+ count(name, value, tags) {
18
+ events.push({ name, tags, type: 'count', value })
19
+ },
20
+ flush() {
21
+ flushes += 1
22
+ },
23
+ gauge(name, value, tags) {
24
+ events.push({ name, tags, type: 'gauge', value })
25
+ },
26
+ histogram(name, value, tags) {
27
+ events.push({ name, tags, type: 'histogram', value })
28
+ },
29
+ })
30
+ const record = MetricSink.requests(metrics)
31
+
32
+ record({
33
+ duration: 100,
34
+ fundingProviderAttempts: [
35
+ {
36
+ durationMs: 10,
37
+ id: 'relay',
38
+ operation: 'quote/v2',
39
+ outcome: 'available',
40
+ },
41
+ {
42
+ durationMs: 20,
43
+ id: 'rhino',
44
+ operation: 'quote/bridge-swap/public',
45
+ outcome: 'thin',
46
+ },
47
+ {
48
+ durationMs: 30,
49
+ id: 'squid',
50
+ operation: 'v2/route',
51
+ outcome: 'unavailable',
52
+ },
53
+ {
54
+ durationMs: 40,
55
+ failure: 'timeout',
56
+ id: 'across',
57
+ operation: 'swap/approval',
58
+ outcome: 'failed',
59
+ },
60
+ ],
61
+ level: 'info',
62
+ method: 'GET',
63
+ path: '/v1/funding/quotes',
64
+ principal: {
65
+ id: 'key_private',
66
+ orgId: 'org_private',
67
+ projectId: 'project_private',
68
+ type: 'api_key',
69
+ },
70
+ query: 'sourceAmount=secret',
71
+ requestId: 'request_private',
72
+ route: '/v1/funding/quotes',
73
+ status: 200,
74
+ } satisfies Log.Entry)
75
+
76
+ const funding = events.filter((event) => event.name.startsWith('funding_quote_provider_'))
77
+ expect(funding).toMatchInlineSnapshot(`
78
+ [
79
+ {
80
+ "name": "funding_quote_provider_attempt_count",
81
+ "tags": {
82
+ "outcome": "available",
83
+ "provider": "relay",
84
+ },
85
+ "type": "count",
86
+ "value": 1,
87
+ },
88
+ {
89
+ "name": "funding_quote_provider_duration_ms",
90
+ "tags": {
91
+ "outcome": "available",
92
+ "provider": "relay",
93
+ },
94
+ "type": "histogram",
95
+ "value": 10,
96
+ },
97
+ {
98
+ "name": "funding_quote_provider_attempt_count",
99
+ "tags": {
100
+ "outcome": "thin",
101
+ "provider": "rhino",
102
+ },
103
+ "type": "count",
104
+ "value": 1,
105
+ },
106
+ {
107
+ "name": "funding_quote_provider_duration_ms",
108
+ "tags": {
109
+ "outcome": "thin",
110
+ "provider": "rhino",
111
+ },
112
+ "type": "histogram",
113
+ "value": 20,
114
+ },
115
+ {
116
+ "name": "funding_quote_provider_attempt_count",
117
+ "tags": {
118
+ "outcome": "unavailable",
119
+ "provider": "squid",
120
+ },
121
+ "type": "count",
122
+ "value": 1,
123
+ },
124
+ {
125
+ "name": "funding_quote_provider_duration_ms",
126
+ "tags": {
127
+ "outcome": "unavailable",
128
+ "provider": "squid",
129
+ },
130
+ "type": "histogram",
131
+ "value": 30,
132
+ },
133
+ {
134
+ "name": "funding_quote_provider_attempt_count",
135
+ "tags": {
136
+ "failure": "timeout",
137
+ "outcome": "failed",
138
+ "provider": "across",
139
+ },
140
+ "type": "count",
141
+ "value": 1,
142
+ },
143
+ {
144
+ "name": "funding_quote_provider_duration_ms",
145
+ "tags": {
146
+ "failure": "timeout",
147
+ "outcome": "failed",
148
+ "provider": "across",
149
+ },
150
+ "type": "histogram",
151
+ "value": 40,
152
+ },
153
+ ]
154
+ `)
155
+ expect(JSON.stringify(funding)).not.toMatch(
156
+ /key_private|org_private|project_private|request_private|secret/,
157
+ )
158
+ expect(flushes).toBe(1)
159
+ })
160
+ })
@@ -95,6 +95,16 @@ export function requests(backend: Metrics): RequestSink {
95
95
  for (const [operation, duration] of Object.entries(entry.timings ?? {}))
96
96
  backend.histogram('upstream_duration_ms', duration, { ...tags, operation })
97
97
 
98
+ for (const attempt of entry.fundingProviderAttempts ?? []) {
99
+ const tags_attempt = {
100
+ ...(attempt.failure === undefined ? {} : { failure: attempt.failure }),
101
+ outcome: attempt.outcome,
102
+ provider: attempt.id,
103
+ }
104
+ backend.count('funding_quote_provider_attempt_count', 1, tags_attempt)
105
+ backend.histogram('funding_quote_provider_duration_ms', attempt.durationMs, tags_attempt)
106
+ }
107
+
98
108
  backend.flush()
99
109
  }
100
110
  }
@@ -64,6 +64,7 @@ export const errorCodes = {
64
64
  'address_invalid',
65
65
  'token_invalid',
66
66
  'symbol_invalid',
67
+ 'quote_amount_out_of_range',
67
68
  'pair_invalid',
68
69
  'pair_id_invalid',
69
70
  'transaction_invalid',
@@ -71,6 +72,7 @@ export const errorCodes = {
71
72
  'block_invalid',
72
73
  'chain_id_invalid',
73
74
  'chain_id_unsupported',
75
+ 'destination_transition_invalid',
74
76
  'url_invalid',
75
77
  'api_key_malformed',
76
78
  ],
@@ -85,6 +87,7 @@ export const errorCodes = {
85
87
  'receipt_not_found',
86
88
  'order_not_found',
87
89
  'pair_not_found',
90
+ 'quote_not_available',
88
91
  'verified_token_not_found',
89
92
  'webhook_not_found',
90
93
  'delivery_not_found',
@@ -290,6 +290,35 @@ export const DecimalString = z
290
290
  z.meta({ examples: ['1000000'] }),
291
291
  )
292
292
 
293
+ /** A self-contained token quantity in base units and human-readable form. */
294
+ export const TokenAmount = describe(
295
+ z.object({
296
+ baseUnits: DecimalString.check(z.describe("The quantity in the token's smallest unit.")),
297
+ currency: z
298
+ .string()
299
+ .check(
300
+ z.describe('The monetary denomination of this quantity.'),
301
+ z.meta({ examples: ['USD'] }),
302
+ ),
303
+ decimals: z
304
+ .number()
305
+ .check(
306
+ z.int(),
307
+ z.nonnegative(),
308
+ z.describe('Decimal places used to convert `baseUnits` into `formatted`.'),
309
+ z.meta({ examples: [6] }),
310
+ ),
311
+ formatted: z
312
+ .string()
313
+ .check(
314
+ z.regex(/^\d+(\.\d+)?$/),
315
+ z.describe('The quantity rendered in whole token units.'),
316
+ z.meta({ examples: ['1'] }),
317
+ ),
318
+ }),
319
+ 'A token quantity with its denomination and decimal representation.',
320
+ )
321
+
293
322
  /**
294
323
  * Minimum page size accepted across list endpoints. Floored at 5 because the
295
324
  * indexer rejects signature-decoded `Transfer` queries with `LIMIT < 5`
@@ -306,6 +335,22 @@ export const Limit = z
306
335
  z.meta({ examples: [10] }),
307
336
  )
308
337
 
338
+ /** Valuation denomination query parameter shared across valuation-bearing endpoints. */
339
+ export const Denomination = z
340
+ .optional(
341
+ z.pipe(
342
+ z.string().check(z.regex(/^[a-zA-Z]{3}$/)),
343
+ z.transform((value) => value.toUpperCase()),
344
+ ),
345
+ )
346
+ .check(
347
+ z.describe(
348
+ 'Currency to denominate values in (case-insensitive, e.g. `AUD`). ' +
349
+ 'Must be priced by the configured FX oracle. Defaults to `USD`.',
350
+ ),
351
+ z.meta({ examples: ['AUD'] }),
352
+ )
353
+
309
354
  /** Opaque keyset-pagination cursor query parameter shared across list endpoints. */
310
355
  export const Cursor = z
311
356
  .optional(z.string())
@@ -464,17 +509,17 @@ export function timestampBound(noun: string, bound: 'from' | 'to') {
464
509
 
465
510
  /**
466
511
  * Builds the schema for a comma-separated opt-in query parameter (e.g.
467
- * `?include=token,feeToken`), parsed into a validated array of the given enum's
512
+ * `?include=token,totalCount`), parsed into a validated array of the given enum's
468
513
  * members. Absent or empty values yield `[]`. Centralizes the splitting,
469
514
  * trimming, and OpenAPI array-enum metadata that every list endpoint otherwise
470
515
  * repeats.
471
516
  *
472
- * A repeated parameter (`?include=token&include=feeToken`) arrives from the
517
+ * A repeated parameter (`?include=token&include=totalCount`) arrives from the
473
518
  * validator as a `string[]`, so the input accepts both forms and each entry is
474
- * still comma-split the two styles compose freely.
519
+ * still comma-split, so the two styles compose freely.
475
520
  *
476
- * Pass the endpoint's `Include` enum — kept local so its members drive both the
477
- * parsed type and the docs — plus the param description.
521
+ * Pass the endpoint's local `Include` enum so its members drive the parsed type
522
+ * and documentation, plus the parameter description.
478
523
  */
479
524
  export function includeQuery<const T extends Readonly<Record<string, string>>>(
480
525
  member: z.ZodMiniEnum<T>,
@@ -0,0 +1,20 @@
1
+ import * as Value from './Value.js'
2
+
3
+ describe('tokenAmount', () => {
4
+ test('renders a self-contained token quantity', () => {
5
+ expect(
6
+ Value.tokenAmount({
7
+ baseUnits: '1234567',
8
+ currency: 'USD',
9
+ decimals: 6,
10
+ }),
11
+ ).toMatchInlineSnapshot(`
12
+ {
13
+ "baseUnits": "1234567",
14
+ "currency": "USD",
15
+ "decimals": 6,
16
+ "formatted": "1.234567",
17
+ }
18
+ `)
19
+ })
20
+ })
@@ -1,4 +1,4 @@
1
- import { Hex } from 'ox'
1
+ import { Hex, Value as core_Value } from 'ox'
2
2
 
3
3
  /** Coerce a TIDX numeric column (bigint / number / string) to a JS number. */
4
4
  export function toNumber(value: unknown) {
@@ -39,6 +39,29 @@ export function toIntegerString(value: unknown) {
39
39
  return undefined
40
40
  }
41
41
 
42
+ /** Builds a token amount from its base-unit quantity and immutable token metadata. */
43
+ export function tokenAmount(options: tokenAmount.Options) {
44
+ const baseUnits = options.baseUnits.toString()
45
+ return {
46
+ baseUnits,
47
+ currency: options.currency,
48
+ decimals: options.decimals,
49
+ formatted: core_Value.format(BigInt(baseUnits), options.decimals),
50
+ }
51
+ }
52
+
53
+ export declare namespace tokenAmount {
54
+ /** Inputs needed to render a self-contained token amount. */
55
+ type Options = {
56
+ /** Quantity in the token's smallest unit. */
57
+ baseUnits: bigint | string
58
+ /** Monetary denomination reported by the token. */
59
+ currency: string
60
+ /** Decimal places used by the token. */
61
+ decimals: number
62
+ }
63
+ }
64
+
42
65
  /**
43
66
  * Coerce a TIDX numeric column (bigint / number / decimal string) to a minimal
44
67
  * `0x`-prefixed hex quantity (`0x0` for zero), matching the JSON-RPC wire
@@ -535,7 +535,15 @@ export async function snapshot(c: Context<App.Environment>, chainId: ChainId): P
535
535
 
536
536
  // Soft-stale: serve immediately, refresh in the background. Never blocks.
537
537
  if (cell) {
538
- void refresh(c, Db.get(c.get('dbCached')), chainId, cell)
538
+ const inflight = refresh(c, Db.get(c.get('dbCached')), chainId, cell)
539
+ // Workers cancel post-response I/O unless kept alive via `waitUntil`;
540
+ // without it the refresh dies mid-query and publishes never propagate.
541
+ // Node has no execution context, and fire-and-forget survives there.
542
+ try {
543
+ c.executionCtx.waitUntil(inflight)
544
+ } catch {
545
+ void inflight
546
+ }
539
547
  return cell.snapshot
540
548
  }
541
549
 
@@ -78,6 +78,45 @@ describe('createSubscription', () => {
78
78
  )
79
79
  })
80
80
 
81
+ test('counts paused subscriptions toward the live subscription cap', async () => {
82
+ const db = TestApp.database()
83
+ const created = await Webhooks.createSubscription(db, createInput(), { maxPerOwner: 1 })
84
+ await Webhooks.updateSubscription(db, apiKeyOwner, created.id, { status: 'paused' })
85
+
86
+ await expect(
87
+ Webhooks.createSubscription(db, createInput(), { maxPerOwner: 1 }),
88
+ ).rejects.toBeInstanceOf(Webhooks.LimitExceededError)
89
+ await Webhooks.deleteSubscription(db, apiKeyOwner, created.id)
90
+ await expect(
91
+ Webhooks.createSubscription(db, createInput(), { maxPerOwner: 1 }),
92
+ ).resolves.toMatchObject({ status: 'active' })
93
+ })
94
+
95
+ test('enforces the per-owner cap across concurrent creates', async () => {
96
+ const getDb = TestApp.databaseFactory()
97
+ const results = await Promise.allSettled(
98
+ Array.from({ length: 8 }, () =>
99
+ Webhooks.createSubscription(getDb(), createInput(), { maxPerOwner: 3 }),
100
+ ),
101
+ )
102
+
103
+ expect(results.map((result) => result.status).sort()).toMatchInlineSnapshot(`
104
+ [
105
+ "fulfilled",
106
+ "fulfilled",
107
+ "fulfilled",
108
+ "rejected",
109
+ "rejected",
110
+ "rejected",
111
+ "rejected",
112
+ "rejected",
113
+ ]
114
+ `)
115
+ for (const result of results)
116
+ if (result.status === 'rejected') expect(result.reason).toBeInstanceOf(Webhooks.LimitExceededError) // prettier-ignore
117
+ expect(await Webhooks.listSubscriptions(getDb(), apiKeyOwner)).toHaveLength(3)
118
+ })
119
+
81
120
  test('rejects an undeliverable URL before persisting', async () => {
82
121
  const db = TestApp.database()
83
122
  await expect(
@@ -204,6 +243,27 @@ describe('updateSubscription', () => {
204
243
  `[Webhooks.InvalidUrlError: Webhook URL rejected (protocol): http://10.0.0.1]`,
205
244
  )
206
245
  })
246
+
247
+ test('requires recreation before activating URL signing for a provider destination', async () => {
248
+ const db = TestApp.database()
249
+ const created = await Webhooks.createSubscription(
250
+ db,
251
+ createInput({
252
+ destination: {
253
+ type: 'slack',
254
+ url: 'https://hooks.slack.com/services/T000/B000/xxxx',
255
+ },
256
+ }),
257
+ )
258
+
259
+ await expect(
260
+ Webhooks.updateSubscription(db, apiKeyOwner, created.id, {
261
+ destination: { type: 'url', url: 'https://hooks.example.com/v2' },
262
+ }),
263
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
264
+ `[Webhooks.InvalidDestinationTransitionError: Create a new webhook to change a provider-managed destination to an HTTPS endpoint.]`,
265
+ )
266
+ })
207
267
  })
208
268
 
209
269
  describe('listActive', () => {
@@ -548,6 +608,86 @@ describe('delivery log', () => {
548
608
  `)
549
609
  })
550
610
 
611
+ test('deliverCurrentAndRecord skips paused and deleted queued subscriptions', async () => {
612
+ const db = TestApp.database()
613
+ const paused = await Webhooks.createSubscription(db, createInput())
614
+ const removed = await Webhooks.createSubscription(db, createInput())
615
+ const pausedEnvelope = Webhooks.buildEnvelope({
616
+ blockNumber: 1,
617
+ data: { amount: '1' },
618
+ logIndex: 0,
619
+ subscription: paused,
620
+ })
621
+ const removedEnvelope = Webhooks.buildEnvelope({
622
+ blockNumber: 1,
623
+ data: { amount: '1' },
624
+ logIndex: 1,
625
+ subscription: removed,
626
+ })
627
+ await Webhooks.updateSubscription(db, apiKeyOwner, paused.id, { status: 'paused' })
628
+ await Webhooks.deleteSubscription(db, apiKeyOwner, removed.id)
629
+ let requests = 0
630
+ const fetch = (async () => {
631
+ requests++
632
+ return new Response(null, { status: 200 })
633
+ }) satisfies typeof globalThis.fetch
634
+
635
+ await expect(
636
+ Webhooks.deliverCurrentAndRecord(db, paused, pausedEnvelope, { fetch }),
637
+ ).resolves.toEqual({ status: 'skipped' })
638
+ await expect(
639
+ Webhooks.deliverCurrentAndRecord(db, removed, removedEnvelope, { fetch }),
640
+ ).resolves.toEqual({ status: 'skipped' })
641
+ expect(requests).toBe(0)
642
+ })
643
+
644
+ test('deliverCurrentAndRecord uses the current destination and secret', async () => {
645
+ const db = TestApp.database()
646
+ const created = await Webhooks.createSubscription(db, createInput())
647
+ const envelope = Webhooks.buildEnvelope({
648
+ blockNumber: 1,
649
+ data: { amount: '1' },
650
+ logIndex: 0,
651
+ subscription: created,
652
+ })
653
+ const current = await Webhooks.updateSubscription(db, apiKeyOwner, created.id, {
654
+ destination: { type: 'url', url: 'https://hooks.example.com/current' },
655
+ })
656
+ const queued = { ...created, secret: `whsec_${'ff'.repeat(32)}` }
657
+ const captured: { body: string; signature: string | null; url: string } = {
658
+ body: '',
659
+ signature: null,
660
+ url: '',
661
+ }
662
+ const fetch = (async (input, init) => {
663
+ captured.body = typeof init?.body === 'string' ? init.body : ''
664
+ captured.signature = new Headers(init?.headers).get(Webhooks.signatureHeader)
665
+ captured.url = input instanceof Request ? input.url : input.toString()
666
+ return new Response(null, { status: 200 })
667
+ }) satisfies typeof globalThis.fetch
668
+
669
+ const delivery = await Webhooks.deliverCurrentAndRecord(db, queued, envelope, { fetch })
670
+
671
+ expect(delivery.status).toBe('delivered')
672
+ expect(captured.url).toBe('https://hooks.example.com/current')
673
+ expect(
674
+ Webhooks.verify({
675
+ body: captured.body,
676
+ secret: current!.secret,
677
+ signature: captured.signature ?? '',
678
+ tolerance: Number.POSITIVE_INFINITY,
679
+ }),
680
+ ).toBe(true)
681
+ expect(
682
+ Webhooks.verify({
683
+ body: captured.body,
684
+ secret: queued.secret,
685
+ signature: captured.signature ?? '',
686
+ tolerance: Number.POSITIVE_INFINITY,
687
+ }),
688
+ ).toBe(false)
689
+ })
690
+
551
691
  test('deleteSubscription removes the delivery log', async () => {
552
692
  const db = TestApp.database()
553
693
  const created = await Webhooks.createSubscription(db, createInput())
@@ -161,8 +161,8 @@ export type Delivery = {
161
161
 
162
162
  /**
163
163
  * Creates a subscription, validating its URL and (optionally) enforcing a
164
- * per-owner cap, then persists it. The cap check stays best-effort (count then
165
- * insert, non-transactional) parity with the old store behavior.
164
+ * per-owner cap, then persists it. Capped writes serialize per owner so
165
+ * concurrent requests cannot exceed the limit.
166
166
  */
167
167
  export async function createSubscription(
168
168
  db: Db.Db,
@@ -174,11 +174,6 @@ export async function createSubscription(
174
174
  const now = (options.now ?? (() => new Date()))()
175
175
  const iso = now.toISOString()
176
176
 
177
- if (options.maxPerOwner !== undefined) {
178
- const existing = await WebhookSubscriptions.count(db, input.owner, iso)
179
- if (existing >= options.maxPerOwner) throw new LimitExceededError(options.maxPerOwner)
180
- }
181
-
182
177
  const subscription: Subscription = {
183
178
  chainId: input.chainId,
184
179
  ...(input.context === undefined ? {} : { context: input.context }),
@@ -197,14 +192,20 @@ export async function createSubscription(
197
192
  : { expiresAt: new Date(now.getTime() + input.ttl).toISOString() }),
198
193
  }
199
194
 
200
- await WebhookSubscriptions.insert(db, subscription)
195
+ if (options.maxPerOwner !== undefined) {
196
+ const inserted = await WebhookSubscriptions.insertWithinLimit(db, subscription, {
197
+ maxPerOwner: options.maxPerOwner,
198
+ now: iso,
199
+ })
200
+ if (!inserted) throw new LimitExceededError(options.maxPerOwner)
201
+ } else await WebhookSubscriptions.insert(db, subscription)
201
202
  return subscription
202
203
  }
203
204
 
204
205
  export declare namespace createSubscription {
205
206
  /** Options for {@link createSubscription}. */
206
207
  type Options = {
207
- /** Reject creation once the owner has this many subscriptions. */
208
+ /** Reject creation once the owner has this many live subscriptions. */
208
209
  maxPerOwner?: number | undefined
209
210
  /** Clock used for timestamps (injectable for tests). */
210
211
  now?: (() => Date) | undefined
@@ -268,6 +269,8 @@ export async function updateSubscription(
268
269
  const current = await getSubscription(db, owner, id)
269
270
  if (!current) return null
270
271
 
272
+ if (current.destination.type !== 'url' && patch.destination?.type === 'url')
273
+ throw new InvalidDestinationTransitionError()
271
274
  if (patch.destination !== undefined) WebhookDestination.assertDestination(patch.destination)
272
275
 
273
276
  const now = (options.now ?? (() => new Date()))()
@@ -550,6 +553,39 @@ export declare namespace deliverAndRecord {
550
553
  }
551
554
  }
552
555
 
556
+ /**
557
+ * Re-resolves an enqueued subscription before delivery. Missing or inactive
558
+ * subscriptions are skipped; delivery uses the current destination and secret.
559
+ */
560
+ export async function deliverCurrentAndRecord(
561
+ db: Db.Db,
562
+ queued: Subscription,
563
+ envelope: Envelope,
564
+ options: deliverAndRecord.Options = {},
565
+ ): Promise<deliverCurrentAndRecord.Result> {
566
+ const subscription = await getSubscription(db, queued.owner, queued.id)
567
+ if (!subscription || subscription.status !== 'active') return { status: 'skipped' }
568
+ return {
569
+ result: await deliverAndRecord(db, subscription, envelope, options),
570
+ status: 'delivered',
571
+ }
572
+ }
573
+
574
+ export declare namespace deliverCurrentAndRecord {
575
+ /** Authoritative delivery outcome, or a skipped stale queue message. */
576
+ type Result =
577
+ | {
578
+ /** Transport result from the current subscription state. */
579
+ result: WebhookDestination.Result
580
+ /** The current subscription was delivered. */
581
+ status: 'delivered'
582
+ }
583
+ | {
584
+ /** The queued subscription is absent or inactive. */
585
+ status: 'skipped'
586
+ }
587
+ }
588
+
553
589
  /** Builds a delivery-log row from an attempt's envelope and classified result. */
554
590
  function buildDelivery(
555
591
  input: {
@@ -783,6 +819,14 @@ export class LimitExceededError extends Error {
783
819
  }
784
820
  }
785
821
 
822
+ /** Thrown when an update would activate URL signing with an unrecoverable secret. */
823
+ export class InvalidDestinationTransitionError extends Error {
824
+ constructor() {
825
+ super('Create a new webhook to change a provider-managed destination to an HTTPS endpoint.')
826
+ this.name = 'Webhooks.InvalidDestinationTransitionError'
827
+ }
828
+ }
829
+
786
830
  /**
787
831
  * Thrown when a subscription's `filters` fail to validate against the schema for
788
832
  * its `eventType`. The detection path treats stored filters as **fail-closed**: