Skip to content

Commit

Permalink
UserOperation Request Router (#57)
Browse files Browse the repository at this point in the history
Recent version of near-ca added a better EIP712TypedData type for our request router.

We update the encodeSignRequest to call into a custom built request router that should support several important Safe signature requests.
  • Loading branch information
bh2smith authored Sep 19, 2024
1 parent 4342da4 commit 32800b9
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 97 deletions.
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

0 comments on commit 32800b9

Please sign in to comment.