tapimo 0.12.0 → 0.13.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 (257) hide show
  1. package/dist/App.d.ts +5561 -5026
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +38 -3
  4. package/dist/App.js.map +1 -1
  5. package/dist/Scope.d.ts +2 -0
  6. package/dist/Scope.d.ts.map +1 -1
  7. package/dist/Scope.js +17 -0
  8. package/dist/Scope.js.map +1 -1
  9. package/dist/admin/App.d.ts +123 -74
  10. package/dist/admin/App.d.ts.map +1 -1
  11. package/dist/admin/apps/api-keys.d.ts +80 -0
  12. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  13. package/dist/admin/apps/api-keys.js +54 -0
  14. package/dist/admin/apps/api-keys.js.map +1 -1
  15. package/dist/admin/apps/earn-vaults.d.ts +15 -15
  16. package/dist/admin/apps/earn-vaults.js +1 -1
  17. package/dist/admin/apps/verified-tokens.d.ts +81 -81
  18. package/dist/admin/ui.generated.d.ts +1 -1
  19. package/dist/admin/ui.generated.d.ts.map +1 -1
  20. package/dist/admin/ui.generated.js +151 -151
  21. package/dist/admin/ui.generated.js.map +1 -1
  22. package/dist/apps/Zones.d.ts +16 -16
  23. package/dist/apps/Zones.js +4 -4
  24. package/dist/apps/data/App.js +18 -18
  25. package/dist/apps/data/App.js.map +1 -1
  26. package/dist/apps/data/ZoneSelection.d.ts +8 -8
  27. package/dist/apps/data/ZoneSelection.d.ts.map +1 -1
  28. package/dist/apps/data/ZoneSelection.js +1 -1
  29. package/dist/apps/data/ZoneSelection.js.map +1 -1
  30. package/dist/apps/data/routes/activities.d.ts +588 -584
  31. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  32. package/dist/apps/data/routes/activities.js +1561 -1239
  33. package/dist/apps/data/routes/activities.js.map +1 -1
  34. package/dist/apps/data/routes/balances.d.ts +26 -26
  35. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  36. package/dist/apps/data/routes/balances.js +43 -16
  37. package/dist/apps/data/routes/balances.js.map +1 -1
  38. package/dist/apps/data/routes/blocks.d.ts +44 -44
  39. package/dist/apps/data/routes/blocks.d.ts.map +1 -1
  40. package/dist/apps/data/routes/blocks.js +21 -11
  41. package/dist/apps/data/routes/blocks.js.map +1 -1
  42. package/dist/apps/data/routes/coingecko.d.ts +3 -3
  43. package/dist/apps/data/routes/earn.d.ts +18 -18
  44. package/dist/apps/data/routes/earn.js +13 -13
  45. package/dist/apps/data/routes/exchanges.d.ts +145 -145
  46. package/dist/apps/data/routes/fee-amm.d.ts +2 -2
  47. package/dist/apps/data/routes/receipts.d.ts +155 -55
  48. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  49. package/dist/apps/data/routes/receipts.js +28 -10
  50. package/dist/apps/data/routes/receipts.js.map +1 -1
  51. package/dist/apps/data/routes/tokenlist.d.ts +13 -13
  52. package/dist/apps/data/routes/tokens.d.ts +98 -98
  53. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  54. package/dist/apps/data/routes/tokens.js +51 -20
  55. package/dist/apps/data/routes/tokens.js.map +1 -1
  56. package/dist/apps/data/routes/transactions.d.ts +7 -2
  57. package/dist/apps/data/routes/transactions.d.ts.map +1 -1
  58. package/dist/apps/data/routes/transactions.js +37 -17
  59. package/dist/apps/data/routes/transactions.js.map +1 -1
  60. package/dist/apps/data/routes/transfers.d.ts +1 -1
  61. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  62. package/dist/apps/data/routes/transfers.js +18 -12
  63. package/dist/apps/data/routes/transfers.js.map +1 -1
  64. package/dist/apps/data/routes/valuation.d.ts +1 -1
  65. package/dist/apps/data/routes/valuation.d.ts.map +1 -1
  66. package/dist/apps/data/routes/valuation.js +12 -7
  67. package/dist/apps/data/routes/valuation.js.map +1 -1
  68. package/dist/apps/data/routes/verified-tokens.d.ts +3 -3
  69. package/dist/apps/data/routes/webhooks.d.ts +11 -11
  70. package/dist/apps/data/routes/zones.d.ts +14 -14
  71. package/dist/apps/data/routes/zones.js +1 -1
  72. package/dist/apps/funding/App.d.ts +11 -0
  73. package/dist/apps/funding/App.d.ts.map +1 -1
  74. package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
  75. package/dist/apps/funding/routes/deposit-addresses.js +16 -11
  76. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
  77. package/dist/apps/funding/routes/transfers.d.ts +60 -48
  78. package/dist/apps/funding/routes/transfers.d.ts.map +1 -1
  79. package/dist/apps/funding/routes/transfers.js +27 -19
  80. package/dist/apps/funding/routes/transfers.js.map +1 -1
  81. package/dist/apps/management/App.d.ts +4487 -3963
  82. package/dist/apps/management/App.d.ts.map +1 -1
  83. package/dist/apps/management/routes/api-keys.d.ts +710 -159
  84. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  85. package/dist/apps/management/routes/api-keys.js +236 -4
  86. package/dist/apps/management/routes/api-keys.js.map +1 -1
  87. package/dist/apps/management/routes/faucet.d.ts +6 -6
  88. package/dist/apps/management/routes/invite-links.d.ts +42 -42
  89. package/dist/apps/management/routes/members.d.ts +30 -30
  90. package/dist/apps/management/routes/projects.d.ts +40 -40
  91. package/dist/apps/management/routes/usage.d.ts +24 -24
  92. package/dist/apps/management/routes/verified-token-requests.d.ts +1 -1
  93. package/dist/apps/management/routes/webhooks.d.ts +55 -55
  94. package/dist/db/Db.d.ts.map +1 -1
  95. package/dist/db/Db.js +12 -1
  96. package/dist/db/Db.js.map +1 -1
  97. package/dist/db/Schema.d.ts +3 -2
  98. package/dist/db/Schema.d.ts.map +1 -1
  99. package/dist/db/Schema.js +5 -2
  100. package/dist/db/Schema.js.map +1 -1
  101. package/dist/db/tables/fundingCatalog.d.ts +4 -2
  102. package/dist/db/tables/fundingCatalog.d.ts.map +1 -1
  103. package/dist/db/tables/fundingCatalog.js +5 -1
  104. package/dist/db/tables/fundingCatalog.js.map +1 -1
  105. package/dist/handlers/internal/utils.d.ts.map +1 -1
  106. package/dist/handlers/internal/utils.js +9 -0
  107. package/dist/handlers/internal/utils.js.map +1 -1
  108. package/dist/internal/Auth.d.ts +3 -1
  109. package/dist/internal/Auth.d.ts.map +1 -1
  110. package/dist/internal/Auth.js +6 -3
  111. package/dist/internal/Auth.js.map +1 -1
  112. package/dist/internal/EarnVaults.d.ts +3 -3
  113. package/dist/internal/EarnVaults.js +3 -3
  114. package/dist/internal/Fetch.d.ts +23 -0
  115. package/dist/internal/Fetch.d.ts.map +1 -0
  116. package/dist/internal/Fetch.js +26 -0
  117. package/dist/internal/Fetch.js.map +1 -0
  118. package/dist/internal/Log.d.ts +3 -1
  119. package/dist/internal/Log.d.ts.map +1 -1
  120. package/dist/internal/Log.js +16 -3
  121. package/dist/internal/Log.js.map +1 -1
  122. package/dist/internal/OpenApi.d.ts +5 -3
  123. package/dist/internal/OpenApi.d.ts.map +1 -1
  124. package/dist/internal/OpenApi.js +13 -3
  125. package/dist/internal/OpenApi.js.map +1 -1
  126. package/dist/internal/Response.d.ts +1 -1
  127. package/dist/internal/Response.d.ts.map +1 -1
  128. package/dist/internal/Schema.d.ts +6 -3
  129. package/dist/internal/Schema.d.ts.map +1 -1
  130. package/dist/internal/Schema.js +8 -2
  131. package/dist/internal/Schema.js.map +1 -1
  132. package/dist/internal/Store.d.ts +16 -3
  133. package/dist/internal/Store.d.ts.map +1 -1
  134. package/dist/internal/Store.js +82 -16
  135. package/dist/internal/Store.js.map +1 -1
  136. package/dist/internal/Tidx.d.ts.map +1 -1
  137. package/dist/internal/Tidx.js +7 -2
  138. package/dist/internal/Tidx.js.map +1 -1
  139. package/dist/internal/Timeout.d.ts +36 -0
  140. package/dist/internal/Timeout.d.ts.map +1 -0
  141. package/dist/internal/Timeout.js +37 -0
  142. package/dist/internal/Timeout.js.map +1 -0
  143. package/dist/internal/Timing.d.ts +9 -0
  144. package/dist/internal/Timing.d.ts.map +1 -1
  145. package/dist/internal/Timing.js +7 -0
  146. package/dist/internal/Timing.js.map +1 -1
  147. package/dist/internal/WebhookDestination.d.ts.map +1 -1
  148. package/dist/internal/WebhookDestination.js +3 -0
  149. package/dist/internal/WebhookDestination.js.map +1 -1
  150. package/dist/internal/funding/Action.d.ts +9 -0
  151. package/dist/internal/funding/Action.d.ts.map +1 -1
  152. package/dist/internal/funding/Action.js +16 -8
  153. package/dist/internal/funding/Action.js.map +1 -1
  154. package/dist/internal/funding/Catalog.d.ts +9 -2
  155. package/dist/internal/funding/Catalog.d.ts.map +1 -1
  156. package/dist/internal/funding/Catalog.js +19 -8
  157. package/dist/internal/funding/Catalog.js.map +1 -1
  158. package/dist/internal/funding/Provider.d.ts +68 -13
  159. package/dist/internal/funding/Provider.d.ts.map +1 -1
  160. package/dist/internal/funding/Provider.js +29 -8
  161. package/dist/internal/funding/Provider.js.map +1 -1
  162. package/dist/internal/funding/Transfer.d.ts +13 -12
  163. package/dist/internal/funding/Transfer.d.ts.map +1 -1
  164. package/dist/internal/funding/Transfer.js +23 -21
  165. package/dist/internal/funding/Transfer.js.map +1 -1
  166. package/dist/internal/funding/providers/stargate.d.ts +28 -9
  167. package/dist/internal/funding/providers/stargate.d.ts.map +1 -1
  168. package/dist/internal/funding/providers/stargate.js +251 -117
  169. package/dist/internal/funding/providers/stargate.js.map +1 -1
  170. package/dist/internal/index.d.ts +1 -0
  171. package/dist/internal/index.d.ts.map +1 -1
  172. package/dist/internal/index.js +1 -0
  173. package/dist/internal/index.js.map +1 -1
  174. package/package.json +2 -2
  175. package/src/App.test.ts +100 -0
  176. package/src/App.ts +42 -3
  177. package/src/Client.test-d.ts +46 -0
  178. package/src/Scope.test.ts +20 -0
  179. package/src/Scope.ts +16 -0
  180. package/src/admin/apps/api-keys.test.ts +75 -0
  181. package/src/admin/apps/api-keys.ts +64 -0
  182. package/src/admin/apps/earn-vaults.test.ts +1 -1
  183. package/src/admin/apps/earn-vaults.ts +1 -1
  184. package/src/admin/ui/src/lib/api-keys.ts +19 -0
  185. package/src/admin/ui/src/routes/api-keys.tsx +89 -2
  186. package/src/admin/ui.generated.ts +189 -151
  187. package/src/apps/Zones.test.ts +4 -4
  188. package/src/apps/Zones.ts +4 -4
  189. package/src/apps/composition.test.ts +1 -1
  190. package/src/apps/data/App.test.ts +41 -0
  191. package/src/apps/data/App.ts +18 -18
  192. package/src/apps/data/ZoneSelection.test.ts +2 -2
  193. package/src/apps/data/ZoneSelection.ts +8 -8
  194. package/src/apps/data/routes/activities.test.ts +1526 -12
  195. package/src/apps/data/routes/activities.ts +1774 -1356
  196. package/src/apps/data/routes/balances.ts +133 -97
  197. package/src/apps/data/routes/blocks.test.ts +533 -0
  198. package/src/apps/data/routes/blocks.ts +35 -16
  199. package/src/apps/data/routes/earn.test.ts +4 -2
  200. package/src/apps/data/routes/earn.ts +13 -13
  201. package/src/apps/data/routes/earn.zone-access.test.ts +6 -6
  202. package/src/apps/data/routes/receipts.ts +47 -16
  203. package/src/apps/data/routes/tokens.test.ts +578 -0
  204. package/src/apps/data/routes/tokens.ts +283 -225
  205. package/src/apps/data/routes/transactions.ts +68 -29
  206. package/src/apps/data/routes/transfers.test.ts +421 -0
  207. package/src/apps/data/routes/transfers.ts +45 -27
  208. package/src/apps/data/routes/valuation.ts +94 -78
  209. package/src/apps/data/routes/zones.test.ts +2 -2
  210. package/src/apps/data/routes/zones.ts +1 -1
  211. package/src/apps/funding/routes/deposit-addresses.test.ts +25 -3
  212. package/src/apps/funding/routes/deposit-addresses.ts +17 -12
  213. package/src/apps/funding/routes/quotes.test.ts +5 -9
  214. package/src/apps/funding/routes/transfers.test.ts +395 -8
  215. package/src/apps/funding/routes/transfers.ts +76 -59
  216. package/src/apps/management/routes/api-keys.test.ts +220 -0
  217. package/src/apps/management/routes/api-keys.ts +280 -3
  218. package/src/db/Db.ts +13 -1
  219. package/src/db/Schema.ts +5 -2
  220. package/src/db/tables/earnVaults.test.ts +9 -9
  221. package/src/db/tables/fundingCatalog.test.ts +16 -2
  222. package/src/db/tables/fundingCatalog.ts +9 -3
  223. package/src/handlers/internal/utils.test.ts +53 -0
  224. package/src/handlers/internal/utils.ts +15 -0
  225. package/src/internal/Auth.ts +12 -4
  226. package/src/internal/EarnVaults.capabilities.test.ts +2 -2
  227. package/src/internal/EarnVaults.test.ts +1 -1
  228. package/src/internal/EarnVaults.ts +3 -3
  229. package/src/internal/Fetch.test.ts +46 -0
  230. package/src/internal/Fetch.ts +56 -0
  231. package/src/internal/Log.test.ts +21 -0
  232. package/src/internal/Log.ts +20 -5
  233. package/src/internal/OpenApi.ts +14 -4
  234. package/src/internal/Response.ts +1 -1
  235. package/src/internal/Schema.test.ts +2 -0
  236. package/src/internal/Schema.ts +12 -2
  237. package/src/internal/Store.test.ts +178 -0
  238. package/src/internal/Store.ts +104 -18
  239. package/src/internal/Tidx.test.ts +2 -1
  240. package/src/internal/Tidx.ts +7 -2
  241. package/src/internal/Timeout.test.ts +35 -0
  242. package/src/internal/Timeout.ts +73 -0
  243. package/src/internal/Timing.ts +16 -0
  244. package/src/internal/WebhookDestination.ts +3 -0
  245. package/src/internal/funding/Action.test.ts +41 -1
  246. package/src/internal/funding/Action.ts +69 -41
  247. package/src/internal/funding/Catalog.test.ts +34 -0
  248. package/src/internal/funding/Catalog.ts +28 -11
  249. package/src/internal/funding/Provider.test.ts +73 -0
  250. package/src/internal/funding/Provider.ts +82 -28
  251. package/src/internal/funding/Transfer.ts +133 -110
  252. package/src/internal/funding/providers/index.test.ts +4 -167
  253. package/src/internal/funding/providers/relay.test.ts +2 -2
  254. package/src/internal/funding/providers/stargate.test.ts +163 -0
  255. package/src/internal/funding/providers/stargate.ts +359 -134
  256. package/src/internal/index.test.ts +1 -0
  257. package/src/internal/index.ts +1 -0
