Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Near Adapter for Signature #1

Merged
merged 12 commits into from
Jun 14, 2024
8 changes: 8 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
ETH_RPC=https://rpc2.sepolia.org
NEAR_MULTICHAIN_CONTRACT=v2.multichain-mpc.testnet

NEAR_ACCOUNT_ID=
NEAR_ACCOUNT_PRIVATE_KEY=

SAFE_SALT_NONCE=
ERC4337_BUNDLER_URL=
170 changes: 137 additions & 33 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
// TODO(nlordell): Should probably be configured correctly with tsconfig.json
/// <reference lib="dom" />

import {
getProxyFactoryDeployment,
getSafeL2SingletonDeployment,
Expand All @@ -11,9 +8,10 @@ import {
} from "@safe-global/safe-modules-deployments";
import dotenv from "dotenv";
import { ethers } from "ethers";
import { NearEthAdapter, MultichainContract } from "near-ca";

dotenv.config();
const { NEAR_EVM_ADDRESS, SAFE_SALT_NONCE, ERC4337_BUNDLER_URL } = process.env;
const { SAFE_SALT_NONCE, ERC4337_BUNDLER_URL, ETH_RPC } = process.env;

type DeploymentFunction = (filter?: {
version: string;
Expand All @@ -40,13 +38,25 @@ async function getDeployment(
);
}

async function getNearSignature(hash: ethers.BytesLike) {
// TODO(bh2smith): do your thing
//assert(ethers.recoverAddress(hash, "0x...") === NEAR_EVM_ADDRESS);
return ethers.solidityPacked(["uint256", "uint256", "uint8"], [1, 2, 27]);
async function getNearSignature(
adapter: NearEthAdapter,
hash: ethers.BytesLike,
): Promise<`0x${string}`> {
const viemHash = typeof hash === "string" ? (hash as `0x${string}`) : hash;
// MPC Contract produces two possible signatures.
const signatures = await adapter.sign(viemHash);
for (const sig of signatures) {
if (
ethers.recoverAddress(hash, sig).toLocaleLowerCase() ===
adapter.address.toLocaleLowerCase()
) {
return sig;
}
}
throw new Error("Invalid signature!");
}

async function sendUserOperation(userOp: unknown, entryPoint: string) {
async function sendUserOperation(userOp: UserOperation, entryPoint: string) {
const response = await fetch(ERC4337_BUNDLER_URL!, {
method: "POST",
headers: {
Expand All @@ -65,34 +75,106 @@ async function sendUserOperation(userOp: unknown, entryPoint: string) {
}
const json = JSON.parse(body);
if (json.error) {
throw new Error(json.error.message ?? JSON.stringify(json.error));
throw new Error(JSON.stringify(json.error));
}
// This does not contain a transaction receipt! It is the `userOpHash`
return json.result;
}

async function getUserOpReceipt(userOpHash: string) {
const response = await fetch(ERC4337_BUNDLER_URL!, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
jsonrpc: "2.0",
method: "eth_getUserOperationReceipt",
id: 4337,
params: [userOpHash],
}),
});
const body = await response.text();
if (!response.ok) {
throw new Error(`Failed to send user op ${body}`);
}
const json = JSON.parse(body);
if (json.error) {
throw new Error(JSON.stringify(json.error));
}
return json.result;
}

interface ContractSuite {
singleton: ethers.Contract;
proxyFactory: ethers.Contract;
m4337: ethers.Contract;
moduleSetup: ethers.Contract;
entryPoint: ethers.Contract;
}

async function getSafeAddressForSetup(
contracts: ContractSuite,
setup: ethers.BytesLike,
saltNonce?: string,
): Promise<ethers.AddressLike> {
// 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 || 0],
),
);

// 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 contracts.proxyFactory.proxyCreationCode(),
await contracts.singleton.getAddress(),
],
);
return ethers.getCreate2Address(
await contracts.proxyFactory.getAddress(),
salt,
ethers.keccak256(initCode),
);
}

