permissionless 0.2.23 → 0.2.25

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.
@@ -13,7 +13,6 @@ import {
13
13
  type TypedDataDefinition,
14
14
  type WalletClient,
15
15
  concat,
16
- concatHex,
17
16
  encodeAbiParameters,
18
17
  encodeFunctionData,
19
18
  encodePacked,
@@ -36,12 +35,13 @@ import {
36
35
  entryPoint07Address,
37
36
  toSmartAccount
38
37
  } from "viem/account-abstraction"
39
- import { getChainId, readContract, signTypedData } from "viem/actions"
38
+ import { getChainId, readContract } from "viem/actions"
40
39
  import { getAction } from "viem/utils"
41
40
  import { getAccountNonce } from "../../actions/public/getAccountNonce.js"
42
41
  import { encode7579Calls } from "../../utils/encode7579Calls.js"
43
42
  import { isSmartAccountDeployed } from "../../utils/isSmartAccountDeployed.js"
44
43
  import { type EthereumProvider, toOwner } from "../../utils/toOwner.js"
44
+ import { signUserOperation } from "./signUserOperation.js"
45
45
 
46
46
  export type SafeVersion = "1.4.1"
47
47
 
@@ -364,7 +364,7 @@ const executeUserOpWithErrorStringAbi = [
364
364
  }
365
365
  ] as const
366
366
 