@@ -1,6 +1,8 @@
1
1
  import { AbiFunction, Address, type Hex, Solidity } from 'ox'
2
2
  import * as z from 'zod/mini'
3
3
 
4
+ import * as OpenApi from '../OpenApi.js'
5
+
4
6
  /**
5
7
  * Executable source-chain actions returned by funding transfer creation. A
6
8
  * strict discriminated union: transfer reads never include an action.
@@ -12,49 +14,58 @@ const approveSelector = AbiFunction.getSelector(approve)
12
14
  /** Zod schemas owned by funding actions. */
13
15
  export namespace schema {
14
16
  /** One unsigned EVM call, in the exchange quote API's call shape. */
15
- export const Call = z
16
- .object({
17
- data: z
18
- .string()
19
- .check(
20
- z.regex(/^0x(?:[0-9a-fA-F]{2})*$/),
21
- z.describe('ABI-encoded calldata.'),
22
- z.meta({ examples: ['0x095ea7b3'] }),
23
- ),
24
- to: z
25
- .string()
26
- .check(
27
- z.regex(/^0x[0-9a-fA-F]{40}$/),
28
- z.describe('Contract the call targets.'),
29
- z.meta({ examples: [`0x${'44'.repeat(20)}`] }),
30
- ),
31
- value: z
32
- .string()
33
- .check(
34
- z.regex(/^0x[0-9a-fA-F]+$/),
35
- z.describe('Native value to send, as hex.'),
36
- z.meta({ examples: ['0x0'] }),
37
- ),
38
- })
39
- .check(z.describe('One unsigned EVM call.'))
17
+ export const Call = OpenApi.component(
18
+ z
19
+ .object({
20
+ data: z
21
+ .string()
22
+ .check(
23
+ z.regex(/^0x(?:[0-9a-fA-F]{2})*$/),
24
+ z.describe('ABI-encoded calldata.'),
25
+ z.meta({ examples: ['0x095ea7b3'] }),
26
+ ),
27
+ to: z
28
+ .string()
29
+ .check(
30
+ z.regex(/^0x[0-9a-fA-F]{40}$/),
31
+ z.describe('Contract the call targets.'),
32
+ z.meta({ examples: [`0x${'44'.repeat(20)}`] }),
33
+ ),
34
+ value: z
35
+ .string()
36
+ .check(
37
+ z.regex(/^0x[0-9a-fA-F]+$/),
38
+ z.describe('Native value to send, as hex.'),
39
+ z.meta({ examples: ['0x0'] }),
40
+ ),
41
+ })
42
+ .check(z.describe('One unsigned EVM call.')),
43
+ 'FundingActionCall',
44
+ )
40
45
 
41
46
  /** Ordered unsigned EVM calls the caller signs and submits on the source chain. */
42
- export const EvmCalls = z
43
- .object({
44
- calls: z
45
- .array(Call)
46
- .check(
47
- z.minLength(1),
48
- z.describe('Calls in submission order; they may require separate transactions.'),
49
- ),
50
- type: z.literal('evm:calls').check(z.describe('Action kind.')),
51
- })
52
- .check(z.describe('Ordered unsigned EVM calls to sign and submit on the source chain.'))
47
+ export const EvmCalls = OpenApi.component(
48
+ z
49
+ .object({
50
+ calls: z
51
+ .array(Call)
52
+ .check(
53
+ z.minLength(1),
54
+ z.describe('Calls in submission order; they may require separate transactions.'),
55
+ ),
56
+ type: z.literal('evm:calls').check(z.describe('Action kind.')),
57
+ })
58
+ .check(z.describe('Ordered unsigned EVM calls to sign and submit on the source chain.')),
59
+ 'FundingEvmCallsAction',
60
+ )
53
61
 
54
62
  /** Executable source-chain action attached to a creation response. */
55
- export const Action = z
56
- .discriminatedUnion('type', [EvmCalls])
57
- .check(z.describe('Executable source-chain action. Never returned by transfer reads.'))
63
+ export const Action = OpenApi.component(
64
+ z
65
+ .discriminatedUnion('type', [EvmCalls])
66
+ .check(z.describe('Executable source-chain action. Never returned by transfer reads.')),
67
+ 'FundingAction',
68
+ )
58
69
  }
