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.
Files changed (239) hide show
  1. package/README.md +76 -18
  2. package/dist/account-userop-policy-definition.d.ts +14 -0
  3. package/dist/account-userop-policy-definition.js +12 -0
  4. package/dist/account-userop-policy-relation.d.ts +371 -0
  5. package/dist/account-userop-policy-relation.js +87 -0
  6. package/dist/adapter/cdp-signer.d.ts +76 -9
  7. package/dist/adapter/cdp-signer.js +274 -33
  8. package/dist/adapter/evm-call-failure.d.ts +29 -0
  9. package/dist/adapter/evm-call-failure.js +109 -0
  10. package/dist/adapter/refusal.d.ts +1 -1
  11. package/dist/adapter/refusal.js +13 -0
  12. package/dist/adapter/route-simulator.d.ts +91 -0
  13. package/dist/adapter/route-simulator.js +143 -0
  14. package/dist/adapter-cdp-index.d.ts +1 -1
  15. package/dist/executable-equities.d.ts +55 -0
  16. package/dist/executable-equities.js +116 -0
  17. package/dist/executable-yield-markets.d.ts +261 -0
  18. package/dist/executable-yield-markets.js +154 -0
  19. package/dist/execution-account-gas-quote.d.ts +141 -0
  20. package/dist/execution-account-gas-quote.js +443 -0
  21. package/dist/execution-account-gas-review.d.ts +123 -0
  22. package/dist/execution-account-gas-review.js +104 -0
  23. package/dist/execution-account-gas-runtime.d.ts +33 -0
  24. package/dist/execution-account-gas-runtime.js +131 -0
  25. package/dist/execution-account-gas-view.d.ts +936 -0
  26. package/dist/execution-account-gas-view.js +56 -0
  27. package/dist/execution-config.d.ts +3 -0
  28. package/dist/execution-config.js +4 -0
  29. package/dist/execution-delegated-definition.d.ts +7 -6
  30. package/dist/execution-delegated-definition.js +13 -2
  31. package/dist/execution-delegation-admin-definition.d.ts +127 -5
  32. package/dist/execution-delegation-admin-definition.js +155 -5
  33. package/dist/execution-delegation-admin.d.ts +48 -2
  34. package/dist/execution-delegation-admin.js +422 -12
  35. package/dist/execution-delegation-cap-store.d.ts +386 -0
  36. package/dist/execution-delegation-cap-store.js +1150 -0
  37. package/dist/execution-delegation-exit-definition.d.ts +309 -0
  38. package/dist/execution-delegation-exit-definition.js +204 -0
  39. package/dist/execution-delegation-exit-store.d.ts +114 -0
  40. package/dist/execution-delegation-exit-store.js +671 -0
  41. package/dist/execution-delegation-exit.d.ts +4 -0
  42. package/dist/execution-delegation-exit.js +614 -0
  43. package/dist/execution-delegation-finality.d.ts +22 -0
  44. package/dist/execution-delegation-finality.js +90 -0
  45. package/dist/execution-delegation-operation-store.d.ts +129 -0
  46. package/dist/execution-delegation-operation-store.js +1107 -0
  47. package/dist/execution-delegation-operations-definition.d.ts +282 -0
  48. package/dist/execution-delegation-operations-definition.js +117 -0
  49. package/dist/execution-delegation-operations.d.ts +6 -0
  50. package/dist/execution-delegation-operations.js +282 -0
  51. package/dist/execution-delegation-policy.d.ts +154 -8
  52. package/dist/execution-delegation-policy.js +273 -40
  53. package/dist/execution-delegation.d.ts +51 -0
  54. package/dist/execution-delegation.js +441 -31
  55. package/dist/execution-errors.d.ts +1 -1
  56. package/dist/execution-errors.js +122 -0
  57. package/dist/execution-index.d.ts +7 -2
  58. package/dist/execution-index.js +7 -2
  59. package/dist/execution-journal-schema.d.ts +96 -0
  60. package/dist/execution-journal-schema.js +368 -0
  61. package/dist/execution-journal-tables.d.ts +14 -0
  62. package/dist/execution-journal-tables.js +227 -0
  63. package/dist/execution-policy-compatibility.d.ts +34 -0
  64. package/dist/execution-policy-compatibility.js +76 -0
  65. package/dist/execution-registration.d.ts +54 -2
  66. package/dist/execution-registration.js +151 -5
  67. package/dist/execution-sponsored-account-profile.d.ts +32 -0
  68. package/dist/execution-sponsored-account-profile.js +59 -0
  69. package/dist/execution-sponsored-account.d.ts +1519 -0
  70. package/dist/execution-sponsored-account.js +222 -0
  71. package/dist/execution-sponsored-definition.d.ts +1713 -0
  72. package/dist/execution-sponsored-definition.js +185 -0
  73. package/dist/execution-sponsored-gas-policy.d.ts +27 -0
  74. package/dist/execution-sponsored-gas-policy.js +110 -0
  75. package/dist/execution-sponsored-hosted.d.ts +3 -0
  76. package/dist/execution-sponsored-hosted.js +71 -0
  77. package/dist/execution-sponsored-index.d.ts +1322 -0
  78. package/dist/execution-sponsored-index.js +102 -0
  79. package/dist/execution-sponsored-mcp-definition.d.ts +1662 -0
  80. package/dist/execution-sponsored-mcp-definition.js +94 -0
  81. package/dist/execution-sponsored-receipt.d.ts +47 -0
  82. package/dist/execution-sponsored-receipt.js +74 -0
  83. package/dist/execution-sponsored-registration.d.ts +14 -0
  84. package/dist/execution-sponsored-registration.js +77 -0
  85. package/dist/execution-sponsored-runtime.d.ts +84 -0
  86. package/dist/execution-sponsored-runtime.js +149 -0
  87. package/dist/execution-sponsored-settlement.d.ts +15 -0
  88. package/dist/execution-sponsored-settlement.js +88 -0
  89. package/dist/execution-sponsored-store.d.ts +67 -0
  90. package/dist/execution-sponsored-store.js +517 -0
  91. package/dist/execution-sponsored-transport.d.ts +90 -0
  92. package/dist/execution-sponsored-transport.js +180 -0
  93. package/dist/execution-tool-definitions.d.ts +21 -21
  94. package/dist/execution-tool-definitions.js +0 -7
  95. package/dist/execution-tools.d.ts +53 -1
  96. package/dist/execution-tools.js +171 -13
  97. package/dist/execution-worker-guard.d.ts +28 -0
  98. package/dist/execution-worker-guard.js +124 -0
  99. package/dist/hybrid-yield-index.d.ts +10 -0
  100. package/dist/hybrid-yield-index.js +11 -0
  101. package/dist/hybrid-yield-markets.d.ts +96 -0
  102. package/dist/hybrid-yield-markets.js +124 -0
  103. package/dist/hybrid-yield.d.ts +411 -0
  104. package/dist/hybrid-yield.js +155 -0
  105. package/dist/hyperliquid-order-builder.js +4 -0
  106. package/dist/lifi-execution-client.d.ts +6 -0
  107. package/dist/lifi-execution-client.js +3 -0
  108. package/dist/lp-userop-account.d.ts +461 -0
  109. package/dist/lp-userop-account.js +417 -0
  110. package/dist/lp-userop-authority.d.ts +49 -0
  111. package/dist/lp-userop-authority.js +211 -0
  112. package/dist/lp-userop-definition.d.ts +233 -0
  113. package/dist/lp-userop-definition.js +85 -0
  114. package/dist/lp-userop-hosted.d.ts +3 -0
  115. package/dist/lp-userop-hosted.js +73 -0
  116. package/dist/lp-userop-index.d.ts +1569 -0
  117. package/dist/lp-userop-index.js +112 -0
  118. package/dist/lp-userop-mcp-definition.d.ts +1220 -0
  119. package/dist/lp-userop-mcp-definition.js +46 -0
  120. package/dist/lp-userop-operation.d.ts +1230 -0
  121. package/dist/lp-userop-operation.js +140 -0
  122. package/dist/lp-userop-registration.d.ts +14 -0
  123. package/dist/lp-userop-registration.js +70 -0
  124. package/dist/lp-userop-settlement.d.ts +89 -0
  125. package/dist/lp-userop-settlement.js +184 -0
  126. package/dist/lp-userop.d.ts +643 -0
  127. package/dist/lp-userop.js +343 -0
  128. package/dist/native-yield-account-definition.d.ts +1192 -0
  129. package/dist/native-yield-account-definition.js +232 -0
  130. package/dist/native-yield-account-gas.d.ts +58 -0
  131. package/dist/native-yield-account-gas.js +228 -0
  132. package/dist/native-yield-account-index.d.ts +23 -0
  133. package/dist/native-yield-account-index.js +28 -0
  134. package/dist/native-yield-account-policy.d.ts +168 -0
  135. package/dist/native-yield-account-policy.js +525 -0
  136. package/dist/native-yield-account-rpc.d.ts +8 -0
  137. package/dist/native-yield-account-rpc.js +128 -0
  138. package/dist/native-yield-account-runtime.d.ts +71 -0
  139. package/dist/native-yield-account-runtime.js +125 -0
  140. package/dist/native-yield-account-store.d.ts +40 -0
  141. package/dist/native-yield-account-store.js +307 -0
  142. package/dist/native-yield-account.d.ts +662 -0
  143. package/dist/native-yield-account.js +371 -0
  144. package/dist/native-yield-consent-store.d.ts +20 -0
  145. package/dist/native-yield-consent-store.js +317 -0
  146. package/dist/native-yield-definition.d.ts +288 -0
  147. package/dist/native-yield-definition.js +105 -0
  148. package/dist/native-yield-reader.d.ts +4 -0
  149. package/dist/native-yield-reader.js +137 -0
  150. package/dist/native-yield-settlement.d.ts +48 -0
  151. package/dist/native-yield-settlement.js +167 -0
  152. package/dist/native-yield-simulation.d.ts +18 -0
  153. package/dist/native-yield-simulation.js +45 -0
  154. package/dist/native-yield-userop-account.d.ts +675 -0
  155. package/dist/native-yield-userop-account.js +357 -0
  156. package/dist/native-yield-userop-authority.d.ts +214 -0
  157. package/dist/native-yield-userop-authority.js +296 -0
  158. package/dist/native-yield-userop-definition.d.ts +331 -0
  159. package/dist/native-yield-userop-definition.js +105 -0
  160. package/dist/native-yield-userop-hosted.d.ts +3 -0
  161. package/dist/native-yield-userop-hosted.js +71 -0
  162. package/dist/native-yield-userop-index.d.ts +1522 -0
  163. package/dist/native-yield-userop-index.js +142 -0
  164. package/dist/native-yield-userop-mcp-definition.d.ts +1356 -0
  165. package/dist/native-yield-userop-mcp-definition.js +50 -0
  166. package/dist/native-yield-userop-operation.d.ts +845 -0
  167. package/dist/native-yield-userop-operation.js +127 -0
  168. package/dist/native-yield-userop-policy.d.ts +23 -0
  169. package/dist/native-yield-userop-policy.js +25 -0
  170. package/dist/native-yield-userop-registration.d.ts +17 -0
  171. package/dist/native-yield-userop-registration.js +76 -0
  172. package/dist/native-yield-userop-settlement.d.ts +8 -0
  173. package/dist/native-yield-userop-settlement.js +79 -0
  174. package/dist/native-yield-userop-simulation.d.ts +18 -0
  175. package/dist/native-yield-userop-simulation.js +34 -0
  176. package/dist/native-yield-userop.d.ts +35 -0
  177. package/dist/native-yield-userop.js +156 -0
  178. package/dist/native-yield-verify.d.ts +30 -0
  179. package/dist/native-yield-verify.js +141 -0
  180. package/dist/native-yield.d.ts +17 -0
  181. package/dist/native-yield.js +119 -0
  182. package/dist/stdio-main.js +0 -0
  183. package/dist/stock-buy-definition.d.ts +338 -0
  184. package/dist/stock-buy-definition.js +127 -0
  185. package/dist/stock-buy-oracle.d.ts +8 -0
  186. package/dist/stock-buy-oracle.js +22 -0
  187. package/dist/stock-buy.d.ts +17 -0
  188. package/dist/stock-buy.js +164 -0
  189. package/dist/tool-definitions.d.ts +3 -1
  190. package/dist/tool-definitions.js +55 -2
  191. package/dist/vendor/lp-v3/lp-v3-atomic-contract.d.ts +445 -0
  192. package/dist/vendor/lp-v3/lp-v3-atomic-contract.js +90 -0
  193. package/dist/vendor/lp-v3/lp-v3-atomic-evidence.d.ts +14 -0
  194. package/dist/vendor/lp-v3/lp-v3-atomic-evidence.js +82 -0
  195. package/dist/vendor/lp-v3/lp-v3-atomic-index.d.ts +80 -0
  196. package/dist/vendor/lp-v3/lp-v3-atomic-index.js +19 -0
  197. package/dist/vendor/lp-v3/lp-v3-atomic-plan.d.ts +43 -0
  198. package/dist/vendor/lp-v3/lp-v3-atomic-plan.js +112 -0
  199. package/dist/vendor/lp-v3/lp-v3-atomic-read-contract.d.ts +312 -0
  200. package/dist/vendor/lp-v3/lp-v3-atomic-read-contract.js +41 -0
  201. package/dist/vendor/lp-v3/lp-v3-atomic-read-index.d.ts +3 -0
  202. package/dist/vendor/lp-v3/lp-v3-atomic-read-index.js +3 -0
  203. package/dist/vendor/lp-v3/lp-v3-atomic-read-rpc.d.ts +49 -0
  204. package/dist/vendor/lp-v3/lp-v3-atomic-read-rpc.js +88 -0
  205. package/dist/vendor/lp-v3/lp-v3-atomic-read.d.ts +17 -0
  206. package/dist/vendor/lp-v3/lp-v3-atomic-read.js +232 -0
  207. package/dist/vendor/lp-v3/lp-v3-atomic-settlement.d.ts +105 -0
  208. package/dist/vendor/lp-v3/lp-v3-atomic-settlement.js +162 -0
  209. package/dist/vendor/lp-v3/lp-v3-collect-protocol-contract.d.ts +606 -0
  210. package/dist/vendor/lp-v3/lp-v3-collect-protocol-contract.js +59 -0
  211. package/dist/vendor/lp-v3/lp-v3-collect-protocol-index.d.ts +168 -0
  212. package/dist/vendor/lp-v3/lp-v3-collect-protocol-index.js +23 -0
  213. package/dist/vendor/lp-v3/lp-v3-collect-protocol-observation.d.ts +32 -0
  214. package/dist/vendor/lp-v3/lp-v3-collect-protocol-observation.js +228 -0
  215. package/dist/vendor/lp-v3/lp-v3-collect-protocol-plan.d.ts +11 -0
  216. package/dist/vendor/lp-v3/lp-v3-collect-protocol-plan.js +91 -0
  217. package/dist/vendor/lp-v3/lp-v3-collect-protocol-reconcile.d.ts +9 -0
  218. package/dist/vendor/lp-v3/lp-v3-collect-protocol-reconcile.js +124 -0
  219. package/dist/vendor/lp-v3/lp-v3-contract.d.ts +538 -0
  220. package/dist/vendor/lp-v3/lp-v3-contract.js +137 -0
  221. package/dist/vendor/lp-v3/lp-v3-evidence.d.ts +25 -0
  222. package/dist/vendor/lp-v3/lp-v3-evidence.js +168 -0
  223. package/dist/vendor/lp-v3/lp-v3-plan.d.ts +22 -0
  224. package/dist/vendor/lp-v3/lp-v3-plan.js +135 -0
  225. package/dist/vendor/lp-v3/types.d.ts +131 -0
  226. package/dist/vendor/lp-v3/types.js +6 -0
  227. package/dist/vendor/lp-v3/v4-math.d.ts +18 -0
  228. package/dist/vendor/lp-v3/v4-math.js +90 -0
  229. package/dist/yield-deposit-definition.d.ts +374 -0
  230. package/dist/yield-deposit-definition.js +123 -0
  231. package/dist/yield-deposit.d.ts +16 -0
  232. package/dist/yield-deposit.js +149 -0
  233. package/dist/yield-route-semantics.d.ts +27 -0
  234. package/dist/yield-route-semantics.js +71 -0
  235. package/dist/yield-withdraw-definition.d.ts +357 -0
  236. package/dist/yield-withdraw-definition.js +90 -0
  237. package/dist/yield-withdraw.d.ts +11 -0
  238. package/dist/yield-withdraw.js +122 -0
  239. 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
