@gol/sdk 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/owner.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { concatHex, encodeAbiParameters, encodeFunctionData, getAddress, hashTypedData, keccak256, numberToHex, padHex, parseAbi, size, stringToHex, } from "viem";
2
2
  import { BASE_SEPOLIA_CHAIN_ID, ownerUserOpNonceKey, packOwnerSignature, packOwnerUserOpSignature, } from "./account-dialects.js";
3
3
  import { hashGasMandate, hashOwnerGasPolicy, } from "./gas-approval.js";
4
+ import { contractProjectId } from "./project-id.js";
5
+ import { CHARGEABLE_OUTCOMES, CORE_DOMAIN_NAME, CORE_DOMAIN_VERSION, coreDomain, GAS_FORMULA_INLINE, HOSTED_TREE_LIMITS, NATIVE_FEE_ASSET, } from "./v3.js";
4
6
  /** Circle USDC on Base Sepolia, the only transfer asset in this candidate. */
5
7
  export const BASE_SEPOLIA_USDC = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
6
8
  export const ENTRY_POINT_V07 = "0x0000000071727De22E5E9d8BAf0edAc6f37da032";
@@ -53,7 +55,37 @@ export function encodeTransferPolicy(input) {
53
55
  ...rules,
54
56
  ]).toLowerCase();
55
57
  }