59
70
 
60
71
  /** One unsigned EVM call (see {@link schema.Call}). */
@@ -66,6 +77,14 @@ export type EvmCalls = z.output<typeof schema.EvmCalls>
66
77
  /** Executable source-chain action (see {@link schema.Action}). */
67
78
  export type Action = z.output<typeof schema.Action>
68
79
 
80
+ /** One exact native-value allowance established by provider-specific validation. */
81
+ export type NativeValue = {
82
+ /** Contract that may receive the native value. */
83
+ to: string
84
+ /** Exact native value allowed, as a JSON-RPC quantity. */
85
+ value: string
86
+ }
87
+
69
88
  /**
70
89
  * Validates prepared EVM calls before persistence. Token calls, approval
71
90
  * targets, spenders, amounts, selectors, and native value must match the route.
@@ -73,9 +92,16 @@ export type Action = z.output<typeof schema.Action>
73
92
  export function validateEvmCalls(options: validateEvmCalls.Options): void {
74
93
  const action = schema.EvmCalls.safeParse(options.action)
75
94
  if (!action.success) throw new InvalidActionError('Malformed EVM calls.')
95
+ const nativeValues = new Set<string>()
96
+ for (const { to, value } of options.nativeValues ?? []) {
97
+ if (!Address.validate(to, { strict: false }) || !/^0x[0-9a-fA-F]+$/.test(value))
98
+ throw new InvalidActionError('Malformed native value allowance.')
99
+ nativeValues.add(`${to.toLowerCase()}:${BigInt(value).toString()}`)
100
+ }
76
101
  for (const [index, call] of action.data.calls.entries()) {
77
- if (BigInt(call.value) !== 0n)
78
- throw new InvalidActionError('Stablecoin funding calls must not carry native value.')
102
+ const callValue = BigInt(call.value)
103
+ if (callValue > 0n && !nativeValues.has(`${call.to.toLowerCase()}:${callValue.toString()}`))
104
+ throw new InvalidActionError('Native value must match a validated provider fee.')
79
105
  const targetsSourceToken = Address.isEqual(
80
106
  call.to as Address.Address,
81
107
  options.sourceTokenAddress as Address.Address,
@@ -104,6 +130,8 @@ export declare namespace validateEvmCalls {
104
130
  type Options = {
105
131
  /** The prepared action to validate. */