- /** Per rig: the users whose revocation has been REQUESTED (set before the raw revoke is issued; never cleared). */
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
- /** True once `rig.revoke(userId)` has been called on this rig, whatever it returned. */
42
- export function revocationRequested(rig, userId) {
43
- return REVOKE_REQUESTED.get(rig)?.has(userId) ?? false;
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, `allowanceCleared` says whether the
56
- * router allowance is known to be zero again. The message is for the operator; a consumer that must not
57
- * relay upstream text reads `cause` and the two fields only.
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
- requested.add(userId);
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 never existed (or is already gone) reads back as absent below; other failures surface there.
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({ userId: user.userId, address: user.address, transaction, network: NETWORK });
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
- async function buildUnsignedTransaction(pub, from, step) {
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
- // An out-of-policy transaction is still a valid transaction; a node-side estimate failure must not
205
- // be confused with the policy engine refusing. Keep a generous static limit.
206
- gas = 400000n;
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
- const transaction = await buildUnsignedTransaction(this.#raw.pub, this.#user.address, step);
248
- beforeSubmit?.();
249
- return this.#raw.submit(this.#user, transaction);
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
- /** Fail-closed: once revocation was requested for this user on this rig, nothing is submitted for them again. */
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 (revocationRequested(this.#rig, this.#user.userId))
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 AND again immediately before each hand-off to CDP (after the nonce /
259
- * fee / gas round trips, so an artifact that expires during preparation is never submitted). The plan
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
- hash = await this.#submit(step, () => {
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 = error.errorType;
295
- throw await this.#halt(plan, index, `delegated step ${index} (${step.kind}) failed: ${typeof errorType === 'string' && errorType ? `${errorType} | ` : ''}${error instanceof Error ? error.message : String(error)}`, haltCauseOf(error));
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 not
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
- hash = await this.#submit({
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;
@@ -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',