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,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* eoa-signer.ts — the bring-your-own-EOA capability provider (spec §2 SigningAdapter, §9 handshakes).
|
|
3
|
+
*
|
|
4
|
+
* Provides `send_evm_transaction` (one signed tx per plan step, in order; nonce/gas/serialization are
|
|
5
|
+
* adapter-owned per spec §6) and `sign_x402_payment` (challenge → signed `x_payment`, exactly the
|
|
6
|
+
* hosted MCP's existing retry protocol). The key never leaves this process; it is read from the
|
|
7
|
+
* environment once (env.ts registers it with the output guard) or generated in-process for a
|
|
8
|
+
* dry run and discarded. On this path the USER's key both signs and submits.
|
|
9
|
+
*
|
|
10
|
+
* Dry run = sign, never broadcast: every step is signed with placeholder nonce/gas, parsed back, and
|
|
11
|
+
* the parsed {to, value, data} must equal the verified plan step byte-for-byte, and the signature
|
|
12
|
+
* must recover to this account. That proves "what the signer signs is what the adapter verified"
|
|
13
|
+
* without touching a chain.
|
|
14
|
+
*/
|
|
15
|
+
import { createPublicClient, encodeFunctionData, getAddress, http, parseTransaction, recoverTransactionAddress, } from 'viem';
|
|
16
|
+
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
|
|
17
|
+
import { base } from 'viem/chains';
|
|
18
|
+
import { waitForSealedReceipt } from './chain.js';
|
|
19
|
+
import { ERC20_APPROVE_ABI, readAllowance } from './erc20.js';
|
|
20
|
+
import { x402Client } from '@x402/core/client';
|
|
21
|
+
import { encodePaymentSignatureHeader } from '@x402/core/http';
|
|
22
|
+
import { ExactEvmScheme } from '@x402/evm/exact/client';
|
|
23
|
+
import { toClientEvmSigner } from '@x402/evm';
|
|
24
|
+
import { requirementMatchesRow, validateX402Challenge, X402_TABLE, X402Refusal, } from './x402-table.js';
|
|
25
|
+
import { assertPlanSignableBy } from './verify.js';
|
|
26
|
+
export const EOA_CAPABILITIES = Object.freeze([
|
|
27
|
+
'send_evm_transaction',
|
|
28
|
+
'sign_x402_payment',
|
|
29
|
+
]);
|
|
30
|
+
export const CHAIN_ID = 8453;
|
|
31
|
+
/** Clock tolerance when bounding an EIP-3009 `validBefore` against the row's window. */
|
|
32
|
+
const VALID_BEFORE_SKEW_SECONDS = 60;
|
|
33
|
+
/**
|
|
34
|
+
* The INDEPENDENT per-payment ceiling applied to every x402 signature, stated here rather than
|
|
35
|
+
* inherited from `@x402/core`'s default (which is the same `$1`, but is the SDK's number to change,
|
|
36
|
+
* not ours). Every live table row is $0.001–$0.002, so this has ~500x of headroom over real traffic;
|
|
37
|
+
* it exists to bound a table that is wrong, not to bound normal use. Raising it is a deliberate edit.
|
|
38
|
+
*/
|
|
39
|
+
export const PER_PAYMENT_SPEND_CAP = '$1';
|
|
40
|
+
/**
|
|
41
|
+
* True only for `@x402/core`'s spend-control rejection. Matched narrowly on the SDK's own message so a
|
|
42
|
+
* different failure is never mistaken for a budget refusal; if the SDK ever rewords this, the
|
|
43
|
+
* mutation-proved test below fails loudly rather than the translation silently going quiet.
|
|
44
|
+
*/
|
|
45
|
+
function isSpendControlRejection(error) {
|
|
46
|
+
return error instanceof Error && /rejected by spendControls/.test(error.message);
|
|
47
|
+
}
|
|
48
|
+
/** A plan step failed after an approval was live; the adapter tried to clear the allowance. */
|
|
49
|
+
export class PlanHaltedError extends Error {
|
|
50
|
+
failedStep;
|
|
51
|
+
allowanceCleared;
|
|
52
|
+
clearTransactionHash;
|
|
53
|
+
constructor(message, failedStep, allowanceCleared, clearTransactionHash) {
|
|
54
|
+
super(message);
|
|
55
|
+
this.failedStep = failedStep;
|
|
56
|
+
this.allowanceCleared = allowanceCleared;
|
|
57
|
+
this.clearTransactionHash = clearTransactionHash;
|
|
58
|
+
this.name = 'PlanHaltedError';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export class EoaSigner {
|
|
62
|
+
account;
|
|
63
|
+
rpcUrl;
|
|
64
|
+
address;
|
|
65
|
+
capabilities = EOA_CAPABILITIES;
|
|
66
|
+
chainId = CHAIN_ID;
|
|
67
|
+
constructor(account, rpcUrl) {
|
|
68
|
+
this.account = account;
|
|
69
|
+
this.rpcUrl = rpcUrl;
|
|
70
|
+
this.address = getAddress(account.address);
|
|
71
|
+
}
|
|
72
|
+
static fromPrivateKey(privateKey, rpcUrl) {
|
|
73
|
+
return new EoaSigner(privateKeyToAccount(privateKey), rpcUrl);
|
|
74
|
+
}
|
|
75
|
+
/** A throwaway in-process key for dry runs. Never printed, never persisted, gone with the process. */
|
|
76
|
+
static ephemeral() {
|
|
77
|
+
return new EoaSigner(privateKeyToAccount(generatePrivateKey()), undefined);
|
|
78
|
+
}
|
|
79
|
+
publicClient() {
|
|
80
|
+
return createPublicClient({ chain: base, transport: http(this.rpcUrl) });
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* `send_evm_transaction`, dry: sign every step, decode back, recover — broadcast nothing. `clock` is
|
|
84
|
+
* either a fixed instant (deterministic tests) or a clock FUNCTION read afresh before every signature
|
|
85
|
+
* (the default, `Date.now`), so an artifact that expires between two signatures is caught.
|
|
86
|
+
*/
|
|
87
|
+
async signPlanDryRun(plan, clock = Date.now) {
|
|
88
|
+
const now = typeof clock === 'number' ? () => clock : clock;
|
|
89
|
+
assertPlanSignableBy(plan, this, now());
|
|
90
|
+
const out = [];
|
|
91
|
+
for (const [index, step] of plan.steps.entries()) {
|
|
92
|
+
// Re-asserted immediately before each signature, from a fresh clock read, exactly as the live path does.
|
|
93
|
+
assertPlanSignableBy(plan, this, now());
|
|
94
|
+
const raw = await this.account.signTransaction({
|
|
95
|
+
chainId: CHAIN_ID,
|
|
96
|
+
type: 'eip1559',
|
|
97
|
+
nonce: index,
|
|
98
|
+
to: step.to,
|
|
99
|
+
value: step.value,
|
|
100
|
+
data: step.data,
|
|
101
|
+
gas: 400000n,
|
|
102
|
+
maxFeePerGas: 1000000n,
|
|
103
|
+
maxPriorityFeePerGas: 1000n,
|
|
104
|
+
});
|
|
105
|
+
const parsed = parseTransaction(raw);
|
|
106
|
+
if (parsed.type !== 'eip1559' ||
|
|
107
|
+
parsed.chainId !== CHAIN_ID ||
|
|
108
|
+
!parsed.to ||
|
|
109
|
+
getAddress(parsed.to) !== step.to ||
|
|
110
|
+
(parsed.value ?? 0n) !== step.value ||
|
|
111
|
+
(parsed.data ?? '0x').toLowerCase() !== step.data.toLowerCase()) {
|
|
112
|
+
throw new Error(`dry-run step ${index} (${step.kind}) did not decode back to the verified plan step`);
|
|
113
|
+
}
|
|
114
|
+
const recovered = getAddress(await recoverTransactionAddress({ serializedTransaction: raw }));
|
|
115
|
+
if (recovered !== this.address) {
|
|
116
|
+
throw new Error(`dry-run step ${index} recovered to ${recovered}, not ${this.address}`);
|
|
117
|
+
}
|
|
118
|
+
out.push(Object.freeze({
|
|
119
|
+
index,
|
|
120
|
+
kind: step.kind,
|
|
121
|
+
to: step.to,
|
|
122
|
+
signed_bytes: (raw.length - 2) / 2,
|
|
123
|
+
recovered_signer: recovered,
|
|
124
|
+
decoded_matches_plan: true,
|
|
125
|
+
broadcast: false,
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Nonce / fee / gas round trips first, then `beforeSign` (the plan's freshness + binding re-check,
|
|
132
|
+
* AFTER those round trips and immediately before the signature), then sign and broadcast. Resolves
|
|
133
|
+
* only with the SEALED, CANONICAL receipt (`waitForSealedReceipt`): the next step's gas estimate and
|
|
134
|
+
* any read-back must see this transaction, and a Flashblocks preconfirmation alone guarantees neither
|
|
135
|
+
* that they do nor that the transaction survived sealing.
|
|
136
|
+
*/
|
|
137
|
+
async submit(pub, step, beforeSign, sealedWait) {
|
|
138
|
+
const [nonce, fees, gasEstimate] = await Promise.all([
|
|
139
|
+
pub.getTransactionCount({ address: this.address, blockTag: 'pending' }),
|
|
140
|
+
pub.estimateFeesPerGas(),
|
|
141
|
+
pub.estimateGas({ account: this.address, to: step.to, value: step.value, data: step.data }),
|
|
142
|
+
]);
|
|
143
|
+
beforeSign?.();
|
|
144
|
+
const raw = await this.account.signTransaction({
|
|
145
|
+
chainId: CHAIN_ID,
|
|
146
|
+
type: 'eip1559',
|
|
147
|
+
nonce,
|
|
148
|
+
to: step.to,
|
|
149
|
+
value: step.value,
|
|
150
|
+
data: step.data,
|
|
151
|
+
gas: (gasEstimate * 12n) / 10n,
|
|
152
|
+
maxFeePerGas: fees.maxFeePerGas,
|
|
153
|
+
maxPriorityFeePerGas: fees.maxPriorityFeePerGas,
|
|
154
|
+
});
|
|
155
|
+
const hash = await pub.sendRawTransaction({ serializedTransaction: raw });
|
|
156
|
+
await pub.waitForTransactionReceipt({ hash }); // first sight (may be a preconfirmation) …
|
|
157
|
+
const receipt = await waitForSealedReceipt(pub, hash, sealedWait); // … then only the sealed canonical receipt counts
|
|
158
|
+
return { hash, status: receipt.status, blockNumber: receipt.blockNumber, gasUsed: receipt.gasUsed };
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* `send_evm_transaction`, live: one tx per step, in order, the plan re-checked before every step AND
|
|
162
|
+
* again immediately before each signature (after the nonce / fee / gas round trips, so an artifact
|
|
163
|
+
* that expires during preparation is never signed). The plan halts at the first step that does not
|
|
164
|
+
* succeed; after ANY failure a bounded `approve(spender, 0)` is attempted and its real outcome —
|
|
165
|
+
* confirmed by an allowance read-back — is reported, so no allowance outlives the halted plan unnoticed.
|
|
166
|
+
*/
|
|
167
|
+
async sendPlan(plan, pub = this.publicClient(), sealedWait) {
|
|
168
|
+
assertPlanSignableBy(plan, this, Date.now());
|
|
169
|
+
const out = [];
|
|
170
|
+
for (const [index, step] of plan.steps.entries()) {
|
|
171
|
+
// Freshness is re-checked before EVERY live step: an artifact that expires mid-plan halts (and
|
|
172
|
+
// clears the allowance) instead of sending its remaining steps.
|
|
173
|
+
try {
|
|
174
|
+
assertPlanSignableBy(plan, this, Date.now());
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
throw await this.halt(plan, pub, index, `plan no longer signable before step ${index}: ${error instanceof Error ? error.message : String(error)}`, sealedWait);
|
|
178
|
+
}
|
|
179
|
+
let result;
|
|
180
|
+
try {
|
|
181
|
+
result = await this.submit(pub, step, () => assertPlanSignableBy(plan, this, Date.now()), sealedWait);
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
throw await this.halt(plan, pub, index, `step ${index} (${step.kind}) could not be submitted: ${error instanceof Error ? error.message : String(error)}`, sealedWait);
|
|
185
|
+
}
|
|
186
|
+
if (result.status !== 'success') {
|
|
187
|
+
throw await this.halt(plan, pub, index, `step ${index} (${step.kind}) reverted on-chain: ${result.hash}`, sealedWait);
|
|
188
|
+
}
|
|
189
|
+
out.push(Object.freeze({
|
|
190
|
+
index,
|
|
191
|
+
kind: step.kind,
|
|
192
|
+
to: step.to,
|
|
193
|
+
transaction_hash: result.hash,
|
|
194
|
+
status: 'success',
|
|
195
|
+
block_number: result.blockNumber.toString(),
|
|
196
|
+
gas_used: result.gasUsed.toString(),
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
async halt(plan, pub, failedStep, message, sealedWait) {
|
|
202
|
+
// The clear is attempted after ANY failure, step 0 included: a failed reset may leave a
|
|
203
|
+
// PRE-EXISTING allowance, and from step 1 on the approval may already be broadcast even when the
|
|
204
|
+
// receipt or the transport failed. Clearing a never-set allowance is a harmless approve(spender, 0),
|
|
205
|
+
// and `allowanceCleared` reports the clear's real outcome, never an assumption.
|
|
206
|
+
const handOff = `clear approve(${plan.spender}, 0) on ${plan.token} by hand`;
|
|
207
|
+
let hash;
|
|
208
|
+
try {
|
|
209
|
+
const clear = await this.submit(pub, { to: plan.token, value: 0n, data: encodeFunctionData({ abi: ERC20_APPROVE_ABI, functionName: 'approve', args: [plan.spender, 0n] }) }, undefined, sealedWait);
|
|
210
|
+
hash = clear.hash;
|
|
211
|
+
if (clear.status !== 'success') {
|
|
212
|
+
return new PlanHaltedError(`${message}; allowance clear ${hash} reverted — ${handOff}`, failedStep, false, hash);
|
|
213
|
+
}
|
|
214
|
+
// A mined clear is a claim about a transaction; the read-back is the fact about the token.
|
|
215
|
+
const remaining = await readAllowance(pub, plan.token, this.address, plan.spender);
|
|
216
|
+
if (remaining !== 0n) {
|
|
217
|
+
return new PlanHaltedError(`${message}; allowance clear ${hash} was mined but ${remaining} still reads back — ${handOff}`, failedStep, false, hash);
|
|
218
|
+
}
|
|
219
|
+
return new PlanHaltedError(`${message}; allowance cleared in ${hash} (read-back 0)`, failedStep, true, hash);
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
223
|
+
return new PlanHaltedError(`${message}; allowance clear ${hash ? `${hash} could not be confirmed` : 'FAILED'} (${reason}) — ${handOff}`, failedStep, false, hash);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* `sign_x402_payment`: turn a table-validated challenge into the signed `x_payment` value.
|
|
228
|
+
* Validation and hand-off are two moments. Immediately before signing, the document is validated
|
|
229
|
+
* AGAIN against the static table and must resolve to the same row and the same bound fields as the
|
|
230
|
+
* earlier validation; the requirement selector accepts only a candidate matching that ROW; and the
|
|
231
|
+
* produced payload's `accepted` requirement and authorization are checked against the ROW (not
|
|
232
|
+
* against anything the challenge supplied) before the header is returned.
|
|
233
|
+
*/
|
|
234
|
+
async signX402Payment(paymentRequired, validated, table = X402_TABLE, nowSeconds = Math.floor(Date.now() / 1000)) {
|
|
235
|
+
const fresh = validateX402Challenge(paymentRequired, table);
|
|
236
|
+
if (fresh.row !== validated.row || JSON.stringify(fresh.requirement) !== JSON.stringify(validated.requirement)) {
|
|
237
|
+
throw new Error('the challenge no longer validates to the same row and requirement it was validated against; refusing to sign');
|
|
238
|
+
}
|
|
239
|
+
const row = validated.row;
|
|
240
|
+
const signer = toClientEvmSigner(this.account, this.publicClient());
|
|
241
|
+
const client = new x402Client((_version, requirements) => {
|
|
242
|
+
const match = requirements.find((candidate) => requirementMatchesRow(candidate, row));
|
|
243
|
+
if (!match)
|
|
244
|
+
throw new Error('no offered requirement matches the validated table row');
|
|
245
|
+
return match;
|
|
246
|
+
});
|
|
247
|
+
// `@x402/core` >= 2.23 seeds `spendControls = {}` in the bare constructor, which the SDK reads as
|
|
248
|
+
// "default assets only, capped at $1 per payment". We state that cap EXPLICITLY rather than inherit
|
|
249
|
+
// it, so the number is visible in this file and a future SDK default cannot move it silently.
|
|
250
|
+
//
|
|
251
|
+
// It is deliberately KEPT, not disabled. The table checks more FIELDS, but it is not an independent
|
|
252
|
+
// authority: `table` is a parameter, and even the default table's `amount_atomic` values are
|
|
253
|
+
// hand-edited constants that nothing else cross-checks. A price typo (5000 -> 5000000) makes the
|
|
254
|
+
// table agree with itself perfectly, and only this cap — derived from the requirement amount rather
|
|
255
|
+
// than from the table — stands between that typo and a signature for 1000x the intended amount.
|
|
256
|
+
// Field coverage is not the same as a second opinion.
|
|
257
|
+
client.setSpendControls({ maxAmountPerPayment: PER_PAYMENT_SPEND_CAP });
|
|
258
|
+
client.register('eip155:*', new ExactEvmScheme(signer));
|
|
259
|
+
let payload;
|
|
260
|
+
try {
|
|
261
|
+
payload = (await client.createPaymentPayload(paymentRequired));
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
// Translate ONLY the spend-control rejection into this package's typed refusal vocabulary, so a
|
|
265
|
+
// caller keying on X402Refusal sees a code rather than an SDK string. Anything else rethrows
|
|
266
|
+
// untouched — this must never become a catch-all that turns a real failure into a tidy refusal.
|
|
267
|
+
if (isSpendControlRejection(error)) {
|
|
268
|
+
throw new X402Refusal('X402_SPEND_LIMIT_EXCEEDED', 'amount', `at most ${PER_PAYMENT_SPEND_CAP} per payment`, `${row.amount_atomic} atomic units of ${row.asset}`, table.table_version);
|
|
269
|
+
}
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
272
|
+
if (!requirementMatchesRow(payload.accepted, row)) {
|
|
273
|
+
throw new Error('payment payload accepted a requirement that does not match the validated table row');
|
|
274
|
+
}
|
|
275
|
+
const inner = (payload.payload ?? {});
|
|
276
|
+
const authorization = inner.authorization ?? {};
|
|
277
|
+
if (typeof authorization.from !== 'string' || getAddress(authorization.from) !== this.address ||
|
|
278
|
+
typeof authorization.to !== 'string' || getAddress(authorization.to) !== row.pay_to ||
|
|
279
|
+
authorization.value !== row.amount_atomic) {
|
|
280
|
+
throw new Error('payment authorization does not bind this signer to the table pay_to and amount');
|
|
281
|
+
}
|
|
282
|
+
const validBefore = Number(authorization.validBefore);
|
|
283
|
+
if (!Number.isFinite(validBefore) || validBefore > nowSeconds + row.max_timeout_seconds + VALID_BEFORE_SKEW_SECONDS) {
|
|
284
|
+
throw new Error(`payment authorization validBefore ${String(authorization.validBefore)} exceeds the row window of ${row.max_timeout_seconds}s`);
|
|
285
|
+
}
|
|
286
|
+
return Object.freeze({ header: encodePaymentSignatureHeader(payload), payload });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
//# sourceMappingURL=eoa-signer.js.map
|
package/dist/erc20.d.ts
ADDED
package/dist/erc20.js
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* otto-execute — the USER'S side of Model B.
|
|
3
|
+
*
|
|
4
|
+
* Otto's hosted MCP constructs transactions prepare-only (`otto_prepare_*`, unsigned envelopes with
|
|
5
|
+
* Otto's fee attribution baked in). This package is what a user's own signer runs BEFORE and WHILE
|
|
6
|
+
* signing one: verify (integrity → intent → policy → attribution), then sign/submit with the user's
|
|
7
|
+
* key (BYO/EOA: the user signs and submits) or under a delegation the user minted (CDP-delegated:
|
|
8
|
+
* user-authorized, Otto-submitted, Otto-governed, Otto-revocable). Otto never holds user keys or funds.
|
|
9
|
+
*/
|
|
10
|
+
export { ADAPTER_REFUSAL_CODES, AdapterRefusal, isAdapterRefusal, isAdapterRefusalCode, type AdapterRefusalCode, } from './refusal.js';
|
|
11
|
+
export { COLLECT_TOKEN_FEES_SELECTOR, GENERIC_SWAP_SELECTORS, decodeCollectTokenFees, decodeGenericSwap, encodeCollectTokenFees, encodeGenericSwap, type DecodedCollectTokenFees, type DecodedGenericSwap, type FeeDistribution, type SwapLeg, } from './lifi-decode.js';
|
|
12
|
+
export { VERIFY_CHECK_IDS, assertVerifiedPlanIntact, defaultAdapterPolicy, isVerifierOutput, verifySwapEnvelope, type AdapterPolicy, type AttributionPolicy, type SigningCapability, type SwapIntent, type VerifiedCheck, type VerifiedStep, type VerifiedSwap, type VerifyExpectations, } from './verify.js';
|
|
13
|
+
export { waitForSealedReceipt, type SealedClient, type SealedWaitOptions } from './chain.js';
|
|
14
|
+
export { EOA_CAPABILITIES, EoaSigner, PlanHaltedError, type DryRunSignedStep, type SentStep, type SignedX402Payment, } from './eoa-signer.js';
|
|
15
|
+
export { BASE_USDC, OTTO_X402_PAY_TO, X402_REFUSAL_CODES, X402_TABLE, X402_TABLE_VERSION, X402Refusal, canonicalResourceUrl, requirementMatchesRow, validateX402Challenge, type PaymentRequirementLike, type ValidatedX402Challenge, type X402ExpectedRow, type X402RefusalCode, type X402Table, } from './x402-table.js';
|
|
16
|
+
export { FOREIGN_ADDRESS, FOREIGN_TOKEN, MUTATION_CASES, applyMutation, recommit, recommitCommitmentOnly, recommitPayloadOnly, type CatchingLayer, type MutationCase, type MutationContext, } from './mutations.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* otto-execute — the USER'S side of Model B.
|
|
3
|
+
*
|
|
4
|
+
* Otto's hosted MCP constructs transactions prepare-only (`otto_prepare_*`, unsigned envelopes with
|
|
5
|
+
* Otto's fee attribution baked in). This package is what a user's own signer runs BEFORE and WHILE
|
|
6
|
+
* signing one: verify (integrity → intent → policy → attribution), then sign/submit with the user's
|
|
7
|
+
* key (BYO/EOA: the user signs and submits) or under a delegation the user minted (CDP-delegated:
|
|
8
|
+
* user-authorized, Otto-submitted, Otto-governed, Otto-revocable). Otto never holds user keys or funds.
|
|
9
|
+
*/
|
|
10
|
+
export { ADAPTER_REFUSAL_CODES, AdapterRefusal, isAdapterRefusal, isAdapterRefusalCode, } from './refusal.js';
|
|
11
|
+
export { COLLECT_TOKEN_FEES_SELECTOR, GENERIC_SWAP_SELECTORS, decodeCollectTokenFees, decodeGenericSwap, encodeCollectTokenFees, encodeGenericSwap, } from './lifi-decode.js';
|
|
12
|
+
export { VERIFY_CHECK_IDS, assertVerifiedPlanIntact, defaultAdapterPolicy, isVerifierOutput, verifySwapEnvelope, } from './verify.js';
|
|
13
|
+
export { waitForSealedReceipt } from './chain.js';
|
|
14
|
+
export { EOA_CAPABILITIES, EoaSigner, PlanHaltedError, } from './eoa-signer.js';
|
|
15
|
+
// The CDP-delegated signer lives on its own entry, `otto-execute/cdp`: only its `openCdpRig` needs the
|
|
16
|
+
// optional `@coinbase/cdp-sdk` peer (loaded lazily); this root never references the SDK at all.
|
|
17
|
+
export { BASE_USDC, OTTO_X402_PAY_TO, X402_REFUSAL_CODES, X402_TABLE, X402_TABLE_VERSION, X402Refusal, canonicalResourceUrl, requirementMatchesRow, validateX402Challenge, } from './x402-table.js';
|
|
18
|
+
export { FOREIGN_ADDRESS, FOREIGN_TOKEN, MUTATION_CASES, applyMutation, recommit, recommitCommitmentOnly, recommitPayloadOnly, } from './mutations.js';
|
|
19
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/** lifi-decode.ts — re-exported from `./vendor/otto-intel-mcp/adapter-index.js` (moved 2026-08-29; see that entry's header). */
|
|
2
|
+
export { COLLECT_TOKEN_FEES_SELECTOR, GENERIC_SWAP_SELECTORS, decodeCollectTokenFees, decodeGenericSwap, encodeCollectTokenFees, encodeGenericSwap, type DecodedCollectTokenFees, type DecodedGenericSwap, type FeeDistribution, type SwapLeg, } from './vendor/otto-intel-mcp/adapter-index.js';
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** lifi-decode.ts — re-exported from `./vendor/otto-intel-mcp/adapter-index.js` (moved 2026-08-29; see that entry's header). */
|
|
2
|
+
export { COLLECT_TOKEN_FEES_SELECTOR, GENERIC_SWAP_SELECTORS, decodeCollectTokenFees, decodeGenericSwap, encodeCollectTokenFees, encodeGenericSwap, } from './vendor/otto-intel-mcp/adapter-index.js';
|
|
3
|
+
//# sourceMappingURL=lifi-decode.js.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mutations.ts — the consistency matrix (mutation cases the adapter MUST refuse).
|
|
3
|
+
*
|
|
4
|
+
* Each entry derives an INCONSISTENT variant of a VALID artifact and names the exact refusal the
|
|
5
|
+
* adapter must produce. Used twice: by the test suite (every refusal must redden — delete a check
|
|
6
|
+
* in verify.ts and its row fails) and by the sitting's fence section (the BYO "policy engine" is the
|
|
7
|
+
* adapter itself; these rows are its out-of-policy inputs, graded BLOCKED / LEAKED by the ladder).
|
|
8
|
+
* Same vocabulary as the constructor's own mutation proofs (`test/*-mutation-proof.ts`).
|
|
9
|
+
*
|
|
10
|
+
* Two mutation models, deliberately both:
|
|
11
|
+
* - `recommit: false` — bytes change, digests are left stale. The integrity layer catches it.
|
|
12
|
+
* - `recommit: true` — payload + commitment digests are recomputed (they are unkeyed keccak over
|
|
13
|
+
* public content, so any party holding the artifact can). Only the adapter's INTENT and POLICY
|
|
14
|
+
* layers can catch these. A verifier that stopped at digests would grade every one of them LEAKED.
|
|
15
|
+
*
|
|
16
|
+
* Every row names the layer expected to catch it. Rows caught by the shared envelope schema are kept
|
|
17
|
+
* (they document the contract) but are NOT evidence for a verifier-local check; the verifier-local
|
|
18
|
+
* checks each have their own row(s) below.
|
|
19
|
+
*
|
|
20
|
+
* Deliberately NOT a row: an internal DEX leg re-pointed at a foreign contract with the receiver and
|
|
21
|
+
* minimum output intact. The adapter does not hold an allowlist of every DEX; that leg is bounded
|
|
22
|
+
* ON-CHAIN by the router's `_receiver` + `_minAmount` enforcement (GenericSwapV3 reverts unless the
|
|
23
|
+
* receiver gets at least the minimum), which is why the intent floor must be positive and the
|
|
24
|
+
* caller's own (verify.ts layer 2).
|
|
25
|
+
*/
|
|
26
|
+
import { type Address } from 'viem';
|
|
27
|
+
import type { AdapterRefusalCode } from './refusal.js';
|
|
28
|
+
import { type ExecutionEnvelope } from './vendor/otto-intel-mcp/execution-index.js';
|
|
29
|
+
/** A foreign address no reviewed table names; the mutation target for every substitution row. */
|
|
30
|
+
export declare const FOREIGN_ADDRESS: `0x${string}`;
|
|
31
|
+
/** A foreign ERC20 no intent names (Base USDbC), for token substitutions. */
|
|
32
|
+
export declare const FOREIGN_TOKEN: `0x${string}`;
|
|
33
|
+
/** Recompute both digests with the constructor's public canonicalization, as any holder of the artifact could. */
|
|
34
|
+
export declare function recommit(artifact: ExecutionEnvelope): ExecutionEnvelope;
|
|
35
|
+
/** Which adapter layer is expected to catch the mutation (recorded in evidence). */
|
|
36
|
+
export type CatchingLayer = 'schema' | 'integrity' | 'intent' | 'policy';
|
|
37
|
+
export interface MutationCase {
|
|
38
|
+
readonly id: string;
|
|
39
|
+
readonly what: string;
|
|
40
|
+
readonly recommit: boolean;
|
|
41
|
+
readonly expect: AdapterRefusalCode;
|
|
42
|
+
readonly layer: CatchingLayer;
|
|
43
|
+
readonly mutate: (artifact: ExecutionEnvelope, context: MutationContext) => ExecutionEnvelope;
|
|
44
|
+
}
|
|
45
|
+
export interface MutationContext {
|
|
46
|
+
readonly forwarders: readonly Address[];
|
|
47
|
+
readonly ottoRecipient: Address;
|
|
48
|
+
readonly nowMs: number;
|
|
49
|
+
}
|
|
50
|
+
export declare const MUTATION_CASES: readonly MutationCase[];
|
|
51
|
+
/** Recompute ONLY the payload digest (commitment left stale) — isolates the commitment check. */
|
|
52
|
+
export declare function recommitPayloadOnly(artifact: ExecutionEnvelope): ExecutionEnvelope;
|
|
53
|
+
/** Recompute ONLY the whole-envelope commitment (payload digest left stale) — isolates the payload-digest check. */
|
|
54
|
+
export declare function recommitCommitmentOnly(artifact: ExecutionEnvelope): ExecutionEnvelope;
|
|
55
|
+
export declare function applyMutation(mutation: MutationCase, artifact: ExecutionEnvelope, context: MutationContext): ExecutionEnvelope;
|