tapimo 0.4.0 → 0.4.3

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 (255) hide show
  1. package/README.md +1 -1
  2. package/dist/ApiKey.d.ts +1 -9
  3. package/dist/ApiKey.d.ts.map +1 -1
  4. package/dist/ApiKey.js +2 -3
  5. package/dist/ApiKey.js.map +1 -1
  6. package/dist/ApiKeys.d.ts +53 -27
  7. package/dist/ApiKeys.d.ts.map +1 -1
  8. package/dist/ApiKeys.js +32 -17
  9. package/dist/ApiKeys.js.map +1 -1
  10. package/dist/App.d.ts +2451 -1533
  11. package/dist/App.d.ts.map +1 -1
  12. package/dist/App.js +23 -9
  13. package/dist/App.js.map +1 -1
  14. package/dist/Assets.d.ts +10 -0
  15. package/dist/Assets.d.ts.map +1 -1
  16. package/dist/Assets.js +4 -0
  17. package/dist/Assets.js.map +1 -1
  18. package/dist/Auth.d.ts +2 -1
  19. package/dist/Auth.d.ts.map +1 -1
  20. package/dist/Auth.js +2 -1
  21. package/dist/Auth.js.map +1 -1
  22. package/dist/Scope.d.ts +40 -15
  23. package/dist/Scope.d.ts.map +1 -1
  24. package/dist/Scope.js +44 -13
  25. package/dist/Scope.js.map +1 -1
  26. package/dist/admin/App.d.ts +560 -9
  27. package/dist/admin/App.d.ts.map +1 -1
  28. package/dist/admin/App.js +38 -6
  29. package/dist/admin/App.js.map +1 -1
  30. package/dist/admin/apps/api-keys.d.ts +10 -24
  31. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  32. package/dist/admin/apps/api-keys.js +10 -3
  33. package/dist/admin/apps/api-keys.js.map +1 -1
  34. package/dist/admin/apps/organizations.d.ts +833 -0
  35. package/dist/admin/apps/organizations.d.ts.map +1 -0
  36. package/dist/admin/apps/organizations.js +733 -0
  37. package/dist/admin/apps/organizations.js.map +1 -0
  38. package/dist/admin/apps/scopes.d.ts +8 -0
  39. package/dist/admin/apps/scopes.d.ts.map +1 -1
  40. package/dist/admin/ui.generated.d.ts +1 -1
  41. package/dist/admin/ui.generated.d.ts.map +1 -1
  42. package/dist/admin/ui.generated.js +53 -13
  43. package/dist/admin/ui.generated.js.map +1 -1
  44. package/dist/analytics/Analytics.d.ts +10 -0
  45. package/dist/analytics/Analytics.d.ts.map +1 -1
  46. package/dist/analytics/Analytics.js +8 -0
  47. package/dist/analytics/Analytics.js.map +1 -1
  48. package/dist/analytics/tables/requestEvents.d.ts +9 -0
  49. package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
  50. package/dist/analytics/tables/requestEvents.js +14 -0
  51. package/dist/analytics/tables/requestEvents.js.map +1 -1
  52. package/dist/apps/data/App.d.ts +1354 -984
  53. package/dist/apps/data/App.d.ts.map +1 -1
  54. package/dist/apps/data/App.js +20 -3
  55. package/dist/apps/data/App.js.map +1 -1
  56. package/dist/apps/data/FundingRouteProviders.d.ts +94 -5
  57. package/dist/apps/data/FundingRouteProviders.d.ts.map +1 -1
  58. package/dist/apps/data/FundingRouteProviders.js +196 -5
  59. package/dist/apps/data/FundingRouteProviders.js.map +1 -1
  60. package/dist/apps/data/routes/activities.d.ts +38 -38
  61. package/dist/apps/data/routes/addresses.d.ts +24 -24
  62. package/dist/apps/data/routes/balances.d.ts +45 -45
  63. package/dist/apps/data/routes/blocks.d.ts +39 -39
  64. package/dist/apps/data/routes/coingecko.d.ts +176 -176
  65. package/dist/apps/data/routes/exchanges.d.ts +453 -453
  66. package/dist/apps/data/routes/fee-amm.d.ts +101 -101
  67. package/dist/apps/data/routes/funding.d.ts +730 -138
  68. package/dist/apps/data/routes/funding.d.ts.map +1 -1
  69. package/dist/apps/data/routes/funding.js +234 -0
  70. package/dist/apps/data/routes/funding.js.map +1 -1
  71. package/dist/apps/data/routes/indexer.d.ts +5 -5
  72. package/dist/apps/data/routes/rpc.d.ts +31 -31
  73. package/dist/apps/data/routes/verified-tokens.d.ts +81 -81
  74. package/dist/apps/management/App.d.ts +1156 -615
  75. package/dist/apps/management/App.d.ts.map +1 -1
  76. package/dist/apps/management/App.js +2 -2
  77. package/dist/apps/management/Billing.d.ts +3 -0
  78. package/dist/apps/management/Billing.d.ts.map +1 -1
  79. package/dist/apps/management/Billing.js +7 -2
  80. package/dist/apps/management/Billing.js.map +1 -1
  81. package/dist/apps/management/routes/api-keys.d.ts +106 -90
  82. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  83. package/dist/apps/management/routes/api-keys.js +41 -30
  84. package/dist/apps/management/routes/api-keys.js.map +1 -1
  85. package/dist/apps/management/routes/billing.d.ts +278 -146
  86. package/dist/apps/management/routes/billing.d.ts.map +1 -1
  87. package/dist/apps/management/routes/billing.js +14 -14
  88. package/dist/apps/management/routes/billing.js.map +1 -1
  89. package/dist/apps/management/routes/invitations.d.ts +93 -20
  90. package/dist/apps/management/routes/invitations.d.ts.map +1 -1
  91. package/dist/apps/management/routes/invitations.js +12 -15
  92. package/dist/apps/management/routes/invitations.js.map +1 -1
  93. package/dist/apps/management/routes/me.d.ts +13 -13
  94. package/dist/apps/management/routes/members.d.ts +89 -18
  95. package/dist/apps/management/routes/members.d.ts.map +1 -1
  96. package/dist/apps/management/routes/members.js +10 -10
  97. package/dist/apps/management/routes/members.js.map +1 -1
  98. package/dist/apps/management/routes/orgs.d.ts +130 -70
  99. package/dist/apps/management/routes/orgs.d.ts.map +1 -1
  100. package/dist/apps/management/routes/orgs.js +19 -19
  101. package/dist/apps/management/routes/orgs.js.map +1 -1
  102. package/dist/apps/management/routes/projects.d.ts +137 -48
  103. package/dist/apps/management/routes/projects.d.ts.map +1 -1
  104. package/dist/apps/management/routes/projects.js +12 -13
  105. package/dist/apps/management/routes/projects.js.map +1 -1
  106. package/dist/apps/management/routes/scopes.d.ts +14 -28
  107. package/dist/apps/management/routes/scopes.d.ts.map +1 -1
  108. package/dist/apps/management/routes/scopes.js +6 -7
  109. package/dist/apps/management/routes/scopes.js.map +1 -1
  110. package/dist/apps/management/routes/usage.d.ts +63 -19
  111. package/dist/apps/management/routes/usage.d.ts.map +1 -1
  112. package/dist/apps/management/routes/usage.js +9 -10
  113. package/dist/apps/management/routes/usage.js.map +1 -1
  114. package/dist/apps/mcp.d.ts.map +1 -1
  115. package/dist/apps/mcp.js +35 -3
  116. package/dist/apps/mcp.js.map +1 -1
  117. package/dist/apps/relay/App.d.ts +26 -4
  118. package/dist/apps/relay/App.d.ts.map +1 -1
  119. package/dist/apps/relay/App.js +17 -14
  120. package/dist/apps/relay/App.js.map +1 -1
  121. package/dist/db/Db.d.ts +5 -3
  122. package/dist/db/Db.d.ts.map +1 -1
  123. package/dist/db/Db.js +52 -0
  124. package/dist/db/Db.js.map +1 -1
  125. package/dist/db/Schema.d.ts +13 -0
  126. package/dist/db/Schema.d.ts.map +1 -1
  127. package/dist/db/Schema.js +16 -1
  128. package/dist/db/Schema.js.map +1 -1
  129. package/dist/db/tables/adminAuditLogs.d.ts +27 -0
  130. package/dist/db/tables/adminAuditLogs.d.ts.map +1 -0
  131. package/dist/db/tables/adminAuditLogs.js +19 -0
  132. package/dist/db/tables/adminAuditLogs.js.map +1 -0
  133. package/dist/db/tables/invitations.d.ts +1 -1
  134. package/dist/db/tables/invitations.d.ts.map +1 -1
  135. package/dist/db/tables/memberships.d.ts +32 -4
  136. package/dist/db/tables/memberships.d.ts.map +1 -1
  137. package/dist/db/tables/memberships.js +31 -0
  138. package/dist/db/tables/memberships.js.map +1 -1
  139. package/dist/db/tables/organizations.d.ts +43 -1
  140. package/dist/db/tables/organizations.d.ts.map +1 -1
  141. package/dist/db/tables/organizations.js +103 -0
  142. package/dist/db/tables/organizations.js.map +1 -1
  143. package/dist/db/tables/projects.d.ts +25 -0
  144. package/dist/db/tables/projects.d.ts.map +1 -1
  145. package/dist/db/tables/projects.js +22 -0
  146. package/dist/db/tables/projects.js.map +1 -1
  147. package/dist/db/tables/webhookDeliveries.d.ts +26 -0
  148. package/dist/db/tables/webhookDeliveries.d.ts.map +1 -1
  149. package/dist/db/tables/webhookDeliveries.js +37 -0
  150. package/dist/db/tables/webhookDeliveries.js.map +1 -1
  151. package/dist/handlers/relay.d.ts +13 -10
  152. package/dist/handlers/relay.d.ts.map +1 -1
  153. package/dist/handlers/relay.js +46 -16
  154. package/dist/handlers/relay.js.map +1 -1
  155. package/dist/internal/Auth.d.ts +13 -18
  156. package/dist/internal/Auth.d.ts.map +1 -1
  157. package/dist/internal/Auth.js +78 -58
  158. package/dist/internal/Auth.js.map +1 -1
  159. package/dist/internal/Cursor.d.ts +1 -1
  160. package/dist/internal/Cursor.d.ts.map +1 -1
  161. package/dist/internal/Cursor.js +4 -0
  162. package/dist/internal/Cursor.js.map +1 -1
  163. package/dist/internal/EdgeCache.d.ts.map +1 -1
  164. package/dist/internal/EdgeCache.js +2 -0
  165. package/dist/internal/EdgeCache.js.map +1 -1
  166. package/dist/internal/FundingRoutes.d.ts +170 -0
  167. package/dist/internal/FundingRoutes.d.ts.map +1 -1
  168. package/dist/internal/FundingRoutes.js +68 -6
  169. package/dist/internal/FundingRoutes.js.map +1 -1
  170. package/dist/internal/Log.d.ts +16 -1
  171. package/dist/internal/Log.d.ts.map +1 -1
  172. package/dist/internal/Log.js +5 -3
  173. package/dist/internal/Log.js.map +1 -1
  174. package/package.json +2 -2
  175. package/src/ApiKey.ts +2 -3
  176. package/src/ApiKeys.ts +95 -17
  177. package/src/App.test.ts +44 -2
  178. package/src/App.ts +29 -9
  179. package/src/Assets.ts +8 -0
  180. package/src/Auth.test-d.ts +36 -0
  181. package/src/Auth.ts +20 -1
  182. package/src/Client.test-d.ts +64 -17
  183. package/src/Scope.test.ts +34 -0
  184. package/src/Scope.ts +53 -17
  185. package/src/admin/App.test.ts +33 -3
  186. package/src/admin/App.ts +49 -14
  187. package/src/admin/apps/api-keys.ts +10 -3
  188. package/src/admin/apps/organizations.test.ts +455 -0
  189. package/src/admin/apps/organizations.ts +953 -0
  190. package/src/admin/apps/scopes.test.ts +10 -0
  191. package/src/admin/ui/plugin.ts +1 -1
  192. package/src/admin/ui/src/components/MobileProfile.tsx +2 -0
  193. package/src/admin/ui/src/components/ThemeToggle.tsx +20 -0
  194. package/src/admin/ui/src/lib/api-keys.ts +36 -19
  195. package/src/admin/ui/src/lib/config.ts +2 -3
  196. package/src/admin/ui/src/lib/organizations.test.ts +89 -0
  197. package/src/admin/ui/src/lib/organizations.ts +262 -0
  198. package/src/admin/ui/src/lib/query-client.ts +3 -1
  199. package/src/admin/ui/src/lib/theme.ts +57 -0
  200. package/src/admin/ui/src/routeTree.gen.ts +47 -4
  201. package/src/admin/ui/src/routes/__root.tsx +18 -3
  202. package/src/admin/ui/src/routes/api-keys.tsx +52 -7
  203. package/src/admin/ui/src/routes/index.tsx +1 -1
  204. package/src/admin/ui/src/routes/organizations.tsx +158 -0
  205. package/src/admin/ui/src/routes/organizations_.$orgId.tsx +710 -0
  206. package/src/admin/ui/src/start.ts +6 -0
  207. package/src/admin/ui.generated.ts +63 -13
  208. package/src/analytics/Analytics.ts +25 -0
  209. package/src/analytics/tables/requestEvents.ts +33 -0
  210. package/src/apps/composition.test.ts +82 -2
  211. package/src/apps/data/App.ts +24 -3
  212. package/src/apps/data/FundingRouteProviders.test.ts +204 -0
  213. package/src/apps/data/FundingRouteProviders.ts +294 -9
  214. package/src/apps/data/routes/funding.test.ts +90 -23
  215. package/src/apps/data/routes/funding.ts +315 -0
  216. package/src/apps/data/routes/rpc.test.ts +33 -0
  217. package/src/apps/management/App.ts +2 -2
  218. package/src/apps/management/Billing.ts +10 -2
  219. package/src/apps/management/routes/api-keys.test.ts +137 -1
  220. package/src/apps/management/routes/api-keys.ts +63 -38
  221. package/src/apps/management/routes/billing.ts +14 -14
  222. package/src/apps/management/routes/invitations.test.ts +102 -1
  223. package/src/apps/management/routes/invitations.ts +13 -15
  224. package/src/apps/management/routes/members.ts +10 -10
  225. package/src/apps/management/routes/orgs.test.ts +78 -16
  226. package/src/apps/management/routes/orgs.ts +21 -20
  227. package/src/apps/management/routes/projects.ts +12 -13
  228. package/src/apps/management/routes/scopes.test.ts +16 -0
  229. package/src/apps/management/routes/scopes.ts +6 -7
  230. package/src/apps/management/routes/usage.ts +9 -14
  231. package/src/apps/mcp.test.ts +25 -1
  232. package/src/apps/mcp.ts +43 -3
  233. package/src/apps/relay/App.test.ts +23 -18
  234. package/src/apps/relay/App.ts +35 -14
  235. package/src/db/Db.ts +59 -3
  236. package/src/db/Schema.ts +20 -1
  237. package/src/db/tables/adminAuditLogs.ts +47 -0
  238. package/src/db/tables/invitations.ts +1 -1
  239. package/src/db/tables/memberships.ts +59 -3
  240. package/src/db/tables/organizations.ts +148 -1
  241. package/src/db/tables/projects.ts +47 -0
  242. package/src/db/tables/webhookDeliveries.ts +83 -0
  243. package/src/handlers/relay.test-d.ts +8 -0
  244. package/src/handlers/relay.test.ts +2 -2
  245. package/src/handlers/relay.ts +62 -22
  246. package/src/internal/Auth.test.ts +174 -7
  247. package/src/internal/Auth.ts +90 -62
  248. package/src/internal/Cursor.test.ts +10 -0
  249. package/src/internal/Cursor.ts +5 -1
  250. package/src/internal/EdgeCache.test.ts +11 -0
  251. package/src/internal/EdgeCache.ts +2 -0
  252. package/src/internal/FundingRoutes.test.ts +26 -0
  253. package/src/internal/FundingRoutes.ts +187 -18
  254. package/src/internal/Log.test.ts +41 -3
  255. package/src/internal/Log.ts +23 -4