106
132
  action: EvmCalls
133
+ /** Exact native values established by provider-specific validation. */
134
+ nativeValues?: readonly NativeValue[] | undefined
107
135
  /** Source amount in base units. */
108
136
  sourceAmount: string
109
137
  /** Source token contract address. */
@@ -309,3 +309,37 @@ describe('route capabilities', () => {
309
309
  ).toThrow()
310
310
  })
311
311
  })
312
+
313
+ describe('route configuration', () => {
314
+ const configuration = { poolAddress: `0x${'11'.repeat(20)}` }
315
+
316
+ test('carries configuration into hydrated provider routes', () => {
317
+ const snapshot = Catalog.compile(rows({ routes: [{ ...data.routes[0]!, configuration }] }))
318
+ expect(snapshot.routes[0]?.configuration).toStrictEqual(configuration)
319
+ expect(snapshot.routesByProvider.get('relay')?.[0]?.configuration).toStrictEqual(configuration)
320
+ })
321
+
322
+ test('omits configuration when it is absent or null', () => {
323
+ const absent = Catalog.compile(rows())
324
+ const nulled = Catalog.compile(rows({ routes: [{ ...data.routes[0]!, configuration: null }] }))
325
+ expect('configuration' in absent.routes[0]!).toBe(false)
326
+ expect('configuration' in nulled.routes[0]!).toBe(false)
327
+ })
328
+
329
+ test('publishes configuration through the write boundary', async () => {
330
+ const db = TestApp.database()
331
+ const snapshot = await Catalog.publish(db, {
332
+ ...data,
333
+ routes: [{ ...data.routes[0]!, configuration }],
334
+ })
335
+ expect(snapshot.routes[0]?.configuration).toStrictEqual(configuration)
336
+ })
337
+
338
+ test('rejects non-object configuration', () => {
339
+ expect(() =>
340
+ Catalog.compile(
341
+ rows({ routes: [{ ...data.routes[0]!, configuration: 'invalid' as never }] }),
342
+ ),
343
+ ).toThrow()
344
+ })
345
+ })
@@ -55,10 +55,16 @@ export namespace schema {
55
55
  z.describe('Funding capabilities enabled on one provider route.'),
56
56
  )