367
- const EIP712_SAFE_OPERATION_TYPE_V06 = {
367
+ export const EIP712_SAFE_OPERATION_TYPE_V06 = {
368
368
  SafeOp: [
369
369
  { type: "address", name: "safe" },
370
370
  { type: "uint256", name: "nonce" },
@@ -382,7 +382,7 @@ const EIP712_SAFE_OPERATION_TYPE_V06 = {
382
382
  ]
383
383
  }
384
384
 
385
- const EIP712_SAFE_OPERATION_TYPE_V07 = {
385
+ export const EIP712_SAFE_OPERATION_TYPE_V07 = {
386
386
  SafeOp: [
387
387
  { type: "address", name: "safe" },
388
388
  { type: "uint256", name: "nonce" },
@@ -525,18 +525,19 @@ const get7579LaunchPadInitData = ({
525
525
  safe4337ModuleAddress,
526
526
  safeSingletonAddress,
527
527
  erc7579LaunchpadAddress,
528
- owner,
528
+ owners,
529
529
  validators,
530
530
  executors,
531
531
  fallbacks,
532
532
  hooks,
533
533
  attesters,
534
+ threshold,
534
535
  attestersThreshold
535
536
  }: {
536
537
  safe4337ModuleAddress: Address
537
538
  safeSingletonAddress: Address
538
539
  erc7579LaunchpadAddress: Address
539
- owner: Address
540
+ owners: Address[]
540
541
  executors: {
541
542
  address: Address
542
543
  context: Address
@@ -545,12 +546,13 @@ const get7579LaunchPadInitData = ({
545
546
  fallbacks: { address: Address; context: Address }[]
546
547
  hooks: { address: Address; context: Address }[]
547
548
  attesters: Address[]
549
+ threshold: bigint
548
550
  attestersThreshold: number
549
551
  }) => {
550
552
  const initData = {
551
553
  singleton: safeSingletonAddress,
552
- owners: [owner],
553
- threshold: BigInt(1),
554
+ owners: owners,
555
+ threshold: threshold,
554
556
  setupTo: erc7579LaunchpadAddress,
555
557
  setupData: encodeFunctionData({
556
558
  abi: initSafe7579Abi,
@@ -581,7 +583,8 @@ const get7579LaunchPadInitData = ({
581
583
  }
582
584
 
583
585
  const getInitializerCode = async ({
584
- owner,
586
+ owners,
587
+ threshold,
585
588
  safeModuleSetupAddress,
586
589
  safe4337ModuleAddress,
587
590
  multiSendAddress,
@@ -599,7 +602,8 @@ const getInitializerCode = async ({
599
602
  payment = BigInt(0),
600
603
  paymentReceiver = zeroAddress
601
604
  }: {
602
- owner: Address
605
+ owners: Address[]
606
+ threshold: bigint
603
607
  safeSingletonAddress: Address
604
608
  safeModuleSetupAddress: Address
605
609
  safe4337ModuleAddress: Address
@@ -629,10 +633,11 @@ const getInitializerCode = async ({
629
633
  safe4337ModuleAddress,
630
634
  safeSingletonAddress,
631
635
  erc7579LaunchpadAddress,
632
- owner,
636
+ owners,
633
637
  validators,
634
638
  executors,
635
639
  fallbacks,
640
+ threshold,
636
641
  hooks,
637
642
  attesters,
638
643
  attestersThreshold
@@ -729,7 +734,7 @@ const getInitializerCode = async ({
729
734
  abi: setupAbi,
730
735
  functionName: "setup",
731
736
  args: [
732
- [owner],
737
+ owners,
733
738
  BigInt(1),
734
739
  multiSendAddress,
735
740
  multiSendCallData,
@@ -741,7 +746,7 @@ const getInitializerCode = async ({
741
746
  })
742
747
  }
743
748
 
744
- function getPaymasterAndData(unpackedUserOperation: UserOperation) {
749
+ export function getPaymasterAndData(unpackedUserOperation: UserOperation) {
745
750
  return unpackedUserOperation.paymaster
746
751
  ? concat([
747
752
  unpackedUserOperation.paymaster,
@@ -768,7 +773,8 @@ function getPaymasterAndData(unpackedUserOperation: UserOperation) {
768
773
  }
769
774
 
770
775
  const getAccountInitCode = async ({
771
- owner,
776
+ owners,
777
+ threshold,
772
778
  safeModuleSetupAddress,
773
779
  safe4337ModuleAddress,
774
780
  safeSingletonAddress,
@@ -787,7 +793,8 @@ const getAccountInitCode = async ({
787
793
  attesters = [],
788
794
  attestersThreshold = 0
789
795
  }: {
790
- owner: Address
796
+ owners: Address[]
797
+ threshold: bigint
791
798
  safeModuleSetupAddress: Address
792
799
  safe4337ModuleAddress: Address
793
800
  safeSingletonAddress: Address
@@ -813,12 +820,9 @@ const getAccountInitCode = async ({
813
820
  payment?: bigint
814
821
  paymentReceiver?: Address
815
822
  }): Promise<Hex> => {
816
- if (!owner) {
817
- throw new Error("Owner account not found")
818
- }
819
-
820
823
  const initializer = await getInitializerCode({
821
- owner,
824
+ owners,
825
+ threshold,
822
826
  safeModuleSetupAddress,
823
827
  safe4337ModuleAddress,
824
828
  multiSendAddress,
@@ -850,7 +854,7 @@ const getAccountInitCode = async ({
850
854
  return initCodeCallData
851
855
  }
852
856
 
853
- const getDefaultAddresses = (
857
+ export const getDefaultAddresses = (
854
858
  safeVersion: SafeVersion,
855
859
  entryPointVersion: "0.6" | "0.7",
856
860
  {
@@ -938,13 +942,8 @@ export type ToSafeSmartAccountParameters<
938
942
  TErc7579 extends Address | undefined
939
943
  > = {
940
944
  client: Client
941
- owners: [
942
- OneOf<
943
- | EthereumProvider
944
- | WalletClient<Transport, Chain | undefined, Account>
945
- | LocalAccount
946
- >
947
- ]
945
+ owners: (Account | WalletClient<Transport, Chain | undefined, Account>)[]
946
+ threshold?: bigint
948
947
  version: SafeVersion
949
948
  entryPoint?: {
950
949
  address: Address
@@ -988,7 +987,8 @@ const proxyCreationCodeAbi = [
988
987
 
989
988
  const getAccountAddress = async ({
990
989
  client,
991
- owner,
990
+ owners,
991
+ threshold,
992
992
  safeModuleSetupAddress,
993
993
  safe4337ModuleAddress,
994
994
  safeProxyFactoryAddress,
@@ -1009,7 +1009,8 @@ const getAccountAddress = async ({
1009
1009
  attestersThreshold = 0
1010
1010
  }: {
1011
1011
  client: Client
1012
- owner: Address
1012
+ owners: Address[]
1013
+ threshold: bigint
1013
1014
  safeModuleSetupAddress: Address
1014
1015
  safe4337ModuleAddress: Address
1015
1016
  safeProxyFactoryAddress: Address
@@ -1043,7 +1044,8 @@ const getAccountAddress = async ({
1043
1044
  })
1044
1045
 
1045
1046
  const initializer = await getInitializerCode({
1046
- owner,
1047
+ owners,
1048
+ threshold,
1047
1049
  safeModuleSetupAddress,
1048
1050
  safe4337ModuleAddress,
1049
1051
  multiSendAddress,
@@ -1119,8 +1121,9 @@ export async function toSafeSmartAccount<
1119
1121
  ): Promise<ToSafeSmartAccountReturnType<entryPointVersion>> {
1120
1122
  const {
1121
1123
  client,
1122
- owners,
1124
+ owners: _owners,
1123
1125
  address,
1126
+ threshold = BigInt(_owners.length),
1124
1127
  version,
1125
1128
  safe4337ModuleAddress: _safe4337ModuleAddress,
1126
1129
  safeProxyFactoryAddress: _safeProxyFactoryAddress,
@@ -1135,7 +1138,38 @@ export async function toSafeSmartAccount<
1135
1138
  paymentReceiver
1136
1139
  } = parameters
1137
1140
 
1138
- const localOwner = await toOwner({ owner: owners[0] })
1141
+ const owners = _owners.map((owner) =>
1142
+ "account" in owner ? owner.account : owner
1143
+ )
1144
+
1145
+ const localOwners = await Promise.all(
1146
+ _owners
1147
+ .filter((owner) => {
1148
+ if ("type" in owner && owner.type === "local") {
1149
+ return true
1150
+ }
1151
+
1152
+ if ("request" in owner) {
1153
+ return true
1154
+ }
1155
+
1156
+ if ("account" in owner) {
1157
+ // walletClient
1158
+ return true
1159
+ }
1160
+
1161
+ return false
1162
+ })
1163
+ .map((owner) =>
1164
+ toOwner({
1165
+ owner: owner as OneOf<
1166
+ | LocalAccount
1167
+ | EthereumProvider
1168
+ | WalletClient<Transport, Chain | undefined, Account>
1169
+ >
1170
+ })
1171
+ )
1172
+ )
1139
1173
 
1140
1174
  const entryPoint = {
1141
1175
  address: parameters.entryPoint?.address ?? entryPoint07Address,
@@ -1211,7 +1245,8 @@ export async function toSafeSmartAccount<
1211
1245
  return {
1212
1246
  factory: safeProxyFactoryAddress,
1213
1247
  factoryData: await getAccountInitCode({
1214
- owner: localOwner.address,
1248
+ owners: owners.map((owner) => owner.address),
1249
+ threshold,
1215
1250
  safeModuleSetupAddress,
1216
1251
  safe4337ModuleAddress,
1217
1252
  safeSingletonAddress,
@@ -1243,7 +1278,8 @@ export async function toSafeSmartAccount<
1243
1278
  // Get the sender address based on the init code
1244
1279
  accountAddress = await getAccountAddress({
1245
1280
  client,
1246
- owner: localOwner.address,
1281
+ owners: owners.map((owner) => owner.address),
1282
+ threshold,
1247
1283
  safeModuleSetupAddress,
1248
1284
  safe4337ModuleAddress,
1249
1285
  safeProxyFactoryAddress,
@@ -1280,7 +1316,8 @@ export async function toSafeSmartAccount<
1280
1316
  safe4337ModuleAddress,
1281
1317
  safeSingletonAddress,
1282
1318
  erc7579LaunchpadAddress,
1283
- owner: localOwner.address,
1319
+ owners: owners.map((owner) => owner.address),
1320
+ threshold,
1284
1321
  validators,
1285
1322
  executors,
1286
1323
  fallbacks,
@@ -1372,12 +1409,30 @@ export async function toSafeSmartAccount<
1372
1409
  })
1373
1410
  },
1374
1411
  async getStubSignature() {
1375
- return "0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
1412
+ return encodePacked(
1413
+ ["uint48", "uint48", "bytes"],
1414
+ [
1415
+ 0,
1416
+ 0,
1417
+ `0x${owners
1418
+ .map(
1419
+ (_) =>
1420
+ "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
1421
+ )
1422
+ .join("")}`
1423
+ ]
1424
+ )
1376
1425
  },
1377
1426
  async sign({ hash }) {
1378
1427
  return this.signMessage({ message: hash })
1379
1428
  },
1380
1429
  async signMessage({ message }) {
1430
+ if (localOwners.length !== owners.length) {
1431
+ throw new Error(
1432
+ "Owners length mismatch, currently not supported"
1433
+ )
1434
+ }
1435
+
1381
1436
  const messageHash = hashTypedData({
1382
1437
  domain: {
1383
1438
  chainId: await getMemoizedChainId(),
@@ -1392,102 +1447,108 @@ export async function toSafeSmartAccount<
1392
1447
  }
1393
1448
  })
1394
1449
 
1395
- return adjustVInSignature(
1396
- "eth_sign",
1397
- await localOwner.signMessage({
1398
- message: {
1399
- raw: toBytes(messageHash)
1400
- }
1401
- })
1450
+ const signatures = await Promise.all(
1451
+ localOwners.map(async (localOwner) => ({
1452
+ signer: localOwner.address,
1453
+ data: adjustVInSignature(
1454
+ "eth_sign",
1455
+ await localOwner.signMessage({
1456
+ message: {
1457
+ raw: toBytes(messageHash)
1458
+ }
1459
+ })
1460
+ )
1461
+ }))
1402
1462
  )
1463
+
1464
+ signatures.sort((left, right) =>
1465
+ left.signer
1466
+ .toLowerCase()
1467
+ .localeCompare(right.signer.toLowerCase())
1468
+ )
1469
+
1470
+ const signatureBytes = concat(signatures.map((sig) => sig.data))
1471
+
1472
+ return signatureBytes
1403
1473
  },
1404
1474
  async signTypedData(typedData) {
1405
- return adjustVInSignature(
1406
- "eth_signTypedData",
1407
- await localOwner.signTypedData({
1408
- domain: {
1409
- chainId: await getMemoizedChainId(),
1410
- verifyingContract: await this.getAddress()
1411
- },
1412
- types: {
1413
- SafeMessage: [{ name: "message", type: "bytes" }]
1414
- },
1415
- primaryType: "SafeMessage",
1416
- message: {
1417
- message: generateSafeMessageMessage(typedData)
1418
- }
1419
- })
1475
+ if (localOwners.length !== owners.length) {
1476
+ throw new Error(
1477
+ "Owners length mismatch, currently not supported"
1478
+ )
1479
+ }
1480
+
1481
+ const signatures = await Promise.all(
1482
+ localOwners.map(async (localOwner) => ({
1483
+ signer: localOwner.address,
1484
+ data: adjustVInSignature(
1485
+ "eth_signTypedData",
1486
+
1487
+ await localOwner.signTypedData({
1488
+ domain: {
1489
+ chainId: await getMemoizedChainId(),
1490
+ verifyingContract: await this.getAddress()
1491
+ },
1492
+ types: {
1493
+ SafeMessage: [
1494
+ { name: "message", type: "bytes" }
1495
+ ]
1496
+ },
1497
+ primaryType: "SafeMessage",
1498
+ message: {
1499
+ message: generateSafeMessageMessage(typedData)
1500
+ }
1501
+ })
1502
+ )
1503
+ }))
1504
+ )
1505
+
1506
+ signatures.sort((left, right) =>
1507
+ left.signer
1508
+ .toLowerCase()
1509
+ .localeCompare(right.signer.toLowerCase())
1420
1510
  )
1511
+
1512
+ const signatureBytes = concat(signatures.map((sig) => sig.data))
1513
+
1514
+ return signatureBytes
1421
1515
  },
1422
1516
  async signUserOperation(parameters) {
1423
1517
  const { chainId = await getMemoizedChainId(), ...userOperation } =
1424
1518
  parameters
1425
1519
 
1426
- const message = {
1427
- safe: await this.getAddress(),
1428
- callData: userOperation.callData,
1429
- nonce: userOperation.nonce,
1430
- initCode: userOperation.initCode ?? "0x",
1431
- maxFeePerGas: userOperation.maxFeePerGas,
1432
- maxPriorityFeePerGas: userOperation.maxPriorityFeePerGas,
1433
- preVerificationGas: userOperation.preVerificationGas,
1434
- verificationGasLimit: userOperation.verificationGasLimit,
1435
- callGasLimit: userOperation.callGasLimit,
1436
- paymasterAndData: userOperation.paymasterAndData ?? "0x",
1437
- validAfter: validAfter,
1438
- validUntil: validUntil,
1439
- entryPoint: entryPoint.address
1520
+ if (localOwners.length !== owners.length) {
1521
+ throw new Error(
1522
+ "Owners length mismatch use SafeSmartAccount.signUserOperation from `permissionless/accounts/safe`"
1523
+ )
1440
1524
  }
1441
1525
 
1442
- if ("initCode" in userOperation) {
1443
- message.paymasterAndData =
1444
- userOperation.paymasterAndData ?? "0x"
1445
- }
1526
+ let signatures: Hex | undefined = undefined
1446
1527
 
1447
- if ("factory" in userOperation) {
1448
- if (userOperation.factory && userOperation.factoryData) {
1449
- message.initCode = concatHex([
1450
- userOperation.factory,
1451
- userOperation.factoryData
1452
- ])
1453
- }
1454
- message.paymasterAndData = getPaymasterAndData({
1528
+ for (const owner of localOwners) {
1529
+ signatures = await signUserOperation({
1455
1530
  ...userOperation,
1456
- sender: userOperation.sender ?? (await this.getAddress())
1531
+ version,
1532
+ entryPoint,
1533
+ owners: localOwners,
1534
+ account: owner as OneOf<
1535
+ | EthereumProvider
1536
+ | WalletClient<Transport, Chain | undefined, Account>
1537
+ | LocalAccount
1538
+ >,
1539
+ chainId: await getMemoizedChainId(),
1540
+ signatures,
1541
+ validAfter,
1542
+ validUntil,
1543
+ safe4337ModuleAddress
1457
1544
  })
1458
1545
  }
1459
1546
 
1460
- const signatures = [
1461
- {
1462
- signer: localOwner.address,
1463
- data: await signTypedData(client, {
1464
- account: localOwner,
1465
- domain: {
1466
- chainId,
1467
- verifyingContract: safe4337ModuleAddress
1468
- },
1469
- types:
1470
- entryPoint.version === "0.6"
1471
- ? EIP712_SAFE_OPERATION_TYPE_V06
1472
- : EIP712_SAFE_OPERATION_TYPE_V07,
1473
- primaryType: "SafeOp",
1474
- message: message
1475
- })
1476
- }
1477
- ]
1478
-
1479
- signatures.sort((left, right) =>
1480
- left.signer
1481
- .toLowerCase()
1482
- .localeCompare(right.signer.toLowerCase())
1483
- )
1484
-
1485
- const signatureBytes = concat(signatures.map((sig) => sig.data))
1547
+ if (!signatures) {
1548
+ throw new Error("No signatures found")
1549
+ }
1486
1550
 
1487
- return encodePacked(
1488
- ["uint48", "uint48", "bytes"],
1489
- [validAfter, validUntil, signatureBytes]
1490
- )
1551
+ return signatures
1491
1552
  }
1492
1553
  }) as Promise<ToSafeSmartAccountReturnType<entryPointVersion>>
1493
1554
  }
@@ -46,8 +46,20 @@ describe.each(getCoreSmartAccounts())(
46
46
  ...rpc
47
47
  })
48
48
 
49
+ if (!smartClient.account) {
50
+ throw new Error("Account not found")
51
+ }
52
+
53
+ if (name.includes("Safe 7579")) {
54
+ // Due to 7579 launchpad, we can't verify the signature before deploying the account.
55
+ await smartClient.sendTransaction({
56
+ calls: [{ to: zeroAddress, value: 0n }]
57
+ })
58
+ }
59
+
49
60
  const signature = await signMessage(smartClient, {
50
- message: "slowly and steadily burning the private keys"
61
+ message: "slowly and steadily burning the private keys",
62
+ account: smartClient.account
51
63
  })
52
64
 
53
65
  const publicClient = getPublicClient(anvilRpc)
@@ -92,18 +104,26 @@ describe.each(getCoreSmartAccounts())(
92
104
  ...rpc
93
105
  })
94
106
 
107
+ if (name === "LightAccount 2.0.0") {
108
+ // LightAccount 2.0.0 doesn't support EIP-1271
109
+ return
110
+ }
111
+
112
+ if (name.includes("Safe 7579")) {
113
+ return
114
+
115
+ // Due to 7579 launchpad, we can't verify the signature before deploying the account.
116
+ // await smartClient.sendTransaction({
117
+ // calls: [{ to: zeroAddress, value: 0n }]
118
+ // })
119
+ }
120
+
95
121
  const signature = await signMessage(smartClient, {
96
122
  message: "slowly and steadily burning the private keys"
97
123
  })
98
124
 
99
125
  const publicClient = getPublicClient(anvilRpc)
100
126
 
101
- if (name === "Safe 7579" || name === "LightAccount 2.0.0") {
102
- // Due to 7579 launchpad, we can't verify the signature as of now.
103
- // Awaiting for the fix
104
- return
105
- }
106
-
107
127
  const isVerified = await publicClient.verifyMessage({
108
128
  address: smartClient.account.address,
109
129
  message: "slowly and steadily burning the private keys",
@@ -124,7 +124,10 @@ describe.each(getCoreSmartAccounts())(
124
124
 
125
125
  const publicClient = getPublicClient(anvilRpc)
126
126
 
127
- if (name === "Safe 7579" || name === "LightAccount 2.0.0") {
127
+ if (
128
+ name.includes("Safe 7579") ||
129
+ name === "LightAccount 2.0.0"
130
+ ) {
128
131
  // Due to 7579 launchpad, we can't verify the signature as of now.
129
132
  // Awaiting for the fix
130
133
  return
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "permissionless",
3
- "version": "0.2.23",
3
+ "version": "0.2.25",
4
4
  "author": "Pimlico",
5
5
  "homepage": "https://docs.pimlico.io/permissionless",
6
6
  "repository": "github:pimlicolabs/permissionless.js",
@@ -30,6 +30,11 @@
30
30
  "import": "./_esm/accounts/index.js",
31
31
  "default": "./_cjs/accounts/index.js"
32
32
  },
33
+ "./accounts/safe": {
34
+ "types": "./_types/accounts/safe/index.d.ts",
35
+ "import": "./_esm/accounts/safe/index.js",
36
+ "default": "./_cjs/accounts/safe/index.js"
37
+ },
33
38
  "./actions": {
34
39
  "types": "./_types/actions/index.d.ts",
35
40
  "import": "./_esm/actions/index.js",