@@ -1,10 +1,48 @@
1
- import type * as FundingRoutes from '../../internal/FundingRoutes.js'
1
+ import * as z from 'zod/mini'
2
+
3
+ import * as FundingRoutes from '../../internal/FundingRoutes.js'
2
4
 
3
5
  type Fetch = typeof globalThis.fetch
4
6
 
7
+ const defaultFetch: Fetch = (input, init) => globalThis.fetch(input, init)
8
+
5
9
  const defaultAcrossBaseUrl = 'https://app.across.to/api'
6
10
  const defaultRelayBaseUrl = 'https://api.relay.link'
7
11
 
12
+ namespace schema {
13
+ const DecimalString = z.string().check(z.regex(/^\d+(\.\d+)?$/))
14
+ const IntegerString = z.string().check(z.regex(/^\d+$/))
15
+
16
+ const AcrossToken = z.object({
17
+ address: z.string(),
18
+ chainId: z.number().check(z.int(), z.nonnegative()),
19
+ decimals: z.number().check(z.int(), z.nonnegative()),
20
+ symbol: z.string(),
21
+ })
22
+
23
+ const AcrossFee = z.object({
24
+ amount: IntegerString,
25
+ amountUsd: DecimalString,
26
+ pct: IntegerString,
27
+ token: AcrossToken,
28
+ })
29
+
30
+ export const AcrossQuote = z.object({
31
+ amountType: z.literal('exactInput'),
32
+ expectedFillTime: z.number().check(z.int(), z.nonnegative()),
33
+ expectedOutputAmount: IntegerString,
34
+ fees: z.object({
35
+ total: AcrossFee,
36
+ totalMax: AcrossFee,
37
+ }),
38
+ inputAmount: IntegerString,
39
+ inputToken: AcrossToken,
40
+ minOutputAmount: IntegerString,
41
+ outputToken: AcrossToken,
42
+ quoteExpiryTimestamp: z.number().check(z.int(), z.nonnegative()),
43
+ })
44
+ }
45
+
8
46
  /** Creates the live funding route adapters configured for this deployment. */