57
57
 
58
- // Absent or null `capabilities` keeps a route quote-only, so transfer and
59
- // deposit-address support enable through reviewed seed edits.
58
+ /** Provider-specific configuration for one funding route. */
59
+ export const Configuration = z
60
+ .record(z.string(), z.unknown())
61
+ .check(z.describe('Provider-specific configuration for one funding route.'))
62
+
63
+ // Absent or null `capabilities` keeps a route quote-only. Absent or null
64
+ // `configuration` lets providers decide whether a route is configured.
60
65
  const Route = z.extend(db_Schema.FundingRoute, {
61
66
  capabilities: z.optional(z.nullable(Capabilities)),
67
+ configuration: z.optional(z.nullable(Configuration)),
62
68
  })
63
69
 
64
70
  /** Complete funding catalog content accepted by the publish boundary. */
@@ -77,10 +83,15 @@ export namespace schema {
77
83
  /** Funding capabilities enabled on one provider route. */
78
84
  export type Capabilities = z.output<typeof schema.Capabilities>
79
85
 
86
+ /** Provider-specific configuration for one funding route. */
87
+ export type Configuration = z.output<typeof schema.Configuration>
88
+
80
89
  /** One hydrated provider route in a funding catalog snapshot. */
81
90
  export type ProviderRoute = {
82
91
  /** Funding capabilities; absent means the route is quote-only. */
83
92
  capabilities?: Capabilities | undefined
93
+ /** Provider-specific route configuration. */
94
+ configuration?: Configuration | undefined
84
95
  /** Funding provider that supports the route. */
85
96
  providerId: string
86
97
  /** Hydrated source-to-destination funding route. */
@@ -93,8 +104,8 @@ export type Snapshot = {
93
104
  chainsByKey: ReadonlyMap<string, Chain.Chain>
94
105
  /** Hydrated provider routes in stable catalog order. */
95
106
  routes: readonly ProviderRoute[]
96
- /** Hydrated routes grouped by provider id. */
97
- routesByProvider: ReadonlyMap<string, readonly Route.Route[]>
107
+ /** Hydrated provider routes grouped by provider id. */
108
+ routesByProvider: ReadonlyMap<string, readonly ProviderRoute[]>
98
109
  /** Canonical funding tokens indexed by stable id. */
99
110
  tokensById: ReadonlyMap<string, Token.Token>
100
111
  /** ISO timestamp of the catalog's last publish. */
@@ -176,7 +187,7 @@ export function compile(rows: core_Catalog.Rows): Snapshot {
176
187
  }
177
188
 
178
189
  const routes: ProviderRoute[] = []
179
- const routesByProvider = new Map<string, Route.Route[]>()
190
+ const routesByProvider = new Map<string, ProviderRoute[]>()
180
191
  for (const row of data.routes) {
181
192
  const route = {
182
193
  destination: resolvePlacement(
@@ -187,14 +198,16 @@ export function compile(rows: core_Catalog.Rows): Snapshot {
187
198
  ),
188
199
  source: resolvePlacement(chainsById, tokensById, row.sourceChainId, row.sourceTokenId),
189
200
  }
190
- routes.push({
201
+ const entry = {
191
202
  ...(row.capabilities ? { capabilities: row.capabilities } : {}),
203
+ ...(row.configuration ? { configuration: row.configuration } : {}),
192
204
  providerId: row.providerId,
193
205
  route,
194
- })
206
+ }
207
+ routes.push(entry)
195
208
  const providerRoutes = routesByProvider.get(row.providerId)
196
- if (providerRoutes) providerRoutes.push(route)
197
- else routesByProvider.set(row.providerId, [route])
209
+ if (providerRoutes) providerRoutes.push(entry)
210
+ else routesByProvider.set(row.providerId, [entry])
198
211
  }
199
212
 
200
213
  return {
@@ -219,10 +232,14 @@ export async function publish(db: Db.Db, input: publish.Input): Promise<Snapshot
219
232
  catalog: { id: 'default', updatedAt: emptyUpdatedAt, version: 'validation' },
220
233
  ...data,
221
234
  })
222
- // Absent `capabilities` persists as null; both mean indicative-only.
235
+ // Optional route data persists as null so every stored row has one shape.
223
236
  await core_Catalog.publish(db, {
224
237
  ...data,
225
- routes: data.routes.map((route) => ({ ...route, capabilities: route.capabilities ?? null })),
238
+ routes: data.routes.map((route) => ({
239
+ ...route,
240
+ capabilities: route.capabilities ?? null,
241
+ configuration: route.configuration ?? null,
242
+ })),
226
243
  })
227
244
  return prime(db, await read(db))
228
245
  }
@@ -5,6 +5,7 @@ import * as Across from './providers/across.js'
5
5
  import * as Relay from './providers/relay.js'
6
6
  import * as Rhino from './providers/rhino.js'
7
7
  import * as Squid from './providers/squid.js'
8
+ import * as Stargate from './providers/stargate.js'
8
9
  import * as Symbiosis from './providers/symbiosis.js'
9
10
 
10
11
  const providers = [
@@ -19,6 +20,7 @@ const providers = [
19
20
  integratorId: 'squid-integrator-id',
20
21
  userAddress: '0x1111111111111111111111111111111111111111',
21
22
  }),
23
+ Stargate.stargate(),
22
24
  Symbiosis.symbiosis({ userAddress: '0x1111111111111111111111111111111111111111' }),
23
25
  ] as const
24
26
 
@@ -831,9 +833,63 @@ describe('prepareTransfer', () => {
831
833
  { operation: 'prepareTransfer', outcome: 'unavailable', provider: 'relay' },
832
834
  ])
833
835
  })
