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

UserOperation Request Router #57

Merged
merged 3 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"@safe-global/safe-gateway-typescript-sdk": "^3.22.2",
"@safe-global/safe-modules-deployments": "^2.2.0",
"near-api-js": "^5.0.0",
"near-ca": "^0.5.2",
"near-ca": "^0.5.6",
"semver": "^7.6.3",
"viem": "^2.16.5"
},
Expand Down
52 changes: 32 additions & 20 deletions src/lib/safe-message.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/// This file is a viem implementation of the useDecodedSafeMessage hook from:
/// https://github.com/safe-global/safe-wallet-web
import { type SafeInfo } from "@safe-global/safe-gateway-typescript-sdk";
import { EIP712TypedData, RecoveryData, toPayload } from "near-ca";
import { gte } from "semver";
import {
Address,
Expand All @@ -9,22 +10,12 @@ import {
hashMessage,
hashTypedData,
isHex,
TypedDataDomain,
} from "viem";

interface TypedDataTypes {
name: string;
type: string;
}
type TypedMessageTypes = {
[key: string]: TypedDataTypes[];
};

export type EIP712TypedData = {
domain: TypedDataDomain;
types: TypedMessageTypes;
message: Record<string, unknown>;
primaryType: string;
export type DecodedSafeMessage = {
decodedMessage: string | EIP712TypedData;
safeMessageMessage: string;
safeMessageHash: Hash;
};

export type MinimalSafeInfo = Pick<SafeInfo, "address" | "version" | "chainId">;
Expand Down Expand Up @@ -116,14 +107,10 @@ const getDecodedMessage = (message: string): string => {
* safeMessageHash
* }`
*/
export function decodedSafeMessage(
export function decodeSafeMessage(
message: string | EIP712TypedData,
safe: MinimalSafeInfo
): {
decodedMessage: string | EIP712TypedData;
safeMessageMessage: string;
safeMessageHash: Hash;
} {
): DecodedSafeMessage {
const decodedMessage =
typeof message === "string" ? getDecodedMessage(message) : message;

Expand All @@ -134,6 +121,31 @@ export function decodedSafeMessage(
};
}

export function safeMessageTxData(
method: string,
message: DecodedSafeMessage,
sender: Address
): {
evmMessage: string;
payload: number[];
// We may eventually be able to abolish this.
recoveryData: RecoveryData;
} {
return {
evmMessage: message.safeMessageMessage,
payload: toPayload(message.safeMessageHash),
recoveryData: {
type: method,
data: {
address: sender,
// TODO - Upgrade Signable Message in near-ca
// @ts-expect-error: Type 'string | EIP712TypedData' is not assignable to type 'SignableMessage'.
message: decodedMessage,
},
},
};
}

// const isEIP712TypedData = (obj: any): obj is EIP712TypedData => {
// return (
// typeof obj === "object" &&
Expand Down
115 changes: 92 additions & 23 deletions src/tx-manager.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
import { FinalExecutionOutcome } from "near-api-js/lib/providers";
import {
NearEthAdapter,
NearEthTxData,
BaseTx,
setupAdapter,
signatureFromOutcome,
SignRequestData,
NearEthTxData,
EthSignParams,
RecoveryData,
toPayload,
PersonalSignParams,
} from "near-ca";
import { Address, Hash, Hex, serializeSignature } from "viem";

import { Erc4337Bundler } from "./lib/bundler";
import { encodeMulti } from "./lib/multisend";
import { ContractSuite } from "./lib/safe";
import { decodeSafeMessage, safeMessageTxData } from "./lib/safe-message";
import { MetaTransaction, UserOperation, UserOperationReceipt } from "./types";
import { getClient, isContract, packSignature } from "./util";
import {
getClient,
isContract,
metaTransactionsFromRequest,
packSignature,
} from "./util";

export class TransactionManager {
readonly nearAdapter: NearEthAdapter;
Expand Down Expand Up @@ -141,27 +151,18 @@ export class TransactionManager {
return this.safePack.getOpHash(userOp);
}

async encodeSignRequest(tx: BaseTx): Promise<NearEthTxData> {
const unsignedUserOp = await this.buildTransaction({
chainId: tx.chainId,
transactions: [
{
to: tx.to!,
value: (tx.value || 0n).toString(),
data: tx.data || "0x",
},
],
usePaymaster: true,
});
const safeOpHash = (await this.opHash(unsignedUserOp)) as `0x${string}`;
const signRequest = await this.nearAdapter.encodeSignRequest({
method: "hash",
chainId: 0,
params: safeOpHash as `0x${string}`,
});
async encodeSignRequest(
signRequest: SignRequestData,
usePaymaster: boolean
): Promise<NearEthTxData> {
const data = await this.requestRouter(signRequest, usePaymaster);
return {
...signRequest,
evmMessage: JSON.stringify(unsignedUserOp),
nearPayload: await this.nearAdapter.mpcContract.encodeSignatureRequestTx({
path: this.nearAdapter.derivationPath,
payload: data.payload,
key_version: 0,
}),
...data,
};
}

Expand Down Expand Up @@ -239,4 +240,72 @@ export class TransactionManager {
);
}
}

/**
* Handles routing of signature requests based on the provided method, chain ID, and parameters.
*
* @async
* @function requestRouter
* @param {SignRequestData} params - An object containing the method, chain ID, and request parameters.
* @returns {Promise<{ evmMessage: string; payload: number[]; recoveryData: RecoveryData }>}
* - Returns a promise that resolves to an object containing the Ethereum Virtual Machine (EVM) message,
* the payload (hashed data), and recovery data needed for reconstructing the signature request.
*/
async requestRouter(
{ method, chainId, params }: SignRequestData,
usePaymaster: boolean
): Promise<{
evmMessage: string;
payload: number[];
// We may eventually be able to abolish this.
recoveryData: RecoveryData;
}> {
const safeInfo = {
address: { value: this.address },
chainId: chainId.toString(),
// TODO: Should be able to read this from on chain.
version: "1.4.1+L2",
};
// TODO: We are provided with sender in the input, but also expect safeInfo.
// We should either confirm they agree or ignore one of the two.
switch (method) {
case "eth_signTypedData":
case "eth_signTypedData_v4":
case "eth_sign": {
const [sender, messageOrData] = params as EthSignParams;
return safeMessageTxData(
method,
decodeSafeMessage(messageOrData, safeInfo),
sender
);
}
case "personal_sign": {
const [messageHash, sender] = params as PersonalSignParams;
return safeMessageTxData(
method,
decodeSafeMessage(messageHash, safeInfo),
sender
);
}
case "eth_sendTransaction": {
const transactions = metaTransactionsFromRequest(params);
const userOp = await this.buildTransaction({
chainId,
transactions,
usePaymaster,
});
const opHash = await this.opHash(userOp);
return {
payload: toPayload(opHash),
evmMessage: JSON.stringify(userOp),
recoveryData: {
type: method,
// TODO: Double check that this is sufficient for UI.
// We may want to adapt and return the `MetaTransactions` instead.
data: opHash,
},
};
}
}
}
}
31 changes: 30 additions & 1 deletion src/util.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { Network } from "near-ca";
import { EthTransactionParams, Network, SessionRequestParams } from "near-ca";
import {
Address,
Hex,
concatHex,
encodePacked,
toHex,
PublicClient,
isHex,
parseTransaction,
zeroAddress,
} from "viem";

import { PaymasterData, MetaTransaction } from "./types";

//
export const PLACEHOLDER_SIG = encodePacked(["uint48", "uint48"], [0, 0]);

type IntLike = Hex | bigint | string | number;
Expand Down Expand Up @@ -55,3 +59,28 @@ export async function isContract(
export function getClient(chainId: number): PublicClient {
return Network.fromChainId(chainId).client;
}

export function metaTransactionsFromRequest(
params: SessionRequestParams
): MetaTransaction[] {
let transactions: EthTransactionParams[];
if (isHex(params)) {
// If RLP hex is given, decode the transaction and build EthTransactionParams
const tx = parseTransaction(params);
transactions = [
{
from: zeroAddress, // TODO: This is a hack - but its unused.
to: tx.to!,
value: tx.value ? toHex(tx.value) : "0x00",
data: tx.data || "0x",
},
];
} else {
transactions = params as EthTransactionParams[];
}
return transactions.map((tx) => ({
to: tx.to,
value: tx.value || "0x00",
data: tx.data || "0x",
}));
}
8 changes: 4 additions & 4 deletions tests/lib/safe-message.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { zeroAddress } from "viem";

import { decodedSafeMessage } from "../../src/lib/safe-message";
import { decodeSafeMessage } from "../../src/lib/safe-message";

describe("Multisend", () => {
const plainMessage = `Welcome to OpenSea!
Expand All @@ -22,7 +22,7 @@ Nonce:
version: "1.4.1+L2",
};
it("decodeSafeMessage", () => {
expect(decodedSafeMessage(plainMessage, safeInfo)).toStrictEqual({
expect(decodeSafeMessage(plainMessage, safeInfo)).toStrictEqual({
decodedMessage: plainMessage,
safeMessageMessage:
"0xc90ef7cffa3b5b1422e6c49ca7a5d7c1e9f514db067ec9bad52db13e83cbbb7c",
Expand All @@ -31,7 +31,7 @@ Nonce:
});
// Lower Safe Version.
expect(
decodedSafeMessage(plainMessage, { ...safeInfo, version: "1.2.1" })
decodeSafeMessage(plainMessage, { ...safeInfo, version: "1.2.1" })
).toStrictEqual({
decodedMessage: plainMessage,
safeMessageMessage:
Expand All @@ -47,7 +47,7 @@ Nonce:
chainId: "1",
version: null,
};
expect(() => decodedSafeMessage(plainMessage, versionlessSafeInfo)).toThrow(
expect(() => decodeSafeMessage(plainMessage, versionlessSafeInfo)).toThrow(
"Cannot create SafeMessage without version information"
);
});
Expand Down
Loading