otto-execute 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 +151 -0
- package/dist/cdp-signer.d.ts +2 -0
- package/dist/cdp-signer.js +3 -0
- package/dist/cdp.d.ts +7 -0
- package/dist/cdp.js +8 -0
- package/dist/chain.d.ts +2 -0
- package/dist/chain.js +3 -0
- package/dist/cli.d.ts +17 -0
- package/dist/cli.js +400 -0
- package/dist/delegate.d.ts +117 -0
- package/dist/delegate.js +394 -0
- package/dist/eoa-signer.d.ts +94 -0
- package/dist/eoa-signer.js +289 -0
- package/dist/erc20.d.ts +2 -0
- package/dist/erc20.js +3 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +19 -0
- package/dist/lifi-decode.d.ts +2 -0
- package/dist/lifi-decode.js +3 -0
- package/dist/mutations.d.ts +55 -0
- package/dist/mutations.js +363 -0
- package/dist/refusal.d.ts +2 -0
- package/dist/refusal.js +3 -0
- package/dist/vendor/otto-intel-mcp/VENDORED.json +52 -0
- package/dist/vendor/otto-intel-mcp/adapter/cdp-signer.d.ts +133 -0
- package/dist/vendor/otto-intel-mcp/adapter/cdp-signer.js +356 -0
- package/dist/vendor/otto-intel-mcp/adapter/chain.d.ts +36 -0
- package/dist/vendor/otto-intel-mcp/adapter/chain.js +65 -0
- package/dist/vendor/otto-intel-mcp/adapter/erc20.d.ts +39 -0
- package/dist/vendor/otto-intel-mcp/adapter/erc20.js +17 -0
- package/dist/vendor/otto-intel-mcp/adapter/lifi-decode.d.ts +52 -0
- package/dist/vendor/otto-intel-mcp/adapter/lifi-decode.js +149 -0
- package/dist/vendor/otto-intel-mcp/adapter/refusal.d.ts +21 -0
- package/dist/vendor/otto-intel-mcp/adapter/refusal.js +55 -0
- package/dist/vendor/otto-intel-mcp/adapter/sent-step.d.ts +15 -0
- package/dist/vendor/otto-intel-mcp/adapter/sent-step.js +6 -0
- package/dist/vendor/otto-intel-mcp/adapter/verify.d.ts +149 -0
- package/dist/vendor/otto-intel-mcp/adapter/verify.js +432 -0
- package/dist/vendor/otto-intel-mcp/adapter-cdp-index.d.ts +6 -0
- package/dist/vendor/otto-intel-mcp/adapter-cdp-index.js +7 -0
- package/dist/vendor/otto-intel-mcp/adapter-index.d.ts +15 -0
- package/dist/vendor/otto-intel-mcp/adapter-index.js +15 -0
- package/dist/vendor/otto-intel-mcp/artifact-id.d.ts +16 -0
- package/dist/vendor/otto-intel-mcp/artifact-id.js +60 -0
- package/dist/vendor/otto-intel-mcp/execution-config.d.ts +232 -0
- package/dist/vendor/otto-intel-mcp/execution-config.js +443 -0
- package/dist/vendor/otto-intel-mcp/execution-delegated-definition.d.ts +165 -0
- package/dist/vendor/otto-intel-mcp/execution-delegated-definition.js +116 -0
- package/dist/vendor/otto-intel-mcp/execution-delegation-admin-definition.d.ts +208 -0
- package/dist/vendor/otto-intel-mcp/execution-delegation-admin-definition.js +170 -0
- package/dist/vendor/otto-intel-mcp/execution-delegation-policy.d.ts +257 -0
- package/dist/vendor/otto-intel-mcp/execution-delegation-policy.js +279 -0
- package/dist/vendor/otto-intel-mcp/execution-errors.d.ts +9 -0
- package/dist/vendor/otto-intel-mcp/execution-errors.js +134 -0
- package/dist/vendor/otto-intel-mcp/execution-index.d.ts +17 -0
- package/dist/vendor/otto-intel-mcp/execution-index.js +16 -0
- package/dist/vendor/otto-intel-mcp/execution-intent.d.ts +14 -0
- package/dist/vendor/otto-intel-mcp/execution-intent.js +36 -0
- package/dist/vendor/otto-intel-mcp/execution-tool-definitions.d.ts +1103 -0
- package/dist/vendor/otto-intel-mcp/execution-tool-definitions.js +1051 -0
- package/dist/vendor/otto-intel-mcp/execution-types.d.ts +274 -0
- package/dist/vendor/otto-intel-mcp/execution-types.js +157 -0
- package/dist/verify.d.ts +2 -0
- package/dist/verify.js +3 -0
- package/dist/x402-table.d.ts +99 -0
- package/dist/x402-table.js +221 -0
- package/package.json +97 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* execution-delegation-policy.ts — the product's CDP project policy that fences the delegated leg
|
|
3
|
+
* (Model B, browser plane; seat ruling D9 = α+, 2026-08-29).
|
|
4
|
+
*
|
|
5
|
+
* WHAT THE ENGINE FENCES, AND WHAT IT CANNOT. Coinbase's policy engine offers END-USER accounts exactly
|
|
6
|
+
* one fence: the project-scope policy ("Only one project-level policy can exist at any time"; account-
|
|
7
|
+
* scoped policies attach only to developer-owned accounts, and no rule can refer to "the sender's own
|
|
8
|
+
* address"). So this policy is a SHAPE fence with constant caps, shared by every delegated user:
|
|
9
|
+
* - approve: only on an allow-listed input token, only to the reviewed LI.FI diamond, only up to the
|
|
10
|
+
* per-swap cap (5,000 USD, Founder word 2026-08-29) in that token's atomic units — a bound on ONE
|
|
11
|
+
* swap, not a daily or total limit (zero stays allowed for the allowance clear);
|
|
12
|
+
* - call: only the LI.FI diamond, zero native value, only the two GenericSwapV3 ERC20→ERC20 entry points.
|
|
13
|
+
* The approve cap is what bounds the swap's notional on-chain: the diamond can only pull what is
|
|
14
|
+
* approved. Everything else — that `_receiver` is the end user, that `_minAmount` is at least the
|
|
15
|
+
* caller's floor, that the fee leg is the ruled attribution — is Otto's SERVER-SIDE 13-check verifier on
|
|
16
|
+
* every call (`adapter/verify.ts`), run with the end user's address as the server reads it from CDP.
|
|
17
|
+
*
|
|
18
|
+
* THE RESIDUAL, STATED PLAINLY (also in the PR body and in the consent copy the user reads):
|
|
19
|
+
* Coinbase's engine limits Otto to swaps through LI.FI up to the per-swap cap; Otto's own server checks
|
|
20
|
+
* where the output goes; the user can revoke at any time. The engine does NOT bind the receiver — a
|
|
21
|
+
* compromised Otto server could re-point `_receiver` inside a diamond swap; that residual is bounded by
|
|
22
|
+
* the per-swap cap, the grant's expiry and the user's revoke, not by the engine.
|
|
23
|
+
*
|
|
24
|
+
* IDENTITY OF THE FENCE (seat ruling D6): the policy is matched by its versioned DESCRIPTION and checked
|
|
25
|
+
* by a digest over its canonical rules on every read-back — never by a policy id in env (a re-created
|
|
26
|
+
* policy silently changes id; a renamed one silently changes shape; the digest catches both). The
|
|
27
|
+
* canonical form lowercases hex strings and sorts object keys, nothing else; if the engine ever
|
|
28
|
+
* augments rules on read-back, `canonical()` is the one place to teach it.
|
|
29
|
+
*
|
|
30
|
+
* Description obeys the SDK's regex (`/^[A-Za-z0-9 ,.]{1,50}$/`): no hyphens, no parentheses.
|
|
31
|
+
*/
|
|
32
|
+
import { type Address } from 'viem';
|
|
33
|
+
/**
|
|
34
|
+
* The LINEAGE: a project policy is Otto's own only when its description matches the ANCHORED pattern
|
|
35
|
+
* `^OTTO model b swap fence v<N>$` (`modelBLineageVersion` returns N). The fence tool updates a lineage policy
|
|
36
|
+
* in place only when N ≤ this binary's version; a newer N is never downgraded; anything that does not match —
|
|
37
|
+
* the prefix in the middle, a suffix, no description — is foreign and is never touched.
|
|
38
|
+
*/
|
|
39
|
+
export declare const MODEL_B_POLICY_LINEAGE_PREFIX = "OTTO model b swap fence";
|
|
40
|
+
/** This binary's fence version. Bump it whenever the rules change shape or caps. */
|
|
41
|
+
export declare const MODEL_B_POLICY_VERSION: 1;
|
|
42
|
+
/** The versioned match key. */
|
|
43
|
+
export declare const MODEL_B_POLICY_NAME_V1: string;
|
|
44
|
+
/** The lineage version N of a policy description, or undefined when the description is not of our lineage. */
|
|
45
|
+
export declare function modelBLineageVersion(description: unknown): number | undefined;
|
|
46
|
+
export declare const MODEL_B_POLICY_NETWORK: "base";
|
|
47
|
+
export declare const MODEL_B_CHAIN_ID: 8453;
|
|
48
|
+
/**
|
|
49
|
+
* Delegation expiry bounds (Founder word `expiry 90/180`, 2026-08-29): the mint page defaults to 90 days
|
|
50
|
+
* and refuses anything past 180; the server routes enforce the same numbers. Coinbase documents no
|
|
51
|
+
* maximum of its own for `expiresAt` (SDK types + vendor doc read 2026-08-29); the first 180-day mint
|
|
52
|
+
* at the sitting is the live confirmation.
|
|
53
|
+
*/
|
|
54
|
+
export declare const MODEL_B_DELEGATION_EXPIRY_DEFAULT_DAYS: 90;
|
|
55
|
+
export declare const MODEL_B_DELEGATION_EXPIRY_MAX_DAYS: 180;
|
|
56
|
+
/** A requested expiry, bounded: a hostile, absent or over-long value never reaches the mint. */
|
|
57
|
+
export declare function boundedDelegationExpiry(requestedDays: unknown, nowMs: number): {
|
|
58
|
+
readonly days: number;
|
|
59
|
+
readonly expiresAt: string;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* The per-swap cap in USD — the Founder's number (word `fence cap 5000`, 2026-08-29). It bounds ONE swap:
|
|
63
|
+
* the engine carries no daily or total limit, and no copy may imply one. Changing it = a new fence version.
|
|
64
|
+
* Engine conditions are on raw calldata, so the cap is expressed per allowed input token: v1 allows USDC
|
|
65
|
+
* only, where the mapping is exact (5,000 USD = 5,000,000,000 atomic on the approve amount, which is what
|
|
66
|
+
* bounds the swap's `fromAmount` on-chain).
|
|
67
|
+
*/
|
|
68
|
+
export declare const MODEL_B_PER_SWAP_CAP_USD: 5000;
|
|
69
|
+
export interface ModelBInputToken {
|
|
70
|
+
readonly symbol: string;
|
|
71
|
+
readonly address: Address;
|
|
72
|
+
readonly decimals: number;
|
|
73
|
+
/** Atomic units per one USD — exact for a USD stablecoin; anything else needs a ruled reference price. */
|
|
74
|
+
readonly atomicPerUsd: bigint;
|
|
75
|
+
}
|
|
76
|
+
export declare const BASE_USDC: Address;
|
|
77
|
+
/**
|
|
78
|
+
* The input tokens a delegated swap may spend, v1: USDC only — the one token whose USD cap is exact.
|
|
79
|
+
* WETH (or any volatile input) needs a Founder-ruled reference price before it can carry a USD cap.
|
|
80
|
+
*/
|
|
81
|
+
export declare const MODEL_B_INPUT_TOKENS_V1: readonly ModelBInputToken[];
|
|
82
|
+
export declare function modelBInputToken(token: Address): ModelBInputToken | undefined;
|
|
83
|
+
/** The cap in the token's atomic units. */
|
|
84
|
+
export declare function modelBPerSwapCapAtomic(token: ModelBInputToken, capUsd?: number): bigint;
|
|
85
|
+
/** The two GenericSwapV3 ERC20→ERC20 entry points a same-chain swap plan may call. */
|
|
86
|
+
export declare const GENERIC_SWAP_V3_ABI: readonly [{
|
|
87
|
+
readonly type: "function";
|
|
88
|
+
readonly name: "swapTokensSingleV3ERC20ToERC20";
|
|
89
|
+
readonly stateMutability: "nonpayable";
|
|
90
|
+
readonly inputs: readonly [{
|
|
91
|
+
readonly name: "_transactionId";
|
|
92
|
+
readonly type: "bytes32";
|
|
93
|
+
}, {
|
|
94
|
+
readonly name: "_integrator";
|
|
95
|
+
readonly type: "string";
|
|
96
|
+
}, {
|
|
97
|
+
readonly name: "_referrer";
|
|
98
|
+
readonly type: "string";
|
|
99
|
+
}, {
|
|
100
|
+
readonly name: "_receiver";
|
|
101
|
+
readonly type: "address";
|
|
102
|
+
}, {
|
|
103
|
+
readonly name: "_minAmount";
|
|
104
|
+
readonly type: "uint256";
|
|
105
|
+
}, {
|
|
106
|
+
readonly name: "_swapData";
|
|
107
|
+
readonly type: "tuple";
|
|
108
|
+
readonly components: readonly [{
|
|
109
|
+
readonly name: "callTo";
|
|
110
|
+
readonly type: "address";
|
|
111
|
+
}, {
|
|
112
|
+
readonly name: "approveTo";
|
|
113
|
+
readonly type: "address";
|
|
114
|
+
}, {
|
|
115
|
+
readonly name: "sendingAssetId";
|
|
116
|
+
readonly type: "address";
|
|
117
|
+
}, {
|
|
118
|
+
readonly name: "receivingAssetId";
|
|
119
|
+
readonly type: "address";
|
|
120
|
+
}, {
|
|
121
|
+
readonly name: "fromAmount";
|
|
122
|
+
readonly type: "uint256";
|
|
123
|
+
}, {
|
|
124
|
+
readonly name: "callData";
|
|
125
|
+
readonly type: "bytes";
|
|
126
|
+
}, {
|
|
127
|
+
readonly name: "requiresDeposit";
|
|
128
|
+
readonly type: "bool";
|
|
129
|
+
}];
|
|
130
|
+
}];
|
|
131
|
+
readonly outputs: readonly [];
|
|
132
|
+
}, {
|
|
133
|
+
readonly type: "function";
|
|
134
|
+
readonly name: "swapTokensMultipleV3ERC20ToERC20";
|
|
135
|
+
readonly stateMutability: "nonpayable";
|
|
136
|
+
readonly inputs: readonly [{
|
|
137
|
+
readonly name: "_transactionId";
|
|
138
|
+
readonly type: "bytes32";
|
|
139
|
+
}, {
|
|
140
|
+
readonly name: "_integrator";
|
|
141
|
+
readonly type: "string";
|
|
142
|
+
}, {
|
|
143
|
+
readonly name: "_referrer";
|
|
144
|
+
readonly type: "string";
|
|
145
|
+
}, {
|
|
146
|
+
readonly name: "_receiver";
|
|
147
|
+
readonly type: "address";
|
|
148
|
+
}, {
|
|
149
|
+
readonly name: "_minAmount";
|
|
150
|
+
readonly type: "uint256";
|
|
151
|
+
}, {
|
|
152
|
+
readonly name: "_swapData";
|
|
153
|
+
readonly type: "tuple[]";
|
|
154
|
+
readonly components: readonly [{
|
|
155
|
+
readonly name: "callTo";
|
|
156
|
+
readonly type: "address";
|
|
157
|
+
}, {
|
|
158
|
+
readonly name: "approveTo";
|
|
159
|
+
readonly type: "address";
|
|
160
|
+
}, {
|
|
161
|
+
readonly name: "sendingAssetId";
|
|
162
|
+
readonly type: "address";
|
|
163
|
+
}, {
|
|
164
|
+
readonly name: "receivingAssetId";
|
|
165
|
+
readonly type: "address";
|
|
166
|
+
}, {
|
|
167
|
+
readonly name: "fromAmount";
|
|
168
|
+
readonly type: "uint256";
|
|
169
|
+
}, {
|
|
170
|
+
readonly name: "callData";
|
|
171
|
+
readonly type: "bytes";
|
|
172
|
+
}, {
|
|
173
|
+
readonly name: "requiresDeposit";
|
|
174
|
+
readonly type: "bool";
|
|
175
|
+
}];
|
|
176
|
+
}];
|
|
177
|
+
readonly outputs: readonly [];
|
|
178
|
+
}];
|
|
179
|
+
/** The rule dialect this fence emits — structurally the SDK's `sendEndUserEvmTransaction` rule. */
|
|
180
|
+
export interface DelegationPolicyParamCondition {
|
|
181
|
+
readonly name: string;
|
|
182
|
+
readonly operator: 'in' | '<=' | '>=';
|
|
183
|
+
readonly value?: string;
|
|
184
|
+
readonly values?: readonly string[];
|
|
185
|
+
}
|
|
186
|
+
export interface DelegationPolicyDataCondition {
|
|
187
|
+
readonly function: string;
|
|
188
|
+
readonly params?: readonly DelegationPolicyParamCondition[];
|
|
189
|
+
}
|
|
190
|
+
export type DelegationPolicyCriterion = {
|
|
191
|
+
readonly type: 'evmNetwork';
|
|
192
|
+
readonly networks: readonly string[];
|
|
193
|
+
readonly operator: 'in';
|
|
194
|
+
} | {
|
|
195
|
+
readonly type: 'evmAddress';
|
|
196
|
+
readonly addresses: readonly string[];
|
|
197
|
+
readonly operator: 'in';
|
|
198
|
+
} | {
|
|
199
|
+
readonly type: 'ethValue';
|
|
200
|
+
readonly ethValue: string;
|
|
201
|
+
readonly operator: '<=';
|
|
202
|
+
} | {
|
|
203
|
+
readonly type: 'evmData';
|
|
204
|
+
readonly abi: 'erc20' | typeof GENERIC_SWAP_V3_ABI;
|
|
205
|
+
readonly conditions: readonly DelegationPolicyDataCondition[];
|
|
206
|
+
};
|
|
207
|
+
export interface DelegationPolicyRule {
|
|
208
|
+
readonly action: 'accept';
|
|
209
|
+
readonly operation: 'sendEndUserEvmTransaction';
|
|
210
|
+
readonly criteria: readonly DelegationPolicyCriterion[];
|
|
211
|
+
}
|
|
212
|
+
/** The static v1 rules: one approve rule per allow-listed input token, one router rule. */
|
|
213
|
+
export declare function modelBProjectPolicyRules(capUsd?: number): readonly DelegationPolicyRule[];
|
|
214
|
+
/** The body PR-2's installer creates or updates: scope, versioned description, the v1 rules. */
|
|
215
|
+
export declare function modelBProjectPolicyBody(): {
|
|
216
|
+
readonly scope: 'project';
|
|
217
|
+
readonly description: string;
|
|
218
|
+
readonly rules: readonly DelegationPolicyRule[];
|
|
219
|
+
};
|
|
220
|
+
/** SHA-256 over the canonical JSON of a rule list — the fence's integrity check at read-back. */
|
|
221
|
+
export declare function policyRulesDigest(rules: unknown): string;
|
|
222
|
+
export declare const MODEL_B_POLICY_DIGEST_V1: string;
|
|
223
|
+
/** What the server reads back from `cdp.policies.listPolicies({ scope: 'project' })`, structurally. */
|
|
224
|
+
export interface DelegationPolicyRecord {
|
|
225
|
+
readonly id: string;
|
|
226
|
+
readonly description?: string;
|
|
227
|
+
readonly scope: string;
|
|
228
|
+
readonly rules: unknown;
|
|
229
|
+
}
|
|
230
|
+
export type ModelBFenceReadBack = {
|
|
231
|
+
readonly present: true;
|
|
232
|
+
readonly id: string;
|
|
233
|
+
readonly rulesDigest: string;
|
|
234
|
+
} | {
|
|
235
|
+
readonly present: false;
|
|
236
|
+
readonly reason: 'absent' | 'ambiguous' | 'name_mismatch' | 'digest_mismatch';
|
|
237
|
+
readonly detail: string;
|
|
238
|
+
};
|
|
239
|
+
/**
|
|
240
|
+
* The fence check the delegated-send path runs BEFORE any send: EXACTLY ONE project-scope policy reads back
|
|
241
|
+
* (Coinbase allows one; more is an anomalous snapshot and never blessed on the strength of "one of them
|
|
242
|
+
* matches"), it carries the versioned name, and its rules digest to exactly the v1 rules. Anything else is
|
|
243
|
+
* "absent" — including a policy of the right name whose rules drifted.
|
|
244
|
+
*/
|
|
245
|
+
export declare function readBackModelBFence(policies: readonly DelegationPolicyRecord[]): ModelBFenceReadBack;
|
|
246
|
+
/**
|
|
247
|
+
* The same bound the engine enforces, checked server-side BEFORE the first step is sent: a plan the
|
|
248
|
+
* engine would refuse is refused here without spending an allowance reset on it.
|
|
249
|
+
*/
|
|
250
|
+
export declare function assertWithinModelBFence(plan: {
|
|
251
|
+
readonly token: Address;
|
|
252
|
+
readonly spender: Address;
|
|
253
|
+
readonly gross: bigint;
|
|
254
|
+
}, capUsd?: number, inputTokens?: readonly ModelBInputToken[]): {
|
|
255
|
+
readonly token: ModelBInputToken;
|
|
256
|
+
readonly capAtomic: bigint;
|
|
257
|
+
};
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* execution-delegation-policy.ts — the product's CDP project policy that fences the delegated leg
|
|
3
|
+
* (Model B, browser plane; seat ruling D9 = α+, 2026-08-29).
|
|
4
|
+
*
|
|
5
|
+
* WHAT THE ENGINE FENCES, AND WHAT IT CANNOT. Coinbase's policy engine offers END-USER accounts exactly
|
|
6
|
+
* one fence: the project-scope policy ("Only one project-level policy can exist at any time"; account-
|
|
7
|
+
* scoped policies attach only to developer-owned accounts, and no rule can refer to "the sender's own
|
|
8
|
+
* address"). So this policy is a SHAPE fence with constant caps, shared by every delegated user:
|
|
9
|
+
* - approve: only on an allow-listed input token, only to the reviewed LI.FI diamond, only up to the
|
|
10
|
+
* per-swap cap (5,000 USD, Founder word 2026-08-29) in that token's atomic units — a bound on ONE
|
|
11
|
+
* swap, not a daily or total limit (zero stays allowed for the allowance clear);
|
|
12
|
+
* - call: only the LI.FI diamond, zero native value, only the two GenericSwapV3 ERC20→ERC20 entry points.
|
|
13
|
+
* The approve cap is what bounds the swap's notional on-chain: the diamond can only pull what is
|
|
14
|
+
* approved. Everything else — that `_receiver` is the end user, that `_minAmount` is at least the
|
|
15
|
+
* caller's floor, that the fee leg is the ruled attribution — is Otto's SERVER-SIDE 13-check verifier on
|
|
16
|
+
* every call (`adapter/verify.ts`), run with the end user's address as the server reads it from CDP.
|
|
17
|
+
*
|
|
18
|
+
* THE RESIDUAL, STATED PLAINLY (also in the PR body and in the consent copy the user reads):
|
|
19
|
+
* Coinbase's engine limits Otto to swaps through LI.FI up to the per-swap cap; Otto's own server checks
|
|
20
|
+
* where the output goes; the user can revoke at any time. The engine does NOT bind the receiver — a
|
|
21
|
+
* compromised Otto server could re-point `_receiver` inside a diamond swap; that residual is bounded by
|
|
22
|
+
* the per-swap cap, the grant's expiry and the user's revoke, not by the engine.
|
|
23
|
+
*
|
|
24
|
+
* IDENTITY OF THE FENCE (seat ruling D6): the policy is matched by its versioned DESCRIPTION and checked
|
|
25
|
+
* by a digest over its canonical rules on every read-back — never by a policy id in env (a re-created
|
|
26
|
+
* policy silently changes id; a renamed one silently changes shape; the digest catches both). The
|
|
27
|
+
* canonical form lowercases hex strings and sorts object keys, nothing else; if the engine ever
|
|
28
|
+
* augments rules on read-back, `canonical()` is the one place to teach it.
|
|
29
|
+
*
|
|
30
|
+
* Description obeys the SDK's regex (`/^[A-Za-z0-9 ,.]{1,50}$/`): no hyphens, no parentheses.
|
|
31
|
+
*/
|
|
32
|
+
import { createHash } from 'node:crypto';
|
|
33
|
+
import { getAddress } from 'viem';
|
|
34
|
+
import { refuse } from './execution-errors.js';
|
|
35
|
+
import { LIFI_DIAMOND_ADDRESS } from './execution-config.js';
|
|
36
|
+
/**
|
|
37
|
+
* The LINEAGE: a project policy is Otto's own only when its description matches the ANCHORED pattern
|
|
38
|
+
* `^OTTO model b swap fence v<N>$` (`modelBLineageVersion` returns N). The fence tool updates a lineage policy
|
|
39
|
+
* in place only when N ≤ this binary's version; a newer N is never downgraded; anything that does not match —
|
|
40
|
+
* the prefix in the middle, a suffix, no description — is foreign and is never touched.
|
|
41
|
+
*/
|
|
42
|
+
export const MODEL_B_POLICY_LINEAGE_PREFIX = 'OTTO model b swap fence';
|
|
43
|
+
/** This binary's fence version. Bump it whenever the rules change shape or caps. */
|
|
44
|
+
export const MODEL_B_POLICY_VERSION = 1;
|
|
45
|
+
/** The versioned match key. */
|
|
46
|
+
export const MODEL_B_POLICY_NAME_V1 = `${MODEL_B_POLICY_LINEAGE_PREFIX} v${MODEL_B_POLICY_VERSION}`;
|
|
47
|
+
const MODEL_B_LINEAGE_PATTERN = new RegExp(`^${MODEL_B_POLICY_LINEAGE_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')} v(\\d+)$`);
|
|
48
|
+
/** The lineage version N of a policy description, or undefined when the description is not of our lineage. */
|
|
49
|
+
export function modelBLineageVersion(description) {
|
|
50
|
+
if (typeof description !== 'string')
|
|
51
|
+
return undefined;
|
|
52
|
+
const match = MODEL_B_LINEAGE_PATTERN.exec(description);
|
|
53
|
+
return match ? Number(match[1]) : undefined;
|
|
54
|
+
}
|
|
55
|
+
export const MODEL_B_POLICY_NETWORK = 'base';
|
|
56
|
+
export const MODEL_B_CHAIN_ID = 8453;
|
|
57
|
+
/**
|
|
58
|
+
* Delegation expiry bounds (Founder word `expiry 90/180`, 2026-08-29): the mint page defaults to 90 days
|
|
59
|
+
* and refuses anything past 180; the server routes enforce the same numbers. Coinbase documents no
|
|
60
|
+
* maximum of its own for `expiresAt` (SDK types + vendor doc read 2026-08-29); the first 180-day mint
|
|
61
|
+
* at the sitting is the live confirmation.
|
|
62
|
+
*/
|
|
63
|
+
export const MODEL_B_DELEGATION_EXPIRY_DEFAULT_DAYS = 90;
|
|
64
|
+
export const MODEL_B_DELEGATION_EXPIRY_MAX_DAYS = 180;
|
|
65
|
+
/** A requested expiry, bounded: a hostile, absent or over-long value never reaches the mint. */
|
|
66
|
+
export function boundedDelegationExpiry(requestedDays, nowMs) {
|
|
67
|
+
const days = requestedDays === undefined || requestedDays === null
|
|
68
|
+
? MODEL_B_DELEGATION_EXPIRY_DEFAULT_DAYS
|
|
69
|
+
: Number(requestedDays);
|
|
70
|
+
if (!Number.isInteger(days) || days < 1 || days > MODEL_B_DELEGATION_EXPIRY_MAX_DAYS) {
|
|
71
|
+
throw new Error(`delegation expiry must be a whole number of days between 1 and ${MODEL_B_DELEGATION_EXPIRY_MAX_DAYS}`);
|
|
72
|
+
}
|
|
73
|
+
return Object.freeze({ days, expiresAt: new Date(nowMs + days * 86_400_000).toISOString() });
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The per-swap cap in USD — the Founder's number (word `fence cap 5000`, 2026-08-29). It bounds ONE swap:
|
|
77
|
+
* the engine carries no daily or total limit, and no copy may imply one. Changing it = a new fence version.
|
|
78
|
+
* Engine conditions are on raw calldata, so the cap is expressed per allowed input token: v1 allows USDC
|
|
79
|
+
* only, where the mapping is exact (5,000 USD = 5,000,000,000 atomic on the approve amount, which is what
|
|
80
|
+
* bounds the swap's `fromAmount` on-chain).
|
|
81
|
+
*/
|
|
82
|
+
export const MODEL_B_PER_SWAP_CAP_USD = 5000;
|
|
83
|
+
export const BASE_USDC = getAddress('0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913');
|
|
84
|
+
/**
|
|
85
|
+
* The input tokens a delegated swap may spend, v1: USDC only — the one token whose USD cap is exact.
|
|
86
|
+
* WETH (or any volatile input) needs a Founder-ruled reference price before it can carry a USD cap.
|
|
87
|
+
*/
|
|
88
|
+
export const MODEL_B_INPUT_TOKENS_V1 = Object.freeze([
|
|
89
|
+
Object.freeze({ symbol: 'USDC', address: BASE_USDC, decimals: 6, atomicPerUsd: 1000000n }),
|
|
90
|
+
]);
|
|
91
|
+
export function modelBInputToken(token) {
|
|
92
|
+
const target = getAddress(token);
|
|
93
|
+
return MODEL_B_INPUT_TOKENS_V1.find((entry) => entry.address === target);
|
|
94
|
+
}
|
|
95
|
+
/** The cap in the token's atomic units. */
|
|
96
|
+
export function modelBPerSwapCapAtomic(token, capUsd = MODEL_B_PER_SWAP_CAP_USD) {
|
|
97
|
+
if (!Number.isSafeInteger(capUsd) || capUsd <= 0)
|
|
98
|
+
throw new Error('per-swap cap must be a positive integer USD amount');
|
|
99
|
+
return BigInt(capUsd) * token.atomicPerUsd;
|
|
100
|
+
}
|
|
101
|
+
/** LibSwap.SwapData tuple, as the GenericSwapV3 facet declares it. */
|
|
102
|
+
const SWAP_DATA_COMPONENTS = [
|
|
103
|
+
{ name: 'callTo', type: 'address' },
|
|
104
|
+
{ name: 'approveTo', type: 'address' },
|
|
105
|
+
{ name: 'sendingAssetId', type: 'address' },
|
|
106
|
+
{ name: 'receivingAssetId', type: 'address' },
|
|
107
|
+
{ name: 'fromAmount', type: 'uint256' },
|
|
108
|
+
{ name: 'callData', type: 'bytes' },
|
|
109
|
+
{ name: 'requiresDeposit', type: 'bool' },
|
|
110
|
+
];
|
|
111
|
+
/** The two GenericSwapV3 ERC20→ERC20 entry points a same-chain swap plan may call. */
|
|
112
|
+
export const GENERIC_SWAP_V3_ABI = [
|
|
113
|
+
{
|
|
114
|
+
type: 'function',
|
|
115
|
+
name: 'swapTokensSingleV3ERC20ToERC20',
|
|
116
|
+
stateMutability: 'nonpayable',
|
|
117
|
+
inputs: [
|
|
118
|
+
{ name: '_transactionId', type: 'bytes32' },
|
|
119
|
+
{ name: '_integrator', type: 'string' },
|
|
120
|
+
{ name: '_referrer', type: 'string' },
|
|
121
|
+
{ name: '_receiver', type: 'address' },
|
|
122
|
+
{ name: '_minAmount', type: 'uint256' },
|
|
123
|
+
{ name: '_swapData', type: 'tuple', components: SWAP_DATA_COMPONENTS },
|
|
124
|
+
],
|
|
125
|
+
outputs: [],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
type: 'function',
|
|
129
|
+
name: 'swapTokensMultipleV3ERC20ToERC20',
|
|
130
|
+
stateMutability: 'nonpayable',
|
|
131
|
+
inputs: [
|
|
132
|
+
{ name: '_transactionId', type: 'bytes32' },
|
|
133
|
+
{ name: '_integrator', type: 'string' },
|
|
134
|
+
{ name: '_referrer', type: 'string' },
|
|
135
|
+
{ name: '_receiver', type: 'address' },
|
|
136
|
+
{ name: '_minAmount', type: 'uint256' },
|
|
137
|
+
{ name: '_swapData', type: 'tuple[]', components: SWAP_DATA_COMPONENTS },
|
|
138
|
+
],
|
|
139
|
+
outputs: [],
|
|
140
|
+
},
|
|
141
|
+
];
|
|
142
|
+
/** The static v1 rules: one approve rule per allow-listed input token, one router rule. */
|
|
143
|
+
export function modelBProjectPolicyRules(capUsd = MODEL_B_PER_SWAP_CAP_USD) {
|
|
144
|
+
const approveRules = MODEL_B_INPUT_TOKENS_V1.map((token) => ({
|
|
145
|
+
action: 'accept',
|
|
146
|
+
operation: 'sendEndUserEvmTransaction',
|
|
147
|
+
criteria: [
|
|
148
|
+
{ type: 'evmNetwork', networks: [MODEL_B_POLICY_NETWORK], operator: 'in' },
|
|
149
|
+
{ type: 'evmAddress', addresses: [token.address], operator: 'in' },
|
|
150
|
+
{ type: 'ethValue', ethValue: '0', operator: '<=' },
|
|
151
|
+
{
|
|
152
|
+
type: 'evmData',
|
|
153
|
+
abi: 'erc20',
|
|
154
|
+
conditions: [
|
|
155
|
+
{
|
|
156
|
+
function: 'approve',
|
|
157
|
+
params: [
|
|
158
|
+
{ name: 'spender', operator: 'in', values: [LIFI_DIAMOND_ADDRESS] },
|
|
159
|
+
{
|
|
160
|
+
name: 'value',
|
|
161
|
+
operator: '<=',
|
|
162
|
+
value: modelBPerSwapCapAtomic(token, capUsd).toString(),
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
}));
|
|
170
|
+
const routerRule = {
|
|
171
|
+
action: 'accept',
|
|
172
|
+
operation: 'sendEndUserEvmTransaction',
|
|
173
|
+
criteria: [
|
|
174
|
+
{ type: 'evmNetwork', networks: [MODEL_B_POLICY_NETWORK], operator: 'in' },
|
|
175
|
+
{ type: 'evmAddress', addresses: [LIFI_DIAMOND_ADDRESS], operator: 'in' },
|
|
176
|
+
{ type: 'ethValue', ethValue: '0', operator: '<=' },
|
|
177
|
+
{
|
|
178
|
+
type: 'evmData',
|
|
179
|
+
abi: GENERIC_SWAP_V3_ABI,
|
|
180
|
+
conditions: [
|
|
181
|
+
{ function: 'swapTokensSingleV3ERC20ToERC20' },
|
|
182
|
+
{ function: 'swapTokensMultipleV3ERC20ToERC20' },
|
|
183
|
+
],
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
};
|
|
187
|
+
return Object.freeze([...approveRules, routerRule]);
|
|
188
|
+
}
|
|
189
|
+
/** The body PR-2's installer creates or updates: scope, versioned description, the v1 rules. */
|
|
190
|
+
export function modelBProjectPolicyBody() {
|
|
191
|
+
return Object.freeze({
|
|
192
|
+
scope: 'project',
|
|
193
|
+
description: MODEL_B_POLICY_NAME_V1,
|
|
194
|
+
rules: modelBProjectPolicyRules(),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
/** Sorted keys, undefined dropped, hex strings lowercased; arrays keep their order. Nothing else. */
|
|
198
|
+
function canonical(value) {
|
|
199
|
+
if (Array.isArray(value))
|
|
200
|
+
return value.map(canonical);
|
|
201
|
+
if (value !== null && typeof value === 'object') {
|
|
202
|
+
const out = {};
|
|
203
|
+
for (const key of Object.keys(value).sort()) {
|
|
204
|
+
const item = value[key];
|
|
205
|
+
if (item !== undefined)
|
|
206
|
+
out[key] = canonical(item);
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
if (typeof value === 'string' && /^0x[0-9a-fA-F]+$/.test(value))
|
|
211
|
+
return value.toLowerCase();
|
|
212
|
+
return value;
|
|
213
|
+
}
|
|
214
|
+
/** SHA-256 over the canonical JSON of a rule list — the fence's integrity check at read-back. */
|
|
215
|
+
export function policyRulesDigest(rules) {
|
|
216
|
+
return createHash('sha256')
|
|
217
|
+
.update(JSON.stringify(canonical(rules)))
|
|
218
|
+
.digest('hex');
|
|
219
|
+
}
|
|
220
|
+
export const MODEL_B_POLICY_DIGEST_V1 = policyRulesDigest(modelBProjectPolicyRules());
|
|
221
|
+
/**
|
|
222
|
+
* The fence check the delegated-send path runs BEFORE any send: EXACTLY ONE project-scope policy reads back
|
|
223
|
+
* (Coinbase allows one; more is an anomalous snapshot and never blessed on the strength of "one of them
|
|
224
|
+
* matches"), it carries the versioned name, and its rules digest to exactly the v1 rules. Anything else is
|
|
225
|
+
* "absent" — including a policy of the right name whose rules drifted.
|
|
226
|
+
*/
|
|
227
|
+
export function readBackModelBFence(policies) {
|
|
228
|
+
const project = policies.filter((policy) => policy.scope === 'project');
|
|
229
|
+
if (project.length === 0)
|
|
230
|
+
return {
|
|
231
|
+
present: false,
|
|
232
|
+
reason: 'absent',
|
|
233
|
+
detail: 'the project holds no project-scope policy',
|
|
234
|
+
};
|
|
235
|
+
if (project.length > 1) {
|
|
236
|
+
return {
|
|
237
|
+
present: false,
|
|
238
|
+
reason: 'ambiguous',
|
|
239
|
+
detail: `${project.length} project-scope policies read back where Coinbase allows one`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
const named = project[0];
|
|
243
|
+
if (named.description !== MODEL_B_POLICY_NAME_V1) {
|
|
244
|
+
return {
|
|
245
|
+
present: false,
|
|
246
|
+
reason: 'name_mismatch',
|
|
247
|
+
detail: `the project policy is not "${MODEL_B_POLICY_NAME_V1}"`,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
const rulesDigest = policyRulesDigest(named.rules);
|
|
251
|
+
if (rulesDigest !== MODEL_B_POLICY_DIGEST_V1) {
|
|
252
|
+
return {
|
|
253
|
+
present: false,
|
|
254
|
+
reason: 'digest_mismatch',
|
|
255
|
+
detail: `the project policy "${MODEL_B_POLICY_NAME_V1}" carries rules that do not digest to v1 (${rulesDigest.slice(0, 16)}… ≠ ${MODEL_B_POLICY_DIGEST_V1.slice(0, 16)}…)`,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
return { present: true, id: named.id, rulesDigest };
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* The same bound the engine enforces, checked server-side BEFORE the first step is sent: a plan the
|
|
262
|
+
* engine would refuse is refused here without spending an allowance reset on it.
|
|
263
|
+
*/
|
|
264
|
+
export function assertWithinModelBFence(plan, capUsd = MODEL_B_PER_SWAP_CAP_USD, inputTokens = MODEL_B_INPUT_TOKENS_V1) {
|
|
265
|
+
const target = getAddress(plan.token);
|
|
266
|
+
const token = inputTokens.find((entry) => entry.address === target);
|
|
267
|
+
if (!token) {
|
|
268
|
+
return refuse('DELEGATION_OUTSIDE_FENCE', `input token ${plan.token} is not an allow-listed delegated input (${inputTokens.map((t) => t.symbol).join(', ') || 'none'})`);
|
|
269
|
+
}
|
|
270
|
+
if (getAddress(plan.spender) !== LIFI_DIAMOND_ADDRESS) {
|
|
271
|
+
return refuse('DELEGATION_OUTSIDE_FENCE', `approval spender ${plan.spender} is not the reviewed LI.FI diamond`);
|
|
272
|
+
}
|
|
273
|
+
const capAtomic = modelBPerSwapCapAtomic(token, capUsd);
|
|
274
|
+
if (plan.gross > capAtomic) {
|
|
275
|
+
return refuse('DELEGATION_OUTSIDE_FENCE', `gross ${plan.gross} ${token.symbol} atomic exceeds the per-swap cap ${capAtomic} (${capUsd} USD)`);
|
|
276
|
+
}
|
|
277
|
+
return { token, capAtomic };
|
|
278
|
+
}
|
|
279
|
+
//# sourceMappingURL=execution-delegation-policy.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const EXECUTION_REFUSAL_CODES: readonly ["ATTRIBUTION_MISSING", "LIFI_FEE_CAP_EXCEEDED", "ACCOUNT_BINDING_MISMATCH", "DIGEST_MISMATCH", "ARTIFACT_INVALID", "STALE_ARTIFACT", "EXECUTION_RATE_LIMITED", "EXECUTION_CONCURRENCY_FULL", "UNSUPPORTED_ACCOUNT_PROFILE", "LIFI_QUOTE_FAILED", "LIFI_QUOTE_INVALID", "UNSUPPORTED_LIFI_ROUTE", "POLYMARKET_METADATA_FAILED", "POLYMARKET_METADATA_INVALID", "UNSUPPORTED_POLYMARKET_MARKET", "POLYMARKET_ORDER_INVALID", "POLYMARKET_BUILDER_FEE_UNRULED", "HYPERLIQUID_META_FAILED", "HYPERLIQUID_META_INVALID", "UNSUPPORTED_HYPERLIQUID_MARKET", "HYPERLIQUID_ORDER_INVALID", "DELEGATION_UNAVAILABLE", "DELEGATION_UNAUTHORIZED", "DELEGATION_INPUT_INVALID", "DELEGATION_BUSY", "DELEGATION_ARTIFACT_IN_FLIGHT", "ARTIFACT_NOT_PREPARED_HERE", "PREPARED_ARTIFACT_CAPACITY", "ARTIFACT_ID_COLLISION", "DELEGATION_REPLAY_MISMATCH", "DELEGATION_FENCE_ABSENT", "DELEGATION_FENCE_FOREIGN", "DELEGATION_FENCE_AMBIGUOUS", "DELEGATION_FENCE_NEWER", "DELEGATION_TOKEN_INVALID", "DELEGATION_REVOKE_NOT_CONFIRMED", "DELEGATION_NOT_ACTIVE", "DELEGATION_EXPIRY_EXCEEDS_BOUND", "DELEGATION_USER_UNKNOWN", "DELEGATION_OUTSIDE_FENCE", "DELEGATED_PLAN_REFUSED", "DELEGATED_PLAN_HALTED", "DELEGATION_UPSTREAM_FAILED"];
|
|
2
|
+
export type ExecutionRefusalCode = (typeof EXECUTION_REFUSAL_CODES)[number];
|
|
3
|
+
/** A fail-closed, machine-readable refusal. No upstream response bodies or secrets are retained. */
|
|
4
|
+
export declare class ExecutionRefusal extends Error {
|
|
5
|
+
readonly code: ExecutionRefusalCode;
|
|
6
|
+
constructor(code: ExecutionRefusalCode, message: string);
|
|
7
|
+
}
|
|
8
|
+
export declare function refuse(code: ExecutionRefusalCode, message: string): never;
|
|
9
|
+
export declare function isExecutionRefusal(error: unknown): error is ExecutionRefusal;
|