diff --git a/examples/send-tx.ts b/examples/send-tx.ts index 3672680..8e04c8b 100644 --- a/examples/send-tx.ts +++ b/examples/send-tx.ts @@ -1,5 +1,4 @@ import dotenv from "dotenv"; -import { ethers } from "ethers"; import { formatEther, isAddress } from "viem"; import { loadArgs, loadEnv } from "./cli"; diff --git a/package.json b/package.json index 74ec398..1e1158d 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "example": "tsx examples/send-tx.ts", "lint": "prettier --check '{src,examples,tests}/**/*.{js,jsx,ts,tsx}' && eslint . --ignore-pattern dist/", "fmt": "prettier --write '{src,examples,tests}/**/*.{js,jsx,ts,tsx}' && yarn lint --fix", - "test": "jest", + "test": "jest --passWithNoTests", "all": "yarn fmt && yarn lint && yarn build" }, "dependencies": { @@ -59,8 +59,6 @@ "dotenv": "^16.4.5", "eslint": "^9.14.0", "eslint-plugin-import": "^2.31.0", - "ethers": "^6.13.4", - "ethers-multisend": "^3.1.0", "jest": "^29.7.0", "prettier": "^3.3.3", "ts-jest": "^29.2.5", diff --git a/src/index.ts b/src/index.ts index 53e5289..83aa344 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,13 +8,13 @@ export * from "./lib/safe-message"; // TODO: Improve re-exports... export { Network, - BaseTx, - SignRequestData, + type BaseTx, + type SignRequestData, populateTx, - NetworkFields, + type NetworkFields, signatureFromOutcome, signatureFromTxHash, requestRouter as mpcRequestRouter, - EthTransactionParams, + type EthTransactionParams, isRlpHex, } from "near-ca"; diff --git a/src/lib/safe.ts b/src/lib/safe.ts index 189b547..ca1512c 100644 --- a/src/lib/safe.ts +++ b/src/lib/safe.ts @@ -240,6 +240,6 @@ export class SafeContractSuite { if (ownerIndex === -1) { throw new Error(`Not a current owner: ${owner}`); } - return ownerIndex > 0 ? currentOwners[ownerIndex - 1] : SENTINEL_OWNERS; + return ownerIndex > 0 ? currentOwners[ownerIndex - 1]! : SENTINEL_OWNERS; } } diff --git a/src/near-safe.ts b/src/near-safe.ts index 810bf05..1fd1e82 100644 --- a/src/near-safe.ts +++ b/src/near-safe.ts @@ -405,7 +405,9 @@ export class NearSafe { // Assert uniqueness assertUnique(fromAddresses); - + if (!fromAddresses[0]) { + throw new Error("No from address provided"); + } // Early return with eoaEncoding if `from` is not the Safe if (!this.encodeForSafe(fromAddresses[0])) { // TODO: near-ca needs to update this for typed data like we did. diff --git a/tests/e2e.spec.ts b/tests/e2e.spec.ts index ffc80ed..8f74525 100644 --- a/tests/e2e.spec.ts +++ b/tests/e2e.spec.ts @@ -123,7 +123,7 @@ describe("Near Safe Requests", () => { method: "eth_signTypedData_v4", params: [adapter.mpcAddress, typedDataString], }); - expect(nearPayload.actions[0].params.args).toStrictEqual({ + expect(nearPayload.actions[0]!.params.args).toStrictEqual({ request: { path: "ethereum,1", payload: [ diff --git a/tests/unit/ethers-safe.ts b/tests/unit/ethers-safe.ts deleted file mode 100644 index dedfb23..0000000 --- a/tests/unit/ethers-safe.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { - getProxyFactoryDeployment, - getSafeL2SingletonDeployment, -} from "@safe-global/safe-deployments"; -import { - getSafe4337ModuleDeployment, - getSafeModuleSetupDeployment, -} from "@safe-global/safe-modules-deployments"; -import { ethers } from "ethers"; -import { Address, Hash, Hex } from "viem"; - -import { DEFAULT_SETUP_RPC } from "../../src"; -import { - GasPrice, - MetaTransaction, - UnsignedUserOperation, - UserOperation, -} from "../../src/types"; -import { PLACEHOLDER_SIG, packGas, packPaymasterData } from "../../src/util"; - -/** - * All contracts used in account creation & execution - */ -export class ContractSuite { - provider: ethers.JsonRpcProvider; - singleton: ethers.Contract; - proxyFactory: ethers.Contract; - m4337: ethers.Contract; - moduleSetup: ethers.Contract; - entryPoint: ethers.Contract; - - constructor( - provider: ethers.JsonRpcProvider, - singleton: ethers.Contract, - proxyFactory: ethers.Contract, - m4337: ethers.Contract, - moduleSetup: ethers.Contract, - entryPoint: ethers.Contract - ) { - this.provider = provider; - this.singleton = singleton; - this.proxyFactory = proxyFactory; - this.m4337 = m4337; - this.moduleSetup = moduleSetup; - this.entryPoint = entryPoint; - } - - static async init(): Promise { - // TODO - this is a cheeky hack. - const provider = new ethers.JsonRpcProvider(DEFAULT_SETUP_RPC); - const safeDeployment = (fn: DeploymentFunction): Promise => - getDeployment(fn, { provider, version: "1.4.1" }); - const m4337Deployment = async ( - fn: DeploymentFunction - ): Promise => { - return getDeployment(fn, { provider, version: "0.3.0" }); - }; - // Need this first to get entryPoint address - const m4337 = await m4337Deployment(getSafe4337ModuleDeployment); - - const [singleton, proxyFactory, moduleSetup, supportedEntryPoint] = - await Promise.all([ - safeDeployment(getSafeL2SingletonDeployment), - safeDeployment(getProxyFactoryDeployment), - m4337Deployment(getSafeModuleSetupDeployment), - m4337.SUPPORTED_ENTRYPOINT(), - ]); - const entryPoint = new ethers.Contract( - supportedEntryPoint, - ["function getNonce(address, uint192 key) view returns (uint256 nonce)"], - provider - ); - console.log("Initialized ERC4337 & Safe Module Contracts:", { - singleton: await singleton.getAddress(), - proxyFactory: await proxyFactory.getAddress(), - m4337: await m4337.getAddress(), - moduleSetup: await moduleSetup.getAddress(), - entryPoint: await entryPoint.getAddress(), - }); - return new ContractSuite( - provider, - singleton, - proxyFactory, - m4337, - moduleSetup, - entryPoint - ); - } - - async addressForSetup( - setup: ethers.BytesLike, - saltNonce: string - ): Promise
{ - // bytes32 salt = keccak256(abi.encodePacked(keccak256(initializer), saltNonce)); - // cf: https://github.com/safe-global/safe-smart-account/blob/499b17ad0191b575fcadc5cb5b8e3faeae5391ae/contracts/proxies/SafeProxyFactory.sol#L58 - const salt = ethers.keccak256( - ethers.solidityPacked( - ["bytes32", "uint256"], - [ethers.keccak256(setup), saltNonce] - ) - ); - - // abi.encodePacked(type(SafeProxy).creationCode, uint256(uint160(_singleton))); - // cf: https://github.com/safe-global/safe-smart-account/blob/499b17ad0191b575fcadc5cb5b8e3faeae5391ae/contracts/proxies/SafeProxyFactory.sol#L29 - const initCode = ethers.solidityPacked( - ["bytes", "uint256"], - [ - await this.proxyFactory.proxyCreationCode(), - await this.singleton.getAddress(), - ] - ); - return ethers.getCreate2Address( - await this.proxyFactory.getAddress(), - salt, - ethers.keccak256(initCode) - ) as Address; - } - - getSetup(owners: string[]): Hex { - return this.singleton.interface.encodeFunctionData("setup", [ - owners, - 1, // We use sign threshold of 1. - this.moduleSetup.target, - this.moduleSetup.interface.encodeFunctionData("enableModules", [ - [this.m4337.target], - ]), - this.m4337.target, - ethers.ZeroAddress, - 0, - ethers.ZeroAddress, - ]) as Hex; - } - - addOwnerData(newOwner: Address): Hex { - return this.singleton.interface.encodeFunctionData( - "addOwnerWithThreshold", - [newOwner, 1] - ) as Hex; - } - - async getOpHash(unsignedUserOp: UserOperation): Promise { - const { - factory, - factoryData, - verificationGasLimit, - callGasLimit, - maxPriorityFeePerGas, - maxFeePerGas, - } = unsignedUserOp; - return this.m4337.getOperationHash({ - ...unsignedUserOp, - initCode: factory - ? ethers.solidityPacked(["address", "bytes"], [factory, factoryData]) - : "0x", - accountGasLimits: packGas(verificationGasLimit, callGasLimit), - gasFees: packGas(maxPriorityFeePerGas, maxFeePerGas), - paymasterAndData: packPaymasterData(unsignedUserOp), - signature: PLACEHOLDER_SIG, - }); - } - - factoryDataForSetup( - safeNotDeployed: boolean, - setup: string, - safeSaltNonce: string - ): { factory?: Address; factoryData?: Hex } { - return safeNotDeployed - ? { - factory: this.proxyFactory.target as Address, - factoryData: this.proxyFactory.interface.encodeFunctionData( - "createProxyWithNonce", - [this.singleton.target, setup, safeSaltNonce] - ) as Hex, - } - : {}; - } - - async buildUserOp( - txData: MetaTransaction, - safeAddress: Address, - feeData: GasPrice, - setup: string, - safeNotDeployed: boolean, - safeSaltNonce: string - ): Promise { - const rawUserOp = { - sender: safeAddress, - nonce: ethers.toBeHex(await this.entryPoint.getNonce(safeAddress, 0)), - ...this.factoryDataForSetup(safeNotDeployed, setup, safeSaltNonce), - // - callData: this.m4337.interface.encodeFunctionData("executeUserOp", [ - txData.to, - BigInt(txData.value), - txData.data, - txData.operation || 0, - ]) as Hex, - ...feeData, - }; - return rawUserOp; - } -} - -type DeploymentFunction = (filter?: { - version: string; -}) => - | { networkAddresses: { [chainId: string]: string }; abi: unknown[] } - | undefined; -type DeploymentArgs = { provider: ethers.JsonRpcProvider; version: string }; - -async function getDeployment( - fn: DeploymentFunction, - { provider, version }: DeploymentArgs -): Promise { - const { chainId } = await provider.getNetwork(); - const deployment = fn({ version }); - if (!deployment) { - throw new Error( - `Deployment not found for ${fn.name} version ${version} on chainId ${chainId}` - ); - } - let address = deployment.networkAddresses[`${chainId}`]; - if (!address) { - // console.warn( - // `Deployment asset ${fn.name} not listed on chainId ${chainId}, using likely fallback. For more info visit https://github.com/safe-global/safe-modules-deployments` - // ); - // TODO: This is a cheeky hack. Real solution proposed in - // https://github.com/Mintbase/near-safe/issues/42 - address = deployment.networkAddresses["11155111"]; - } - return new ethers.Contract( - address, - deployment.abi as ethers.Fragment[], - provider - ); -} diff --git a/tests/unit/lib/multisend.spec.ts b/tests/unit/lib/multisend.spec.ts index aad9b0c..9affab7 100644 --- a/tests/unit/lib/multisend.spec.ts +++ b/tests/unit/lib/multisend.spec.ts @@ -1,4 +1,3 @@ -import { decodeMulti as decodeMultiEthers } from "ethers-multisend"; import { encodeFunctionData, erc20Abi, Hex, parseUnits, toHex } from "viem"; import { OperationType } from "../../../src"; @@ -69,17 +68,17 @@ describe("Multisend", () => { operation: OperationType.Call, to: "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984", value: toHex(BigInt(parseUnits("10", 18))), - data: "0x00", + data: "0x", }, { operation: OperationType.Call, to: "0x36F4BFC9f49Dc5D4b2d10c4a48a6b30128BD79bC", - value: "0x00", + value: "0x0", data: ercTransferData, }, ]; + const multiSendTx = encodeMulti(input); - const result = decodeMultiEthers(multiSendTx.data); - expect(result).toStrictEqual(input); + expect(decodeMultiViem(multiSendTx.data as Hex)).toStrictEqual(input); }); }); diff --git a/tests/unit/lib/safe.spec.ts b/tests/unit/lib/safe.spec.ts index e80069d..9dc5798 100644 --- a/tests/unit/lib/safe.spec.ts +++ b/tests/unit/lib/safe.spec.ts @@ -1,69 +1,47 @@ import { zeroAddress } from "viem"; import { SafeContractSuite as ViemPack } from "../../../src/lib/safe"; -import { ContractSuite as EthPack } from "../ethers-safe"; describe("Safe Pack", () => { - let ethersPack: EthPack; let viemPack: ViemPack; beforeAll(async () => { - ethersPack = await EthPack.init(); viemPack = new ViemPack(); }); it("init", async () => { - expect(ethersPack.singleton.target).toEqual(viemPack.singleton.address); - expect(await ethersPack.singleton.getAddress()).toEqual( - viemPack.singleton.address + expect(viemPack.singleton.address).toEqual( + "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762" ); - - expect(ethersPack.m4337.target).toEqual(viemPack.m4337.address); - expect(await ethersPack.m4337.getAddress()).toEqual(viemPack.m4337.address); - - expect(ethersPack.moduleSetup.target).toEqual(viemPack.moduleSetup.address); - expect(await ethersPack.moduleSetup.getAddress()).toEqual( - viemPack.moduleSetup.address + expect(viemPack.m4337.address).toEqual( + "0x75cf11467937ce3F2f357CE24ffc3DBF8fD5c226" ); - - expect(ethersPack.moduleSetup.target).toEqual(viemPack.moduleSetup.address); - expect(await ethersPack.moduleSetup.getAddress()).toEqual( - viemPack.moduleSetup.address + expect(viemPack.moduleSetup.address).toEqual( + "0x2dd68b007B46fBe91B9A7c3EDa5A7a1063cB5b47" ); - - expect(ethersPack.entryPoint.target).toEqual(viemPack.entryPoint.address); - expect(await ethersPack.entryPoint.getAddress()).toEqual( - viemPack.entryPoint.address + expect(viemPack.entryPoint.address).toEqual( + "0x0000000071727De22E5E9d8BAf0edAc6f37da032" ); - - expect(ethersPack.proxyFactory.target).toEqual( - viemPack.proxyFactory.address - ); - expect(await ethersPack.proxyFactory.getAddress()).toEqual( - viemPack.proxyFactory.address + expect(viemPack.proxyFactory.address).toEqual( + "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67" ); }); it("addOwnerData", () => { - expect(ethersPack.addOwnerData(zeroAddress)).toEqual( - viemPack.addOwnerData(zeroAddress) + expect(viemPack.addOwnerData(zeroAddress)).toEqual( + "0x0d582f1300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001" ); }); it("getSetup", () => { - expect(ethersPack.getSetup([zeroAddress])).toEqual( - viemPack.getSetup([zeroAddress]) + expect(viemPack.getSetup([zeroAddress])).toEqual( + "0xb63e800d000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000002dd68b007b46fbe91b9a7c3eda5a7a1063cb5b47000000000000000000000000000000000000000000000000000000000000014000000000000000000000000075cf11467937ce3f2f357ce24ffc3dbf8fd5c2260000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000648d0dc49f0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000075cf11467937ce3f2f357ce24ffc3dbf8fd5c22600000000000000000000000000000000000000000000000000000000" ); }); it("addressForSetup", async () => { const saltNonce = "1"; const setup = viemPack.getSetup([zeroAddress]); - const [eps0, vps0, eps1, vps1] = await Promise.all([ - ethersPack.addressForSetup(setup, saltNonce), - viemPack.addressForSetup(setup, saltNonce), - ethersPack.addressForSetup(setup, saltNonce), - viemPack.addressForSetup(setup, saltNonce), - ]); - expect(eps0).toEqual(vps0); - expect(eps1).toEqual(vps1); + expect(await viemPack.addressForSetup(setup, saltNonce)).toEqual( + "0x6531e810c91b0bCFc6733B723a37d051Ad6dCbdd" + ); }); }); diff --git a/tests/unit/utils.spec.ts b/tests/unit/utils.spec.ts index cabf233..a05b63c 100644 --- a/tests/unit/utils.spec.ts +++ b/tests/unit/utils.spec.ts @@ -1,5 +1,4 @@ -import { ethers } from "ethers"; -import { zeroAddress } from "viem"; +import { encodePacked, zeroAddress } from "viem"; import { PaymasterData } from "../../src"; import { @@ -16,25 +15,23 @@ import { describe("Utility Functions (mostly byte packing)", () => { it("PLACE_HOLDER_SIG", () => { - expect(PLACEHOLDER_SIG).toEqual( - ethers.solidityPacked(["uint48", "uint48"], [0, 0]) - ); + expect(PLACEHOLDER_SIG).toEqual(encodePacked(["uint48", "uint48"], [0, 0])); }); it("packGas", () => { let [hi, lo] = [15n, 255n]; expect(packGas(hi, lo)).toEqual( - ethers.solidityPacked(["uint128", "uint128"], [hi, lo]) + encodePacked(["uint128", "uint128"], [hi, lo]) ); // Random input expect(packGas(15, "255")).toEqual( - ethers.solidityPacked(["uint128", "uint128"], ["15", "0xff"]) + encodePacked(["uint128", "uint128"], [15n, BigInt("0xff")]) ); }); it("packSignature", () => { const x = "0x491e245db3914b85807f3807f2125b9ed9722d0e9f3fa0fe325b31893fa5e693387178ae4a51f304556c1b2e9dd24f1120d073f93017af006ad801a639214ea61b"; expect(packSignature(x, 1, 2)).toEqual( - ethers.solidityPacked(["uint48", "uint48", "bytes"], [1, 2, x]) + encodePacked(["uint48", "uint48", "bytes"], [1, 2, x]) ); }); @@ -50,16 +47,7 @@ describe("Utility Functions (mostly byte packing)", () => { paymasterPostOpGasLimit: "0x1", }; - expect(packPaymasterData(data).toLowerCase()).toEqual( - ethers.hexlify( - ethers.concat([ - data.paymaster!, - ethers.toBeHex(data.paymasterVerificationGasLimit || "0x", 16), - ethers.toBeHex(data.paymasterPostOpGasLimit || "0x", 16), - data.paymasterData || "0x", - ]) - ) - ); + expect(packPaymasterData(data).toLowerCase()).toEqual("0x4685d9587a7f72da32dc323bfff17627aa632c6100000000000000000000000000004e1700000000000000000000000000000001000000000000000000000000000000000000000000000000000000006682e19400000000000000000000000000000000000000000000000000000000000000007eacbfaa696a236960b8eac0a9725f96c941665b893aa80b2ae3a41814f10b813d07db0b07b89080e4fd436d1966bc2ff7002a686087a310348391db8e9d44881c"); }); it("containsValue", () => { diff --git a/tsconfig.json b/tsconfig.json index efcaf13..120bf04 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,7 @@ "moduleResolution": "Node", "declaration": true, "esModuleInterop": true, + // Best practices "strict": true, /* Enable all strict type-checking options. */ "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ @@ -19,8 +20,8 @@ "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // TODO: Add this back! - // "noUncheckedIndexedAccess": true, + "noUncheckedIndexedAccess": true, + "noPropertyAccessFromIndexSignature": false, "skipLibCheck": true },