9
47
  export function createAdapters(options: createAdapters.Options = {}) {
10
48
  return {
@@ -16,17 +54,17 @@ export function createAdapters(options: createAdapters.Options = {}) {
16
54
  export declare namespace createAdapters {
17
55
  /** Provider adapter factory options. Omit a provider to keep its routes static. */
18
56
  type Options = {
19
- /** Across quote adapter options. */
57
+ /** Across discovery adapter options. */
20
58
  across?: across.Options | false | undefined
21
59
  /** Relay quote adapter options. */
22
60
  relay?: relay.Options | false | undefined
23
61
  }
24
62
  }
25
63
 
26
- /** Creates an Across quote adapter backed by `/suggested-fees`. */
64
+ /** Creates an Across discovery adapter backed by `/suggested-fees`. */
27
65
  export function across(options: across.Options): FundingRoutes.ProviderAdapter {
28
66
  const baseUrl = normalizeBaseUrl(options.baseUrl ?? defaultAcrossBaseUrl)
29
- const fetcher = options.fetch ?? globalThis.fetch
67
+ const fetcher = options.fetch ?? defaultFetch
30
68
  const now = options.now ?? (() => new Date())
31
69
 
32
70
  return {
@@ -65,17 +103,141 @@ export function across(options: across.Options): FundingRoutes.ProviderAdapter {
65
103
  }
66
104
  }
67
105
 
68
- export declare namespace across {
69
- /** Across quote adapter options. */
106
+ /** Builds the authenticated Across `/swap/approval` request for an exact-input quote. */
107
+ export function createAcrossQuoteRequest(options: createAcrossQuoteRequest.Options) {
108
+ assertAcrossIntegratorId(options.integratorId)
109
+ const baseUrl = normalizeBaseUrl(options.baseUrl ?? defaultAcrossBaseUrl)
110
+ const sourceChainId = providerChainId(options.route.sourceChain, 'across')
111
+ const destinationChainId = eip155ChainId(options.route.destinationChain)
112
+ const url = new URL(`${baseUrl}/swap/approval`)
113
+ url.search = new URLSearchParams({
114
+ amount: options.route.sourceAmount.amount,
115
+ depositor: options.depositor,
116
+ destinationChainId,
117
+ inputToken: options.route.sourceToken.address,
118
+ integratorId: options.integratorId,
119
+ originChainId: sourceChainId,
120
+ outputToken: options.route.destinationToken.address,
121
+ skipOriginTxEstimation: 'true',
122
+ strictTradeType: 'true',
123
+ tradeType: 'exactInput',
124
+ }).toString()
125
+ if (options.recipient !== undefined) url.searchParams.set('recipient', options.recipient)
126
+ return { headers: { Authorization: `Bearer ${options.apiKey}` }, url }
127
+ }
128
+
129
+ export declare namespace createAcrossQuoteRequest {
130
+ /** Inputs required to construct an Across exact-input quote request. */
131
+ type Options = {
132
+ /** Across API key sent as a bearer token. */
133
+ apiKey: string
134
+ /** Optional Across API base URL. */
135
+ baseUrl?: string | undefined
136
+ /** Address that would submit the source transaction. */
137
+ depositor: string
138
+ /** Across integrator id sent as the required query parameter. */
139
+ integratorId: string
140
+ /** Optional destination recipient. */
141
+ recipient?: string | undefined
142
+ /** Curated route selected for the quote. */
143
+ route: FundingRoutes.RouteCandidate
144
+ }
145
+ }
146
+
147
+ /** Creates an explicit Across quote adapter backed by `/swap/approval`. */
148
+ export function acrossQuote(options: acrossQuote.Options): FundingRoutes.ProviderQuote {
149
+ assertAcrossIntegratorId(options.integratorId)
150
+
151
+ return async ({ depositor, recipient, route }, signal) => {
152
+ const request = createAcrossQuoteRequest({
153
+ apiKey: options.apiKey,
154
+ baseUrl: options.baseUrl,
155
+ depositor,
156
+ integratorId: options.integratorId,
157
+ recipient,
158
+ route,
159
+ })
160
+
161
+ try {
162
+ const body = await requestJson(request.url, {
163
+ fetcher: defaultFetch,
164
+ headers: request.headers,
165
+ signal,
166
+ })
167
+ return parseAcrossQuote({ body, now: new Date(), route })
168
+ } catch (cause) {
169
+ if (!(cause instanceof ProviderResponseError)) throw cause
170
+ throw mapAcrossQuoteError({ body: cause.body, status: cause.status })
171
+ }
172
+ }
173
+ }
174
+
175
+ export declare namespace acrossQuote {
176
+ /** Across Swap API adapter options. */
70
177
  type Options = {
71
178
  /** Across API key sent as a bearer token. */
72
179
  apiKey: string
180
+ /** Optional Across API base URL. */
181
+ baseUrl?: string | undefined
73
182
  /** Across integrator id sent as the required query parameter. */
74
183
  integratorId: string
184
+ }
185
+ }
186
+
187
+ /** Returns bounded Across quote failure details suitable for structured request logs. */
188
+ export function acrossQuoteFailure(options: acrossQuoteFailure.Options): acrossQuoteFailure.Result {
189
+ const { cause } = options
190
+ const base = { id: 'across', operation: 'swap/approval' } as const
191
+ if (cause instanceof ProviderResponseError) {
192
+ const code = providerErrorCode(cause.body)
193
+ return {
194
+ ...(code === undefined ? {} : { code }),
195
+ failure: 'http' as const,
196
+ ...base,
197
+ status: cause.status,
198
+ }
199
+ }
200
+ if (cause instanceof ProviderPayloadError) return { failure: 'payload', ...base }
201
+ if (cause instanceof TypeError) return { failure: 'network', ...base }
202
+ if (cause instanceof Error) return { failure: 'unknown', ...base }
203
+ return undefined
204
+ }
205
+
206
+ export declare namespace acrossQuoteFailure {
207
+ /** Inputs used to classify an Across quote failure. */
208
+ type Options = {
209
+ /** Error thrown by the Across quote boundary. */
210
+ cause: unknown
211
+ }
212
+
213
+ /** Bounded diagnostic fields, or undefined when the thrown value is not an error. */
214
+ type Result =
215
+ | {
216
+ /** Stable provider error code, when one is safe to retain. */
217
+ code?: string | undefined
218
+ /** Failure boundary that rejected the provider request. */
219
+ failure: 'http' | 'network' | 'payload' | 'unknown'
220
+ /** Provider identifier. */
221
+ id: 'across'
222
+ /** Across operation identifier. */
223
+ operation: 'swap/approval'
224
+ /** Upstream HTTP status, when a response was received. */
225
+ status?: number | undefined
226
+ }
227
+ | undefined
228
+ }
229
+
230
+ export declare namespace across {
231
+ /** Across API adapter options. */
232
+ type Options = {
75
233
  /** Override for tests or non-production Across environments. */
76
234
  baseUrl?: string | undefined
77
235
  /** Fetch implementation override for tests. */
78
236
  fetch?: Fetch | undefined
237
+ /** Across API key sent as a bearer token. */
238
+ apiKey: string
239
+ /** Across integrator id sent as the required query parameter. */
240
+ integratorId: string
79
241
  /** Clock override for deterministic tests. */
80
242
  now?: (() => Date) | undefined
81
243
  }
@@ -84,7 +246,7 @@ export declare namespace across {
84
246
  /** Creates a Relay quote adapter backed by `/chains/liquidity` and `/quote/v2`. */
85
247
  export function relay(options: relay.Options): FundingRoutes.ProviderAdapter {
86
248
  const baseUrl = normalizeBaseUrl(options.baseUrl ?? defaultRelayBaseUrl)
87
- const fetcher = options.fetch ?? globalThis.fetch
249
+ const fetcher = options.fetch ?? defaultFetch
88
250
  const now = options.now ?? (() => new Date())
89
251
 
90
252
  return {
@@ -152,6 +314,85 @@ export declare namespace relay {
152
314
  }
153
315
  }
154
316
 
317
+ /** Maps an Across HTTP error into the stable funding-provider error contract. */
318
+ export function mapAcrossQuoteError(options: mapAcrossQuoteError.Options) {
319
+ if (options.status === 429) return new FundingRoutes.ProviderRateLimitError()
320
+ const cause = new ProviderResponseError(options.status, options.body)
321
+ const unavailable = providerUnavailable(cause, [
322
+ 'AMOUNT_TOO_HIGH',
323
+ 'AMOUNT_TOO_LOW',
324
+ 'INSUFFICIENT_LIQUIDITY',
325
+ 'NO_QUOTES',
326
+ 'NO_ROUTE',
327
+ 'UNSUPPORTED_ROUTE',
328
+ ])
329
+ if (unavailable) return new FundingRoutes.ProviderQuoteUnavailableError()
330
+ return cause
331
+ }
332
+
333
+ export declare namespace mapAcrossQuoteError {
334
+ /** Across HTTP error response to classify. */
335
+ type Options = {
336
+ /** Parsed response body returned by Across. */
337
+ body: unknown
338
+ /** Across HTTP response status. */
339
+ status: number
340
+ }
341
+ }
342
+
343
+ /** Validates and normalizes an Across quote payload against its curated route. */
344
+ export function parseAcrossQuote(
345
+ options: parseAcrossQuote.Options,
346
+ ): FundingRoutes.ProviderQuoteResult {
347
+ const parsed = schema.AcrossQuote.safeParse(options.body)
348
+ if (!parsed.success) throw new ProviderPayloadError()
349
+ const response = parsed.data
350
+ const total = response.fees.total
351
+ const totalMax = response.fees.totalMax
352
+ if (
353
+ response.inputAmount !== options.route.sourceAmount.amount ||
354
+ response.inputToken.address.toLowerCase() !== options.route.sourceToken.address.toLowerCase() ||
355
+ response.inputToken.chainId !== Number(eip155ChainId(options.route.sourceChain)) ||
356
+ response.inputToken.decimals !== options.route.sourceToken.decimals ||
357
+ response.outputToken.address.toLowerCase() !==
358
+ options.route.destinationToken.address.toLowerCase() ||
359
+ response.outputToken.chainId !== Number(eip155ChainId(options.route.destinationChain)) ||
360
+ response.outputToken.decimals !== options.route.destinationToken.decimals ||
361
+ BigInt(response.expectedOutputAmount) <= 0n ||
362
+ BigInt(response.minOutputAmount) <= 0n ||
363
+ BigInt(response.minOutputAmount) > BigInt(response.expectedOutputAmount) ||
364
+ response.quoteExpiryTimestamp * 1_000 <= options.now.getTime() ||
365
+ !matchesFeeToken(total.token, options.route) ||
366
+ !matchesFeeToken(totalMax.token, options.route) ||
367
+ BigInt(totalMax.amount) < BigInt(total.amount) ||
368
+ BigInt(totalMax.pct) < BigInt(total.pct)
369
+ )
370
+ throw new ProviderPayloadError()
371
+ return {
372
+ estimatedSeconds: response.expectedFillTime,
373
+ expiresAt: isoFromUnixSeconds(String(response.quoteExpiryTimestamp)),
374
+ fees: {
375
+ total: formatAcrossFee(response.fees.total),
376
+ totalMax: formatAcrossFee(response.fees.totalMax),
377
+ },
378
+ minimumOutputAmount: response.minOutputAmount,
379
+ outputAmount: response.expectedOutputAmount,
380
+ sampledAt: options.now.toISOString(),
381
+ }
382
+ }
383
+
384
+ export declare namespace parseAcrossQuote {
385
+ /** Inputs required to validate and normalize an Across quote payload. */
386
+ type Options = {
387
+ /** Untrusted JSON body returned by Across. */
388
+ body: unknown
389
+ /** Timestamp when Tempo sampled the response. */
390
+ now: Date
391
+ /** Curated route the response must match. */
392
+ route: FundingRoutes.RouteCandidate
393
+ }
394
+ }
395
+
155
396
  function formatAcrossResult(
156
397
  route: FundingRoutes.RouteCandidate,
157
398
  body: unknown,
@@ -191,6 +432,22 @@ function formatAcrossResult(
191
432
  }
192
433
  }
193
434
 
435
+ function formatAcrossFee(
436
+ fee: z.output<typeof schema.AcrossQuote>['fees']['total'],
437
+ ): FundingRoutes.ProviderQuoteFee {
438
+ return {
439
+ amount: fee.amount,
440
+ amountUsd: fee.amountUsd,
441
+ bps: scaledPctToBps(fee.pct),
442
+ token: {
443
+ address: fee.token.address,
444
+ chainId: `eip155:${fee.token.chainId}`,
445
+ decimals: fee.token.decimals,
446
+ symbol: fee.token.symbol,
447
+ },
448
+ }
449
+ }
450
+
194
451
  function formatRelayResult(
195
452
  route: FundingRoutes.RouteCandidate,
196
453
  quote: unknown,
@@ -333,12 +590,17 @@ function relayHeaders(apiKey: string | undefined): Record<string, string> | unde
333
590
  function providerUnavailable(cause: unknown, codes: readonly string[]) {
334
591
  if (!(cause instanceof ProviderResponseError) || cause.status < 400 || cause.status >= 500)
335
592
  return undefined
336
- const body = record(cause.body)
337
- const code = stringValue(body['code']) ?? stringValue(body['errorCode'])
593
+ const code = providerErrorCode(cause.body)
338
594
  if (code && codes.includes(code)) return code
339
595
  return undefined
340
596
  }
341
597
 
598
+ function providerErrorCode(input: unknown) {
599
+ const body = record(input)
600
+ const code = stringValue(body['code']) ?? stringValue(body['errorCode'])
601
+ return code && /^[A-Za-z0-9_.:-]{1,64}$/.test(code) ? code : undefined
602
+ }
603
+
342
604
  class ProviderResponseError extends Error {
343
605
  override name = 'FundingRouteProviders.ProviderResponseError'
344
606
 
@@ -350,6 +612,14 @@ class ProviderResponseError extends Error {
350
612
  }
351
613
  }
352
614
 
615
+ class ProviderPayloadError extends Error {
616
+ override name = 'FundingRouteProviders.ProviderPayloadError'
617
+ }
618
+
619
+ class ProviderConfigurationError extends Error {
620
+ override name = 'FundingRouteProviders.ProviderConfigurationError'
621
+ }
622
+
353
623
  function providerChainId(
354
624
  routeChain: FundingRoutes.RouteCandidate['sourceChain'],
355
625
  provider: string,
@@ -371,6 +641,21 @@ class UnsupportedProviderRouteError extends Error {
371
641
  override name = 'FundingRouteProviders.UnsupportedProviderRouteError'
372
642
  }
373
643
 
644
+ function assertAcrossIntegratorId(integratorId: string) {
645
+ if (!/^0x[\da-fA-F]{4}$/.test(integratorId)) throw new ProviderConfigurationError()
646
+ }
647
+
648
+ function matchesFeeToken(
649
+ token: z.output<typeof schema.AcrossQuote>['fees']['total']['token'],
650
+ route: FundingRoutes.RouteCandidate,
651
+ ) {
652
+ return (
653
+ token.address.toLowerCase() === route.sourceToken.address.toLowerCase() &&
654
+ token.chainId === Number(eip155ChainId(route.sourceChain)) &&
655
+ token.decimals === route.sourceToken.decimals
656
+ )
657
+ }
658
+
374
659
  function normalizeBaseUrl(input: string) {
375
660
  return input.replace(/\/+$/, '')
376
661
  }
@@ -1,6 +1,8 @@
1
1
  import { Schema } from 'tapimo'
2
2
 
3
3
  import * as TestApp from '../../../../test/App.js'
4
+ import type * as Log from '../../../internal/Log.js'
5
+ import * as FundingRouteProviders from '../FundingRouteProviders.js'
4
6
  import * as Funding from './funding.js'
5
7
 
6
8
  describe('GET /funding/routes', () => {
@@ -233,6 +235,84 @@ describe('GET /funding/routes', () => {
233
235
  })
234
236
  })
235
237
 
238
+ describe('POST /funding/routes/quote', () => {
239
+ test('rejects invalid depositor addresses', async () => {
240
+ const response = await TestApp.create().request('/v1/funding/routes/quote', {
241
+ body: JSON.stringify({
242
+ depositor: 'invalid',
243
+ provider: 'across',
244
+ sourceAmount: '1000000',
245
+ sourceChain: 'base',
246
+ sourceToken: 'USDC',
247
+ }),
248
+ headers: { ...TestApp.auth.headers, 'content-type': 'application/json' },
249
+ method: 'POST',
250
+ })
251
+
252
+ expect(response.status).toBe(400)
253
+ expect(response.headers.get('cache-control')).toBe('no-store')
254
+ expect(await response.json()).toMatchObject({ error: { code: 'body_invalid' } })
255
+ })
256
+
257
+ test('returns a stable error when Across quoting is not configured', async () => {
258
+ const response = await TestApp.create().request('/v1/funding/routes/quote', {
259
+ body: JSON.stringify(quoteRequest()),
260
+ headers: { ...TestApp.auth.headers, 'content-type': 'application/json' },
261
+ method: 'POST',
262
+ })
263
+
264
+ expect(response.status).toBe(501)
265
+ expect(response.headers.get('cache-control')).toBe('no-store')
266
+ expect(await response.json()).toMatchObject({ error: { code: 'provider_unconfigured' } })
267
+ })
268
+
269
+ test('records bounded Across failure details in the request log', async () => {
270
+ const entries: Log.Entry[] = []
271
+ const response = await TestApp.create({
272
+ fundingRoutes: {
273
+ async acrossQuote() {
274
+ throw FundingRouteProviders.mapAcrossQuoteError({
275
+ body: { code: 'AUTH_FAILED', detail: 'not logged' },
276
+ status: 401,
277
+ })
278
+ },
279
+ },
280
+ logger: (entry) => void entries.push(entry),
281
+ }).request('/v1/funding/routes/quote', {
282
+ body: JSON.stringify(quoteRequest()),
283
+ headers: { ...TestApp.auth.headers, 'content-type': 'application/json' },
284
+ method: 'POST',
285
+ })
286
+
287
+ expect(response.status).toBe(502)
288
+ expect(entries[0]).toMatchObject({
289
+ errorCode: 'provider_error',
290
+ provider: {
291
+ code: 'AUTH_FAILED',
292
+ failure: 'http',
293
+ id: 'across',
294
+ operation: 'swap/approval',
295
+ status: 401,
296
+ },
297
+ })
298
+ expect(JSON.stringify(entries[0])).not.toContain('not logged')
299
+ })
300
+
301
+ test('publishes the normalized quote contract in OpenAPI', async () => {
302
+ const response = await TestApp.create().request('/openapi.json')
303
+ const document = (await response.json()) as {
304
+ paths: Record<string, { post?: { operationId?: string; responses?: object } }>
305
+ }
306
+ const operation = document.paths['/v1/funding/routes/quote']?.post
307
+
308
+ expect(operation).toMatchObject({
309
+ operationId: 'quoteFundingRoute',
310
+ responses: { 200: expect.any(Object), 400: expect.any(Object), 502: expect.any(Object) },
311
+ })
312
+ expect(JSON.stringify(operation)).not.toMatch(/approvalTxns|checks|swapTx/)
313
+ })
314
+ })
315
+
236
316
  describe('GET /funding/chains', () => {
237
317
  test('lists source chain and stablecoin inventory', async () => {
238
318
  const response = await TestApp.client().v1.funding.chains.$get(undefined, TestApp.auth)
@@ -252,6 +332,16 @@ describe('GET /funding/chains', () => {
252
332
  })
253
333
  })
254
334
 
335
+ function quoteRequest() {
336
+ return {
337
+ depositor: '0x1111111111111111111111111111111111111111',
338
+ provider: 'across',
339
+ sourceAmount: '1000000',
340
+ sourceChain: 'base',
341
+ sourceToken: 'USDC',
342
+ }
343
+ }
344
+
255
345
  describe('GET /providers', () => {
256
346
  test('lists providers filtered by capability', async () => {
257
347
  const response = await TestApp.client().v1.providers.$get(
@@ -280,29 +370,6 @@ describe('GET /providers', () => {
280
370
  ])
281
371
  })
282
372
 
283
- test('marks configured live provider adapters', async () => {
284
- const app = TestApp.create({
285
- fundingRoutes: {
286
- adapters: {
287
- across: {
288
- async probeQuote() {
289
- throw new Error('not used')
290
- },
291
- },
292
- },
293
- },
294
- })
295
-
296
- const response = await app.request('/v1/providers?capability=fundingRoutes', {
297
- headers: TestApp.auth.headers,
298
- })
299
- expect(response.status).toBe(200)
300
- const body = await TestApp.json(response, Funding.schema.listProviders.Response)
301
-
302
- expect(body.data.find((provider) => provider.id === 'across')?.status).toBe('configured')
303
- expect(body.data.find((provider) => provider.id === 'relay')?.status).toBe('inventoryOnly')
304
- })
305
-
306
373
  test('rejects unsupported provider capabilities', async () => {
307
374
  const response = await TestApp.create().request('/v1/providers?capability=settlement', {
308
375
  headers: TestApp.auth.headers,