56
- /** EIP-712 domain separator of the version 2 core. */
58
+ /** Owner safety actions of the core (encoding section 6.1). */
59
+ export const SAFETY_ACTION_CODES = { pause: 1, resume: 2, revoke: 3 };
60
+ export const GOL_OWNER_PAYLOAD_TYPES = {
61
+ CombinedGasApproval: [
62
+ { name: "mandateId", type: "bytes32" },
63
+ { name: "gasPolicyId", type: "bytes32" },
64
+ ],
65
+ GasRevocation: [
66
+ { name: "gasPolicyId", type: "bytes32" },
67
+ { name: "nonce", type: "uint256" },
68
+ { name: "deadline", type: "uint48" },
69
+ ],
70
+ SafetyAction: [
71
+ { name: "mandateId", type: "bytes32" },
72
+ { name: "action", type: "uint8" },
73
+ { name: "nonce", type: "uint256" },
74
+ { name: "deadline", type: "uint48" },
75
+ ],
76
+ };
77
+ /** EIP-712 struct hash of a GOL owner payload. */
78
+ export function golOwnerPayloadStructHash(payload) {
79
+ if (payload.primaryType === "CombinedGasApproval")
80
+ return combinedApprovalStructHash(payload.message.mandateId, payload.message.gasPolicyId);
81
+ if (payload.primaryType === "GasRevocation")
82
+ return revocationStructHash(payload.message.gasPolicyId, BigInt(payload.message.nonce), Number(payload.message.deadline));
83
+ const action = Number(payload.message.action);
84
+ if (action !== 1 && action !== 2 && action !== 3)
85
+ throw new Error("invalid_safety_action");
86
+ return safetyActionStructHash(payload.message.mandateId, action, BigInt(payload.message.nonce), Number(payload.message.deadline));
87
+ }
88
+ /** EIP-712 domain separator of the version 3 core. */
57
89
  export function coreDomainSeparator(chainId, core) {
58
90
  return keccak256(encodeAbiParameters([
59
91
  { type: "bytes32" },
@@ -63,8 +95,8 @@ export function coreDomainSeparator(chainId, core) {
63
95
  { type: "address" },
64
96
  ], [
65
97
  DOMAIN_TYPEHASH,
66
- keccak256(stringToHex("GOL Mandate")),
67
- keccak256(stringToHex("2")),
98
+ keccak256(stringToHex(CORE_DOMAIN_NAME)),
99
+ keccak256(stringToHex(CORE_DOMAIN_VERSION)),
68
100
  BigInt(chainId),
69
101
  core,
70
102
  ]));
@@ -94,18 +126,33 @@ const FORMAT = {
94
126
  nexus: "nexus_k1",
95
127
  kernel: "kernel_root",
96
128
  alchemy: "alchemy_entity_zero",
129
+ nexus7702: "eoa",
97
130
  };
98
131
  /**
99
132
  * Build the exact `eth_signTypedData_v4` payload whose hash the selected
100
- * account's reviewed owner path checks for a core digest.
133
+ * account's reviewed owner path checks for a core digest. A delegated EOA
134
+ * (`nexus7702`) signs the GOL payload itself, so its hash is the digest.
101
135
  */
102
136
  export function buildWalletSigningPayload(input) {
103
137
  const chainId = input.chainId ?? BASE_SEPOLIA_CHAIN_ID;
104
138
  const account = getAddress(input.account);
139
+ const structHash = golOwnerPayloadStructHash(input.payload);
105
140
  const domainSeparator = coreDomainSeparator(chainId, input.core);
106
- const digest = keccak256(concatHex(["0x1901", domainSeparator, input.structHash]));
141
+ const digest = keccak256(concatHex(["0x1901", domainSeparator, structHash]));
107
142
  let typedData;
108
- if (input.family === "safe") {
143
+ if (input.family === "nexus7702") {
144
+ typedData = {
145
+ domain: coreDomain(chainId, getAddress(input.core)),
146
+ types: {
147
+ [input.payload.primaryType]: [
148
+ ...GOL_OWNER_PAYLOAD_TYPES[input.payload.primaryType],
149
+ ],
150
+ },
151
+ primaryType: input.payload.primaryType,
152
+ message: { ...input.payload.message },
153
+ };
154
+ }
155
+ else if (input.family === "safe") {
109
156
  typedData = {
110
157
  domain: { chainId, verifyingContract: account },
111
158
  types: { SafeMessage: [{ name: "message", type: "bytes" }] },
@@ -126,7 +173,7 @@ export function buildWalletSigningPayload(input) {
126
173
  types: { PersonalSign: [{ name: "prefixed", type: "bytes" }] },
127
174
  primaryType: "PersonalSign",
128
175
  message: {
129
- prefixed: concatHex(["0x1901", domainSeparator, input.structHash]),
176
+ prefixed: concatHex(["0x1901", domainSeparator, structHash]),
130
177
  },
131
178
  };
132
179
  }
@@ -155,11 +202,14 @@ export function buildWalletSigningPayload(input) {
155
202
  message: { hash: digest },
156
203
  };
157
204
  }
205
+ const expectedHash = hashWalletTypedData(typedData);
206
+ if (input.family === "nexus7702" && expectedHash !== digest)
207
+ throw new Error("wallet_payload_mismatch");
158
208
  return {
159
209
  family: input.family,
160
210
  method: "eth_signTypedData_v4",
161
211
  typedData,
162
- expectedHash: hashWalletTypedData(typedData),
212
+ expectedHash,
163
213
  digest,
164
214
  signatureFormat: FORMAT[input.family],
165
215
  };
@@ -193,33 +243,41 @@ export async function signWalletSigningPayload(signer, payload) {
193
243
  // Core calls
194
244
  // ---------------------------------------------------------------------------
195
245
  const coreAbi = parseAbi([
196
- "function createMandateWithGas((bytes32 parentId,address account,address agent,bytes32 accountProfileId,uint64 installEpoch,bytes32 projectId,uint8 mode,uint8 policyVersion,bytes32 policyHash,uint48 validFrom,uint48 expiresAt,uint32 maxDepth,uint32 maxActiveDescendants,uint48 approvalExpiresAt,bytes32 salt) terms,bytes policy,(bytes32 mandateId,address account,bytes32 projectId,bytes32 accountProfileId,uint64 installEpoch,address feeAsset,address recipient,address attester,uint128 maxPerAction,uint128 maxTotal,uint48 expiresAt,uint8 chargeableOutcomes,uint8 formulaVersion,uint256 nonce) gasPolicy,bytes ownerSignature) returns (bytes32 mandateId,bytes32 gasPolicyId)",
246
+ "function createMandateWithGas((bytes32 parentId,address account,address agent,bytes32 accountProfileId,uint64 installEpoch,bytes32 projectId,uint8 mode,uint8 policyVersion,bytes32 policyHash,uint48 validFrom,uint48 expiresAt,uint32 maxDepth,uint32 maxActiveDescendants,uint48 approvalExpiresAt,bytes32 salt) terms,bytes policy,(bytes32 mandateId,address account,bytes32 projectId,bytes32 accountProfileId,uint64 installEpoch,address feeAsset,address submitter,address recipient,address attester,uint128 maxPerAction,uint128 maxTotal,uint48 expiresAt,uint8 chargeableOutcomes,uint8 formulaVersion,uint256 nonce) gasPolicy,bytes ownerSignature) returns (bytes32 mandateId,bytes32 gasPolicyId)",
247
+ "function createChildMandate((bytes32 parentId,address account,address agent,bytes32 accountProfileId,uint64 installEpoch,bytes32 projectId,uint8 mode,uint8 policyVersion,bytes32 policyHash,uint48 validFrom,uint48 expiresAt,uint32 maxDepth,uint32 maxActiveDescendants,uint48 approvalExpiresAt,bytes32 salt) terms,bytes policy,bytes authorization) returns (bytes32 mandateId)",
197
248
  "function revokeGasPolicy(bytes32 policyId,uint48 deadline,bytes signature)",
198
249
  "function safetyAction(bytes32 mandateId,uint8 action,uint48 deadline,bytes signature)",
199
250
  ]);
200
251
  /** Calldata for the core. Anyone may send it; authority comes only from the owner signature. */
252
+ function termsArgument(terms) {
253
+ return {
254
+ parentId: terms.parentId,
255
+ account: terms.account,
256
+ agent: terms.agent,
257
+ accountProfileId: terms.accountProfileId,
258
+ installEpoch: BigInt(terms.installEpoch),
259
+ projectId: terms.projectId,
260
+ mode: Number(terms.mode),
261
+ policyVersion: Number(terms.policyVersion),
262
+ policyHash: terms.policyHash,
263
+ validFrom: Number(terms.validFrom),
264
+ expiresAt: Number(terms.expiresAt),
265
+ maxDepth: Number(terms.maxDepth),
266
+ maxActiveDescendants: Number(terms.maxActiveDescendants),
267
+ approvalExpiresAt: Number(terms.approvalExpiresAt),
268
+ salt: terms.salt,
269
+ };
270
+ }
271
+ /** Normalize API mandate terms into the typed form the encoders hash. */
272
+ export function toGasMandateTerms(terms) {
273
+ return termsArgument(terms);
274
+ }
201
275
  export function encodeCreateMandateWithGas(terms, mandatePolicy, gasPolicy, packedOwnerSignature) {
202
276
  return encodeFunctionData({
203
277
  abi: coreAbi,
204
278
  functionName: "createMandateWithGas",
205
279
  args: [
206
- {
207
- parentId: terms.parentId,
208
- account: terms.account,
209
- agent: terms.agent,
210
- accountProfileId: terms.accountProfileId,
211
- installEpoch: BigInt(terms.installEpoch),
212
- projectId: terms.projectId,
213
- mode: Number(terms.mode),
214
- policyVersion: Number(terms.policyVersion),
215
- policyHash: terms.policyHash,
216
- validFrom: Number(terms.validFrom),
217
- expiresAt: Number(terms.expiresAt),
218
- maxDepth: Number(terms.maxDepth),
219
- maxActiveDescendants: Number(terms.maxActiveDescendants),
220
- approvalExpiresAt: Number(terms.approvalExpiresAt),
221
- salt: terms.salt,
222
- },
280
+ termsArgument(terms),
223
281
  mandatePolicy,
224
282
  {
225
283
  mandateId: gasPolicy.mandateId,
@@ -228,6 +286,7 @@ export function encodeCreateMandateWithGas(terms, mandatePolicy, gasPolicy, pack
228
286
  accountProfileId: gasPolicy.accountProfileId,
229
287
  installEpoch: BigInt(gasPolicy.installEpoch),
230
288
  feeAsset: gasPolicy.feeAsset,
289
+ submitter: gasPolicy.submitter,
231
290
  recipient: gasPolicy.recipient,
232
291
  attester: gasPolicy.attester,
233
292
  maxPerAction: BigInt(gasPolicy.maxPerAction),
@@ -249,7 +308,18 @@ export function encodeRevokeGasPolicy(gasPolicyId, deadline, packedOwnerSignatur
249
308
  args: [gasPolicyId, deadline, packedOwnerSignature],
250
309
  });
251
310
  }
252
- /** Relayed owner pause or resume; a direct account call uses deadline 0 and no signature. */
311
+ /**
312
+ * Child mandate creation. `authorization` is the parent agent's 65-byte
313
+ * signature over the child's `MandateTerms`; anyone may send the call.
314
+ */
315
+ export function encodeCreateChildMandate(terms, mandatePolicy, authorization) {
316
+ return encodeFunctionData({
317
+ abi: coreAbi,
318
+ functionName: "createChildMandate",
319
+ args: [termsArgument(terms), mandatePolicy, authorization],
320
+ });
321
+ }
322
+ /** Relayed owner pause, resume, or revoke; a direct account call uses deadline 0 and no signature. */
253
323
  export function encodeSafetyAction(mandateId, actionCode, deadline, packedOwnerSignature) {
254
324
  return encodeFunctionData({
255
325
  abi: coreAbi,
@@ -262,7 +332,9 @@ export function encodeSafetyAction(mandateId, actionCode, deadline, packedOwnerS
262
332
  // ---------------------------------------------------------------------------
263
333
  /** Make the account call `target` through its own execution path. */
264
334
  export function accountExecuteCall(family, account, target, data) {
265
- if (family === "safe")
335
+ // A Safe module transaction and a delegated EOA's own transaction both
336
+ // call the target directly.
337
+ if (family === "safe" || family === "nexus7702")
266
338
  return { to: target, data, operation: 0 };
267
339
  if (family === "alchemy")
268
340
  return {
@@ -331,6 +403,21 @@ export async function prepareOwnerOperation(client, family, account, call, optio
331
403
  if (chainId !== BASE_SEPOLIA_CHAIN_ID)
332
404
  throw new Error("wrong_chain");
333
405
  account = getAddress(account);
406
+ if (family === "nexus7702") {
407
+ if (call.operation !== 0)
408
+ throw new Error("eoa_call_must_be_direct");
409
+ return {
410
+ kind: "eoa_transaction",
411
+ family,
412
+ account,
413
+ transaction: {
414
+ from: account,
415
+ to: getAddress(call.to),
416
+ data: call.data,
417
+ value: 0n,
418
+ },
419
+ };
420
+ }
334
421
  if (family === "safe") {
335
422
  const nonce = await client.readContract({
336
423
  address: account,
@@ -456,6 +543,8 @@ export async function prepareOwnerOperation(client, family, account, call, optio
456
543
  }
457
544
  /** Sign a prepared owner operation with a caller-supplied owner wallet. */
458
545
  export async function signOwnerOperation(signer, operation) {
546
+ if (operation.kind === "eoa_transaction")
547
+ throw new Error("eoa_transaction_is_sent_by_owner_wallet");
459
548
  if (operation.kind === "safe_transaction")
460
549
  return signer.signTypedData(operation.typedData);
461
550
  if (!signer.signMessage)
@@ -476,29 +565,13 @@ export function verifyPreparedGasPolicy(prepared, expected = {}) {
476
565
  const { authority, review, signing } = prepared;
477
566
  const t = authority.mandateTerms;
478
567
  const g = authority.gasPolicy;
479
- const r = review;
480
568
  const chainId = prepared.chainId;
481
569
  if (chainId !== BASE_SEPOLIA_CHAIN_ID)
482
570
  fail("chain");
483
- if (expected.core && !eq(expected.core, prepared.core))
571
+ const core = getAddress(prepared.core);
572
+ if (expected.core && !eq(expected.core, core))
484
573
  fail("core");
485
- const terms = {
486
- parentId: t.parentId,
487
- account: t.account,
488
- agent: t.agent,
489
- accountProfileId: t.accountProfileId,
490
- installEpoch: BigInt(t.installEpoch),
491
- projectId: t.projectId,
492
- mode: Number(t.mode),
493
- policyVersion: Number(t.policyVersion),
494
- policyHash: t.policyHash,
495
- validFrom: Number(t.validFrom),
496
- expiresAt: Number(t.expiresAt),
497
- maxDepth: Number(t.maxDepth),
498
- maxActiveDescendants: Number(t.maxActiveDescendants),
499
- approvalExpiresAt: Number(t.approvalExpiresAt),
500
- salt: t.salt,
501
- };
574
+ const terms = toGasMandateTerms(t);
502
575
  const policy = {
503
576
  mandateId: g.mandateId,
504
577
  account: g.account,
@@ -506,6 +579,7 @@ export function verifyPreparedGasPolicy(prepared, expected = {}) {
506
579
  accountProfileId: g.accountProfileId,
507
580
  installEpoch: BigInt(g.installEpoch),
508
581
  feeAsset: g.feeAsset,
582
+ submitter: g.submitter,
509
583
  recipient: g.recipient,
510
584
  attester: g.attester,
511
585
  maxPerAction: BigInt(g.maxPerAction),
@@ -517,42 +591,77 @@ export function verifyPreparedGasPolicy(prepared, expected = {}) {
517
591
  };
518
592
  if (terms.parentId !== `0x${"00".repeat(32)}` ||
519
593
  terms.mode !== 1 ||
520
- terms.maxDepth !== 0 ||
521
- terms.maxActiveDescendants !== 0)
594
+ terms.policyVersion !== 1 ||
595
+ terms.maxDepth !== review.maxDepth ||
596
+ terms.maxActiveDescendants !== review.maxActiveDescendants ||
597
+ terms.maxDepth > HOSTED_TREE_LIMITS.maxDepth ||
598
+ terms.maxActiveDescendants > HOSTED_TREE_LIMITS.maxActiveDescendants ||
599
+ terms.validFrom !== review.mandateValidFrom ||
600
+ terms.expiresAt !== review.mandateExpiresAt)
522
601
  fail("mandate shape");
523
602
  if (!eq(terms.account, prepared.account) ||
524
603
  !eq(policy.account, prepared.account))
525
604
  fail("account");
526
- if (policy.feeAsset !== "0x0000000000000000000000000000000000000000" ||
527
- policy.formulaVersion !== 1)
528
- fail("gas asset");
529
- const recipients = r.recipients ?? [];
605
+ const projectBinding = contractProjectId(review.projectId);
606
+ if (!eq(review.contractProjectId, projectBinding) ||
607
+ !eq(terms.projectId, projectBinding) ||
608
+ !eq(policy.projectId, projectBinding) ||
609
+ (expected.projectId !== undefined &&
610
+ !eq(contractProjectId(expected.projectId), projectBinding)))
611
+ fail("project binding");
612
+ if (!eq(policy.accountProfileId, terms.accountProfileId) ||
613
+ policy.installEpoch !== terms.installEpoch)
614
+ fail("account profile");
615
+ if (!eq(policy.feeAsset, NATIVE_FEE_ASSET) ||
616
+ policy.formulaVersion !== GAS_FORMULA_INLINE)
617
+ fail("gas formula");
618
+ if (!eq(review.asset, BASE_SEPOLIA_USDC) || review.assetDecimals !== 6)
619
+ fail("asset");
620
+ const recipients = review.recipients;
530
621
  const compiled = encodeTransferPolicy({
531
- asset: r.asset,
622
+ asset: review.asset,
532
623
  recipients,
533
- maxPerActionBaseUnits: BigInt(r.maxPerActionBaseUnits),
534
- maxTotalBaseUnits: BigInt(r.maxTotalBaseUnits),
624
+ maxPerActionBaseUnits: BigInt(review.maxPerActionBaseUnits),
625
+ maxTotalBaseUnits: BigInt(review.maxTotalBaseUnits),
535
626
  });
536
627
  if (!eq(compiled, authority.mandatePolicy) ||
537
628
  !eq(keccak256(authority.mandatePolicy), terms.policyHash))
538
629
  fail("policy bytes");
539
- if (!eq(r.asset, BASE_SEPOLIA_USDC))
540
- fail("asset");
541
- const mandateId = hashGasMandate(terms, chainId, prepared.core);
630
+ const mandateId = hashGasMandate(terms, chainId, core);
542
631
  if (!eq(mandateId, authority.mandateId) || !eq(policy.mandateId, mandateId))
543
632
  fail("mandate id");
544
- const gasPolicyId = hashOwnerGasPolicy(policy, chainId, prepared.core);
633
+ const gasPolicyId = hashOwnerGasPolicy(policy, chainId, core);
545
634
  if (!eq(gasPolicyId, authority.gasPolicyId))
546
635
  fail("gas policy id");
547
- if (!eq(r.agent, terms.agent) ||
548
- !eq(r.gasRecipient, policy.recipient) ||
549
- !eq(r.attester, policy.attester) ||
550
- BigInt(r.maxGasPerActionWei) !== policy.maxPerAction ||
551
- BigInt(r.maxGasTotalWei) !== policy.maxTotal ||
552
- Number(r.chargeableOutcomesMask) !== policy.chargeableOutcomes ||
553
- Number(r.gasExpiresAt) !== policy.expiresAt ||
554
- Number(r.mandateExpiresAt) !== terms.expiresAt)
636
+ if (!eq(review.agent, terms.agent) ||
637
+ !eq(review.submitter, policy.submitter) ||
638
+ !eq(review.gasRecipient, policy.recipient) ||
639
+ !eq(review.attester, policy.attester) ||
640
+ BigInt(review.maxGasPerActionWei) !== policy.maxPerAction ||
641
+ BigInt(review.maxGasTotalWei) !== policy.maxTotal ||
642
+ review.chargeableOutcomesMask !== policy.chargeableOutcomes ||
643
+ review.gasExpiresAt !== policy.expiresAt)
555
644
  fail("review summary");
645
+ if (policy.maxPerAction <= 0n ||
646
+ policy.maxPerAction > policy.maxTotal ||
647
+ policy.chargeableOutcomes < 1 ||
648
+ policy.chargeableOutcomes > 7 ||
649
+ policy.expiresAt > terms.expiresAt ||
650
+ eq(policy.submitter, NATIVE_FEE_ASSET) ||
651
+ eq(policy.recipient, NATIVE_FEE_ASSET))
652
+ fail("gas policy bounds");
653
+ const revert = (policy.chargeableOutcomes & CHARGEABLE_OUTCOMES.revert) !== 0;
654
+ if (review.relayerMode === "developer") {
655
+ // A developer relayer has no GOL attester, so no revert claim.
656
+ if (revert || !eq(policy.attester, NATIVE_FEE_ASSET))
657
+ fail("developer relayer policy");
658
+ }
659
+ else if (review.relayerMode === "gol") {
660
+ if (revert && eq(policy.attester, NATIVE_FEE_ASSET))
661
+ fail("revert claim attester");
662
+ }
663
+ else
664
+ fail("relayer mode");
556
665
  if (expected.account && !eq(expected.account, terms.account))
557
666
  fail("expected account");
558
667
  if (expected.agent && !eq(expected.agent, terms.agent))
@@ -562,10 +671,10 @@ export function verifyPreparedGasPolicy(prepared, expected = {}) {
562
671
  !expected.recipients.every((a) => recipients.some((b) => eq(a, b)))))
563
672
  fail("expected recipients");
564
673
  if (expected.maxPerActionBaseUnits !== undefined &&
565
- BigInt(r.maxPerActionBaseUnits) !== expected.maxPerActionBaseUnits)
674
+ BigInt(review.maxPerActionBaseUnits) !== expected.maxPerActionBaseUnits)
566
675
  fail("expected per-action cap");
567
676
  if (expected.maxTotalBaseUnits !== undefined &&
568
- BigInt(r.maxTotalBaseUnits) !== expected.maxTotalBaseUnits)
677
+ BigInt(review.maxTotalBaseUnits) !== expected.maxTotalBaseUnits)
569
678
  fail("expected total cap");
570
679
  if (expected.maxGasPerActionWei !== undefined &&
571
680
  policy.maxPerAction !== expected.maxGasPerActionWei)
@@ -573,11 +682,36 @@ export function verifyPreparedGasPolicy(prepared, expected = {}) {
573
682
  if (expected.maxGasTotalWei !== undefined &&
574
683
  policy.maxTotal !== expected.maxGasTotalWei)
575
684
  fail("expected gas total cap");
685
+ if (expected.chargeableOutcomesMask !== undefined &&
686
+ policy.chargeableOutcomes !== expected.chargeableOutcomesMask)
687
+ fail("expected chargeable outcomes");
688
+ if (expected.relayer) {
689
+ if (expected.relayer.mode !== review.relayerMode)
690
+ fail("expected relayer");
691
+ if (expected.relayer.submitter &&
692
+ !eq(expected.relayer.submitter, policy.submitter))
693
+ fail("expected submitter");
694
+ if (expected.relayer.recipient &&
695
+ !eq(expected.relayer.recipient, policy.recipient))
696
+ fail("expected gas recipient");
697
+ }
698
+ if (expected.attester && !eq(expected.attester, policy.attester))
699
+ fail("expected attester");
700
+ if (expected.tree &&
701
+ (expected.tree.maxDepth !== terms.maxDepth ||
702
+ expected.tree.maxActiveDescendants !== terms.maxActiveDescendants))
703
+ fail("expected tree limits");
704
+ if (!eq(prepared.submission.to, core) ||
705
+ prepared.submission.functionName !== "createMandateWithGas")
706
+ fail("submission");
576
707
  verifyWalletSigningPayload(signing, {
577
708
  family: prepared.family,
578
709
  account: prepared.account,
579
- core: prepared.core,
580
- structHash: combinedApprovalStructHash(mandateId, gasPolicyId),
710
+ core,
711
+ payload: {
712
+ primaryType: "CombinedGasApproval",
713
+ message: { mandateId, gasPolicyId },
714
+ },
581
715
  chainId,
582
716
  });
583
717
  if (!eq(signing.digest, authority.combinedDigest))
@@ -588,48 +722,79 @@ export async function signPreparedGasPolicy(signer, prepared, expected = {}) {
588
722
  verifyPreparedGasPolicy(prepared, expected);
589
723
  const signature = await signWalletSigningPayload(signer, prepared.signing);
590
724
  return {
591
- to: prepared.core,
725
+ to: getAddress(prepared.core),
592
726
  data: encodeCreateMandateWithGas(prepared.authority.mandateTerms, prepared.authority.mandatePolicy, prepared.authority.gasPolicy, signature),
593
727
  };
594
728
  }
595
729
  /** Verify a prepared relayed revocation, sign it with the owner wallet, and return the core call. */
596
730
  export async function signPreparedRevocation(signer, prepared) {
597
- verifyWalletSigningPayload(prepared.relayed.signing, {
731
+ const core = getAddress(prepared.core);
732
+ if (prepared.chainId !== BASE_SEPOLIA_CHAIN_ID ||
733
+ prepared.relayed.functionName !== "revokeGasPolicy" ||
734
+ !eq(prepared.directOwnerCall.to, core) ||
735
+ !eq(prepared.directOwnerCall.data, encodeRevokeGasPolicy(prepared.gasPolicyId, 0, "0x")))
736
+ throw new Error("revocation_mismatch");
737
+ const signing = prepared.relayed.signing;
738
+ verifyWalletSigningPayload(signing, {
598
739
  family: prepared.family,
599
740
  account: prepared.account,
600
- core: prepared.core,
601
- structHash: revocationStructHash(prepared.gasPolicyId, BigInt(prepared.nonce), prepared.deadline),
741
+ core,
742
+ payload: {
743
+ primaryType: "GasRevocation",
744
+ message: {
745
+ gasPolicyId: prepared.gasPolicyId,
746
+ nonce: prepared.nonce,
747
+ deadline: prepared.deadline,
748
+ },
749
+ },
602
750
  chainId: prepared.chainId,
603
751
  });
604
- const signature = await signWalletSigningPayload(signer, prepared.relayed.signing);
752
+ if (!eq(prepared.digest, signing.digest))
753
+ throw new Error("revocation_mismatch");
754
+ const signature = await signWalletSigningPayload(signer, signing);
605
755
  return {
606
- to: prepared.core,
756
+ to: core,
607
757
  data: encodeRevokeGasPolicy(prepared.gasPolicyId, prepared.deadline, signature),
608
758
  };
609
759
  }
610
- /** Recompute the core digest and wallet payload before signing an owner pause or resume. */
611
- export async function signPreparedSafetyAction(signer, prepared) {
612
- if (prepared.action !== "pause" && prepared.action !== "resume")
613
- throw new Error("safety_action_mismatch");
614
- const actionCode = prepared.action === "pause" ? 1 : 2;
615
- if (prepared.chainId !== BASE_SEPOLIA_CHAIN_ID ||
760
+ /**
761
+ * Recompute the core digest and wallet payload before signing an owner pause,
762
+ * resume, or revoke of any mandate in the policy's tree.
763
+ */
764
+ export async function signPreparedSafetyAction(signer, prepared, expected = {}) {
765
+ const actionCode = SAFETY_ACTION_CODES[prepared.action];
766
+ const core = getAddress(prepared.core);
767
+ if (actionCode === undefined ||
768
+ prepared.chainId !== BASE_SEPOLIA_CHAIN_ID ||
616
769
  prepared.actionCode !== actionCode ||
617
- prepared.relayed.functionName !== "safetyAction")
770
+ prepared.relayed.functionName !== "safetyAction" ||
771
+ (expected.action !== undefined && expected.action !== prepared.action) ||
772
+ (expected.mandateId !== undefined &&
773
+ !eq(expected.mandateId, prepared.mandateId)) ||
774
+ !eq(prepared.directOwnerCall.to, core) ||
775
+ !eq(prepared.directOwnerCall.data, encodeSafetyAction(prepared.mandateId, actionCode, 0, "0x")))
618
776
  throw new Error("safety_action_mismatch");
619
777
  const signing = prepared.relayed.signing;
620
- const expected = {
778
+ verifyWalletSigningPayload(signing, {
621
779
  family: prepared.family,
622
780
  account: prepared.account,
623
- core: prepared.core,
624
- structHash: safetyActionStructHash(prepared.mandateId, actionCode, BigInt(prepared.nonce), prepared.deadline),
781
+ core,
782
+ payload: {
783
+ primaryType: "SafetyAction",
784
+ message: {
785
+ mandateId: prepared.mandateId,
786
+ action: actionCode,
787
+ nonce: prepared.nonce,
788
+ deadline: prepared.deadline,
789
+ },
790
+ },
625
791
  chainId: prepared.chainId,
626
- };
627
- verifyWalletSigningPayload(signing, expected);
628
- if (prepared.digest.toLowerCase() !== signing.digest.toLowerCase())
792
+ });
793
+ if (!eq(prepared.digest, signing.digest))
629
794
  throw new Error("safety_action_mismatch");
630
795
  const signature = await signWalletSigningPayload(signer, signing);
631
796
  return {
632
- to: prepared.core,
797
+ to: core,
633
798
  data: encodeSafetyAction(prepared.mandateId, actionCode, prepared.deadline, signature),
634
799
  };
635
800
  }