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.
- package/LICENSE +21 -0
- package/README.md +102 -0
- package/dist/adapter/cdp-signer.d.ts +133 -0
- package/dist/adapter/cdp-signer.js +356 -0
- package/dist/adapter/cdp-signer.js.map +1 -0
- package/dist/adapter/chain.d.ts +36 -0
- package/dist/adapter/chain.js +65 -0
- package/dist/adapter/chain.js.map +1 -0
- package/dist/adapter/erc20.d.ts +39 -0
- package/dist/adapter/erc20.js +17 -0
- package/dist/adapter/erc20.js.map +1 -0
- package/dist/adapter/lifi-decode.d.ts +52 -0
- package/dist/adapter/lifi-decode.js +149 -0
- package/dist/adapter/lifi-decode.js.map +1 -0
- package/dist/adapter/refusal.d.ts +21 -0
- package/dist/adapter/refusal.js +55 -0
- package/dist/adapter/refusal.js.map +1 -0
- package/dist/adapter/sent-step.d.ts +15 -0
- package/dist/adapter/sent-step.js +6 -0
- package/dist/adapter/sent-step.js.map +1 -0
- package/dist/adapter/verify.d.ts +149 -0
- package/dist/adapter/verify.js +432 -0
- package/dist/adapter/verify.js.map +1 -0
- package/dist/adapter-cdp-index.d.ts +6 -0
- package/dist/adapter-cdp-index.js +7 -0
- package/dist/adapter-index.d.ts +15 -0
- package/dist/adapter-index.js +15 -0
- package/dist/artifact-id.d.ts +16 -0
- package/dist/artifact-id.js +60 -0
- package/dist/boot-redaction.d.ts +16 -0
- package/dist/boot-redaction.js +54 -0
- package/dist/data-source.d.ts +7 -0
- package/dist/data-source.js +2 -0
- package/dist/errors.d.ts +70 -0
- package/dist/errors.js +221 -0
- package/dist/execution-config.d.ts +232 -0
- package/dist/execution-config.js +443 -0
- package/dist/execution-delegated-definition.d.ts +165 -0
- package/dist/execution-delegated-definition.js +116 -0
- package/dist/execution-delegation-admin-definition.d.ts +208 -0
- package/dist/execution-delegation-admin-definition.js +170 -0
- package/dist/execution-delegation-admin.d.ts +74 -0
- package/dist/execution-delegation-admin.js +290 -0
- package/dist/execution-delegation-policy.d.ts +257 -0
- package/dist/execution-delegation-policy.js +279 -0
- package/dist/execution-delegation.d.ts +190 -0
- package/dist/execution-delegation.js +545 -0
- package/dist/execution-errors.d.ts +9 -0
- package/dist/execution-errors.js +128 -0
- package/dist/execution-index.d.ts +17 -0
- package/dist/execution-index.js +16 -0
- package/dist/execution-intent.d.ts +14 -0
- package/dist/execution-intent.js +36 -0
- package/dist/execution-registration.d.ts +113 -0
- package/dist/execution-registration.js +161 -0
- package/dist/execution-tool-definitions.d.ts +1103 -0
- package/dist/execution-tool-definitions.js +1051 -0
- package/dist/execution-tools.d.ts +85 -0
- package/dist/execution-tools.js +690 -0
- package/dist/execution-types.d.ts +274 -0
- package/dist/execution-types.js +157 -0
- package/dist/hyperliquid-info-client.d.ts +31 -0
- package/dist/hyperliquid-info-client.js +121 -0
- package/dist/hyperliquid-order-assertions.d.ts +89 -0
- package/dist/hyperliquid-order-assertions.js +325 -0
- package/dist/hyperliquid-order-builder.d.ts +47 -0
- package/dist/hyperliquid-order-builder.js +55 -0
- package/dist/lifi-bridge-assertions.d.ts +49 -0
- package/dist/lifi-bridge-assertions.js +303 -0
- package/dist/lifi-execution-client.d.ts +74 -0
- package/dist/lifi-execution-client.js +165 -0
- package/dist/lifi-fee-assertions.d.ts +32 -0
- package/dist/lifi-fee-assertions.js +103 -0
- package/dist/lifi-swap-assertions.d.ts +32 -0
- package/dist/lifi-swap-assertions.js +214 -0
- package/dist/local-config.d.ts +9 -0
- package/dist/local-config.js +45 -0
- package/dist/mcp-server.d.ts +4 -0
- package/dist/mcp-server.js +44 -0
- package/dist/polymarket-clob-client.d.ts +73 -0
- package/dist/polymarket-clob-client.js +186 -0
- package/dist/polymarket-order-assertions.d.ts +60 -0
- package/dist/polymarket-order-assertions.js +273 -0
- package/dist/polymarket-order-builder.d.ts +43 -0
- package/dist/polymarket-order-builder.js +74 -0
- package/dist/prepared-artifacts.d.ts +45 -0
- package/dist/prepared-artifacts.js +92 -0
- package/dist/stdio-main.d.ts +2 -0
- package/dist/stdio-main.js +20 -0
- package/dist/tool-definitions.d.ts +20 -0
- package/dist/tool-definitions.js +162 -0
- package/dist/x402-read-source.d.ts +11 -0
- package/dist/x402-read-source.js +77 -0
- package/package.json +162 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chain.ts — the one chain-state rule both signers follow on Base.
|
|
3
|
+
*
|
|
4
|
+
* With Flashblocks, a transaction receipt can arrive from a PRECONFIRMATION while `latest` — the last
|
|
5
|
+
* SEALED block, the state every default read and gas estimate runs against — still predates it, and a
|
|
6
|
+
* preconfirmation is not final: Base defines a Flashblock reorg as a preconfirmed transaction OMITTED
|
|
7
|
+
* from the sealed block. Observed live on 2026-08-28: the approve was preconfirmed, the swap call was
|
|
8
|
+
* estimated against `latest`, and the router's `transferFrom` reverted `TRANSFER_FROM_FAILED` on an
|
|
9
|
+
* allowance that was already granted. Rule: a transaction counts as mined ONLY once its receipt is
|
|
10
|
+
* refetched by hash and the sealed canonical block at that height carries the receipt's block hash.
|
|
11
|
+
* Until then the signer touches no state (no next estimate, no allowance read-back); if that never
|
|
12
|
+
* happens within the bound, the signer fails closed. This is the conservative mechanism — it is also
|
|
13
|
+
* exactly what a user copying this flow in a wallet observes.
|
|
14
|
+
*/
|
|
15
|
+
import type { Hex, PublicClient, TransactionReceipt } from 'viem';
|
|
16
|
+
export interface SealedWaitOptions {
|
|
17
|
+
/** Delay between polls (default 250 ms). */
|
|
18
|
+
readonly pollMs?: number;
|
|
19
|
+
/** Give up after this many polls (default 240 → 60 s at the default cadence). */
|
|
20
|
+
readonly maxPolls?: number;
|
|
21
|
+
}
|
|
22
|
+
export type SealedClient = Pick<PublicClient, 'getBlockNumber' | 'getTransactionReceipt' | 'getBlock'>;
|
|
23
|
+
/** The wait gave up: no receipt in a sealed canonical block within the bound. A CLASS, so callers classify structurally. */
|
|
24
|
+
export declare class SealedReceiptTimeoutError extends Error {
|
|
25
|
+
readonly hash: Hex;
|
|
26
|
+
readonly polls: number;
|
|
27
|
+
constructor(hash: Hex, polls: number);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Resolve with the receipt of `hash` once it is SEALED and CANONICAL: refetched by hash, its block
|
|
31
|
+
* number ≤ the sealed head, and the sealed block at that number has the receipt's `blockHash`. A
|
|
32
|
+
* preconfirmed receipt that was omitted from the sealed block (Flashblock reorg) is never returned —
|
|
33
|
+
* the wait continues until the transaction is re-included in a sealed block, or throws after
|
|
34
|
+
* `maxPolls` so the caller fails closed instead of treating the transaction as mined.
|
|
35
|
+
*/
|
|
36
|
+
export declare function waitForSealedReceipt(pub: SealedClient, hash: Hex, options?: SealedWaitOptions): Promise<TransactionReceipt>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chain.ts — the one chain-state rule both signers follow on Base.
|
|
3
|
+
*
|
|
4
|
+
* With Flashblocks, a transaction receipt can arrive from a PRECONFIRMATION while `latest` — the last
|
|
5
|
+
* SEALED block, the state every default read and gas estimate runs against — still predates it, and a
|
|
6
|
+
* preconfirmation is not final: Base defines a Flashblock reorg as a preconfirmed transaction OMITTED
|
|
7
|
+
* from the sealed block. Observed live on 2026-08-28: the approve was preconfirmed, the swap call was
|
|
8
|
+
* estimated against `latest`, and the router's `transferFrom` reverted `TRANSFER_FROM_FAILED` on an
|
|
9
|
+
* allowance that was already granted. Rule: a transaction counts as mined ONLY once its receipt is
|
|
10
|
+
* refetched by hash and the sealed canonical block at that height carries the receipt's block hash.
|
|
11
|
+
* Until then the signer touches no state (no next estimate, no allowance read-back); if that never
|
|
12
|
+
* happens within the bound, the signer fails closed. This is the conservative mechanism — it is also
|
|
13
|
+
* exactly what a user copying this flow in a wallet observes.
|
|
14
|
+
*/
|
|
15
|
+
/** The wait gave up: no receipt in a sealed canonical block within the bound. A CLASS, so callers classify structurally. */
|
|
16
|
+
export class SealedReceiptTimeoutError extends Error {
|
|
17
|
+
hash;
|
|
18
|
+
polls;
|
|
19
|
+
constructor(hash, polls) {
|
|
20
|
+
super(`transaction ${hash} has no receipt in a sealed canonical block after ${polls} polls; refusing to treat it as mined`);
|
|
21
|
+
this.hash = hash;
|
|
22
|
+
this.polls = polls;
|
|
23
|
+
this.name = 'SealedReceiptTimeoutError';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** STRUCTURAL: viem's own class name for "no receipt yet". Message text is never consulted — it must not select a class. */
|
|
27
|
+
function isReceiptNotFound(error) {
|
|
28
|
+
return error?.name === 'TransactionReceiptNotFoundError';
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Resolve with the receipt of `hash` once it is SEALED and CANONICAL: refetched by hash, its block
|
|
32
|
+
* number ≤ the sealed head, and the sealed block at that number has the receipt's `blockHash`. A
|
|
33
|
+
* preconfirmed receipt that was omitted from the sealed block (Flashblock reorg) is never returned —
|
|
34
|
+
* the wait continues until the transaction is re-included in a sealed block, or throws after
|
|
35
|
+
* `maxPolls` so the caller fails closed instead of treating the transaction as mined.
|
|
36
|
+
*/
|
|
37
|
+
export async function waitForSealedReceipt(pub, hash, options = {}) {
|
|
38
|
+
const pollMs = options.pollMs ?? 250;
|
|
39
|
+
const maxPolls = options.maxPolls ?? 240;
|
|
40
|
+
for (let poll = 0;; poll++) {
|
|
41
|
+
let receipt;
|
|
42
|
+
try {
|
|
43
|
+
receipt = await pub.getTransactionReceipt({ hash });
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (!isReceiptNotFound(error))
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
if (receipt) {
|
|
50
|
+
// viem caches getBlockNumber for the client's cacheTime by default — a cached head would defeat the wait.
|
|
51
|
+
const head = await pub.getBlockNumber({ cacheTime: 0 });
|
|
52
|
+
if (head >= receipt.blockNumber) {
|
|
53
|
+
const sealed = await pub.getBlock({ blockNumber: receipt.blockNumber });
|
|
54
|
+
if (sealed.hash === receipt.blockHash)
|
|
55
|
+
return receipt;
|
|
56
|
+
// The block this receipt claims is not the sealed block at that height: the preconfirmation did
|
|
57
|
+
// not survive sealing. Keep polling — the transaction may be re-included in a later block.
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (poll + 1 >= maxPolls)
|
|
61
|
+
throw new SealedReceiptTimeoutError(hash, maxPolls);
|
|
62
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=chain.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chain.js","sourceRoot":"","sources":["../../src/adapter/chain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAaH,4HAA4H;AAC5H,MAAM,OAAO,yBAA0B,SAAQ,KAAK;IAC7B;IAAoB;IAAzC,YAAqB,IAAS,EAAW,KAAa;QACpD,KAAK,CAAC,eAAe,IAAI,qDAAqD,KAAK,uCAAuC,CAAC,CAAC;QADzG,SAAI,GAAJ,IAAI,CAAK;QAAW,UAAK,GAAL,KAAK,CAAQ;QAEpD,IAAI,CAAC,IAAI,GAAG,2BAA2B,CAAC;IAC1C,CAAC;CACF;AAED,4HAA4H;AAC5H,SAAS,iBAAiB,CAAC,KAAc;IACvC,OAAQ,KAAmC,EAAE,IAAI,KAAK,iCAAiC,CAAC;AAC1F,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,GAAiB,EAAE,IAAS,EAAE,UAA6B,EAAE;IACtG,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,GAAG,CAAC;IACrC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,GAAG,CAAC;IACzC,KAAK,IAAI,IAAI,GAAG,CAAC,GAAI,IAAI,EAAE,EAAE,CAAC;QAC5B,IAAI,OAAuC,CAAC;QAC5C,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,GAAG,CAAC,qBAAqB,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;QACtD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC;gBAAE,MAAM,KAAK,CAAC;QAC7C,CAAC;QACD,IAAI,OAAO,EAAE,CAAC;YACZ,0GAA0G;YAC1G,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,cAAc,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC,CAAC;YACxD,IAAI,IAAI,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;gBAChC,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,QAAQ,CAAC,EAAE,WAAW,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;gBACxE,IAAI,MAAM,CAAC,IAAI,KAAK,OAAO,CAAC,SAAS;oBAAE,OAAO,OAAO,CAAC;gBACtD,gGAAgG;gBAChG,2FAA2F;YAC7F,CAAC;QACH,CAAC;QACD,IAAI,IAAI,GAAG,CAAC,IAAI,QAAQ;YAAE,MAAM,IAAI,yBAAyB,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QAC9E,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;IAC9D,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* erc20.ts — the two ERC-20 calls the signers make on their OWN behalf: the bounded `approve(spender, 0)`
|
|
3
|
+
* that clears a halted plan's allowance, and the `allowance` read-back that turns "the clear was
|
|
4
|
+
* mined" into "the allowance IS zero" — a receipt status is a claim about a transaction, the read-back
|
|
5
|
+
* is the fact about the token.
|
|
6
|
+
*/
|
|
7
|
+
import { type Address, type PublicClient } from 'viem';
|
|
8
|
+
export declare const ERC20_APPROVE_ABI: readonly [{
|
|
9
|
+
readonly name: "approve";
|
|
10
|
+
readonly type: "function";
|
|
11
|
+
readonly stateMutability: "nonpayable";
|
|
12
|
+
readonly inputs: readonly [{
|
|
13
|
+
readonly type: "address";
|
|
14
|
+
readonly name: "spender";
|
|
15
|
+
}, {
|
|
16
|
+
readonly type: "uint256";
|
|
17
|
+
readonly name: "amount";
|
|
18
|
+
}];
|
|
19
|
+
readonly outputs: readonly [{
|
|
20
|
+
readonly type: "bool";
|
|
21
|
+
}];
|
|
22
|
+
}];
|
|
23
|
+
export declare const ERC20_ALLOWANCE_ABI: readonly [{
|
|
24
|
+
readonly name: "allowance";
|
|
25
|
+
readonly type: "function";
|
|
26
|
+
readonly stateMutability: "view";
|
|
27
|
+
readonly inputs: readonly [{
|
|
28
|
+
readonly type: "address";
|
|
29
|
+
readonly name: "owner";
|
|
30
|
+
}, {
|
|
31
|
+
readonly type: "address";
|
|
32
|
+
readonly name: "spender";
|
|
33
|
+
}];
|
|
34
|
+
readonly outputs: readonly [{
|
|
35
|
+
readonly type: "uint256";
|
|
36
|
+
}];
|
|
37
|
+
}];
|
|
38
|
+
/** The live allowance `owner` has granted `spender` on `token`. Throws if the read itself fails. */
|
|
39
|
+
export declare function readAllowance(pub: PublicClient, token: Address, owner: Address, spender: Address): Promise<bigint>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* erc20.ts — the two ERC-20 calls the signers make on their OWN behalf: the bounded `approve(spender, 0)`
|
|
3
|
+
* that clears a halted plan's allowance, and the `allowance` read-back that turns "the clear was
|
|
4
|
+
* mined" into "the allowance IS zero" — a receipt status is a claim about a transaction, the read-back
|
|
5
|
+
* is the fact about the token.
|
|
6
|
+
*/
|
|
7
|
+
import { parseAbi } from 'viem';
|
|
8
|
+
export const ERC20_APPROVE_ABI = parseAbi(['function approve(address spender, uint256 amount) returns (bool)']);
|
|
9
|
+
export const ERC20_ALLOWANCE_ABI = parseAbi(['function allowance(address owner, address spender) view returns (uint256)']);
|
|
10
|
+
/** The live allowance `owner` has granted `spender` on `token`. Throws if the read itself fails. */
|
|
11
|
+
export async function readAllowance(pub, token, owner, spender) {
|
|
12
|
+
const value = await pub.readContract({ address: token, abi: ERC20_ALLOWANCE_ABI, functionName: 'allowance', args: [owner, spender] });
|
|
13
|
+
if (typeof value !== 'bigint')
|
|
14
|
+
throw new Error('allowance read-back did not return a uint256');
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=erc20.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"erc20.js","sourceRoot":"","sources":["../../src/adapter/erc20.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,QAAQ,EAAmC,MAAM,MAAM,CAAC;AAEjE,MAAM,CAAC,MAAM,iBAAiB,GAAG,QAAQ,CAAC,CAAC,kEAAkE,CAAC,CAAC,CAAC;AAChH,MAAM,CAAC,MAAM,mBAAmB,GAAG,QAAQ,CAAC,CAAC,2EAA2E,CAAC,CAAC,CAAC;AAE3H,oGAAoG;AACpG,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,GAAiB,EAAE,KAAc,EAAE,KAAc,EAAE,OAAgB;IACrG,MAAM,KAAK,GAAG,MAAM,GAAG,CAAC,YAAY,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,mBAAmB,EAAE,YAAY,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;IACtI,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;IAC/F,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lifi-decode.ts — the adapter's OWN decoder for what it is about to sign.
|
|
3
|
+
*
|
|
4
|
+
* Re-specified (not imported) from the reviewed layouts in `src/lib/otto-wallet/lifi-swap-action.ts`
|
|
5
|
+
* (GenericSwapV3 ERC20→ERC20: receiver@96 / minAmount@128, `LibSwap.SwapData` tuple) and the
|
|
6
|
+
* FeeCollector `collectTokenFees(token, (recipient, amount)[])` body. Spec §11 fences the dApp decoder
|
|
7
|
+
* as a live owner-op importer that must not move; spec §1 keeps the constructor's own assertions on
|
|
8
|
+
* the constructor's side. This file is the user-side view: if the two ever disagree, the artifact is
|
|
9
|
+
* refused, which is the correct outcome for a disagreement about calldata that moves money.
|
|
10
|
+
*
|
|
11
|
+
* Decoding runs on the ARG REGION with `decodeAbiParameters` after a separate selector check, so a
|
|
12
|
+
* selector/ABI mismatch cannot mask a correct-looking parameter decode. Every decode is re-encoded
|
|
13
|
+
* and compared to the input: trailing bytes or non-canonical encodings are refused, never ignored.
|
|
14
|
+
*/
|
|
15
|
+
import { type Address, type Hex } from 'viem';
|
|
16
|
+
/** swapTokensSingleV3ERC20ToERC20 / swapTokensMultipleV3ERC20ToERC20 — the only selectors a swap plan may call. */
|
|
17
|
+
export declare const GENERIC_SWAP_SELECTORS: Readonly<{
|
|
18
|
+
single: Hex;
|
|
19
|
+
multiple: Hex;
|
|
20
|
+
}>;
|
|
21
|
+
/** FeeCollector.collectTokenFees — the only calldata a fee-forwarding leg may carry. */
|
|
22
|
+
export declare const COLLECT_TOKEN_FEES_SELECTOR: Hex;
|
|
23
|
+
export interface SwapLeg {
|
|
24
|
+
readonly callTo: Address;
|
|
25
|
+
readonly approveTo: Address;
|
|
26
|
+
readonly sendingAssetId: Address;
|
|
27
|
+
readonly receivingAssetId: Address;
|
|
28
|
+
readonly fromAmount: bigint;
|
|
29
|
+
readonly callData: Hex;
|
|
30
|
+
readonly requiresDeposit: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface DecodedGenericSwap {
|
|
33
|
+
readonly selector: Hex;
|
|
34
|
+
readonly transactionId: Hex;
|
|
35
|
+
readonly integrator: string;
|
|
36
|
+
readonly referrer: string;
|
|
37
|
+
readonly receiver: Address;
|
|
38
|
+
readonly minAmount: bigint;
|
|
39
|
+
readonly legs: readonly SwapLeg[];
|
|
40
|
+
}
|
|
41
|
+
export interface FeeDistribution {
|
|
42
|
+
readonly recipient: Address;
|
|
43
|
+
readonly amount: bigint;
|
|
44
|
+
}
|
|
45
|
+
export interface DecodedCollectTokenFees {
|
|
46
|
+
readonly token: Address;
|
|
47
|
+
readonly distributions: readonly FeeDistribution[];
|
|
48
|
+
}
|
|
49
|
+
export declare function decodeGenericSwap(data: Hex): DecodedGenericSwap;
|
|
50
|
+
export declare function decodeCollectTokenFees(callData: Hex): DecodedCollectTokenFees;
|
|
51
|
+
export declare function encodeGenericSwap(swap: DecodedGenericSwap): Hex;
|
|
52
|
+
export declare function encodeCollectTokenFees(fees: DecodedCollectTokenFees): Hex;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lifi-decode.ts — the adapter's OWN decoder for what it is about to sign.
|
|
3
|
+
*
|
|
4
|
+
* Re-specified (not imported) from the reviewed layouts in `src/lib/otto-wallet/lifi-swap-action.ts`
|
|
5
|
+
* (GenericSwapV3 ERC20→ERC20: receiver@96 / minAmount@128, `LibSwap.SwapData` tuple) and the
|
|
6
|
+
* FeeCollector `collectTokenFees(token, (recipient, amount)[])` body. Spec §11 fences the dApp decoder
|
|
7
|
+
* as a live owner-op importer that must not move; spec §1 keeps the constructor's own assertions on
|
|
8
|
+
* the constructor's side. This file is the user-side view: if the two ever disagree, the artifact is
|
|
9
|
+
* refused, which is the correct outcome for a disagreement about calldata that moves money.
|
|
10
|
+
*
|
|
11
|
+
* Decoding runs on the ARG REGION with `decodeAbiParameters` after a separate selector check, so a
|
|
12
|
+
* selector/ABI mismatch cannot mask a correct-looking parameter decode. Every decode is re-encoded
|
|
13
|
+
* and compared to the input: trailing bytes or non-canonical encodings are refused, never ignored.
|
|
14
|
+
*/
|
|
15
|
+
import { decodeAbiParameters, encodeAbiParameters, getAddress, parseAbiParameters, } from 'viem';
|
|
16
|
+
import { refuse } from './refusal.js';
|
|
17
|
+
/** LibSwap.SwapData — (callTo, approveTo, sendingAssetId, receivingAssetId, fromAmount, callData, requiresDeposit). */
|
|
18
|
+
const SWAP_DATA_TUPLE = '(address callTo, address approveTo, address sendingAssetId, address receivingAssetId, uint256 fromAmount, bytes callData, bool requiresDeposit)';
|
|
19
|
+
const GENERIC_SWAP_SINGLE_PARAMS = parseAbiParameters(`bytes32 _transactionId, string _integrator, string _referrer, address _receiver, uint256 _minAmount, ${SWAP_DATA_TUPLE} _swapData`);
|
|
20
|
+
const GENERIC_SWAP_MULTIPLE_PARAMS = parseAbiParameters(`bytes32 _transactionId, string _integrator, string _referrer, address _receiver, uint256 _minAmount, ${SWAP_DATA_TUPLE}[] _swapData`);
|
|
21
|
+
const COLLECT_TOKEN_FEES_PARAMS = parseAbiParameters('address token, (address recipient, uint256 amount)[] distributions');
|
|
22
|
+
/** swapTokensSingleV3ERC20ToERC20 / swapTokensMultipleV3ERC20ToERC20 — the only selectors a swap plan may call. */
|
|
23
|
+
export const GENERIC_SWAP_SELECTORS = Object.freeze({
|
|
24
|
+
single: '0x4666fc80',
|
|
25
|
+
multiple: '0x5fd9ae2e',
|
|
26
|
+
});
|
|
27
|
+
/** FeeCollector.collectTokenFees — the only calldata a fee-forwarding leg may carry. */
|
|
28
|
+
export const COLLECT_TOKEN_FEES_SELECTOR = '0x332d746b';
|
|
29
|
+
function selectorOf(data) {
|
|
30
|
+
return data.slice(0, 10).toLowerCase();
|
|
31
|
+
}
|
|
32
|
+
function argRegion(data) {
|
|
33
|
+
return `0x${data.slice(10)}`;
|
|
34
|
+
}
|
|
35
|
+
function normalizeLeg(leg) {
|
|
36
|
+
return Object.freeze({
|
|
37
|
+
callTo: getAddress(leg.callTo),
|
|
38
|
+
approveTo: getAddress(leg.approveTo),
|
|
39
|
+
sendingAssetId: getAddress(leg.sendingAssetId),
|
|
40
|
+
receivingAssetId: getAddress(leg.receivingAssetId),
|
|
41
|
+
fromAmount: leg.fromAmount,
|
|
42
|
+
callData: leg.callData,
|
|
43
|
+
requiresDeposit: leg.requiresDeposit,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
export function decodeGenericSwap(data) {
|
|
47
|
+
if (!/^0x[0-9a-fA-F]*$/.test(data) || data.length < 10) {
|
|
48
|
+
refuse('SWAP_CALLDATA_INVALID', 'swap step calldata is not hex or has no selector');
|
|
49
|
+
}
|
|
50
|
+
const selector = selectorOf(data);
|
|
51
|
+
const args = argRegion(data);
|
|
52
|
+
const params = selector === GENERIC_SWAP_SELECTORS.single
|
|
53
|
+
? GENERIC_SWAP_SINGLE_PARAMS
|
|
54
|
+
: selector === GENERIC_SWAP_SELECTORS.multiple
|
|
55
|
+
? GENERIC_SWAP_MULTIPLE_PARAMS
|
|
56
|
+
: undefined;
|
|
57
|
+
if (!params) {
|
|
58
|
+
refuse('SWAP_CALLDATA_INVALID', `selector ${selector} is not an allowlisted GenericSwap ERC20→ERC20 function`);
|
|
59
|
+
}
|
|
60
|
+
let decoded;
|
|
61
|
+
try {
|
|
62
|
+
decoded = decodeAbiParameters(params, args);
|
|
63
|
+
if (encodeAbiParameters(params, decoded).toLowerCase() !== args.toLowerCase()) {
|
|
64
|
+
refuse('SWAP_CALLDATA_INVALID', 'swap calldata is non-canonical or carries trailing bytes');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (error instanceof Error && error.name === 'AdapterRefusal')
|
|
69
|
+
throw error;
|
|
70
|
+
refuse('SWAP_CALLDATA_INVALID', 'GenericSwap calldata failed to decode');
|
|
71
|
+
}
|
|
72
|
+
const [transactionId, integrator, referrer, receiver, minAmount, swapData] = decoded;
|
|
73
|
+
const legs = (Array.isArray(swapData) ? swapData : [swapData]).map(normalizeLeg);
|
|
74
|
+
if (legs.length === 0)
|
|
75
|
+
refuse('SWAP_CALLDATA_INVALID', 'GenericSwap carries no swap legs');
|
|
76
|
+
return Object.freeze({
|
|
77
|
+
selector,
|
|
78
|
+
transactionId,
|
|
79
|
+
integrator,
|
|
80
|
+
referrer,
|
|
81
|
+
receiver: getAddress(receiver),
|
|
82
|
+
minAmount,
|
|
83
|
+
legs: Object.freeze(legs),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
export function decodeCollectTokenFees(callData) {
|
|
87
|
+
if (selectorOf(callData) !== COLLECT_TOKEN_FEES_SELECTOR) {
|
|
88
|
+
refuse('ATTRIBUTION_MISSING', 'fee-forwarding leg does not call collectTokenFees');
|
|
89
|
+
}
|
|
90
|
+
const args = argRegion(callData);
|
|
91
|
+
let decoded;
|
|
92
|
+
try {
|
|
93
|
+
decoded = decodeAbiParameters(COLLECT_TOKEN_FEES_PARAMS, args);
|
|
94
|
+
if (encodeAbiParameters(COLLECT_TOKEN_FEES_PARAMS, decoded).toLowerCase() !==
|
|
95
|
+
args.toLowerCase()) {
|
|
96
|
+
refuse('ATTRIBUTION_MISSING', 'fee calldata is non-canonical or carries trailing bytes');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (error instanceof Error && error.name === 'AdapterRefusal')
|
|
101
|
+
throw error;
|
|
102
|
+
refuse('ATTRIBUTION_MISSING', 'collectTokenFees calldata failed to decode');
|
|
103
|
+
}
|
|
104
|
+
const [token, distributions] = decoded;
|
|
105
|
+
if (distributions.length === 0 || distributions.length > 8) {
|
|
106
|
+
refuse('ATTRIBUTION_MISSING', 'fee calldata has an unexpected distribution count');
|
|
107
|
+
}
|
|
108
|
+
return Object.freeze({
|
|
109
|
+
token: getAddress(token),
|
|
110
|
+
distributions: Object.freeze(distributions.map((distribution) => Object.freeze({ recipient: getAddress(distribution.recipient), amount: distribution.amount }))),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/* ------------------------------------------------------------------ *
|
|
114
|
+
* Encoders — used ONLY by the mutation matrix (tests + the fence section) to
|
|
115
|
+
* derive inconsistent artifacts the verifier must refuse. Production paths never encode.
|
|
116
|
+
* ------------------------------------------------------------------ */
|
|
117
|
+
export function encodeGenericSwap(swap) {
|
|
118
|
+
const legs = swap.legs.map((leg) => ({ ...leg }));
|
|
119
|
+
if (swap.selector === GENERIC_SWAP_SELECTORS.single) {
|
|
120
|
+
if (legs.length !== 1)
|
|
121
|
+
throw new Error('single selector needs exactly one leg');
|
|
122
|
+
const body = encodeAbiParameters(GENERIC_SWAP_SINGLE_PARAMS, [
|
|
123
|
+
swap.transactionId,
|
|
124
|
+
swap.integrator,
|
|
125
|
+
swap.referrer,
|
|
126
|
+
swap.receiver,
|
|
127
|
+
swap.minAmount,
|
|
128
|
+
legs[0],
|
|
129
|
+
]);
|
|
130
|
+
return `${swap.selector}${body.slice(2)}`;
|
|
131
|
+
}
|
|
132
|
+
const body = encodeAbiParameters(GENERIC_SWAP_MULTIPLE_PARAMS, [
|
|
133
|
+
swap.transactionId,
|
|
134
|
+
swap.integrator,
|
|
135
|
+
swap.referrer,
|
|
136
|
+
swap.receiver,
|
|
137
|
+
swap.minAmount,
|
|
138
|
+
legs,
|
|
139
|
+
]);
|
|
140
|
+
return `${swap.selector}${body.slice(2)}`;
|
|
141
|
+
}
|
|
142
|
+
export function encodeCollectTokenFees(fees) {
|
|
143
|
+
const body = encodeAbiParameters(COLLECT_TOKEN_FEES_PARAMS, [
|
|
144
|
+
fees.token,
|
|
145
|
+
fees.distributions.map((distribution) => ({ ...distribution })),
|
|
146
|
+
]);
|
|
147
|
+
return `${COLLECT_TOKEN_FEES_SELECTOR}${body.slice(2)}`;
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=lifi-decode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lifi-decode.js","sourceRoot":"","sources":["../../src/adapter/lifi-decode.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* refusal.ts — the adapter's refusal taxonomy.
|
|
3
|
+
*
|
|
4
|
+
* Every refusal names a defect in the artifact or in the request (spec §10). A refusal that cannot
|
|
5
|
+
* name one is not a refusal but a service fault, and is thrown as a plain Error so the ladder grades
|
|
6
|
+
* it INCONCLUSIVE rather than BLOCKED.
|
|
7
|
+
*
|
|
8
|
+
* `errorType` mirrors `code` so the CDP kit's ladder — which only grades a *structured* refusal as
|
|
9
|
+
* the fence biting — recognises adapter refusals without a second grading path.
|
|
10
|
+
*/
|
|
11
|
+
export declare const ADAPTER_REFUSAL_CODES: readonly ["SCHEMA_INVALID", "UNSUPPORTED_OPERATION", "DIGEST_MISMATCH", "STALE_ARTIFACT", "ACCOUNT_BINDING_MISMATCH", "CAPABILITY_MISSING", "CHAIN_MISMATCH", "PLAN_SHAPE_INVALID", "APPROVAL_INVALID", "TARGET_NOT_ALLOWED", "SWAP_CALLDATA_INVALID", "RECEIVER_MISMATCH", "INTENT_MISMATCH", "MIN_OUTPUT_TOO_LOW", "ATTRIBUTION_MISSING", "ATTRIBUTION_MISMATCH", "FEE_CAP_EXCEEDED"];
|
|
12
|
+
export type AdapterRefusalCode = (typeof ADAPTER_REFUSAL_CODES)[number];
|
|
13
|
+
export declare class AdapterRefusal extends Error {
|
|
14
|
+
readonly code: AdapterRefusalCode;
|
|
15
|
+
/** Same value as `code`; lets `normalizeError` treat this as a structured refusal. */
|
|
16
|
+
readonly errorType: AdapterRefusalCode;
|
|
17
|
+
constructor(code: AdapterRefusalCode, message: string);
|
|
18
|
+
}
|
|
19
|
+
export declare function refuse(code: AdapterRefusalCode, message: string): never;
|
|
20
|
+
export declare function isAdapterRefusal(error: unknown): error is AdapterRefusal;
|
|
21
|
+
export declare function isAdapterRefusalCode(value: string): value is AdapterRefusalCode;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* refusal.ts — the adapter's refusal taxonomy.
|
|
3
|
+
*
|
|
4
|
+
* Every refusal names a defect in the artifact or in the request (spec §10). A refusal that cannot
|
|
5
|
+
* name one is not a refusal but a service fault, and is thrown as a plain Error so the ladder grades
|
|
6
|
+
* it INCONCLUSIVE rather than BLOCKED.
|
|
7
|
+
*
|
|
8
|
+
* `errorType` mirrors `code` so the CDP kit's ladder — which only grades a *structured* refusal as
|
|
9
|
+
* the fence biting — recognises adapter refusals without a second grading path.
|
|
10
|
+
*/
|
|
11
|
+
export const ADAPTER_REFUSAL_CODES = [
|
|
12
|
+
// Envelope integrity (the constructor's own assertions, surfaced unchanged)
|
|
13
|
+
'SCHEMA_INVALID',
|
|
14
|
+
'UNSUPPORTED_OPERATION',
|
|
15
|
+
'DIGEST_MISMATCH',
|
|
16
|
+
'STALE_ARTIFACT',
|
|
17
|
+
'ACCOUNT_BINDING_MISMATCH',
|
|
18
|
+
// Adapter contract (spec §9)
|
|
19
|
+
'CAPABILITY_MISSING',
|
|
20
|
+
'CHAIN_MISMATCH',
|
|
21
|
+
// Adapter-side plan policy (what the user's signer is willing to sign)
|
|
22
|
+
'PLAN_SHAPE_INVALID',
|
|
23
|
+
'APPROVAL_INVALID',
|
|
24
|
+
'TARGET_NOT_ALLOWED',
|
|
25
|
+
'SWAP_CALLDATA_INVALID',
|
|
26
|
+
'RECEIVER_MISMATCH',
|
|
27
|
+
// Intent binding — the artifact must be the trade the USER asked for
|
|
28
|
+
'INTENT_MISMATCH',
|
|
29
|
+
'MIN_OUTPUT_TOO_LOW',
|
|
30
|
+
// Attribution — assert what you are about to sign, against the RULED constants
|
|
31
|
+
'ATTRIBUTION_MISSING',
|
|
32
|
+
'ATTRIBUTION_MISMATCH',
|
|
33
|
+
'FEE_CAP_EXCEEDED',
|
|
34
|
+
];
|
|
35
|
+
export class AdapterRefusal extends Error {
|
|
36
|
+
code;
|
|
37
|
+
/** Same value as `code`; lets `normalizeError` treat this as a structured refusal. */
|
|
38
|
+
errorType;
|
|
39
|
+
constructor(code, message) {
|
|
40
|
+
super(`[${code}] ${message}`);
|
|
41
|
+
this.code = code;
|
|
42
|
+
this.name = 'AdapterRefusal';
|
|
43
|
+
this.errorType = code;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function refuse(code, message) {
|
|
47
|
+
throw new AdapterRefusal(code, message);
|
|
48
|
+
}
|
|
49
|
+
export function isAdapterRefusal(error) {
|
|
50
|
+
return error instanceof AdapterRefusal;
|
|
51
|
+
}
|
|
52
|
+
export function isAdapterRefusalCode(value) {
|
|
53
|
+
return ADAPTER_REFUSAL_CODES.includes(value);
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=refusal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refusal.js","sourceRoot":"","sources":["../../src/adapter/refusal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,4EAA4E;IAC5E,gBAAgB;IAChB,uBAAuB;IACvB,iBAAiB;IACjB,gBAAgB;IAChB,0BAA0B;IAC1B,6BAA6B;IAC7B,oBAAoB;IACpB,gBAAgB;IAChB,uEAAuE;IACvE,oBAAoB;IACpB,kBAAkB;IAClB,oBAAoB;IACpB,uBAAuB;IACvB,mBAAmB;IACnB,qEAAqE;IACrE,iBAAiB;IACjB,oBAAoB;IACpB,+EAA+E;IAC/E,qBAAqB;IACrB,sBAAsB;IACtB,kBAAkB;CACV,CAAC;AAIX,MAAM,OAAO,cAAe,SAAQ,KAAK;IAK5B;IAJX,sFAAsF;IAC7E,SAAS,CAAqB;IAEvC,YACW,IAAwB,EACjC,OAAe;QAEf,KAAK,CAAC,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC,CAAC;QAHrB,SAAI,GAAJ,IAAI,CAAoB;QAIjC,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;QAC7B,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;IACxB,CAAC;CACF;AAED,MAAM,UAAU,MAAM,CAAC,IAAwB,EAAE,OAAe;IAC9D,MAAM,IAAI,cAAc,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC7C,OAAO,KAAK,YAAY,cAAc,CAAC;AACzC,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,KAAa;IAChD,OAAQ,qBAA2C,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AACtE,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sent-step.ts — one submitted plan step as both signers report it: the step's index and kind, its
|
|
3
|
+
* target, the transaction hash, and the SEALED canonical receipt's block number and gas used.
|
|
4
|
+
*/
|
|
5
|
+
import type { Address, Hex } from 'viem';
|
|
6
|
+
import type { VerifiedStep } from './verify.js';
|
|
7
|
+
export interface SentStep {
|
|
8
|
+
readonly index: number;
|
|
9
|
+
readonly kind: VerifiedStep['kind'];
|
|
10
|
+
readonly to: Address;
|
|
11
|
+
readonly transaction_hash: Hex;
|
|
12
|
+
readonly status: 'success';
|
|
13
|
+
readonly block_number: string;
|
|
14
|
+
readonly gas_used: string;
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sent-step.js","sourceRoot":"","sources":["../../src/adapter/sent-step.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* verify.ts — the adapter core: what the user's signer checks BEFORE it signs anything.
|
|
3
|
+
*
|
|
4
|
+
* Model B (docs/0808_model_b_scope_and_verdict.md, spec docs/0816_step1_execution_mcp_spec_draft.md):
|
|
5
|
+
* Otto constructs; the user's own custody signs and submits; Otto's fee is attribution baked into
|
|
6
|
+
* the constructed payload. The constructor is prepare-only and asserts what it built. This module is
|
|
7
|
+
* the OTHER half of spec §8 — "the consuming adapter re-checks digest + expected signer before
|
|
8
|
+
* signing" — extended to a full adapter-side policy, because an artifact's own digests prove
|
|
9
|
+
* integrity relative to the artifact's own claims and nothing more: an inconsistent artifact with
|
|
10
|
+
* correctly recomputed digests is still inconsistent. The checks run in three layers:
|
|
11
|
+
*
|
|
12
|
+
* 1. INTEGRITY — the constructor's own assertions, unchanged (schema, payload digest, whole-envelope
|
|
13
|
+
* commitment + freshness, account binding).
|
|
14
|
+
* 2. INTENT — the artifact is the trade the USER asked for: the approved token and amount ARE the
|
|
15
|
+
* intent's, the deposit legs pull exactly that amount, the output token IS the intent's, and the
|
|
16
|
+
* on-chain minimum output is at least the caller's own floor (> 0). The caller owns the floor;
|
|
17
|
+
* it never comes from the artifact. This is the fence for the swap legs themselves: LI.FI's
|
|
18
|
+
* GenericSwapV3 reverts unless `_receiver` gets at least `_minAmount` of the output token, so an
|
|
19
|
+
* internal DEX leg is bounded on-chain by the receiver + minimum-output binds rather than by an
|
|
20
|
+
* allowlist of every DEX contract (which this adapter deliberately does not attempt to hold).
|
|
21
|
+
* 3. POLICY — capability contract, chain, plan shape, approvals only to the reviewed spender
|
|
22
|
+
* that IS the call target, `_receiver` == this account, and the fee leg decoded with the
|
|
23
|
+
* adapter's own decoder and matched against the RULED attribution constants (10 bps to Otto's
|
|
24
|
+
* recipient, only the reviewed FeeForwarder deployments, nothing to strangers, aggregate under
|
|
25
|
+
* the cap).
|
|
26
|
+
*
|
|
27
|
+
* P0 LIVENESS binds here exactly as it binds the constructor (spec §7.0): a valid artifact for the
|
|
28
|
+
* stated intent MUST pass. The test suite's first test is that property; the mutation matrix proves
|
|
29
|
+
* every refusal reddens. Every check that passes is recorded by id so the sitting's evidence shows
|
|
30
|
+
* what was verified, not just that "verification passed".
|
|
31
|
+
*/
|
|
32
|
+
import { type Address, type Hex } from 'viem';
|
|
33
|
+
import { type AccountProfile, type ExecutionEnvelope } from '../execution-index.js';
|
|
34
|
+
import { type DecodedCollectTokenFees } from './lifi-decode.js';
|
|
35
|
+
export type SigningCapability = 'send_evm_transaction' | 'submit_evm_batch' | 'sign_eip712' | 'sign_hyperliquid_l1' | 'sign_x402_payment';
|
|
36
|
+
/** What the USER asked for. Owned by the caller; never read out of the artifact. */
|
|
37
|
+
export interface SwapIntent {
|
|
38
|
+
readonly fromToken: Address;
|
|
39
|
+
readonly toToken: Address;
|
|
40
|
+
/** Exact input amount in atomic units. */
|
|
41
|
+
readonly fromAmount: bigint;
|
|
42
|
+
/**
|
|
43
|
+
* The caller's OWN minimum acceptable output (atomic units), from a price source the caller
|
|
44
|
+
* trusts. Must be positive: a zero floor would let a swap leg deliver nothing.
|
|
45
|
+
*/
|
|
46
|
+
readonly minAmountOut: bigint;
|
|
47
|
+
}
|
|
48
|
+
export interface AttributionPolicy {
|
|
49
|
+
/** The RULED fee config version the envelope must carry (spec §8: mechanism + version, never values). */
|
|
50
|
+
readonly configVersion: string;
|
|
51
|
+
/** The RULED Otto fee in basis points, asserted against the decoded fee leg. */
|
|
52
|
+
readonly feeBps: bigint;
|
|
53
|
+
readonly integrator: string;
|
|
54
|
+
readonly ottoRecipient: Address;
|
|
55
|
+
/** Recipients other than Otto's that a fee leg may pay (LI.FI's own platform share). Anything else is refused. */
|
|
56
|
+
readonly otherAllowedRecipients: readonly Address[];
|
|
57
|
+
/** Reviewed FeeForwarder deployments for the plan's chain (v1 + v2.0.0 today); a leg calling any other address is refused by name. */
|
|
58
|
+
readonly forwarders: readonly Address[];
|
|
59
|
+
/** Aggregate (Otto + LI.FI) fee ceiling in basis points; strictly above it is refused. */
|
|
60
|
+
readonly aggregateCapBps: bigint;
|
|
61
|
+
}
|
|
62
|
+
export interface AdapterPolicy {
|
|
63
|
+
readonly allowedCallTargets: readonly Address[];
|
|
64
|
+
readonly allowedApproveSpenders: readonly Address[];
|
|
65
|
+
readonly attribution: AttributionPolicy;
|
|
66
|
+
}
|
|
67
|
+
export interface VerifyExpectations {
|
|
68
|
+
readonly profile: AccountProfile;
|
|
69
|
+
readonly intent: SwapIntent;
|
|
70
|
+
readonly capabilities: readonly SigningCapability[];
|
|
71
|
+
readonly policy: AdapterPolicy;
|
|
72
|
+
readonly nowMs?: number;
|
|
73
|
+
/**
|
|
74
|
+
* The EXACT `otto_prepare_swap` input the caller sent (raw, as sent). When supplied, the artifact's
|
|
75
|
+
* `intent_digest` is recomputed with the constructor's own normalizer (which parses and checksums
|
|
76
|
+
* exactly as the constructor does) and must match — the artifact is then bound not only to the
|
|
77
|
+
* semantic intent fields but to the very request that produced it.
|
|
78
|
+
*/
|
|
79
|
+
readonly prepareInput?: unknown;
|
|
80
|
+
}
|
|
81
|
+
export interface VerifiedStep {
|
|
82
|
+
readonly kind: 'approve_reset' | 'approve' | 'call';
|
|
83
|
+
readonly to: Address;
|
|
84
|
+
readonly value: bigint;
|
|
85
|
+
readonly data: Hex;
|
|
86
|
+
}
|
|
87
|
+
export interface VerifiedCheck {
|
|
88
|
+
readonly id: string;
|
|
89
|
+
readonly result: 'passed';
|
|
90
|
+
}
|
|
91
|
+
export interface VerifiedSwap {
|
|
92
|
+
readonly artifact: ExecutionEnvelope;
|
|
93
|
+
readonly steps: readonly [VerifiedStep, VerifiedStep, VerifiedStep];
|
|
94
|
+
readonly intent: SwapIntent;
|
|
95
|
+
readonly token: Address;
|
|
96
|
+
readonly outputToken: Address;
|
|
97
|
+
readonly spender: Address;
|
|
98
|
+
readonly gross: bigint;
|
|
99
|
+
readonly receiver: Address;
|
|
100
|
+
readonly minAmountOut: bigint;
|
|
101
|
+
readonly ottoFee: bigint;
|
|
102
|
+
readonly fees: DecodedCollectTokenFees;
|
|
103
|
+
readonly checks: readonly VerifiedCheck[];
|
|
104
|
+
}
|
|
105
|
+
/** The reviewed v1 policy for a same-chain LI.FI swap on `chainId`. */
|
|
106
|
+
export declare function defaultAdapterPolicy(chainId: number): AdapterPolicy;
|
|
107
|
+
export declare function verifySwapEnvelope(raw: unknown, expected: VerifyExpectations): VerifiedSwap;
|
|
108
|
+
/** The verifier's record of a plan at verification time. Signers bind to THIS, not to the plan's fields. */
|
|
109
|
+
export interface PlanIdentity {
|
|
110
|
+
readonly address: Address;
|
|
111
|
+
readonly chainId: number;
|
|
112
|
+
readonly capabilities: readonly SigningCapability[];
|
|
113
|
+
readonly validUntilMs: number;
|
|
114
|
+
readonly artifactId: string;
|
|
115
|
+
readonly artifactDigest: Hex;
|
|
116
|
+
readonly stepTargets: readonly [Address, Address, Address];
|
|
117
|
+
}
|
|
118
|
+
/** True iff every object reachable from `value` is frozen. */
|
|
119
|
+
export declare function isDeepFrozen(value: unknown): boolean;
|
|
120
|
+
/** True iff `plan` is an object `verifySwapEnvelope` itself returned (identity, not shape). */
|
|
121
|
+
export declare function isVerifierOutput(plan: unknown): boolean;
|
|
122
|
+
/** The verifier's own record of an intact verified plan (throws for anything else). */
|
|
123
|
+
export declare function verifiedPlanIdentity(plan: VerifiedSwap): PlanIdentity;
|
|
124
|
+
/** What a signer must present to be allowed to sign a verified plan. */
|
|
125
|
+
export interface SignerIdentity {
|
|
126
|
+
readonly address: Address;
|
|
127
|
+
readonly chainId: number;
|
|
128
|
+
readonly capabilities: readonly SigningCapability[];
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The FULL signer-side gate, called at signer entry and again immediately before every signature or
|
|
132
|
+
* submission: provenance + frozen shape (`assertVerifiedPlanIntact`), then — against the verifier's
|
|
133
|
+
* RECORD of the plan, not the plan's fields — the bound account IS this signer, the chain matches,
|
|
134
|
+
* every required capability is provided, and the plan is still fresh at `nowMs` (the record's
|
|
135
|
+
* deadline AND the artifact's own commitment-bound deadline). A plan verified for another account,
|
|
136
|
+
* another chain, or that has since expired is refused here even though it was genuinely verified.
|
|
137
|
+
*/
|
|
138
|
+
export declare function assertPlanSignableBy(plan: VerifiedSwap, signer: SignerIdentity, nowMs?: number): void;
|
|
139
|
+
/**
|
|
140
|
+
* Signer-side gate: a plan may be signed only if it IS the object `verifySwapEnvelope` returned
|
|
141
|
+
* (provenance, checked by identity), it is still the deep-frozen shape it was returned as — the
|
|
142
|
+
* artifact's nested objects included — and it still matches the verifier's own record of it. A thawed
|
|
143
|
+
* copy, a JSON round trip, a mutated clone, a fully frozen look-alike that imitates the shape, or a
|
|
144
|
+
* re-pointed artifact with a recomputed commitment are all refused, so the only way to sign
|
|
145
|
+
* different bytes is to verify different bytes.
|
|
146
|
+
*/
|
|
147
|
+
export declare function assertVerifiedPlanIntact(plan: VerifiedSwap): void;
|
|
148
|
+
/** The check ids `verifySwapEnvelope` records on a passing artifact, in order. Tests pin this list. */
|
|
149
|
+
export declare const VERIFY_CHECK_IDS: readonly ["schema", "payload_digest", "commitment", "freshness", "account_binding", "capabilities", "chain", "plan_shape", "approvals", "swap_decode", "min_output", "fee_attribution"];
|