otto-intel-mcp 0.1.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 (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +102 -0
  3. package/dist/adapter/cdp-signer.d.ts +133 -0
  4. package/dist/adapter/cdp-signer.js +356 -0
  5. package/dist/adapter/cdp-signer.js.map +1 -0
  6. package/dist/adapter/chain.d.ts +36 -0
  7. package/dist/adapter/chain.js +65 -0
  8. package/dist/adapter/chain.js.map +1 -0
  9. package/dist/adapter/erc20.d.ts +39 -0
  10. package/dist/adapter/erc20.js +17 -0
  11. package/dist/adapter/erc20.js.map +1 -0
  12. package/dist/adapter/lifi-decode.d.ts +52 -0
  13. package/dist/adapter/lifi-decode.js +149 -0
  14. package/dist/adapter/lifi-decode.js.map +1 -0
  15. package/dist/adapter/refusal.d.ts +21 -0
  16. package/dist/adapter/refusal.js +55 -0
  17. package/dist/adapter/refusal.js.map +1 -0
  18. package/dist/adapter/sent-step.d.ts +15 -0
  19. package/dist/adapter/sent-step.js +6 -0
  20. package/dist/adapter/sent-step.js.map +1 -0
  21. package/dist/adapter/verify.d.ts +149 -0
  22. package/dist/adapter/verify.js +432 -0
  23. package/dist/adapter/verify.js.map +1 -0
  24. package/dist/adapter-cdp-index.d.ts +6 -0
  25. package/dist/adapter-cdp-index.js +7 -0
  26. package/dist/adapter-index.d.ts +15 -0
  27. package/dist/adapter-index.js +15 -0
  28. package/dist/artifact-id.d.ts +16 -0
  29. package/dist/artifact-id.js +60 -0
  30. package/dist/boot-redaction.d.ts +16 -0
  31. package/dist/boot-redaction.js +54 -0
  32. package/dist/data-source.d.ts +7 -0
  33. package/dist/data-source.js +2 -0
  34. package/dist/errors.d.ts +70 -0
  35. package/dist/errors.js +221 -0
  36. package/dist/execution-config.d.ts +232 -0
  37. package/dist/execution-config.js +443 -0
  38. package/dist/execution-delegated-definition.d.ts +165 -0
  39. package/dist/execution-delegated-definition.js +116 -0
  40. package/dist/execution-delegation-admin-definition.d.ts +208 -0
  41. package/dist/execution-delegation-admin-definition.js +170 -0
  42. package/dist/execution-delegation-admin.d.ts +74 -0
  43. package/dist/execution-delegation-admin.js +290 -0
  44. package/dist/execution-delegation-policy.d.ts +257 -0
  45. package/dist/execution-delegation-policy.js +279 -0
  46. package/dist/execution-delegation.d.ts +190 -0
  47. package/dist/execution-delegation.js +545 -0
  48. package/dist/execution-errors.d.ts +9 -0
  49. package/dist/execution-errors.js +128 -0
  50. package/dist/execution-index.d.ts +17 -0
  51. package/dist/execution-index.js +16 -0
  52. package/dist/execution-intent.d.ts +14 -0
  53. package/dist/execution-intent.js +36 -0
  54. package/dist/execution-registration.d.ts +113 -0
  55. package/dist/execution-registration.js +161 -0
  56. package/dist/execution-tool-definitions.d.ts +1103 -0
  57. package/dist/execution-tool-definitions.js +1051 -0
  58. package/dist/execution-tools.d.ts +85 -0
  59. package/dist/execution-tools.js +690 -0
  60. package/dist/execution-types.d.ts +274 -0
  61. package/dist/execution-types.js +157 -0
  62. package/dist/hyperliquid-info-client.d.ts +31 -0
  63. package/dist/hyperliquid-info-client.js +121 -0
  64. package/dist/hyperliquid-order-assertions.d.ts +89 -0
  65. package/dist/hyperliquid-order-assertions.js +325 -0
  66. package/dist/hyperliquid-order-builder.d.ts +47 -0
  67. package/dist/hyperliquid-order-builder.js +55 -0
  68. package/dist/lifi-bridge-assertions.d.ts +49 -0
  69. package/dist/lifi-bridge-assertions.js +303 -0
  70. package/dist/lifi-execution-client.d.ts +74 -0
  71. package/dist/lifi-execution-client.js +165 -0
  72. package/dist/lifi-fee-assertions.d.ts +32 -0
  73. package/dist/lifi-fee-assertions.js +103 -0
  74. package/dist/lifi-swap-assertions.d.ts +32 -0
  75. package/dist/lifi-swap-assertions.js +214 -0
  76. package/dist/local-config.d.ts +9 -0
  77. package/dist/local-config.js +45 -0
  78. package/dist/mcp-server.d.ts +4 -0
  79. package/dist/mcp-server.js +44 -0
  80. package/dist/polymarket-clob-client.d.ts +73 -0
  81. package/dist/polymarket-clob-client.js +186 -0
  82. package/dist/polymarket-order-assertions.d.ts +60 -0
  83. package/dist/polymarket-order-assertions.js +273 -0
  84. package/dist/polymarket-order-builder.d.ts +43 -0
  85. package/dist/polymarket-order-builder.js +74 -0
  86. package/dist/prepared-artifacts.d.ts +45 -0
  87. package/dist/prepared-artifacts.js +92 -0
  88. package/dist/stdio-main.d.ts +2 -0
  89. package/dist/stdio-main.js +20 -0
  90. package/dist/tool-definitions.d.ts +20 -0
  91. package/dist/tool-definitions.js +162 -0
  92. package/dist/x402-read-source.d.ts +11 -0
  93. package/dist/x402-read-source.js +77 -0
  94. package/package.json +162 -0
@@ -0,0 +1,303 @@
1
+ import { decodeAbiParameters, encodeAbiParameters, getAddress, parseAbiParameters, } from 'viem';
2
+ import { attributedFeeAmount, isEnumeratedCctpBridgeRoute, LIFI_DIAMOND_ADDRESS, isReviewedLiFiFeeForwarder, reviewedLiFiFeeForwarders, unreviewedFeeForwarderMessage, LIFI_INTEGRATOR, LIFI_POLYMER_BRIDGE_TOOL, } from './execution-config.js';
3
+ import { isExecutionRefusal, refuse } from './execution-errors.js';
4
+ import { assertFeeDistributionBody, FORWARD_ERC20_FEES_SELECTOR, } from './lifi-fee-assertions.js';
5
+ /** `ILiFi.BridgeData` — the universal first parameter of every LI.FI bridge function. */
6
+ const BRIDGE_DATA_TUPLE = '(bytes32 transactionId, string bridge, string integrator, address referrer, address sendingAssetId, address receiver, uint256 minAmount, uint256 destinationChainId, bool hasSourceSwaps, bool hasDestinationCall)';
7
+ /** `LibSwap.SwapData` — the source-swap legs LI.FI `transferFrom`s the signer for. */
8
+ const SWAP_DATA_TUPLE = '(address callTo, address approveTo, address sendingAssetId, address receivingAssetId, uint256 fromAmount, bytes callData, bool requiresDeposit)';
9
+ /**
10
+ * The live `PolymerData` tuple. Captured 2026-08-17 from li.quest and confirmed by selector
11
+ * derivation: keccak of
12
+ * `swapAndStartBridgeTokensViaPolymerCCTP(BridgeData,SwapData[],(uint256,uint256,bytes32,bytes32,uint32,address,bytes))`
13
+ * is exactly 0x17917a4e, the selector the live route emits.
14
+ *
15
+ * 🔴 This is a SEVEN-field tuple. The dApp's owner-op bridge validator pins the older FIVE-field
16
+ * shape (selector 0x780c82ab), which the live LI.FI deployment no longer emits — so the invariants
17
+ * below are re-specified here rather than inherited, and two fields have no counterpart there:
18
+ * - `f5` (address) carries the SENDER — `action.fromAddress`, the source-chain refund role — not
19
+ * the destination receiver. Established by capturing a live quote with `fromAddress != toAddress`
20
+ * (2026-08-17, Base->Arbitrum): `BridgeData.receiver` moved to the declared destination while
21
+ * `f5` stayed on the sender. Captures where the two roles share one address cannot tell the
22
+ * fields apart, so binding `f5` to the destination recipient — as this validator first did —
23
+ * refuses every valid request that bridges to any address other than the sender's own. It is
24
+ * bound to the sender, which is the role the live rail actually populates.
25
+ * - `f6` (bytes) is an unbounded arbitrary-payload field, bound empty fail-closed.
26
+ *
27
+ * `f2`/`f3` (bytes32) are zero on every live capture, including the differing-recipient one, so
28
+ * they are pinned to zero rather than being allowed to carry an address-shaped value nothing on
29
+ * the live rail emits.
30
+ */
31
+ const POLYMER_DATA_TUPLE = '(uint256 f0, uint256 f1, bytes32 f2, bytes32 f3, uint32 f4, address f5, bytes f6)';
32
+ const LIFI_POLYMER_PARAMS = parseAbiParameters(`${BRIDGE_DATA_TUPLE} _bridgeData, ${SWAP_DATA_TUPLE}[] _swapData, ${POLYMER_DATA_TUPLE} _polymerData`);
33
+ /** The enumerated v1 bridge facet. Anything else — eco, stargate, a plain no-swap layout, or a
34
+ * post-redeploy selector — is outside the reviewed set and refused by name. */
35
+ export const LIFI_BRIDGE_SELECTORS = Object.freeze({
36
+ polymerCctp: '0x17917a4e',
37
+ });
38
+ /** CCTP fee/finality controls pinned to the verified permissionless route shape. */
39
+ const POLYMER_ZERO_FIELD = 0n;
40
+ const POLYMER_FINALITY_THRESHOLD = 2000;
41
+ const ZERO_BYTES32 = `0x${'00'.repeat(32)}`;
42
+ const ZERO_ADDRESS = getAddress('0x0000000000000000000000000000000000000000');
43
+ function quoteRefuse(message) {
44
+ return refuse('LIFI_QUOTE_INVALID', message);
45
+ }
46
+ function routeRefuse(message) {
47
+ return refuse('UNSUPPORTED_LIFI_ROUTE', message);
48
+ }
49
+ function asAddress(value, field) {
50
+ if (typeof value !== 'string')
51
+ quoteRefuse(`${field} is missing`);
52
+ try {
53
+ return getAddress(value);
54
+ }
55
+ catch {
56
+ return quoteRefuse(`${field} is not a valid EVM address`);
57
+ }
58
+ }
59
+ function positiveBigInt(value, field) {
60
+ if (typeof value !== 'string' || !/^[1-9]\d*$/.test(value)) {
61
+ return quoteRefuse(`${field} must be a positive decimal integer`);
62
+ }
63
+ return BigInt(value);
64
+ }
65
+ function transactionData(value) {
66
+ if (typeof value !== 'string' || !/^0x(?:[0-9a-fA-F]{2}){4,}$/.test(value)) {
67
+ return quoteRefuse('transaction calldata is missing or malformed');
68
+ }
69
+ return value;
70
+ }
71
+ /**
72
+ * Decode the reviewed PolymerCCTP calldata. The selector is checked FIRST, so only the one exact
73
+ * reviewed layout is ever decoded, and the decoded arguments are re-encoded and compared so a
74
+ * non-canonical encoding or trailing bytes cannot smuggle a second interpretation past the asserts.
75
+ */
76
+ function decodeBridge(data) {
77
+ const selector = data.slice(0, 10).toLowerCase();
78
+ if (selector !== LIFI_BRIDGE_SELECTORS.polymerCctp) {
79
+ return routeRefuse(`LI.FI selector ${selector} is not the reviewed PolymerCCTP facet of the v1 bridge route set`);
80
+ }
81
+ const args = `0x${data.slice(10)}`;
82
+ try {
83
+ const decoded = decodeAbiParameters(LIFI_POLYMER_PARAMS, args);
84
+ if (encodeAbiParameters(LIFI_POLYMER_PARAMS, decoded).toLowerCase() !== args.toLowerCase()) {
85
+ quoteRefuse('bridge calldata is non-canonical or carries trailing bytes');
86
+ }
87
+ const [bridgeData, legs, polymerData] = decoded;
88
+ return { bridgeData, legs, polymerData };
89
+ }
90
+ catch (error) {
91
+ if (isExecutionRefusal(error))
92
+ throw error;
93
+ return quoteRefuse('bridge calldata could not be decoded as the reviewed PolymerCCTP facet');
94
+ }
95
+ }
96
+ function assertQuoteBinding(input) {
97
+ const { action, estimate, transactionRequest } = input.quote;
98
+ if (action.fromChainId !== input.fromChainId || action.toChainId !== input.toChainId) {
99
+ quoteRefuse('LI.FI quote is not bound to the requested chain pair');
100
+ }
101
+ if (action.fromAmount !== input.fromAmount.toString()) {
102
+ quoteRefuse('LI.FI quote input amount does not match the intent');
103
+ }
104
+ if (asAddress(action.fromToken.address, 'quote input token') !== input.fromToken ||
105
+ asAddress(action.toToken.address, 'quote output token') !== input.toToken) {
106
+ quoteRefuse('LI.FI quote token pair does not match the intent');
107
+ }
108
+ if (asAddress(action.fromAddress, 'quote sender') !== getAddress(input.profile.address)) {
109
+ return refuse('ACCOUNT_BINDING_MISMATCH', 'LI.FI quote sender is not the bound account');
110
+ }
111
+ if (asAddress(action.toAddress, 'quote receiver') !== input.destinationRecipient) {
112
+ return refuse('ACCOUNT_BINDING_MISMATCH', 'LI.FI quote receiver is not the declared destination recipient');
113
+ }
114
+ if (Math.round(action.slippage * 10_000) !== input.maxSlippageBps) {
115
+ quoteRefuse('LI.FI quote slippage does not match the requested bound');
116
+ }
117
+ const approvalSpender = asAddress(estimate.approvalAddress, 'LI.FI approval target');
118
+ const transactionTarget = asAddress(transactionRequest.to, 'LI.FI transaction target');
119
+ if (approvalSpender !== LIFI_DIAMOND_ADDRESS || transactionTarget !== LIFI_DIAMOND_ADDRESS) {
120
+ quoteRefuse('LI.FI approval or transaction target is not the reviewed Diamond');
121
+ }
122
+ if (transactionRequest.from !== undefined &&
123
+ asAddress(transactionRequest.from, 'LI.FI transaction sender') !==
124
+ getAddress(input.profile.address)) {
125
+ return refuse('ACCOUNT_BINDING_MISMATCH', 'LI.FI transaction sender is not the bound account');
126
+ }
127
+ let transactionValue;
128
+ try {
129
+ transactionValue = BigInt(transactionRequest.value);
130
+ }
131
+ catch {
132
+ return quoteRefuse('LI.FI transaction value is invalid');
133
+ }
134
+ if (transactionValue !== 0n) {
135
+ quoteRefuse('ERC20-input LI.FI bridge unexpectedly transfers native value');
136
+ }
137
+ return { approvalSpender, transactionTarget, data: transactionData(transactionRequest.data) };
138
+ }
139
+ /**
140
+ * Decode the constructed bridge calldata and assert every fund-safety bind, fail-closed.
141
+ *
142
+ * The destination binds matter more here than on a same-chain swap: a wrong destination recipient
143
+ * is an irreversible cross-chain loss, and the source-chain calldata is the last place it is
144
+ * checkable. CCTP burns the source USDC and mints canonical USDC to `BridgeData.receiver` on the
145
+ * destination, so the destination asset is same-token by construction once the facet and the
146
+ * enumerated route are pinned — that, plus every recipient-shaped field being bound, is what makes
147
+ * the destination side assertable from source calldata alone.
148
+ */
149
+ export function decodeAndAssertLiFiBridge(input) {
150
+ if (input.profile.kind !== 'eoa' && input.profile.kind !== 'safe') {
151
+ return refuse('UNSUPPORTED_ACCOUNT_PROFILE', 'only EOA and Safe account profiles are supported');
152
+ }
153
+ if (input.profile.chainId !== input.fromChainId) {
154
+ return refuse('ACCOUNT_BINDING_MISMATCH', 'account profile chain does not match the source chain');
155
+ }
156
+ if (!isEnumeratedCctpBridgeRoute(input.fromChainId, input.toChainId, input.fromToken, input.toToken)) {
157
+ return routeRefuse(`route ${input.fromChainId}:${input.fromToken} -> ${input.toChainId}:${input.toToken} is outside the enumerated v1 PolymerCCTP USDC route set`);
158
+ }
159
+ if (!reviewedLiFiFeeForwarders(input.fromChainId)) {
160
+ return routeRefuse('source chain is outside the reviewed LI.FI fee-forwarder set');
161
+ }
162
+ const boundQuote = assertQuoteBinding(input);
163
+ const { bridgeData, legs, polymerData } = decodeBridge(boundQuote.data);
164
+ // --- attribution -------------------------------------------------------------------------
165
+ if (bridgeData.integrator !== LIFI_INTEGRATOR) {
166
+ refuse('ATTRIBUTION_MISSING', 'bridge calldata does not carry the exact Otto integrator');
167
+ }
168
+ if (getAddress(bridgeData.referrer) !== ZERO_ADDRESS) {
169
+ refuse('ATTRIBUTION_MISSING', 'bridge calldata carries a third-party referrer');
170
+ }
171
+ if (bridgeData.bridge !== LIFI_POLYMER_BRIDGE_TOOL) {
172
+ routeRefuse(`bridge calldata names tool ${bridgeData.bridge}, not the reviewed PolymerCCTP tool`);
173
+ }
174
+ // --- destination binds -------------------------------------------------------------------
175
+ if (getAddress(bridgeData.receiver) !== input.destinationRecipient) {
176
+ refuse('ACCOUNT_BINDING_MISMATCH', 'bridge calldata receiver is not the declared destination recipient');
177
+ }
178
+ if (bridgeData.destinationChainId !== BigInt(input.toChainId)) {
179
+ quoteRefuse('bridge calldata destination chain does not match the intent');
180
+ }
181
+ if (bridgeData.hasDestinationCall) {
182
+ quoteRefuse('bridge calldata declares a destination call, which the v1 route set excludes');
183
+ }
184
+ if (!bridgeData.hasSourceSwaps) {
185
+ quoteRefuse('bridge calldata declares no source swaps, which the reviewed fee layout requires');
186
+ }
187
+ if (getAddress(bridgeData.sendingAssetId) !== input.fromToken) {
188
+ quoteRefuse('bridge calldata sending asset does not match the intent input token');
189
+ }
190
+ // --- over-spend / foreign-token bind -----------------------------------------------------
191
+ // LI.FI pulls every `requiresDeposit` leg from the signer, not `minAmount`. Binding the whole
192
+ // leg set (not just the first leg, and not just the summed amount) is what closes the class
193
+ // where an extra or split leg carries foreign calldata or masks additional spend.
194
+ if (legs.length !== 1) {
195
+ quoteRefuse('bridge calldata must carry exactly one reviewed fee-collection leg');
196
+ }
197
+ const leg = legs[0];
198
+ if (!leg)
199
+ quoteRefuse('bridge calldata has no source-swap leg');
200
+ // The drift alarm: an unreviewed collector is refused BY ADDRESS (see execution-config.ts).
201
+ if (!isReviewedLiFiFeeForwarder(input.fromChainId, leg.callTo) ||
202
+ !isReviewedLiFiFeeForwarder(input.fromChainId, leg.approveTo) ||
203
+ getAddress(leg.callTo) !== getAddress(leg.approveTo)) {
204
+ refuse('ATTRIBUTION_MISSING', unreviewedFeeForwarderMessage(input.fromChainId, leg.callTo, leg.approveTo));
205
+ }
206
+ if (!leg.requiresDeposit ||
207
+ leg.callData.slice(0, 10).toLowerCase() !== FORWARD_ERC20_FEES_SELECTOR ||
208
+ getAddress(leg.sendingAssetId) !== input.fromToken ||
209
+ getAddress(leg.receivingAssetId) !== input.fromToken ||
210
+ leg.fromAmount !== input.fromAmount) {
211
+ refuse('ATTRIBUTION_MISSING', 'bridge fee-collection leg is not bound to the reviewed forwarder, input token, and exact amount');
212
+ }
213
+ assertFeeDistributionBody({
214
+ callData: leg.callData,
215
+ expectedToken: input.fromToken,
216
+ grossAmount: input.fromAmount,
217
+ config: input.config,
218
+ });
219
+ // --- PolymerData: every recipient-shaped field bound, every control pinned ----------------
220
+ if (polymerData.f0 !== POLYMER_ZERO_FIELD ||
221
+ polymerData.f1 !== POLYMER_ZERO_FIELD ||
222
+ polymerData.f4 !== POLYMER_FINALITY_THRESHOLD) {
223
+ quoteRefuse('bridge calldata PolymerData controls are outside the reviewed pinned shape');
224
+ }
225
+ // f2/f3 are zero on every live capture of this facet, differing-recipient ones included. An
226
+ // address-shaped value here would be an unreviewed destination-caller / hook binding, so they
227
+ // are pinned to zero rather than being allowed to carry a value the live rail never emits.
228
+ for (const field of [polymerData.f2, polymerData.f3]) {
229
+ if (field.toLowerCase() !== ZERO_BYTES32) {
230
+ refuse('ACCOUNT_BINDING_MISMATCH', 'bridge calldata PolymerData carries a non-empty recipient-shaped control field');
231
+ }
232
+ }
233
+ // f5 is the SENDER / source-chain refund role, not the destination receiver — see the tuple
234
+ // comment above. Binding it to the sender is what the live rail actually emits; binding it to
235
+ // the destination recipient instead would refuse every bridge to a different address, and
236
+ // leaving it free would make it a refund redirect. The zero address is NOT accepted: a zero
237
+ // refund target burns anything the source leg returns.
238
+ if (getAddress(polymerData.f5) !== getAddress(input.profile.address)) {
239
+ refuse('ACCOUNT_BINDING_MISMATCH', 'bridge calldata PolymerData refund address is not the bound sender account');
240
+ }
241
+ // f6 is an unbounded arbitrary-payload field with no reviewed use; bound empty fail-closed.
242
+ if (polymerData.f6 !== '0x') {
243
+ quoteRefuse('bridge calldata PolymerData carries an unreviewed arbitrary payload');
244
+ }
245
+ // --- destination value floor -------------------------------------------------------------
246
+ const quotedOutput = positiveBigInt(input.quote.estimate.toAmount, 'LI.FI quoted output');
247
+ const quotedMinimum = positiveBigInt(input.quote.estimate.toAmountMin, 'LI.FI quoted minimum output');
248
+ if (bridgeData.minAmount !== quotedMinimum) {
249
+ quoteRefuse('bridge calldata minimum output does not match the quoted minimum');
250
+ }
251
+ const slippageFloor = (quotedOutput * BigInt(10_000 - input.maxSlippageBps)) / 10000n;
252
+ if (bridgeData.minAmount < slippageFloor) {
253
+ quoteRefuse('bridge calldata minimum output is inconsistent with the slippage bound');
254
+ }
255
+ // Independent of anything the quote asserts about itself: CCTP mints 1:1, so the amount reaching
256
+ // the destination cannot be less than the gross minus the configured aggregate fee cap. This
257
+ // floor is derived only from the caller's own amount and Otto's config, so a manipulated
258
+ // toAmount/toAmountMin pair cannot shrink what actually arrives.
259
+ const aggregateFeeCap = attributedFeeAmount(input.fromAmount, input.config.lifiAggregateFeeCap);
260
+ if (bridgeData.minAmount < input.fromAmount - aggregateFeeCap) {
261
+ // Same class as the fee-body cap, reached by a different route: the fee legs were inside the
262
+ // cap but the amount actually reaching the destination is not. A CCTP relay fee or a new
263
+ // finality tier lands here rather than in the fee body, so it carries the same distinguishable
264
+ // code — the total take exceeded the reviewed cap. It is NOT an attribution defect: Otto's fee
265
+ // was already asserted exact above.
266
+ refuse('LIFI_FEE_CAP_EXCEEDED', `bridge destination amount ${bridgeData.minAmount} is below the gross ${input.fromAmount} minus the configured aggregate fee cap ${aggregateFeeCap}`);
267
+ }
268
+ if (bridgeData.minAmount > input.fromAmount) {
269
+ quoteRefuse('bridge destination amount exceeds the gross input amount');
270
+ }
271
+ if (input.minAmountOut !== undefined && bridgeData.minAmount < input.minAmountOut) {
272
+ quoteRefuse('bridge destination amount is below the caller constraint');
273
+ }
274
+ return Object.freeze({
275
+ approvalSpender: boundQuote.approvalSpender,
276
+ transaction: Object.freeze({
277
+ to: boundQuote.transactionTarget,
278
+ value: '0',
279
+ data: boundQuote.data,
280
+ }),
281
+ destinationMinAmount: bridgeData.minAmount,
282
+ assertions: Object.freeze([
283
+ Object.freeze({ id: 'lifi_bridge_facet_reviewed', result: 'passed' }),
284
+ Object.freeze({ id: 'lifi_bridge_route_enumerated', result: 'passed' }),
285
+ Object.freeze({ id: 'lifi_integrator_exact', result: 'passed' }),
286
+ Object.freeze({ id: 'lifi_bridge_referrer_unset', result: 'passed' }),
287
+ Object.freeze({ id: 'lifi_fee_recipient_exact', result: 'passed' }),
288
+ Object.freeze({ id: 'lifi_fee_amount_exact', result: 'passed' }),
289
+ Object.freeze({ id: 'lifi_fee_recipient_set_versioned', result: 'passed' }),
290
+ Object.freeze({ id: 'lifi_fee_aggregate_cap', result: 'passed' }),
291
+ Object.freeze({ id: 'lifi_bridge_receiver_bound', result: 'passed' }),
292
+ Object.freeze({ id: 'lifi_bridge_dest_chain_exact', result: 'passed' }),
293
+ Object.freeze({ id: 'lifi_bridge_no_destination_call', result: 'passed' }),
294
+ Object.freeze({ id: 'lifi_bridge_same_token_bound', result: 'passed' }),
295
+ Object.freeze({ id: 'lifi_bridge_deposit_legs_exact', result: 'passed' }),
296
+ Object.freeze({ id: 'lifi_bridge_polymer_fields_pinned', result: 'passed' }),
297
+ Object.freeze({ id: 'lifi_bridge_refund_address_bound', result: 'passed' }),
298
+ Object.freeze({ id: 'lifi_bridge_dest_value_floor', result: 'passed' }),
299
+ Object.freeze({ id: 'lifi_bridge_targets_value_bound', result: 'passed' }),
300
+ ]),
301
+ });
302
+ }
303
+ //# sourceMappingURL=lifi-bridge-assertions.js.map
@@ -0,0 +1,74 @@
1
+ import { z } from 'zod';
2
+ import type { ExecutionServiceConfig } from './execution-config.js';
3
+ export declare const LIFI_QUOTE_SCHEMA: z.ZodObject<{
4
+ id: z.ZodOptional<z.ZodString>;
5
+ tool: z.ZodOptional<z.ZodString>;
6
+ type: z.ZodOptional<z.ZodString>;
7
+ action: z.ZodObject<{
8
+ fromToken: z.ZodObject<{
9
+ address: z.ZodString;
10
+ }, z.core.$loose>;
11
+ fromAmount: z.ZodString;
12
+ toToken: z.ZodObject<{
13
+ address: z.ZodString;
14
+ }, z.core.$loose>;
15
+ fromChainId: z.ZodNumber;
16
+ toChainId: z.ZodNumber;
17
+ slippage: z.ZodNumber;
18
+ fromAddress: z.ZodString;
19
+ toAddress: z.ZodString;
20
+ }, z.core.$loose>;
21
+ estimate: z.ZodObject<{
22
+ approvalAddress: z.ZodString;
23
+ toAmount: z.ZodString;
24
+ toAmountMin: z.ZodString;
25
+ }, z.core.$loose>;
26
+ transactionRequest: z.ZodObject<{
27
+ to: z.ZodString;
28
+ data: z.ZodString;
29
+ value: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
30
+ from: z.ZodOptional<z.ZodString>;
31
+ }, z.core.$loose>;
32
+ }, z.core.$loose>;
33
+ export type LiFiQuote = z.infer<typeof LIFI_QUOTE_SCHEMA>;
34
+ export interface SameChainSwapQuoteRequest {
35
+ readonly chainId: number;
36
+ readonly fromToken: string;
37
+ readonly toToken: string;
38
+ readonly fromAmount: string;
39
+ readonly sender: string;
40
+ readonly receiver: string;
41
+ readonly maxSlippageBps: number;
42
+ }
43
+ export interface CrossChainBridgeQuoteRequest {
44
+ readonly fromChainId: number;
45
+ readonly toChainId: number;
46
+ readonly fromToken: string;
47
+ readonly toToken: string;
48
+ readonly fromAmount: string;
49
+ readonly sender: string;
50
+ readonly receiver: string;
51
+ readonly maxSlippageBps: number;
52
+ }
53
+ export interface LiFiQuoteClient {
54
+ getSameChainSwapQuote(request: SameChainSwapQuoteRequest, signal?: AbortSignal): Promise<LiFiQuote>;
55
+ getCrossChainBridgeQuote(request: CrossChainBridgeQuoteRequest, signal?: AbortSignal): Promise<LiFiQuote>;
56
+ }
57
+ export declare function slippageBpsToDecimal(bps: number): string;
58
+ /** Semantic request fields first; Otto-owned attribution is appended last and has no caller seam. */
59
+ export declare function buildLiFiQuoteUrl(request: SameChainSwapQuoteRequest, config: Pick<ExecutionServiceConfig, 'lifiBaseUrl' | 'lifiFee'>): URL;
60
+ /**
61
+ * Semantic request fields first, then the bridge-tool pin, then Otto attribution appended last.
62
+ * `allowBridges` is pinned to the one reviewed facet so an enumerated route cannot silently come
63
+ * back on an unreviewed bridge (eco, stargate, across …) that this constructor would then refuse —
64
+ * pinning it keeps a supported route live rather than making liveness depend on LI.FI's routing.
65
+ */
66
+ export declare function buildLiFiBridgeQuoteUrl(request: CrossChainBridgeQuoteRequest, config: Pick<ExecutionServiceConfig, 'lifiBaseUrl' | 'lifiFee'>): URL;
67
+ export declare class RestLiFiQuoteClient implements LiFiQuoteClient {
68
+ private readonly config;
69
+ private readonly fetchImpl;
70
+ constructor(config: ExecutionServiceConfig, fetchImpl?: typeof fetch);
71
+ getSameChainSwapQuote(request: SameChainSwapQuoteRequest, callerSignal?: AbortSignal): Promise<LiFiQuote>;
72
+ getCrossChainBridgeQuote(request: CrossChainBridgeQuoteRequest, callerSignal?: AbortSignal): Promise<LiFiQuote>;
73
+ private fetchQuote;
74
+ }
@@ -0,0 +1,165 @@
1
+ import { z } from 'zod';
2
+ import { LIFI_INTEGRATOR, LIFI_POLYMER_BRIDGE_TOOL } from './execution-config.js';
3
+ import { isExecutionRefusal, refuse } from './execution-errors.js';
4
+ const LIFI_TOKEN_SCHEMA = z
5
+ .object({
6
+ address: z.string(),
7
+ })
8
+ .passthrough();
9
+ const LIFI_ACTION_SCHEMA = z
10
+ .object({
11
+ fromToken: LIFI_TOKEN_SCHEMA,
12
+ fromAmount: z.string(),
13
+ toToken: LIFI_TOKEN_SCHEMA,
14
+ fromChainId: z.number(),
15
+ toChainId: z.number(),
16
+ slippage: z.number(),
17
+ fromAddress: z.string(),
18
+ toAddress: z.string(),
19
+ })
20
+ .passthrough();
21
+ const LIFI_ESTIMATE_SCHEMA = z
22
+ .object({
23
+ approvalAddress: z.string(),
24
+ toAmount: z.string(),
25
+ toAmountMin: z.string(),
26
+ })
27
+ .passthrough();
28
+ const LIFI_TRANSACTION_REQUEST_SCHEMA = z
29
+ .object({
30
+ to: z.string(),
31
+ data: z.string(),
32
+ value: z.union([z.string(), z.number()]),
33
+ from: z.string().optional(),
34
+ })
35
+ .passthrough();
36
+ export const LIFI_QUOTE_SCHEMA = z
37
+ .object({
38
+ id: z.string().optional(),
39
+ tool: z.string().optional(),
40
+ type: z.string().optional(),
41
+ action: LIFI_ACTION_SCHEMA,
42
+ estimate: LIFI_ESTIMATE_SCHEMA,
43
+ transactionRequest: LIFI_TRANSACTION_REQUEST_SCHEMA,
44
+ })
45
+ .passthrough();
46
+ export function slippageBpsToDecimal(bps) {
47
+ if (!Number.isSafeInteger(bps) || bps <= 0 || bps >= 10_000) {
48
+ throw new Error('slippage bps must be an integer in [1, 9999]');
49
+ }
50
+ const fractional = bps.toString().padStart(4, '0').replace(/0+$/, '');
51
+ return `0.${fractional}`;
52
+ }
53
+ /** Semantic request fields first; Otto-owned attribution is appended last and has no caller seam. */
54
+ export function buildLiFiQuoteUrl(request, config) {
55
+ const url = new URL(`${config.lifiBaseUrl}/quote`);
56
+ const semanticFields = [
57
+ ['fromChain', String(request.chainId)],
58
+ ['toChain', String(request.chainId)],
59
+ ['fromToken', request.fromToken],
60
+ ['toToken', request.toToken],
61
+ ['fromAmount', request.fromAmount],
62
+ ['fromAddress', request.sender],
63
+ ['toAddress', request.receiver],
64
+ ['slippage', slippageBpsToDecimal(request.maxSlippageBps)],
65
+ ];
66
+ for (const [name, value] of semanticFields)
67
+ url.searchParams.append(name, value);
68
+ url.searchParams.append('integrator', LIFI_INTEGRATOR);
69
+ url.searchParams.append('fee', config.lifiFee.decimal);
70
+ return url;
71
+ }
72
+ /**
73
+ * Semantic request fields first, then the bridge-tool pin, then Otto attribution appended last.
74
+ * `allowBridges` is pinned to the one reviewed facet so an enumerated route cannot silently come
75
+ * back on an unreviewed bridge (eco, stargate, across …) that this constructor would then refuse —
76
+ * pinning it keeps a supported route live rather than making liveness depend on LI.FI's routing.
77
+ */
78
+ export function buildLiFiBridgeQuoteUrl(request, config) {
79
+ const url = new URL(`${config.lifiBaseUrl}/quote`);
80
+ const semanticFields = [
81
+ ['fromChain', String(request.fromChainId)],
82
+ ['toChain', String(request.toChainId)],
83
+ ['fromToken', request.fromToken],
84
+ ['toToken', request.toToken],
85
+ ['fromAmount', request.fromAmount],
86
+ ['fromAddress', request.sender],
87
+ ['toAddress', request.receiver],
88
+ ['slippage', slippageBpsToDecimal(request.maxSlippageBps)],
89
+ ['allowBridges', LIFI_POLYMER_BRIDGE_TOOL],
90
+ ];
91
+ for (const [name, value] of semanticFields)
92
+ url.searchParams.append(name, value);
93
+ url.searchParams.append('integrator', LIFI_INTEGRATOR);
94
+ url.searchParams.append('fee', config.lifiFee.decimal);
95
+ return url;
96
+ }
97
+ function boundedSignal(callerSignal, timeoutMs) {
98
+ const controller = new AbortController();
99
+ const timeout = setTimeout(() => controller.abort(new Error('LI.FI quote timeout')), timeoutMs);
100
+ const onAbort = () => controller.abort(callerSignal?.reason);
101
+ if (callerSignal?.aborted)
102
+ onAbort();
103
+ else
104
+ callerSignal?.addEventListener('abort', onAbort, { once: true });
105
+ return {
106
+ signal: controller.signal,
107
+ cleanup: () => {
108
+ clearTimeout(timeout);
109
+ callerSignal?.removeEventListener('abort', onAbort);
110
+ },
111
+ };
112
+ }
113
+ export class RestLiFiQuoteClient {
114
+ config;
115
+ fetchImpl;
116
+ constructor(config, fetchImpl = fetch) {
117
+ this.config = config;
118
+ this.fetchImpl = fetchImpl;
119
+ }
120
+ async getSameChainSwapQuote(request, callerSignal) {
121
+ return this.fetchQuote(buildLiFiQuoteUrl(request, this.config), callerSignal);
122
+ }
123
+ async getCrossChainBridgeQuote(request, callerSignal) {
124
+ return this.fetchQuote(buildLiFiBridgeQuoteUrl(request, this.config), callerSignal);
125
+ }
126
+ async fetchQuote(requestUrl, callerSignal) {
127
+ const abort = boundedSignal(callerSignal, this.config.lifiTimeoutMs);
128
+ try {
129
+ const response = await this.fetchImpl(requestUrl, {
130
+ method: 'GET',
131
+ headers: {
132
+ Accept: 'application/json',
133
+ // Operational quote-provider authentication only. It never enters an intent schema,
134
+ // execution envelope, error, caller-wallet path, or transaction-signing path.
135
+ ...(this.config.lifiApiKey ? { 'x-lifi-api-key': this.config.lifiApiKey } : {}),
136
+ },
137
+ signal: abort.signal,
138
+ });
139
+ if (!response.ok) {
140
+ refuse('LIFI_QUOTE_FAILED', `LI.FI quote request failed with HTTP ${response.status}`);
141
+ }
142
+ let body;
143
+ try {
144
+ body = await response.json();
145
+ }
146
+ catch {
147
+ refuse('LIFI_QUOTE_FAILED', 'LI.FI quote response was not valid JSON');
148
+ }
149
+ const parsed = LIFI_QUOTE_SCHEMA.safeParse(body);
150
+ if (!parsed.success) {
151
+ refuse('LIFI_QUOTE_INVALID', 'LI.FI quote response is missing required fields');
152
+ }
153
+ return parsed.data;
154
+ }
155
+ catch (error) {
156
+ if (isExecutionRefusal(error))
157
+ throw error;
158
+ refuse('LIFI_QUOTE_FAILED', 'LI.FI quote request failed');
159
+ }
160
+ finally {
161
+ abort.cleanup();
162
+ }
163
+ }
164
+ }
165
+ //# sourceMappingURL=lifi-execution-client.js.map
@@ -0,0 +1,32 @@
1
+ import { type Address, type Hex } from 'viem';
2
+ import { type ExecutionServiceConfig } from './execution-config.js';
3
+ /** `FeeCollector.collectTokenFees(...)` — the only calldata a reviewed fee-forwarding leg may carry. */
4
+ export declare const FORWARD_ERC20_FEES_SELECTOR: Hex;
5
+ export interface FeeDistribution {
6
+ readonly recipient: Address;
7
+ readonly amount: bigint;
8
+ }
9
+ /**
10
+ * Decode a `collectTokenFees` body. Shared by the same-chain swap and cross-chain bridge
11
+ * constructors so the fund-critical attribution shape has exactly one implementation: two copies
12
+ * of this check would drift, and a drifted copy is an unattributed or over-charged payload.
13
+ */
14
+ export declare function decodeFeeDistributions(callData: Hex): {
15
+ readonly token: Address;
16
+ readonly distributions: readonly FeeDistribution[];
17
+ };
18
+ export interface FeeDistributionAssertionInput {
19
+ /** The reviewed fee-forwarding leg's calldata, exactly as it will be submitted. */
20
+ readonly callData: Hex;
21
+ /** The token the whole payload spends; the fee body must name the same one. */
22
+ readonly expectedToken: Address;
23
+ /** The exact gross input amount the caller asked for; every rate is applied to it. */
24
+ readonly grossAmount: bigint;
25
+ readonly config: ExecutionServiceConfig;
26
+ }
27
+ /**
28
+ * Assert the decoded fee body carries exactly the versioned recipient set, the exact configured
29
+ * Otto amount, and an aggregate within the configured cap. Assert-what-you-built: this runs on the
30
+ * serialized calldata that is about to be returned, not on the values the constructor intended.
31
+ */
32
+ export declare function assertFeeDistributionBody(input: FeeDistributionAssertionInput): void;