async function main() {
const provider = new ethers.JsonRpcProvider("https://rpc.sepolia.org");
const provider = new ethers.JsonRpcProvider(ETH_RPC);
const nearAdapter = await NearEthAdapter.fromConfig({
mpcContract: await MultichainContract.fromEnv(),
});
console.log(
`NearEth Adapter: ${nearAdapter.nearAccountId()} <> ${nearAdapter.address}`,
);

const safeDeployment = (fn: DeploymentFunction) =>
getDeployment(fn, { provider, version: "1.4.1" });
const m4337Deployment = (fn: DeploymentFunction) =>
getDeployment(fn, { provider, version: "0.3.0" });
// Need this first to get entryPoint address
const m4337 = await m4337Deployment(getSafe4337ModuleDeployment);
const contracts = {
singleton: await safeDeployment(getSafeL2SingletonDeployment),
proxyFactory: await safeDeployment(getProxyFactoryDeployment),
m4337: await m4337Deployment(getSafe4337ModuleDeployment),
m4337,
moduleSetup: await m4337Deployment(getSafeModuleSetupDeployment),
entryPoint: new ethers.Contract(
"0x0000000071727De22E5E9d8BAf0edAc6f37da032",
await m4337.SUPPORTED_ENTRYPOINT(),
[`function getNonce(address, uint192 key) view returns (uint256 nonce)`],
provider,
),
};
console.log("Proxy Factory", await contracts.proxyFactory.getAddress());

const setup = await contracts.singleton.interface.encodeFunctionData(
"setup",
[
[NEAR_EVM_ADDRESS],
[nearAdapter.address],
1,
contracts.moduleSetup.target,
contracts.moduleSetup.interface.encodeFunctionData("enableModules", [
Expand All @@ -104,13 +186,12 @@ async function main() {
ethers.ZeroAddress,
],
);
const safeAddress =
await contracts.proxyFactory.createProxyWithNonce.staticCall(
contracts.singleton,
setup,
SAFE_SALT_NONCE,
);
Comment on lines -108 to -112
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was removed in favour of getCreate2Address (which should work in all cases - instead of only pre-deployment).


const safeAddress = await getSafeAddressForSetup(
contracts,
setup,
SAFE_SALT_NONCE,
);
console.log("Safe Address:", safeAddress);
const safeNotDeployed = (await provider.getCode(safeAddress)) === "0x";
const { maxPriorityFeePerGas, maxFeePerGas } = await provider.getFeeData();
if (!maxPriorityFeePerGas || !maxFeePerGas) {
Expand All @@ -130,9 +211,9 @@ async function main() {
: {}),
// <https://github.com/safe-global/safe-modules/blob/9a18245f546bf2a8ed9bdc2b04aae44f949ec7a0/modules/4337/contracts/Safe4337Module.sol#L172>
callData: contracts.m4337.interface.encodeFunctionData("executeUserOp", [
ethers.ZeroAddress,
0,
"0x",
nearAdapter.address,
1n, // 1 wei
ethers.hexlify(ethers.toUtf8Bytes("https://github.com/bh2smith/nearly-safe")),
0,
]),
verificationGasLimit: ethers.toBeHex(safeNotDeployed ? 500000 : 100000),
Expand All @@ -145,7 +226,7 @@ async function main() {
//paymasterGasLimit: ethers.toBeHex(100000),
//paymasterData: paymasterCallData,
};
console.log(unsignedUserOp);
// console.log("Unsigned UserOp", unsignedUserOp);

const packGas = (hi: ethers.BigNumberish, lo: ethers.BigNumberish) =>
ethers.solidityPacked(["uint128", "uint128"], [hi, lo]);
Expand All @@ -168,15 +249,38 @@ async function main() {
paymasterAndData: "0x",
signature: ethers.solidityPacked(["uint48", "uint48"], [0, 0]),
});
console.log(safeOpHash);

const signature = await getNearSignature(safeOpHash);
console.log(
await sendUserOperation(
{ ...unsignedUserOp, signature },
await contracts.entryPoint.getAddress(),
),
console.log("Safe Op Hash", safeOpHash);
console.log("Signing with Near...");
const signature = ethers.solidityPacked(
["uint48", "uint48", "bytes"],
[0, 0, await getNearSignature(nearAdapter, safeOpHash)],
);
const userOpHash = await sendUserOperation(
{ ...unsignedUserOp, signature },
await contracts.entryPoint.getAddress(),
);
console.log("UserOp Hash", userOpHash);
// TODO(bh2smith) this is returning null because we are requesting it too soon!
// Maybe better to `eth_getUserOperationByHash` (although this also returns null).
const userOpReceipt = await getUserOpReceipt(userOpHash);
console.log("userOp Receipt", userOpReceipt);
}

/**
* Supported Representation of UserOperation for EntryPoint v0.7
*/
interface UserOperation {
sender: ethers.AddressLike;
nonce: string;
factory?: ethers.AddressLike;
factoryData?: ethers.BytesLike;
callData: string;
verificationGasLimit: string;
callGasLimit: string;
preVerificationGas: string;
maxPriorityFeePerGas: string;
maxFeePerGas: string;
signature: string;
}

main().catch((err) => {
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@
"@safe-global/safe-modules-deployments": "^2.1.1",
"dotenv": "^16.4.5",
"ethers": "^6.13.0",
"near-ca": "^0.0.9"
"near-ca": "^0.0.10-alpha.1"
},
"devDependencies": {
"@types/node": "^20.14.2",
bh2smith marked this conversation as resolved.
Show resolved Hide resolved
"prettier": "^3.3.2",
"ts-node": "^10.9.2",
"typescript": "^5.4.5"
Expand Down
24 changes: 18 additions & 6 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,13 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469"
integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==

"@types/node@^20.14.2":
version "20.14.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.2.tgz#a5f4d2bcb4b6a87bffcaa717718c5a0f208f4a18"
integrity sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==
dependencies:
undici-types "~5.26.4"

"@walletconnect/[email protected]":
version "2.1.2"
resolved "https://registry.yarnpkg.com/@walletconnect/auth-client/-/auth-client-2.1.2.tgz#cee304fb0cdca76f6bf4aafac96ef9301862a7e8"
Expand Down Expand Up @@ -1544,16 +1551,16 @@ near-api-js@^3.0.3:
near-abi "0.1.1"
node-fetch "2.6.7"

near-ca@^0.0.9:
version "0.0.9"
resolved "https://registry.yarnpkg.com/near-ca/-/near-ca-0.0.9.tgz#1bb6e36203af1957b1dfd646cccb6bcd2349edfe"
integrity sha512-7gGxWAUI/DfzbYzBtTW+e5V6gtyomarDBK+XL6aGiQ7/EGwt0Kpqx5/0uGqbNS20GS7tvRH2UrZm5dLzz0SPfw==
near-ca@^0.0.10-alpha.1:
version "0.0.10-alpha.1"
resolved "https://registry.yarnpkg.com/near-ca/-/near-ca-0.0.10-alpha.1.tgz#d5a06fa1e5208484798bbaf2fdaa218c1b9d4dc4"
integrity sha512-M2mA3pPBshJJfWh1icFssNiUwao+hZk0ePXoAjFbXtxBZ60mP+QqGw5DtoVNWCj1w0GhzNNnA2KooJZOI0At3Q==
dependencies:
"@near-wallet-selector/core" "^8.9.5"
"@walletconnect/web3wallet" "^1.12.0"
elliptic "^6.5.5"
near-api-js "^3.0.3"
viem "^2.10.8"
viem "^2.12.5"

node-addon-api@^7.0.0:
version "7.1.0"
Expand Down Expand Up @@ -1946,6 +1953,11 @@ uncrypto@^0.1.3:
resolved "https://registry.yarnpkg.com/uncrypto/-/uncrypto-0.1.3.tgz#e1288d609226f2d02d8d69ee861fa20d8348ef2b"
integrity sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==

undici-types@~5.26.4:
version "5.26.5"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==

unenv@^1.9.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/unenv/-/unenv-1.9.0.tgz#469502ae85be1bd3a6aa60f810972b1a904ca312"
Expand Down Expand Up @@ -2009,7 +2021,7 @@ v8-compile-cache-lib@^3.0.1:
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==

viem@^2.10.8:
viem@^2.12.5:
version "2.13.8"
resolved "https://registry.yarnpkg.com/viem/-/viem-2.13.8.tgz#d6aaeecc84e5ee5cac1566f103ed4cf0335ef811"
integrity sha512-JX8dOrCJKazNVs7YAahXnX+NANp0nlK16GyYjtQXILnar1daCPsLy4uzKgZDBVBD6DdRP2lsbPfo4X7QX3q5EQ==
Expand Down