Skip to content

Commit

Permalink
Basic WC Recover Signature (#41)
Browse files Browse the repository at this point in the history
  • Loading branch information
bh2smith authored May 15, 2024
1 parent 1663057 commit 4c98911
Show file tree
Hide file tree
Showing 4 changed files with 387 additions and 91 deletions.
58 changes: 49 additions & 9 deletions src/chains/ethereum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,23 @@ import {
TypedDataDefinition,
parseTransaction,
TransactionSerializable,
keccak256,
} from "viem";
import {
BaseTx,
NearEthAdapterParams,
NearContractFunctionPayload,
TxPayload,
TransactionWithSignature,
MPCSignature,
RecoveryData,
} from "../types/types";
import { MultichainContract } from "../mpcContract";
import { buildTxPayload, addSignature, populateTx } from "../utils/transaction";
import { Network } from "../network";
import { pickValidSignature } from "../utils/signature";
import { Web3WalletTypes } from "@walletconnect/web3wallet";
import { wcRouter } from "../wallet-connect/handlers";
import { offChainRecovery, wcRouter } from "../wallet-connect/handlers";

export class NearEthAdapter {
readonly mpcContract: MultichainContract;
Expand Down Expand Up @@ -169,16 +172,24 @@ export class NearEthAdapter {
* @param serializedTransaction - Signed Ethereum transaction.
* @returns Transaction Hash of relayed transaction.
*/
private async relaySignedTransaction(
serializedTransaction: Hex
async relaySignedTransaction(
serializedTransaction: Hex,
wait: boolean = true
): Promise<Hash> {
const tx = parseTransaction(serializedTransaction);
const network = Network.fromChainId(tx.chainId!);
const hash = await network.client.sendRawTransaction({
serializedTransaction,
});
console.log(`Transaction Confirmed: ${network.scanUrl}/tx/${hash}`);
return hash;
if (wait) {
const hash = await network.client.sendRawTransaction({
serializedTransaction,
});
console.log(`Transaction Confirmed: ${network.scanUrl}/tx/${hash}`);
return hash;
} else {
network.client.sendRawTransaction({
serializedTransaction,
});
return keccak256(serializedTransaction);
}
}
// Below code is inspired by https://github.com/Connor-ETHSeoul/near-viem

Expand Down Expand Up @@ -251,24 +262,53 @@ export class NearEthAdapter {
];
}

async recoverSignature(
recoveryData: RecoveryData,
signatureData: MPCSignature
): Promise<Hex> {
const { big_r, big_s } = signatureData;
if (recoveryData.type === "eth_sendTransaction") {
const signature = addSignature(
{ transaction: recoveryData.data as Hex, signature: signatureData },
this.address
);
// Returns relayed transaction hash (without waiting for confirmation).
return this.relaySignedTransaction(signature, false);
}
const r = `0x${big_r.substring(2)}` as Hex;
const s = `0x${big_s}` as Hex;
const sigs: [Hex, Hex] = [
serializeSignature({ r, s, yParity: 0 }),
serializeSignature({ r, s, yParity: 1 }),
];
return offChainRecovery(recoveryData, sigs);
}

/// Mintbase Wallet
async handleSessionRequest(request: Web3WalletTypes.SessionRequest): Promise<{
evmMessage: string | TransactionSerializable;
nearPayload: NearContractFunctionPayload;
recoveryData: RecoveryData;
}> {
const {
chainId,
request: { method, params },
} = request.params;
console.log(`Session Request of type ${method} for chainId ${chainId}`);
const { evmMessage, payload } = await wcRouter(method, chainId, params);
const { evmMessage, payload, signatureRecoveryData } = await wcRouter(
method,
chainId,
params
);
console.log("Parsed Request:", payload, signatureRecoveryData);
return {
nearPayload: this.mpcContract.encodeSignatureRequestTx({
path: this.derivationPath,
payload,
key_version: 0,
}),
evmMessage,
recoveryData: signatureRecoveryData,
};
}
}
23 changes: 22 additions & 1 deletion src/types/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { MultichainContract } from "../mpcContract";
import { FunctionCallAction } from "@near-wallet-selector/core";
import { Hex } from "viem";
import { Hex, SignableMessage } from "viem";

export interface BaseTx {
/// Recipient of the transaction
Expand Down Expand Up @@ -67,6 +67,27 @@ export interface MPCSignature {
big_s: string;
}

export interface MessageData {
address: Hex;
message: SignableMessage;
}

export interface TypedMessageData {
address: Hex;
/* eslint-disable @typescript-eslint/no-explicit-any */
types: any;
primaryType: any;
message: any;
domain: any;
/* eslint-enable @typescript-eslint/no-explicit-any */
}

export interface RecoveryData {
// TODO use enum!
type: string;
data: MessageData | TypedMessageData | Hex;
}

/**
* Sufficient data required to construct a signed Ethereum Transaction.
*/
Expand Down
90 changes: 80 additions & 10 deletions src/wallet-connect/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ import {
hashTypedData,
keccak256,
serializeTransaction,
verifyMessage,
verifyTypedData,
} from "viem";
import { populateTx, toPayload } from "../utils/transaction";
import { MessageData, RecoveryData, TypedMessageData } from "../types/types";
import { pickValidSignature } from "../utils/signature";

// Interface for Ethereum transaction parameters
export interface EthTransactionParams {
Expand All @@ -18,15 +22,19 @@ export interface EthTransactionParams {
data?: Hex;
}

// Interface for personal sign parameters (message and address)
/// Interface for personal sign parameters (message and address)
export type PersonalSignParams = [Hex, Hex];

/// Interface for eth_sign parameters (address and message)
export type EthSignParams = [Hex, Hex];

// Interface for complex structured parameters like EIP-712
export type TypedDataParams = [Hex, string];

type SessionRequestParams =
| EthTransactionParams[]
| PersonalSignParams
| EthSignParams
| TypedDataParams;

export async function wcRouter(
Expand All @@ -36,17 +44,35 @@ export async function wcRouter(
): Promise<{
evmMessage: TransactionSerializable | string;
payload: number[];
signatureRecoveryData: RecoveryData;
}> {
switch (method) {
// I believe {personal,eth}_sign both get routed to the same place.
case "eth_sign":
case "eth_sign": {
const [sender, messageHash] = params as EthSignParams;
return {
evmMessage: fromHex(messageHash, "string"),
payload: toPayload(hashMessage({ raw: messageHash })),
signatureRecoveryData: {
type: method,
data: {
address: sender,
message: { raw: messageHash },
},
},
};
}
case "personal_sign": {
const [messageHash, sender] = params as PersonalSignParams;
const message = fromHex(messageHash, "string");
console.log(`Message to be signed by ${sender}: ${message}`);
return {
evmMessage: fromHex(messageHash, "string"),
payload: toPayload(hashMessage(messageHash)),
payload: toPayload(hashMessage({ raw: messageHash })),
signatureRecoveryData: {
type: method,
data: {
address: sender,
message: { raw: messageHash },
},
},
};
}
case "eth_sendTransaction": {
Expand All @@ -55,7 +81,7 @@ export async function wcRouter(
{
to: tx.to,
chainId: parseInt(stripEip155Prefix(chainId)),
value: fromHex(tx.value || "0x", "bigint"),
value: fromHex(tx.value || "0x0", "bigint"),
data: tx.data,
gas: tx.gas ? fromHex(tx.gas, "bigint") : undefined,
},
Expand All @@ -64,24 +90,68 @@ export async function wcRouter(
return {
payload: toPayload(keccak256(serializeTransaction(transaction))),
evmMessage: transaction,
signatureRecoveryData: {
type: "eth_sendTransaction",
data: serializeTransaction(transaction),
},
};
}
case "eth_signTypedData":
case "eth_signTypedData_v4": {
const [sender, dataString] = params as TypedDataParams;
const typedData = JSON.parse(dataString);
console.log(
`Received Typed Data signature request from ${sender}: ${JSON.stringify(typedData)}`
);
return {
evmMessage: dataString,
payload: toPayload(hashTypedData(typedData)),
signatureRecoveryData: {
type: "eth_signTypedData",
data: {
address: sender,
types: typedData.types,
primaryType: typedData.primaryType,
message: typedData.message,
domain: typedData.domain,
},
},
};
}
}
throw new Error(`Unhandled session_request method: ${method}`);
}

export async function offChainRecovery(
recoveryData: RecoveryData,
sigs: [Hex, Hex]
): Promise<Hex> {
let validity: [boolean, boolean];
if (recoveryData.type === "personal_sign") {
validity = await Promise.all([
verifyMessage({
signature: sigs[0],
...(recoveryData.data as MessageData),
}),
verifyMessage({
signature: sigs[1],
...(recoveryData.data as MessageData),
}),
]);
} else if (recoveryData.type === "eth_signTypedData") {
validity = await Promise.all([
verifyTypedData({
signature: sigs[0],
...(recoveryData.data as TypedMessageData),
}),
verifyTypedData({
signature: sigs[1],
...(recoveryData.data as TypedMessageData),
}),
]);
} else {
throw new Error("Invalid Path");
}
return pickValidSignature(validity, sigs);
}

function stripEip155Prefix(eip155Address: string): string {
return eip155Address.split(":").pop() ?? "";
}
Loading

0 comments on commit 4c98911

Please sign in to comment.