otto-intel-mcp 0.1.2 → 0.1.4
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/README.md +76 -18
- package/dist/account-userop-policy-definition.d.ts +14 -0
- package/dist/account-userop-policy-definition.js +12 -0
- package/dist/account-userop-policy-relation.d.ts +371 -0
- package/dist/account-userop-policy-relation.js +87 -0
- package/dist/adapter/cdp-signer.d.ts +76 -9
- package/dist/adapter/cdp-signer.js +274 -33
- package/dist/adapter/evm-call-failure.d.ts +29 -0
- package/dist/adapter/evm-call-failure.js +109 -0
- package/dist/adapter/refusal.d.ts +1 -1
- package/dist/adapter/refusal.js +13 -0
- package/dist/adapter/route-simulator.d.ts +91 -0
- package/dist/adapter/route-simulator.js +143 -0
- package/dist/adapter-cdp-index.d.ts +1 -1
- package/dist/executable-equities.d.ts +55 -0
- package/dist/executable-equities.js +116 -0
- package/dist/executable-yield-markets.d.ts +261 -0
- package/dist/executable-yield-markets.js +154 -0
- package/dist/execution-account-gas-quote.d.ts +141 -0
- package/dist/execution-account-gas-quote.js +443 -0
- package/dist/execution-account-gas-review.d.ts +123 -0
- package/dist/execution-account-gas-review.js +104 -0
- package/dist/execution-account-gas-runtime.d.ts +33 -0
- package/dist/execution-account-gas-runtime.js +131 -0
- package/dist/execution-account-gas-view.d.ts +936 -0
- package/dist/execution-account-gas-view.js +56 -0
- package/dist/execution-config.d.ts +3 -0
- package/dist/execution-config.js +4 -0
- package/dist/execution-delegated-definition.d.ts +7 -6
- package/dist/execution-delegated-definition.js +13 -2
- package/dist/execution-delegation-admin-definition.d.ts +127 -5
- package/dist/execution-delegation-admin-definition.js +155 -5
- package/dist/execution-delegation-admin.d.ts +48 -2
- package/dist/execution-delegation-admin.js +422 -12
- package/dist/execution-delegation-cap-store.d.ts +386 -0
- package/dist/execution-delegation-cap-store.js +1150 -0
- package/dist/execution-delegation-exit-definition.d.ts +309 -0
- package/dist/execution-delegation-exit-definition.js +204 -0
- package/dist/execution-delegation-exit-store.d.ts +114 -0
- package/dist/execution-delegation-exit-store.js +671 -0
- package/dist/execution-delegation-exit.d.ts +4 -0
- package/dist/execution-delegation-exit.js +614 -0
- package/dist/execution-delegation-finality.d.ts +22 -0
- package/dist/execution-delegation-finality.js +90 -0
- package/dist/execution-delegation-operation-store.d.ts +129 -0
- package/dist/execution-delegation-operation-store.js +1107 -0
- package/dist/execution-delegation-operations-definition.d.ts +282 -0
- package/dist/execution-delegation-operations-definition.js +117 -0
- package/dist/execution-delegation-operations.d.ts +6 -0
- package/dist/execution-delegation-operations.js +282 -0
- package/dist/execution-delegation-policy.d.ts +154 -8
- package/dist/execution-delegation-policy.js +273 -40
- package/dist/execution-delegation.d.ts +51 -0
- package/dist/execution-delegation.js +441 -31
- package/dist/execution-errors.d.ts +1 -1
- package/dist/execution-errors.js +122 -0
- package/dist/execution-index.d.ts +7 -2
- package/dist/execution-index.js +7 -2
- package/dist/execution-journal-schema.d.ts +96 -0
- package/dist/execution-journal-schema.js +368 -0
- package/dist/execution-journal-tables.d.ts +14 -0
- package/dist/execution-journal-tables.js +227 -0
- package/dist/execution-policy-compatibility.d.ts +34 -0
- package/dist/execution-policy-compatibility.js +76 -0
- package/dist/execution-registration.d.ts +54 -2
- package/dist/execution-registration.js +151 -5
- package/dist/execution-sponsored-account-profile.d.ts +32 -0
- package/dist/execution-sponsored-account-profile.js +59 -0
- package/dist/execution-sponsored-account.d.ts +1519 -0
- package/dist/execution-sponsored-account.js +222 -0
- package/dist/execution-sponsored-definition.d.ts +1713 -0
- package/dist/execution-sponsored-definition.js +185 -0
- package/dist/execution-sponsored-gas-policy.d.ts +27 -0
- package/dist/execution-sponsored-gas-policy.js +110 -0
- package/dist/execution-sponsored-hosted.d.ts +3 -0
- package/dist/execution-sponsored-hosted.js +71 -0
- package/dist/execution-sponsored-index.d.ts +1322 -0
- package/dist/execution-sponsored-index.js +102 -0
- package/dist/execution-sponsored-mcp-definition.d.ts +1662 -0
- package/dist/execution-sponsored-mcp-definition.js +94 -0
- package/dist/execution-sponsored-receipt.d.ts +47 -0
- package/dist/execution-sponsored-receipt.js +74 -0
- package/dist/execution-sponsored-registration.d.ts +14 -0
- package/dist/execution-sponsored-registration.js +77 -0
- package/dist/execution-sponsored-runtime.d.ts +84 -0
- package/dist/execution-sponsored-runtime.js +149 -0
- package/dist/execution-sponsored-settlement.d.ts +15 -0
- package/dist/execution-sponsored-settlement.js +88 -0
- package/dist/execution-sponsored-store.d.ts +67 -0
- package/dist/execution-sponsored-store.js +517 -0
- package/dist/execution-sponsored-transport.d.ts +90 -0
- package/dist/execution-sponsored-transport.js +180 -0
- package/dist/execution-tool-definitions.d.ts +21 -21
- package/dist/execution-tool-definitions.js +0 -7
- package/dist/execution-tools.d.ts +53 -1
- package/dist/execution-tools.js +171 -13
- package/dist/execution-worker-guard.d.ts +28 -0
- package/dist/execution-worker-guard.js +124 -0
- package/dist/hybrid-yield-index.d.ts +10 -0
- package/dist/hybrid-yield-index.js +11 -0
- package/dist/hybrid-yield-markets.d.ts +96 -0
- package/dist/hybrid-yield-markets.js +124 -0
- package/dist/hybrid-yield.d.ts +411 -0
- package/dist/hybrid-yield.js +155 -0
- package/dist/hyperliquid-order-builder.js +4 -0
- package/dist/lifi-execution-client.d.ts +6 -0
- package/dist/lifi-execution-client.js +3 -0
- package/dist/lp-userop-account.d.ts +461 -0
- package/dist/lp-userop-account.js +417 -0
- package/dist/lp-userop-authority.d.ts +49 -0
- package/dist/lp-userop-authority.js +211 -0
- package/dist/lp-userop-definition.d.ts +233 -0
- package/dist/lp-userop-definition.js +85 -0
- package/dist/lp-userop-hosted.d.ts +3 -0
- package/dist/lp-userop-hosted.js +73 -0
- package/dist/lp-userop-index.d.ts +1569 -0
- package/dist/lp-userop-index.js +112 -0
- package/dist/lp-userop-mcp-definition.d.ts +1220 -0
- package/dist/lp-userop-mcp-definition.js +46 -0
- package/dist/lp-userop-operation.d.ts +1230 -0
- package/dist/lp-userop-operation.js +140 -0
- package/dist/lp-userop-registration.d.ts +14 -0
- package/dist/lp-userop-registration.js +70 -0
- package/dist/lp-userop-settlement.d.ts +89 -0
- package/dist/lp-userop-settlement.js +184 -0
- package/dist/lp-userop.d.ts +643 -0
- package/dist/lp-userop.js +343 -0
- package/dist/native-yield-account-definition.d.ts +1192 -0
- package/dist/native-yield-account-definition.js +232 -0
- package/dist/native-yield-account-gas.d.ts +58 -0
- package/dist/native-yield-account-gas.js +228 -0
- package/dist/native-yield-account-index.d.ts +23 -0
- package/dist/native-yield-account-index.js +28 -0
- package/dist/native-yield-account-policy.d.ts +168 -0
- package/dist/native-yield-account-policy.js +525 -0
- package/dist/native-yield-account-rpc.d.ts +8 -0
- package/dist/native-yield-account-rpc.js +128 -0
- package/dist/native-yield-account-runtime.d.ts +71 -0
- package/dist/native-yield-account-runtime.js +125 -0
- package/dist/native-yield-account-store.d.ts +40 -0
- package/dist/native-yield-account-store.js +307 -0
- package/dist/native-yield-account.d.ts +662 -0
- package/dist/native-yield-account.js +371 -0
- package/dist/native-yield-consent-store.d.ts +20 -0
- package/dist/native-yield-consent-store.js +317 -0
- package/dist/native-yield-definition.d.ts +288 -0
- package/dist/native-yield-definition.js +105 -0
- package/dist/native-yield-reader.d.ts +4 -0
- package/dist/native-yield-reader.js +137 -0
- package/dist/native-yield-settlement.d.ts +48 -0
- package/dist/native-yield-settlement.js +167 -0
- package/dist/native-yield-simulation.d.ts +18 -0
- package/dist/native-yield-simulation.js +45 -0
- package/dist/native-yield-userop-account.d.ts +675 -0
- package/dist/native-yield-userop-account.js +357 -0
- package/dist/native-yield-userop-authority.d.ts +214 -0
- package/dist/native-yield-userop-authority.js +296 -0
- package/dist/native-yield-userop-definition.d.ts +331 -0
- package/dist/native-yield-userop-definition.js +105 -0
- package/dist/native-yield-userop-hosted.d.ts +3 -0
- package/dist/native-yield-userop-hosted.js +71 -0
- package/dist/native-yield-userop-index.d.ts +1522 -0
- package/dist/native-yield-userop-index.js +142 -0
- package/dist/native-yield-userop-mcp-definition.d.ts +1356 -0
- package/dist/native-yield-userop-mcp-definition.js +50 -0
- package/dist/native-yield-userop-operation.d.ts +845 -0
- package/dist/native-yield-userop-operation.js +127 -0
- package/dist/native-yield-userop-policy.d.ts +23 -0
- package/dist/native-yield-userop-policy.js +25 -0
- package/dist/native-yield-userop-registration.d.ts +17 -0
- package/dist/native-yield-userop-registration.js +76 -0
- package/dist/native-yield-userop-settlement.d.ts +8 -0
- package/dist/native-yield-userop-settlement.js +79 -0
- package/dist/native-yield-userop-simulation.d.ts +18 -0
- package/dist/native-yield-userop-simulation.js +34 -0
- package/dist/native-yield-userop.d.ts +35 -0
- package/dist/native-yield-userop.js +156 -0
- package/dist/native-yield-verify.d.ts +30 -0
- package/dist/native-yield-verify.js +141 -0
- package/dist/native-yield.d.ts +17 -0
- package/dist/native-yield.js +119 -0
- package/dist/stdio-main.js +0 -0
- package/dist/stock-buy-definition.d.ts +338 -0
- package/dist/stock-buy-definition.js +127 -0
- package/dist/stock-buy-oracle.d.ts +8 -0
- package/dist/stock-buy-oracle.js +22 -0
- package/dist/stock-buy.d.ts +17 -0
- package/dist/stock-buy.js +164 -0
- package/dist/tool-definitions.d.ts +3 -1
- package/dist/tool-definitions.js +55 -2
- package/dist/vendor/lp-v3/lp-v3-atomic-contract.d.ts +445 -0
- package/dist/vendor/lp-v3/lp-v3-atomic-contract.js +90 -0
- package/dist/vendor/lp-v3/lp-v3-atomic-evidence.d.ts +14 -0
- package/dist/vendor/lp-v3/lp-v3-atomic-evidence.js +82 -0
- package/dist/vendor/lp-v3/lp-v3-atomic-index.d.ts +80 -0
- package/dist/vendor/lp-v3/lp-v3-atomic-index.js +19 -0
- package/dist/vendor/lp-v3/lp-v3-atomic-plan.d.ts +43 -0
- package/dist/vendor/lp-v3/lp-v3-atomic-plan.js +112 -0
- package/dist/vendor/lp-v3/lp-v3-atomic-read-contract.d.ts +312 -0
- package/dist/vendor/lp-v3/lp-v3-atomic-read-contract.js +41 -0
- package/dist/vendor/lp-v3/lp-v3-atomic-read-index.d.ts +3 -0
- package/dist/vendor/lp-v3/lp-v3-atomic-read-index.js +3 -0
- package/dist/vendor/lp-v3/lp-v3-atomic-read-rpc.d.ts +49 -0
- package/dist/vendor/lp-v3/lp-v3-atomic-read-rpc.js +88 -0
- package/dist/vendor/lp-v3/lp-v3-atomic-read.d.ts +17 -0
- package/dist/vendor/lp-v3/lp-v3-atomic-read.js +232 -0
- package/dist/vendor/lp-v3/lp-v3-atomic-settlement.d.ts +105 -0
- package/dist/vendor/lp-v3/lp-v3-atomic-settlement.js +162 -0
- package/dist/vendor/lp-v3/lp-v3-collect-protocol-contract.d.ts +606 -0
- package/dist/vendor/lp-v3/lp-v3-collect-protocol-contract.js +59 -0
- package/dist/vendor/lp-v3/lp-v3-collect-protocol-index.d.ts +168 -0
- package/dist/vendor/lp-v3/lp-v3-collect-protocol-index.js +23 -0
- package/dist/vendor/lp-v3/lp-v3-collect-protocol-observation.d.ts +32 -0
- package/dist/vendor/lp-v3/lp-v3-collect-protocol-observation.js +228 -0
- package/dist/vendor/lp-v3/lp-v3-collect-protocol-plan.d.ts +11 -0
- package/dist/vendor/lp-v3/lp-v3-collect-protocol-plan.js +91 -0
- package/dist/vendor/lp-v3/lp-v3-collect-protocol-reconcile.d.ts +9 -0
- package/dist/vendor/lp-v3/lp-v3-collect-protocol-reconcile.js +124 -0
- package/dist/vendor/lp-v3/lp-v3-contract.d.ts +538 -0
- package/dist/vendor/lp-v3/lp-v3-contract.js +137 -0
- package/dist/vendor/lp-v3/lp-v3-evidence.d.ts +25 -0
- package/dist/vendor/lp-v3/lp-v3-evidence.js +168 -0
- package/dist/vendor/lp-v3/lp-v3-plan.d.ts +22 -0
- package/dist/vendor/lp-v3/lp-v3-plan.js +135 -0
- package/dist/vendor/lp-v3/types.d.ts +131 -0
- package/dist/vendor/lp-v3/types.js +6 -0
- package/dist/vendor/lp-v3/v4-math.d.ts +18 -0
- package/dist/vendor/lp-v3/v4-math.js +90 -0
- package/dist/yield-deposit-definition.d.ts +374 -0
- package/dist/yield-deposit-definition.js +123 -0
- package/dist/yield-deposit.d.ts +16 -0
- package/dist/yield-deposit.js +149 -0
- package/dist/yield-route-semantics.d.ts +27 -0
- package/dist/yield-route-semantics.js +71 -0
- package/dist/yield-withdraw-definition.d.ts +357 -0
- package/dist/yield-withdraw-definition.js +90 -0
- package/dist/yield-withdraw.d.ts +11 -0
- package/dist/yield-withdraw.js +122 -0
- package/package.json +239 -7
|
@@ -28,19 +28,43 @@ import { createPublicClient, encodeFunctionData, http, serializeTransaction, } f
|
|
|
28
28
|
import { base } from 'viem/chains';
|
|
29
29
|
import { SealedReceiptTimeoutError, waitForSealedReceipt } from './chain.js';
|
|
30
30
|
import { ERC20_APPROVE_ABI, readAllowance } from './erc20.js';
|
|
31
|
-
import { isAdapterRefusal } from './refusal.js';
|
|
31
|
+
import { AdapterRefusal, isAdapterRefusal } from './refusal.js';
|
|
32
|
+
import { classifyEvmCallFailure, evmCallFailureDetail } from './evm-call-failure.js';
|
|
32
33
|
import { assertPlanSignableBy } from './verify.js';
|
|
33
34
|
export const CDP_CAPABILITIES = Object.freeze(['send_evm_transaction']);
|
|
34
35
|
export const CHAIN_ID = 8453;
|
|
35
36
|
export const NETWORK = 'base';
|
|
37
|
+
/** Internal proof that a step failed before the raw CDP submission function was entered. */
|
|
38
|
+
class DelegatedPreHandoffError extends Error {
|
|
39
|
+
original;
|
|
40
|
+
constructor(original) {
|
|
41
|
+
super('delegated step refused before the CDP hand-off');
|
|
42
|
+
this.original = original;
|
|
43
|
+
this.name = 'DelegatedPreHandoffError';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
36
46
|
const RAW = new WeakMap();
|
|
37
47
|
/** One lifecycle lock PER RIG — shared by every signer over it and by the rig's own public `revoke`. */
|
|
38
48
|
const LOCKS = new WeakMap();
|
|
39
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Per rig: the GRANTS whose revocation has been REQUESTED, keyed `userId\nexpiryMs`.
|
|
51
|
+
*
|
|
52
|
+
* 🔴 PER GRANT, NEVER PER USER (gate finding, HIGH — round 3). It used to be a set of user ids, added to
|
|
53
|
+
* before the raw revoke and never cleared. Once revoke became available in every screen state, a Stop
|
|
54
|
+
* press on a screen with NO grant marked the person permanently and bricked every later permission of
|
|
55
|
+
* theirs on that process. The key is now the grant itself, so a revoke can only ever stop the
|
|
56
|
+
* permission it was aimed at, and a revoke that found nothing to revoke marks nothing at all.
|
|
57
|
+
*/
|
|
40
58
|
const REVOKE_REQUESTED = new WeakMap();
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
59
|
+
function grantKey(userId, expiresAt) {
|
|
60
|
+
return `${userId}\n${Date.parse(expiresAt)}`;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* True once revocation was requested on this rig for THIS grant — the one expiring at `expiresAt`.
|
|
64
|
+
* A user with no live grant, or with a different one, is not marked.
|
|
65
|
+
*/
|
|
66
|
+
export function revocationRequested(rig, userId, expiresAt) {
|
|
67
|
+
return REVOKE_REQUESTED.get(rig)?.has(grantKey(userId, expiresAt)) ?? false;
|
|
44
68
|
}
|
|
45
69
|
/** Run `operation` after everything previously queued on this rig has settled; nothing else starts until it settles. */
|
|
46
70
|
function serializedOn(rig, operation) {
|
|
@@ -52,9 +76,11 @@ function serializedOn(rig, operation) {
|
|
|
52
76
|
return run;
|
|
53
77
|
}
|
|
54
78
|
/**
|
|
55
|
-
* A delegated plan halted; `cause` is the structured class above
|
|
56
|
-
*
|
|
57
|
-
*
|
|
79
|
+
* A delegated plan halted; `cause` is the structured class above. `allowanceCleared` reports only
|
|
80
|
+
* whether the bounded cleanup's live, unpinned allowance read returned zero. It is diagnostic and is
|
|
81
|
+
* never sufficient to release a durable operation claim; finalized-canonical recovery owns that proof.
|
|
82
|
+
* The message is for the operator; a consumer that must not relay upstream text reads `cause` and the
|
|
83
|
+
* two fields only.
|
|
58
84
|
*/
|
|
59
85
|
export class DelegatedPlanHaltedError extends Error {
|
|
60
86
|
failedStep;
|
|
@@ -82,6 +108,10 @@ const ABSENT_DELEGATION_ERROR_TYPES = ['delegation_not_found', 'delegation_revok
|
|
|
82
108
|
export function haltCauseOf(error) {
|
|
83
109
|
if (error instanceof RevocationRequestedError)
|
|
84
110
|
return 'delegation_not_active';
|
|
111
|
+
if (error instanceof DelegatedTransactionWouldRevertError)
|
|
112
|
+
return 'would_revert';
|
|
113
|
+
if (error instanceof DelegatedTransactionEstimateUnavailableError)
|
|
114
|
+
return 'estimate_unavailable';
|
|
85
115
|
if (error instanceof SealedReceiptTimeoutError)
|
|
86
116
|
return 'receipt_not_sealed';
|
|
87
117
|
if (isAdapterRefusal(error))
|
|
@@ -111,13 +141,22 @@ export function createCdpRig(raw) {
|
|
|
111
141
|
// it lands only after any in-flight plan on this rig and its allowance clear have settled, and a
|
|
112
142
|
// plan queued behind it runs only after the read-back — where it is refused, because the user was
|
|
113
143
|
// marked revocation-requested BEFORE the raw revoke was issued.
|
|
144
|
+
/**
|
|
145
|
+
* Revoke the grant this user holds RIGHT NOW, marking that grant — and only that grant —
|
|
146
|
+
* revocation-requested before the raw call, so a plan already queued behind this is refused whatever
|
|
147
|
+
* the read-back says. A user with NO live grant is a no-op success: there is nothing to stop, and
|
|
148
|
+
* marking them would take away permissions they have not been given yet.
|
|
149
|
+
*/
|
|
114
150
|
revoke: (userId) => serializedOn(rig, async () => {
|
|
115
|
-
|
|
151
|
+
const before = await raw.readDelegation(userId);
|
|
152
|
+
if (!before)
|
|
153
|
+
return;
|
|
154
|
+
requested.add(grantKey(userId, before.expiresAt));
|
|
116
155
|
try {
|
|
117
156
|
await raw.revoke(userId);
|
|
118
157
|
}
|
|
119
158
|
catch {
|
|
120
|
-
// A grant that
|
|
159
|
+
// A grant that is already gone reads back as absent below; other failures surface there.
|
|
121
160
|
}
|
|
122
161
|
const after = await raw.readDelegation(userId);
|
|
123
162
|
if (after)
|
|
@@ -164,8 +203,14 @@ export async function openCdpRig(credentials, rpcUrl) {
|
|
|
164
203
|
throw new Error(`RPC reports chainId ${chainId}, expected ${CHAIN_ID} (Base mainnet)`);
|
|
165
204
|
return createCdpRig({
|
|
166
205
|
pub,
|
|
167
|
-
submit: async (user, transaction) => {
|
|
168
|
-
const result = await cdp.endUser.sendEvmTransaction({
|
|
206
|
+
submit: async (user, transaction, idempotencyKey) => {
|
|
207
|
+
const result = await cdp.endUser.sendEvmTransaction({
|
|
208
|
+
userId: user.userId,
|
|
209
|
+
address: user.address,
|
|
210
|
+
transaction,
|
|
211
|
+
network: NETWORK,
|
|
212
|
+
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
213
|
+
});
|
|
169
214
|
return result.transactionHash;
|
|
170
215
|
},
|
|
171
216
|
revoke: async (userId) => {
|
|
@@ -191,7 +236,41 @@ export async function openCdpRig(credentials, rpcUrl) {
|
|
|
191
236
|
export function revokeDelegationByUserId(rig, userId) {
|
|
192
237
|
return rig.revoke(userId);
|
|
193
238
|
}
|
|
194
|
-
|
|
239
|
+
/**
|
|
240
|
+
* A plan step whose gas estimate REVERTED. The node evaluates nothing of Coinbase's policy — the policy
|
|
241
|
+
* engine answers at submission, not at `eth_estimateGas` — so a reverting estimate is a transaction that
|
|
242
|
+
* would revert on chain, and broadcasting it anyway would spend the user's gas and buy nothing.
|
|
243
|
+
*
|
|
244
|
+
* 🔴 THIS IS WHAT THE STATIC 400,000 FALLBACK USED TO DO (2026-09-03, the first live delegated swap):
|
|
245
|
+
* the estimate reverted, the step went out at 400k, reverted on chain (234k gas burned), and only then
|
|
246
|
+
* did the halt path clear the allowance. 400k was also below every LI.FI swap's real need (~320–580k
|
|
247
|
+
* used, 1.1–1.4M quoted), so even a healthy route would have reverted under it. There is no fallback
|
|
248
|
+
* now: an estimate that reverts is a refusal, by name.
|
|
249
|
+
*/
|
|
250
|
+
export class DelegatedTransactionWouldRevertError extends AdapterRefusal {
|
|
251
|
+
stepKind;
|
|
252
|
+
constructor(stepKind, detail) {
|
|
253
|
+
super('DELEGATED_TX_WOULD_REVERT', `the ${stepKind} transaction would revert on chain (gas estimate reverted${detail ? `: ${detail}` : ''}); it was not broadcast, and no allowance was changed by this plan`);
|
|
254
|
+
this.stepKind = stepKind;
|
|
255
|
+
this.name = 'DelegatedTransactionWouldRevertError';
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* A plan step the node could not estimate AT ALL — a timeout, a rate limit, a dropped connection. Nothing
|
|
260
|
+
* is proven about the transaction, so nothing is broadcast without an estimate; but it is NOT a revert:
|
|
261
|
+
* the caller may retry, and no route is blamed (gate r7, finding 2).
|
|
262
|
+
*/
|
|
263
|
+
export class DelegatedTransactionEstimateUnavailableError extends AdapterRefusal {
|
|
264
|
+
stepKind;
|
|
265
|
+
constructor(stepKind, detail) {
|
|
266
|
+
super('DELEGATED_TX_ESTIMATE_UNAVAILABLE', `the ${stepKind} transaction could not be estimated by the node (${detail || 'no detail'}); nothing was broadcast — retry`);
|
|
267
|
+
this.stepKind = stepKind;
|
|
268
|
+
this.name = 'DelegatedTransactionEstimateUnavailableError';
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/** The bounded limit the halt path's `approve(spender, 0)` uses when the node cannot estimate it. */
|
|
272
|
+
export const ALLOWANCE_CLEAR_STATIC_GAS = 80000n;
|
|
273
|
+
async function buildUnsignedTransaction(pub, from, step, onEstimateFailure) {
|
|
195
274
|
const [nonce, fees] = await Promise.all([
|
|
196
275
|
pub.getTransactionCount({ address: from, blockTag: 'pending' }),
|
|
197
276
|
pub.estimateFeesPerGas(),
|
|
@@ -200,10 +279,24 @@ async function buildUnsignedTransaction(pub, from, step) {
|
|
|
200
279
|
try {
|
|
201
280
|
gas = ((await pub.estimateGas({ account: from, to: step.to, value: step.value, data: step.data })) * 12n) / 10n;
|
|
202
281
|
}
|
|
203
|
-
catch {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
282
|
+
catch (error) {
|
|
283
|
+
/**
|
|
284
|
+
* 🔴 TWO FACTS, TWO ANSWERS (gate r7, findings 1 and 2). The node either PROVED the transaction would
|
|
285
|
+
* revert, or it could not answer. A proven revert is never broadcast — not a plan step, and not the
|
|
286
|
+
* halt path's `approve(spender, 0)` either: a clear the node has already shown to revert is a spend
|
|
287
|
+
* with nothing bought, and the halt reports it as NOT cleared with the hand-off. A node that could
|
|
288
|
+
* not answer proves nothing: a plan step is refused as retriable (no route is blamed), while the
|
|
289
|
+
* halt's clear — the safety action, of a fixed shape — still goes out on a bounded static limit.
|
|
290
|
+
*/
|
|
291
|
+
const failure = classifyEvmCallFailure(error);
|
|
292
|
+
const detail = evmCallFailureDetail(error);
|
|
293
|
+
if (failure === 'revert') {
|
|
294
|
+
throw new DelegatedTransactionWouldRevertError(onEstimateFailure.kind === 'refuse' ? onEstimateFailure.stepKind : 'allowance clear', detail);
|
|
295
|
+
}
|
|
296
|
+
if (onEstimateFailure.kind === 'refuse') {
|
|
297
|
+
throw new DelegatedTransactionEstimateUnavailableError(onEstimateFailure.stepKind, detail);
|
|
298
|
+
}
|
|
299
|
+
gas = ALLOWANCE_CLEAR_STATIC_GAS;
|
|
207
300
|
}
|
|
208
301
|
return serializeTransaction({
|
|
209
302
|
type: 'eip1559',
|
|
@@ -224,6 +317,37 @@ export class CdpDelegatedSigner {
|
|
|
224
317
|
#raw;
|
|
225
318
|
#user;
|
|
226
319
|
#sealedWait;
|
|
320
|
+
/**
|
|
321
|
+
* An ASYNC re-check the caller runs before every step (`execution-delegation.ts` passes one that
|
|
322
|
+
* re-reads the grant and its recorded per-swap cap). The synchronous guards below cover revocation and
|
|
323
|
+
* the plan's own freshness; they cannot cover a fact that lives behind a network read, and a plan
|
|
324
|
+
* cleared under one permission must not finish executing under the permission that replaced it.
|
|
325
|
+
* It THROWS to stop the plan; the caller's refusal is halted and the allowance cleared like any other.
|
|
326
|
+
*/
|
|
327
|
+
#beforeEachStep;
|
|
328
|
+
/**
|
|
329
|
+
* The expiry of the grant this plan was CLEARED AGAINST. The revocation marker is per grant now, so
|
|
330
|
+
* the signer has to name the grant it is asking about; without it the check would be back to "has
|
|
331
|
+
* anything ever been revoked for this person", which is the sticky, over-broad shape that was wrong.
|
|
332
|
+
*/
|
|
333
|
+
#grantExpiresAt;
|
|
334
|
+
/**
|
|
335
|
+
* 🔴 THE LAST AUTHORITATIVE CHECK, AND IT RUNS AFTER THE TRANSACTION IS BUILT (gate finding, HIGH —
|
|
336
|
+
* round 3): "the final asynchronous grant/cap check still happens before transaction construction …
|
|
337
|
+
* only a synchronous abort check precedes raw submission. A revoke/re-mint to a narrower cap during
|
|
338
|
+
* transaction building therefore escapes the last authoritative check."
|
|
339
|
+
*
|
|
340
|
+
* So there are two hooks, and the split is the fix:
|
|
341
|
+
* `#afterBuild` — ASYNC, run once the nonce/fee/gas round trips are done. It RE-READS the grant and
|
|
342
|
+
* the confirmed record, so a change that landed DURING construction is seen.
|
|
343
|
+
* `#beforeSubmit` — SYNCHRONOUS, run immediately before the raw hand-off, asserting what that
|
|
344
|
+
* re-read just found. It cannot await, because awaiting is what opens the window it closes.
|
|
345
|
+
*
|
|
346
|
+
* A signer given only the sync hook keeps the old behaviour; the delegated tool passes both.
|
|
347
|
+
*/
|
|
348
|
+
#afterBuild;
|
|
349
|
+
#beforeSubmit;
|
|
350
|
+
#submissionJournal;
|
|
227
351
|
constructor(rig, user, options = {}) {
|
|
228
352
|
const raw = RAW.get(rig);
|
|
229
353
|
if (!raw)
|
|
@@ -232,6 +356,11 @@ export class CdpDelegatedSigner {
|
|
|
232
356
|
this.#raw = raw;
|
|
233
357
|
this.#user = Object.freeze({ userId: user.userId, address: user.address });
|
|
234
358
|
this.#sealedWait = options.sealedWait;
|
|
359
|
+
this.#beforeEachStep = options.beforeEachStep;
|
|
360
|
+
this.#afterBuild = options.afterBuild;
|
|
361
|
+
this.#beforeSubmit = options.beforeSubmit;
|
|
362
|
+
this.#grantExpiresAt = options.grantExpiresAt;
|
|
363
|
+
this.#submissionJournal = options.submissionJournal;
|
|
235
364
|
}
|
|
236
365
|
get address() {
|
|
237
366
|
return this.#user.address;
|
|
@@ -243,20 +372,87 @@ export class CdpDelegatedSigner {
|
|
|
243
372
|
* Assemble the unsigned transaction from chain reads, run `beforeSubmit` (the plan's freshness /
|
|
244
373
|
* binding re-check, AFTER the RPC round trips and immediately before the hand-off), then submit.
|
|
245
374
|
*/
|
|
246
|
-
async #submit(step, beforeSubmit) {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
375
|
+
async #submit(step, beforeSubmit, journalStep) {
|
|
376
|
+
let idempotencyKey;
|
|
377
|
+
let handedToCdp = false;
|
|
378
|
+
try {
|
|
379
|
+
// A plan step (it carries `beforeSubmit`) refuses on a reverting estimate; the halt path's clear does not.
|
|
380
|
+
// The build and journal prepare are inside this boundary: both are provably before the raw hand-off.
|
|
381
|
+
const transaction = await buildUnsignedTransaction(this.#raw.pub, this.#user.address, step, beforeSubmit ? { kind: 'refuse', stepKind: step.kind ?? 'plan' } : { kind: 'allowance_clear' });
|
|
382
|
+
if (this.#submissionJournal) {
|
|
383
|
+
if (!journalStep)
|
|
384
|
+
throw new Error('a journaled submission needs a stable step identity');
|
|
385
|
+
({ idempotencyKey } = await this.#submissionJournal.prepare({
|
|
386
|
+
index: journalStep.index,
|
|
387
|
+
kind: journalStep.kind,
|
|
388
|
+
userId: this.#user.userId,
|
|
389
|
+
address: this.#user.address,
|
|
390
|
+
transaction,
|
|
391
|
+
network: NETWORK,
|
|
392
|
+
}));
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* 🔴 THE AUTHORITATIVE RE-READ, AFTER CONSTRUCTION. A revoke or a narrower re-mint that landed while
|
|
396
|
+
* the nonce, fees and gas were being fetched is visible here and nowhere earlier.
|
|
397
|
+
*
|
|
398
|
+
* 🔴 AND ONLY FOR A PLAN STEP. `beforeSubmit` is passed by `sendPlan` and by nothing else, so it is
|
|
399
|
+
* the signal that this submission is part of the plan. The halt path's `approve(spender, 0)` goes
|
|
400
|
+
* out through this same method WITHOUT it, and it must never be gated: the clear is the safety
|
|
401
|
+
* action that runs precisely BECAUSE something went wrong, including the grant changing under it.
|
|
402
|
+
* Refusing to clear an allowance because the permission that opened it has gone is the one outcome
|
|
403
|
+
* nobody wants.
|
|
404
|
+
*/
|
|
405
|
+
if (beforeSubmit) {
|
|
406
|
+
// The journal write above is asynchronous. Re-read authority AFTER it, so the final synchronous
|
|
407
|
+
// checks still stand immediately beside the CDP hand-off rather than before a database wait.
|
|
408
|
+
await this.#afterBuild?.();
|
|
409
|
+
beforeSubmit();
|
|
410
|
+
this.#beforeSubmit?.();
|
|
411
|
+
}
|
|
412
|
+
handedToCdp = true;
|
|
413
|
+
const hash = await this.#raw.submit(this.#user, transaction, idempotencyKey);
|
|
414
|
+
if (this.#submissionJournal && idempotencyKey && journalStep)
|
|
415
|
+
await this.#submissionJournal.submitted(journalStep.index, idempotencyKey, hash);
|
|
416
|
+
return Object.freeze({ hash, ...(idempotencyKey ? { idempotencyKey } : {}) });
|
|
417
|
+
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
if (this.#submissionJournal && idempotencyKey && journalStep) {
|
|
420
|
+
try {
|
|
421
|
+
if (handedToCdp)
|
|
422
|
+
await this.#submissionJournal.unknown(journalStep.index, idempotencyKey);
|
|
423
|
+
else
|
|
424
|
+
await this.#submissionJournal.notSubmitted(journalStep.index, idempotencyKey);
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
// The durable row already remains unresolved. Never replace the causal refusal with a journal
|
|
428
|
+
// cleanup error, and never retry CDP to compensate for a local write failure.
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
// `handedToCdp` flips immediately before entering the raw submission function. While it is false,
|
|
432
|
+
// zero requests are a local fact, independent of the error class (build, journal, authority recheck,
|
|
433
|
+
// or any other pre-handoff failure). Preserve that fact for step 0 so no cleanup transaction creates
|
|
434
|
+
// the first ambiguous request.
|
|
435
|
+
if (!handedToCdp)
|
|
436
|
+
throw new DelegatedPreHandoffError(error);
|
|
437
|
+
throw error;
|
|
438
|
+
}
|
|
250
439
|
}
|
|
251
|
-
/**
|
|
440
|
+
/**
|
|
441
|
+
* Fail-closed: once revocation was requested for THE GRANT THIS PLAN IS BOUND TO, nothing more is
|
|
442
|
+
* submitted under it. A signer built without a grant expiry (the adapter's own direct use) keeps the
|
|
443
|
+
* old, broader behaviour of refusing nothing here — the delegated tool always passes one.
|
|
444
|
+
*/
|
|
252
445
|
#assertNotRevoked() {
|
|
253
|
-
if (
|
|
446
|
+
if (this.#grantExpiresAt !== undefined &&
|
|
447
|
+
revocationRequested(this.#rig, this.#user.userId, this.#grantExpiresAt)) {
|
|
254
448
|
throw new RevocationRequestedError(this.#user.userId);
|
|
449
|
+
}
|
|
255
450
|
}
|
|
256
451
|
/**
|
|
257
452
|
* `send_evm_transaction`, under delegation: only a plan `assertPlanSignableBy` accepts for THIS user,
|
|
258
|
-
* re-checked before every step
|
|
259
|
-
*
|
|
453
|
+
* re-checked before every step — together with the caller's async `beforeEachStep`, which re-reads the
|
|
454
|
+
* GRANT and its recorded per-swap cap — AND again immediately before each hand-off to CDP (after the
|
|
455
|
+
* nonce / fee / gas round trips, so an artifact that expires during preparation is never submitted). The plan
|
|
260
456
|
* halts at the first step that does not succeed; after ANY failure a bounded `approve(spender, 0)` is
|
|
261
457
|
* attempted under the same policy and its real outcome — confirmed by an allowance read-back — is
|
|
262
458
|
* reported. A failed reset may leave a pre-existing allowance, so step 0 is not exempt.
|
|
@@ -272,27 +468,61 @@ export class CdpDelegatedSigner {
|
|
|
272
468
|
try {
|
|
273
469
|
this.#assertNotRevoked();
|
|
274
470
|
assertPlanSignableBy(plan, this, Date.now());
|
|
471
|
+
// The caller's async re-check: the grant is still the one this plan was cleared against, and its
|
|
472
|
+
// recorded cap still admits it. Runs before EVERY step, so a delegation replaced mid-plan stops
|
|
473
|
+
// the remaining ones rather than being carried by the check made before step 0.
|
|
474
|
+
await this.#beforeEachStep?.();
|
|
275
475
|
}
|
|
276
476
|
catch (error) {
|
|
477
|
+
// Before step 0 no transaction has been built, journaled, or handed to CDP. Propagate the
|
|
478
|
+
// refusal directly so the caller's zero-step finalizer can release the exact claim. Trying an
|
|
479
|
+
// allowance clear here would create the first ambiguous request under a grant that may already
|
|
480
|
+
// have been revoked, turning provable no-send into an unnecessary unknown operation.
|
|
481
|
+
if (index === 0)
|
|
482
|
+
throw error;
|
|
277
483
|
throw await this.#halt(plan, index, `plan no longer signable before step ${index}: ${error instanceof Error ? error.message : String(error)}`, haltCauseOf(error));
|
|
278
484
|
}
|
|
279
485
|
let hash;
|
|
280
486
|
let receipt;
|
|
281
487
|
try {
|
|
282
|
-
|
|
488
|
+
const submitted = await this.#submit(step, () => {
|
|
283
489
|
this.#assertNotRevoked();
|
|
284
490
|
assertPlanSignableBy(plan, this, Date.now());
|
|
285
|
-
});
|
|
491
|
+
}, { index, kind: step.kind });
|
|
492
|
+
hash = submitted.hash;
|
|
286
493
|
await this.#raw.pub.waitForTransactionReceipt({ hash }); // first sight (may be a preconfirmation) …
|
|
287
494
|
// … then only the SEALED, CANONICAL receipt counts: the next step is estimated against sealed state,
|
|
288
495
|
// and a preconfirmation omitted from the sealed block is never reported as success.
|
|
289
496
|
receipt = await waitForSealedReceipt(this.#raw.pub, hash, this.#sealedWait);
|
|
497
|
+
if (this.#submissionJournal && submitted.idempotencyKey) {
|
|
498
|
+
await this.#submissionJournal.receipt(index, submitted.idempotencyKey, {
|
|
499
|
+
transactionHash: hash,
|
|
500
|
+
status: receipt.status,
|
|
501
|
+
blockNumber: receipt.blockNumber.toString(),
|
|
502
|
+
blockHash: receipt.blockHash,
|
|
503
|
+
gasUsed: receipt.gasUsed.toString(),
|
|
504
|
+
});
|
|
505
|
+
}
|
|
290
506
|
}
|
|
291
507
|
catch (error) {
|
|
508
|
+
const cause = error instanceof DelegatedPreHandoffError ? error.original : error;
|
|
509
|
+
if (index === 0 && error instanceof DelegatedPreHandoffError)
|
|
510
|
+
throw cause;
|
|
511
|
+
/**
|
|
512
|
+
* 🔴 A REVERTING ESTIMATE BEFORE THE FIRST STEP IS A REFUSAL, NOT A HALT. Nothing of this plan has
|
|
513
|
+
* reached the chain, so there is no allowance of ours to clear and no reason to spend the user's gas
|
|
514
|
+
* on a clear; the caller gets the typed reason and may prepare again. From step 1 on, an earlier step
|
|
515
|
+
* has landed (an allowance may stand), so the halt path runs and the cause is `would_revert`.
|
|
516
|
+
*/
|
|
517
|
+
if ((cause instanceof DelegatedTransactionWouldRevertError ||
|
|
518
|
+
cause instanceof DelegatedTransactionEstimateUnavailableError) &&
|
|
519
|
+
index === 0) {
|
|
520
|
+
throw cause;
|
|
521
|
+
}
|
|
292
522
|
// The CAUSE is classified from the error's structure (`haltCauseOf`); the message carries the upstream
|
|
293
523
|
// text for the operator and is never what a consumer classifies on.
|
|
294
|
-
const errorType =
|
|
295
|
-
throw await this.#halt(plan, index, `delegated step ${index} (${step.kind}) failed: ${typeof errorType === 'string' && errorType ? `${errorType} | ` : ''}${
|
|
524
|
+
const errorType = cause.errorType;
|
|
525
|
+
throw await this.#halt(plan, index, `delegated step ${index} (${step.kind}) failed: ${typeof errorType === 'string' && errorType ? `${errorType} | ` : ''}${cause instanceof Error ? cause.message : String(cause)}`, haltCauseOf(cause));
|
|
296
526
|
}
|
|
297
527
|
if (receipt.status !== 'success') {
|
|
298
528
|
throw await this.#halt(plan, index, `delegated step ${index} (${step.kind}) reverted on-chain: ${hash}`, 'reverted');
|
|
@@ -312,21 +542,32 @@ export class CdpDelegatedSigner {
|
|
|
312
542
|
}
|
|
313
543
|
/**
|
|
314
544
|
* After ANY failure: attempt `approve(spender, 0)` under the delegation, then READ the allowance back.
|
|
315
|
-
* `allowanceCleared` is true only when the read-back is zero — a mined clear whose read-back is
|
|
316
|
-
* zero, a reverted clear, a failed submission or a failed read all report false with the hand-off.
|
|
545
|
+
* `allowanceCleared` is true only when the live read-back is zero — a mined clear whose read-back is
|
|
546
|
+
* not zero, a reverted clear, a failed submission or a failed read all report false with the hand-off.
|
|
547
|
+
* This is operator context, not finality or durable-claim release evidence.
|
|
317
548
|
*/
|
|
318
549
|
async #halt(plan, failedStep, message, cause) {
|
|
319
550
|
const handOff = `clear approve(${plan.spender}, 0) on ${plan.token} BEFORE revoking the delegation`;
|
|
320
551
|
let hash;
|
|
321
552
|
try {
|
|
322
|
-
|
|
553
|
+
const submitted = await this.#submit({
|
|
323
554
|
to: plan.token,
|
|
324
555
|
value: 0n,
|
|
325
556
|
data: encodeFunctionData({ abi: ERC20_APPROVE_ABI, functionName: 'approve', args: [plan.spender, 0n] }),
|
|
326
|
-
});
|
|
557
|
+
}, undefined, { index: 3, kind: 'allowance_clear' });
|
|
558
|
+
hash = submitted.hash;
|
|
327
559
|
await this.#raw.pub.waitForTransactionReceipt({ hash });
|
|
328
560
|
// The read-back below must see the clear: only its sealed canonical receipt counts.
|
|
329
561
|
const clearReceipt = await waitForSealedReceipt(this.#raw.pub, hash, this.#sealedWait);
|
|
562
|
+
if (this.#submissionJournal && submitted.idempotencyKey) {
|
|
563
|
+
await this.#submissionJournal.receipt(3, submitted.idempotencyKey, {
|
|
564
|
+
transactionHash: hash,
|
|
565
|
+
status: clearReceipt.status,
|
|
566
|
+
blockNumber: clearReceipt.blockNumber.toString(),
|
|
567
|
+
blockHash: clearReceipt.blockHash,
|
|
568
|
+
gasUsed: clearReceipt.gasUsed.toString(),
|
|
569
|
+
});
|
|
570
|
+
}
|
|
330
571
|
if (clearReceipt.status !== 'success') {
|
|
331
572
|
return new DelegatedPlanHaltedError(`${message}; allowance clear ${hash} reverted — ${handOff}`, failedStep, false, cause, hash);
|
|
332
573
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* evm-call-failure.ts — ONE classifier for "the node did not execute this call", shared by the delegated
|
|
3
|
+
* signer's gas estimate and the constructor's route simulation.
|
|
4
|
+
*
|
|
5
|
+
* 🔴 TWO DIFFERENT FACTS HIDE BEHIND ONE THROW (gate r7, findings 2 and 3). `eth_estimateGas` and
|
|
6
|
+
* `eth_call` reject for a transaction that WOULD REVERT — and also for a node that timed out, rate-limited
|
|
7
|
+
* (429), or dropped the connection. The first is proof about the transaction: never broadcast it, never
|
|
8
|
+
* prepare that route. The second proves nothing about the transaction: refusing to broadcast without an
|
|
9
|
+
* estimate is still right, but it is not a revert, it is retriable, and it must not blacklist a healthy
|
|
10
|
+
* route or deny a person a working plan. Calling a timeout `would_revert` is the error this module exists
|
|
11
|
+
* to prevent.
|
|
12
|
+
*
|
|
13
|
+
* viem wraps errors in a `cause` chain; the revert lives on an inner node (`ExecutionRevertedError`,
|
|
14
|
+
* `ContractFunctionRevertedError`, or a node whose `details` carry the node's "execution reverted …"
|
|
15
|
+
* text). Transport lives on the transport CLASSES (`HttpRequestError`, `TimeoutError`, socket/websocket,
|
|
16
|
+
* `LimitExceededRpcError`, …); `RpcRequestError` is a NODE answer, not transport. Anything not PROVEN a
|
|
17
|
+
* revert is `unavailable` — the conservative side.
|
|
18
|
+
*
|
|
19
|
+
* Nothing here returns a URL: viem's `message` embeds the RPC request (whose path carries an API key);
|
|
20
|
+
* only `details`/`shortMessage` survive, with any URL that slipped through replaced.
|
|
21
|
+
*/
|
|
22
|
+
export type EvmCallFailure = 'revert' | 'unavailable';
|
|
23
|
+
/**
|
|
24
|
+
* `revert` only when some node in the cause chain PROVES it AND no node in the chain is transport
|
|
25
|
+
* evidence; everything else is `unavailable`.
|
|
26
|
+
*/
|
|
27
|
+
export declare function classifyEvmCallFailure(error: unknown): EvmCallFailure;
|
|
28
|
+
/** A short, URL-free reason for the caller: the node's `details`, else the class, else the message. */
|
|
29
|
+
export declare function evmCallFailureDetail(error: unknown): string;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* evm-call-failure.ts — ONE classifier for "the node did not execute this call", shared by the delegated
|
|
3
|
+
* signer's gas estimate and the constructor's route simulation.
|
|
4
|
+
*
|
|
5
|
+
* 🔴 TWO DIFFERENT FACTS HIDE BEHIND ONE THROW (gate r7, findings 2 and 3). `eth_estimateGas` and
|
|
6
|
+
* `eth_call` reject for a transaction that WOULD REVERT — and also for a node that timed out, rate-limited
|
|
7
|
+
* (429), or dropped the connection. The first is proof about the transaction: never broadcast it, never
|
|
8
|
+
* prepare that route. The second proves nothing about the transaction: refusing to broadcast without an
|
|
9
|
+
* estimate is still right, but it is not a revert, it is retriable, and it must not blacklist a healthy
|
|
10
|
+
* route or deny a person a working plan. Calling a timeout `would_revert` is the error this module exists
|
|
11
|
+
* to prevent.
|
|
12
|
+
*
|
|
13
|
+
* viem wraps errors in a `cause` chain; the revert lives on an inner node (`ExecutionRevertedError`,
|
|
14
|
+
* `ContractFunctionRevertedError`, or a node whose `details` carry the node's "execution reverted …"
|
|
15
|
+
* text). Transport lives on the transport CLASSES (`HttpRequestError`, `TimeoutError`, socket/websocket,
|
|
16
|
+
* `LimitExceededRpcError`, …); `RpcRequestError` is a NODE answer, not transport. Anything not PROVEN a
|
|
17
|
+
* revert is `unavailable` — the conservative side.
|
|
18
|
+
*
|
|
19
|
+
* Nothing here returns a URL: viem's `message` embeds the RPC request (whose path carries an API key);
|
|
20
|
+
* only `details`/`shortMessage` survive, with any URL that slipped through replaced.
|
|
21
|
+
*/
|
|
22
|
+
const REVERT_ERROR_NAMES = new Set([
|
|
23
|
+
'ExecutionRevertedError',
|
|
24
|
+
'ContractFunctionRevertedError',
|
|
25
|
+
]);
|
|
26
|
+
/** The node's own revert text, in every phrasing seen from Base RPCs. */
|
|
27
|
+
const REVERT_DETAIL_PATTERN = /execution reverted|\brevert(ed)?\b|out of gas|invalid opcode/i;
|
|
28
|
+
/**
|
|
29
|
+
* 🔴 AFFIRMATIVE TRANSPORT EVIDENCE DOMINATES (gate r7 round 2). viem synthesises `ExecutionRevertedError`
|
|
30
|
+
* from any `details` text that looks like a revert — including a body an HTTP 429 happened to carry — so
|
|
31
|
+
* a chain can read `CallExecutionError → ExecutionRevertedError → HttpRequestError(status 429)`. That
|
|
32
|
+
* proves rate limiting, not a revert. The whole chain is scanned for a transport CLASS first; only a chain
|
|
33
|
+
* with none may be called a revert.
|
|
34
|
+
*
|
|
35
|
+
* 🔴 BY CLASS, NEVER BY A FIELD OF A RAW OBJECT (gate r7 rounds 3 and 4). viem keeps the raw JSON-RPC
|
|
36
|
+
* error object as `RpcRequestError.cause`, and a node can put ANYTHING in it: a stray `status: 503`
|
|
37
|
+
* (round 3) or a fabricated `name: "HttpRequestError"` (round 4) beside a code-3 "execution reverted".
|
|
38
|
+
* Both are PROVEN reverts; calling either transport would have let the halt path's clear broadcast on its
|
|
39
|
+
* static limit against a proven revert. So transport evidence is accepted ONLY from an actual `Error`
|
|
40
|
+
* instance whose name is one of viem's transport classes — a plain object can never impersonate one —
|
|
41
|
+
* plus the one node-level form viem itself treats as rate limiting: an actual `RpcRequestError` whose
|
|
42
|
+
* JSON-RPC `code` is 429 (HTTP 200), whatever its text says. Nothing else is transport.
|
|
43
|
+
*/
|
|
44
|
+
const TRANSPORT_ERROR_NAMES = new Set([
|
|
45
|
+
'HttpRequestError',
|
|
46
|
+
'TimeoutError',
|
|
47
|
+
'WebSocketRequestError',
|
|
48
|
+
'SocketClosedError',
|
|
49
|
+
'LimitExceededRpcError',
|
|
50
|
+
'ResourceUnavailableRpcError',
|
|
51
|
+
]);
|
|
52
|
+
/** JSON-RPC's rate-limit code, which viem retries on and which is never a statement about execution. */
|
|
53
|
+
const JSON_RPC_RATE_LIMITED = 429;
|
|
54
|
+
function* chain(error) {
|
|
55
|
+
let node = error;
|
|
56
|
+
for (let depth = 0; depth < 12 && node && typeof node === 'object'; depth += 1) {
|
|
57
|
+
yield node;
|
|
58
|
+
node = node.cause;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function isTransport(node) {
|
|
62
|
+
// A raw JSON object hanging off `RpcRequestError.cause` is never an Error, so it can claim nothing here.
|
|
63
|
+
if (!(node instanceof Error))
|
|
64
|
+
return false;
|
|
65
|
+
if (TRANSPORT_ERROR_NAMES.has(node.name))
|
|
66
|
+
return true;
|
|
67
|
+
return node.name === 'RpcRequestError' && node.code === JSON_RPC_RATE_LIMITED;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* `revert` only when some node in the cause chain PROVES it AND no node in the chain is transport
|
|
71
|
+
* evidence; everything else is `unavailable`.
|
|
72
|
+
*/
|
|
73
|
+
export function classifyEvmCallFailure(error) {
|
|
74
|
+
const nodes = [...chain(error)];
|
|
75
|
+
if (nodes.some(isTransport))
|
|
76
|
+
return 'unavailable';
|
|
77
|
+
for (const node of nodes) {
|
|
78
|
+
// A revert CLASS counts only on an actual Error instance too; the node's own text counts wherever it is.
|
|
79
|
+
if (node instanceof Error && REVERT_ERROR_NAMES.has(node.name))
|
|
80
|
+
return 'revert';
|
|
81
|
+
const details = node.details;
|
|
82
|
+
if (typeof details === 'string' && REVERT_DETAIL_PATTERN.test(details))
|
|
83
|
+
return 'revert';
|
|
84
|
+
}
|
|
85
|
+
return 'unavailable';
|
|
86
|
+
}
|
|
87
|
+
/** A short, URL-free reason for the caller: the node's `details`, else the class, else the message. */
|
|
88
|
+
export function evmCallFailureDetail(error) {
|
|
89
|
+
let text = '';
|
|
90
|
+
for (const node of chain(error)) {
|
|
91
|
+
if (typeof node.details === 'string' && node.details.trim()) {
|
|
92
|
+
text = node.details;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (!text) {
|
|
97
|
+
const top = error;
|
|
98
|
+
text =
|
|
99
|
+
typeof top?.shortMessage === 'string' && top.shortMessage
|
|
100
|
+
? top.shortMessage
|
|
101
|
+
: typeof top?.name === 'string' && top.name !== 'Error'
|
|
102
|
+
? top.name
|
|
103
|
+
: typeof top?.message === 'string'
|
|
104
|
+
? top.message
|
|
105
|
+
: String(error);
|
|
106
|
+
}
|
|
107
|
+
return text.replace(/https?:\/\/\S+/g, '[url]').slice(0, 200);
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=evm-call-failure.js.map
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* `errorType` mirrors `code` so the CDP kit's ladder — which only grades a *structured* refusal as
|
|
9
9
|
* the fence biting — recognises adapter refusals without a second grading path.
|
|
10
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"];
|
|
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", "DELEGATED_TX_WOULD_REVERT", "DELEGATED_TX_ESTIMATE_UNAVAILABLE", "INTENT_MISMATCH", "MIN_OUTPUT_TOO_LOW", "ATTRIBUTION_MISSING", "ATTRIBUTION_MISMATCH", "FEE_CAP_EXCEEDED"];
|
|
12
12
|
export type AdapterRefusalCode = (typeof ADAPTER_REFUSAL_CODES)[number];
|
|
13
13
|
export declare class AdapterRefusal extends Error {
|
|
14
14
|
readonly code: AdapterRefusalCode;
|
package/dist/adapter/refusal.js
CHANGED
|
@@ -24,6 +24,19 @@ export const ADAPTER_REFUSAL_CODES = [
|
|
|
24
24
|
'TARGET_NOT_ALLOWED',
|
|
25
25
|
'SWAP_CALLDATA_INVALID',
|
|
26
26
|
'RECEIVER_MISMATCH',
|
|
27
|
+
/**
|
|
28
|
+
* The node's gas estimate REVERTED for a plan step, so the transaction would revert on chain: it is not
|
|
29
|
+
* broadcast (a knowingly-reverting broadcast spends the user's gas and buys nothing). Raised by the
|
|
30
|
+
* delegated signer; surfaced to a delegated caller as `DELEGATED_PLAN_REFUSED [DELEGATED_TX_WOULD_REVERT]`
|
|
31
|
+
* when nothing of the plan had reached the chain, or as the cause of `DELEGATED_PLAN_HALTED` otherwise.
|
|
32
|
+
*/
|
|
33
|
+
'DELEGATED_TX_WOULD_REVERT',
|
|
34
|
+
/**
|
|
35
|
+
* The node could not estimate a plan step at all — a timeout, a rate limit, a dropped connection —
|
|
36
|
+
* so nothing was proven about the transaction and nothing is broadcast without an estimate. Distinct
|
|
37
|
+
* from `DELEGATED_TX_WOULD_REVERT` on purpose: this one is RETRIABLE and blacklists nothing.
|
|
38
|
+
*/
|
|
39
|
+
'DELEGATED_TX_ESTIMATE_UNAVAILABLE',
|
|
27
40
|
// Intent binding — the artifact must be the trade the USER asked for
|
|
28
41
|
'INTENT_MISMATCH',
|
|
29
42
|
'MIN_OUTPUT_TOO_LOW',
|