836
+
837
+ test('accepts preparations without correlation', async () => {
838
+ const { correlation: _correlation, ...result } = preparedResult()
839
+ const spender = `0x${'44'.repeat(20)}`
840
+ const selected = FundingProvider.prepareTransfer({
841
+ ...prepareOptions,
842
+ providers: [
843
+ withPrepareTransfer('relay', async () => {
844
+ return {
845
+ ...result,
846
+ action: {
847
+ calls: [
848
+ {
849
+ data: `0x095ea7b3${spender.slice(2).padStart(64, '0')}${(1_000_000).toString(16).padStart(64, '0')}`,
850
+ to: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
851
+ value: '0x0',
852
+ },
853
+ { data: '0xe8017952', to: spender, value: '0x0' },
854
+ ],
855
+ type: 'evm:calls',
856
+ },
857
+ }
858
+ }),
859
+ ],
860
+ })
861
+
862
+ expect((await selected)?.result.correlation).toBeUndefined()
863
+ })
834
864
  })
835
865
 
836
866
  describe('getPrepareCandidates', () => {
867
+ test('restricts candidates to the requested provider', () => {
868
+ const relay = withPrepareTransfer('relay', async () => preparedResult())
869
+ const stargate = withPrepareTransfer('stargate', async () => preparedResult())
870
+ const route = TestFunding.data.routes.find(
871
+ (entry) => entry.providerId === 'relay' && entry.capabilities?.transfer,
872
+ )!
873
+ const catalog = FundingCatalog.compile({
874
+ catalog: {
875
+ id: 'default',
876
+ updatedAt: '2026-01-01T00:00:00.000Z',
877
+ version: 'test',
878
+ },
879
+ ...TestFunding.data,
880
+ routes: [...TestFunding.data.routes, { ...route, providerId: 'stargate' }],
881
+ })
882
+
883
+ expect(
884
+ FundingProvider.getPrepareCandidates({
885
+ ...prepareOptions,
886
+ catalog,
887
+ provider: 'stargate',
888
+ providers: [relay, stargate],
889
+ }).map(({ provider }) => provider.id),
890
+ ).toEqual(['stargate'])
891
+ })
892
+
837
893
  test('restricts candidates to the selected destination chain', () => {
838
894
  const provider = withPrepareTransfer('relay', async () => preparedResult())
839
895
  expect(
@@ -1110,6 +1166,19 @@ describe('getQuoteCandidates', () => {
1110
1166
  expect(candidate?.sourceAmount).toEqual({ amount: '1250000', formatted: '1.25' })
1111
1167
  })
1112
1168
 
1169
+ test('carries provider route configuration without duplicating token metadata', () => {
1170
+ const [candidate] = FundingProvider.getQuoteCandidates({
1171
+ ...baseUsdcRequest,
1172
+ provider: 'stargate',
1173
+ providers,
1174
+ })
1175
+
1176
+ expect(candidate?.configuration).toEqual({
1177
+ poolAddress: '0x27a16dc786820B16E5c9028b75B99F6f604b5d26',
1178
+ })
1179
+ expect(candidate?.sourceToken.address).toBe('0x833589fcd6edb6e08f4c7c32d4f71b54bda02913')
1180
+ })
1181
+
1113
1182
  test('resolves normalized catalog aliases', () => {
1114
1183
  const catalog = FundingCatalog.compile({
1115
1184
  catalog: {
@@ -1341,6 +1410,10 @@ describe('getProviders', () => {
1341
1410
  "id": "squid",
1342
1411
  "name": "Squid",
1343
1412
  },
1413
+ {
1414
+ "id": "stargate",
1415
+ "name": "Stargate",
1416
+ },
1344
1417
  {
1345
1418
  "id": "symbiosis",
1346
1419
  "name": "Symbiosis",
@@ -216,10 +216,12 @@ export namespace schema {
216
216
  /** Executable preparation returned by a funding provider. */
217
217
  export const PrepareResult = z.object({
218
218
  action: Action.schema.Action,
219
- correlation: z.object({
220
- checkEndpoint: z.optional(z.string()),
221
- requestId: z.string().check(z.minLength(1)),
222
- }),
219
+ correlation: z.optional(
220
+ z.object({
221
+ checkEndpoint: z.optional(z.string()),
222
+ requestId: z.string().check(z.minLength(1)),
223
+ }),
224
+ ),
223
225
  destinationAmount: PositiveInteger,
224
226
  destinationAmountMin: PositiveInteger,
225
227
  expiresAt: z.optional(z.iso.datetime()),
@@ -227,6 +229,7 @@ export namespace schema {
227
229
  z.object({
228
230
  amount: Schema.DecimalString,
229
231
  side: z.enum(['destination', 'source']),
232
+ token: z.optional(TokenRef),
230
233
  }),
231
234
  ),
232
235
  quality: GetQuoteResult.shape.quality,
@@ -467,6 +470,21 @@ export declare namespace prepareTransfer {
467
470
  type ReturnType = z.output<typeof schema.PrepareResult>
468
471
  }
469
472
 
473
+ /** Contract for provider-specific executable action validation. */
474
+ export declare namespace validateTransferAction {
475
+ /** Prepared action and its curated route terms. */
476
+ type Parameters = prepareTransfer.Parameters & {
477
+ /** Executable action returned by the provider. */
478
+ action: Action.Action
479
+ }
480
+
481
+ /** Provider-specific constraints established by validation. */
482
+ type ReturnType = {
483
+ /** Exact native values independently validated against provider calldata. */
484
+ nativeValues?: readonly Action.NativeValue[] | undefined
485
+ }
486
+ }
487
+
470
488
  /** Contract of the provider `createDepositAddress` method. */
471
489
  export declare namespace createDepositAddress {
472
490
  /** Input passed to a provider provisioning one reusable deposit address. */
@@ -683,6 +701,10 @@ export type Provider<id extends string = string> = core_Provider.Provider<id, 'f
683
701
  input: prepareTransfer.Parameters,
684
702
  signal: AbortSignal,
685
703
  ): Promise<prepareTransfer.ReturnType>
704
+ /** Validates provider-specific action semantics before generic persistence checks. */
705
+ validateTransferAction?(
706
+ input: validateTransferAction.Parameters,
707
+ ): validateTransferAction.ReturnType
686
708
  /** Receives authenticated provider status events, when configured. */
687
709
  webhook?: Webhook | undefined
688
710
  }
@@ -952,6 +974,7 @@ export function getQuoteCandidates(options: getQuoteCandidates.Options) {
952
974
  .filter((entry) => provider === undefined || entry.provider.id === provider.id)
953
975
  .map((entry) =>
954
976
  createQuoteCandidate({
977
+ configuration: entry.configuration,
955
978
  destinationToken: entry.route.destination,
956
979
  provider: entry.provider,
957
980
  route: entry.route,
@@ -1124,20 +1147,17 @@ export async function prepareTransfer(
1124
1147
  candidates.map(async ({ candidate, provider }) => {
1125
1148
  const start = performance.now()
1126
1149
  try {
1150
+ const parameters = {
1151
+ candidate,
1152
+ method: options.method,
1153
+ mode: options.mode,
1154
+ recipient: options.recipient,
1155
+ sender: options.sender,
1156
+ slippageBps: options.slippageBps,
1157
+ }
1127
1158
  const parsed = schema.PrepareResult.safeParse(
1128
1159
  await withTimeout(
1129
- (signal) =>
1130
- provider.prepareTransfer(
1131
- {
1132
- candidate,
1133
- method: options.method,
1134
- mode: options.mode,
1135
- recipient: options.recipient,
1136
- sender: options.sender,
1137
- slippageBps: options.slippageBps,
1138
- },
1139
- signal,
1140
- ),
1160
+ (signal) => provider.prepareTransfer(parameters, signal),
1141
1161
  options.providerTimeoutMs ?? defaultPrepareTimeoutMs,
1142
1162
  ),
1143
1163
  )
@@ -1160,8 +1180,13 @@ export async function prepareTransfer(
1160
1180
  }
1161
1181
  if (result.action.type === 'evm:calls')
1162
1182
  try {
1183
+ const constraints = provider.validateTransferAction?.({
1184
+ ...parameters,
1185
+ action: result.action,
1186
+ })
1163
1187
  Action.validateEvmCalls({
1164
1188
  action: result.action,
1189
+ nativeValues: constraints?.nativeValues,
1165
1190
  sourceAmount: candidate.sourceAmount.amount,
1166
1191
  sourceTokenAddress: candidate.sourceToken.address,
1167
1192
  })
@@ -1275,7 +1300,14 @@ export function getDepositAddressCandidates(options: getDepositAddressCandidates
1275
1300
  const provider = options.providers.find((candidate) => candidate.id === entry.providerId)
1276
1301
  if (!provider || !canCreateDepositAddress(provider)) return []
1277
1302
  return [
1278
- { candidate: createDepositAddressCandidate({ provider, route: entry.route }), provider },
1303
+ {
1304
+ candidate: createDepositAddressCandidate({
1305
+ configuration: entry.configuration,
1306
+ provider,
1307
+ route: entry.route,
1308
+ }),
1309
+ provider,
1310
+ },
1279
1311
  ]
1280
1312
  })
1281
1313
  }
@@ -1308,6 +1340,9 @@ export function getPrepareCandidates(options: getPrepareCandidates.Options) {
1308
1340
  if (!amount.success) throw new InvalidAmountError()
1309
1341
  const sourceChain = resolveSourceChain(options.sourceChain, options.catalog)
1310
1342
  const destinationChainId = `eip155:${options.destinationChainId}`
1343
+ const selectedProvider = options.provider
1344
+ ? resolveProvider(options.provider, options.providers)
1345
+ : undefined
1311
1346
 
1312
1347
  return options.catalog.routes.flatMap((entry) => {
1313
1348
  if (entry.route.source.chain.id !== sourceChain.id) return []
@@ -1315,11 +1350,13 @@ export function getPrepareCandidates(options: getPrepareCandidates.Options) {
1315
1350
  if (!matchesToken(entry.route.source, options.sourceToken)) return []
1316
1351
  if (!matchesToken(entry.route.destination, options.destinationToken)) return []
1317
1352
  if (!entry.capabilities?.transfer?.modes.includes(options.mode)) return []
1318
- const provider = options.providers.find((candidate) => candidate.id === entry.providerId)
1319
- if (!provider || !canPrepareTransfer(provider)) return []
1353
+ const provider =
1354
+ selectedProvider ?? options.providers.find((candidate) => candidate.id === entry.providerId)
1355
+ if (!provider || provider.id !== entry.providerId || !canPrepareTransfer(provider)) return []
1320
1356
  return [
1321
1357
  {
1322
1358
  candidate: createQuoteCandidate({
1359
+ configuration: entry.configuration,
1323
1360
  destinationToken: entry.route.destination,
1324
1361
  provider,
1325
1362
  route: entry.route,
@@ -1349,6 +1386,8 @@ export declare namespace getPrepareCandidates {
1349
1386
  method: Transfer.Method
1350
1387
  /** Amount mode the caller selected. */
1351
1388
  mode: Transfer.Mode
1389
+ /** Funding provider ID selected by the caller. */
1390
+ provider?: string | undefined
1352
1391
  /** Funding providers considered by the request. */
1353
1392
  providers: readonly Provider[]
1354
1393
  /** Source amount in base units. */
@@ -1572,8 +1611,9 @@ function chainRef(chain: Chain): ChainRef {
1572
1611
  })
1573
1612
  }
1574
1613
 
1575
- function createDepositAddressCandidate(options: { provider: Provider; route: core_Route.Route }) {
1614
+ function createDepositAddressCandidate(options: createDepositAddressCandidate.Options) {
1576
1615
  return {
1616
+ ...(options.configuration ? { configuration: options.configuration } : {}),
1577
1617
  destinationChain: chainRef(options.route.destination.chain),
1578
1618
  destinationToken: tokenRef(options.route.destination),
1579
1619
  provider: schema.ProviderRef.parse(options.provider),
@@ -1583,14 +1623,17 @@ function createDepositAddressCandidate(options: { provider: Provider; route: cor
1583
1623
  } as const
1584
1624
  }
1585
1625
 
1586
- function createQuoteCandidate(options: {
1587
- destinationToken: Token
1588
- provider: Provider
1589
- route: core_Route.Route
1590
- sourceAmount: ReturnType<typeof resolveSourceAmount>
1591
- sourceToken: Token
1592
- }) {
1626
+ declare namespace createDepositAddressCandidate {
1627
+ type Options = {
1628
+ configuration?: core_Catalog.Configuration | undefined
1629
+ provider: Provider
1630
+ route: core_Route.Route
1631
+ }
1632
+ }
1633
+
1634
+ function createQuoteCandidate(options: createQuoteCandidate.Options) {
1593
1635
  return {
1636
+ ...(options.configuration ? { configuration: options.configuration } : {}),
1594
1637
  destinationChain: chainRef(options.destinationToken.chain),
1595
1638
  destinationToken: tokenRef(options.destinationToken),
1596
1639
  id: [
@@ -1608,6 +1651,17 @@ function createQuoteCandidate(options: {
1608
1651
  } as const
1609
1652
  }
1610
1653
 
1654
+ declare namespace createQuoteCandidate {
1655
+ type Options = {
1656
+ configuration?: core_Catalog.Configuration | undefined
1657
+ destinationToken: Token
1658
+ provider: Provider
1659
+ route: core_Route.Route
1660
+ sourceAmount: ReturnType<typeof resolveSourceAmount>
1661
+ sourceToken: Token
1662
+ }
1663
+ }
1664
+
1611
1665
  function normalizeInput(value: string) {
1612
1666
  return value.trim().toLowerCase()
1613
1667
  }
@@ -1807,7 +1861,7 @@ function resolveSourceChain(input: string, catalog: core_Catalog.Snapshot) {
1807
1861
 
1808
1862
  function getRoutes(catalog: core_Catalog.Snapshot, providers: readonly Provider[]) {
1809
1863
  return providers.flatMap((provider) =>
1810
- (catalog.routesByProvider.get(provider.id) ?? []).map((route) => ({ provider, route })),
1864
+ (catalog.routesByProvider.get(provider.id) ?? []).map((entry) => ({ ...entry, provider })),
1811
1865
  )
1812
1866
  }
1813
1867