Skip to content

Commit

Permalink
MPC Contract Update (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
bh2smith authored Mar 23, 2024
1 parent adfe6f1 commit bf31a15
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 78 deletions.
139 changes: 89 additions & 50 deletions src/chains/ethereum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,21 @@ import {
import {
BaseTx,
NearEthAdapterParams,
NearSignPayload,
NearContractFunctionPayload,
TxPayload,
} from "../types";
import { queryGasPrice } from "../utils/gasPrice";
import { MultichainContract } from "../mpcContract";
import BN from "bn.js";

export class NearEthAdapter {
private client: PublicClient;
private ethClient: PublicClient;
private scanUrl: string;
private gasStationUrl: string;

private mpcContract: MultichainContract;
private derivationPath: string;
sender: Address;
private sender: Address;

private constructor(config: {
providerUrl: string;
Expand All @@ -35,14 +36,29 @@ export class NearEthAdapter {
derivationPath: string;
sender: Address;
}) {
this.client = createPublicClient({ transport: http(config.providerUrl) });
this.ethClient = createPublicClient({
transport: http(config.providerUrl),
});
this.scanUrl = config.scanUrl;
this.mpcContract = config.mpcContract;
this.gasStationUrl = config.gasStationUrl;
this.derivationPath = config.derivationPath;
this.sender = config.sender;
}

/**
* @returns ETH address derived by Near account via `derivationPath`.
*/
ethPublicKey(): Address {
return this.sender;
}
/**
* @returns Near accountId linked to derived ETH.
*/
nearAccountId(): string {
return this.mpcContract.contract.account.accountId;
}

/**
* Constructs an EVM instance with the provided configuration.
* @param {NearEthAdapterParams} args - The configuration object for the Adapter instance.
Expand All @@ -65,76 +81,61 @@ export class NearEthAdapter {
* acquires signature from Near MPC Contract and submits transaction to public mempool.
*
* @param {BaseTx} txData - Minimal transaction data to be signed by Near MPC and executed on EVM.
* @param {BN} nearGas - manually specified gas to be sent with signature request (default 200 TGAS).
* Note that the signature request is a recursive function.
*/
async signAndSendTransaction(txData: BaseTx, nearGas?: BN): Promise<Hash> {
console.log("Creating Payload for sender:", this.sender);
const { transaction, payload } = await this.createTxPayload(txData);
const { transaction, signArgs } = await this.createTxPayload(txData);
console.log("Requesting signature from Near...");
const { big_r, big_s } = await this.mpcContract.requestSignature(
payload,
this.derivationPath,
signArgs,
nearGas
);

const signedTx = this.reconstructSignature(transaction, big_r, big_s);
console.log("Relaying signed tx to EVM...");
return this.relayTransaction(signedTx);
return this.relayTransaction(transaction, big_r, big_s);
}

/**
* Takes a minimally declared Ethereum Transaction,
* builds the full transaction payload (with gas estimates, prices etc...),
* acquires signature from Near MPC Contract and submits transaction to public mempool.
*
* @param {BaseTx} txData - Minimal transaction data to be signed by Near MPC and executed on EVM.
* @param {BN} nearGas - manually specified gas to be sent with signature request (default 200 TGAS).
* Note that the signature request is a recursive function.
*/
async getSignatureRequestPayload(
txData: BaseTx,
nearGas?: BN
): Promise<{
transaction: FeeMarketEIP1559Transaction;
requestPayload: NearSignPayload;
requestPayload: NearContractFunctionPayload;
}> {
console.log("Creating Payload for sender:", this.sender);
const { transaction, payload } = await this.createTxPayload(txData);
const { transaction, signArgs } = await this.createTxPayload(txData);
console.log("Requesting signature from Near...");
return {
transaction,
requestPayload: await this.mpcContract.buildSignatureRequestTx(
payload,
this.derivationPath,
requestPayload: await this.mpcContract.encodeSignatureRequestTx(
signArgs,
nearGas
),
};
}

reconstructSignature = (
transaction: FeeMarketEIP1559Transaction,
big_r: string,
big_s: string
): FeeMarketEIP1559Transaction => {
const r = Buffer.from(big_r.substring(2), "hex");
const s = Buffer.from(big_s, "hex");

const candidates = [0n, 1n].map((v) => transaction.addSignature(v, r, s));
const signature = candidates.find(
(c) =>
c.getSenderAddress().toString().toLowerCase() ===
this.sender.toLowerCase()
);

if (!signature) {
throw new Error("Signature is not valid");
}

return signature;
};

/**
* Relays signed transaction to Etherem mempool for execution.
* @param signedTx - Signed Ethereum transaction.
* @returns Transaction Hash of relayed transaction.
*/
async relayTransaction(signedTx: FeeMarketEIP1559Transaction): Promise<Hash> {
const serializedTx = bytesToHex(signedTx.serialize()) as Hex;
const txHash = await this.client.sendRawTransaction({
serializedTransaction: serializedTx,
});
console.log(`Transaction Confirmed: ${this.scanUrl}/tx/${txHash}`);
return txHash;
async relayTransaction(
transaction: FeeMarketEIP1559Transaction,
big_r: string,
big_s: string
): Promise<Hash> {
const signedTx = await this.reconstructSignature(transaction, big_r, big_s);
return this.relaySignedTransaction(signedTx);
}

/**
Expand All @@ -146,17 +147,18 @@ export class NearEthAdapter {
*/
async createTxPayload(tx: BaseTx): Promise<TxPayload> {
const transaction = await this.buildTransaction(tx);
console.log("Built Transaction", JSON.stringify(transaction));
console.log("Built (unsigned) Transaction", transaction.toJSON());
const payload = Array.from(
new Uint8Array(transaction.getHashedMessageToSign().slice().reverse())
);
return { transaction, payload };
const signArgs = { payload, path: this.derivationPath, key_version: 0 };
return { transaction, signArgs };
}

private async buildTransaction(
tx: BaseTx
): Promise<FeeMarketEIP1559Transaction> {
const nonce = await this.client.getTransactionCount({
const nonce = await this.ethClient.getTransactionCount({
address: this.sender,
});
const { maxFeePerGas, maxPriorityFeePerGas } = await queryGasPrice(
Expand All @@ -169,15 +171,52 @@ export class NearEthAdapter {
value: parseEther(tx.amount.toString()),
data: tx.data || "0x",
};
const estimatedGas = await this.client.estimateGas(transactionData);
const estimatedGas = await this.ethClient.estimateGas(transactionData);
const transactionDataWithGasLimit = {
...transactionData,
gasLimit: BigInt(estimatedGas.toString()),
maxFeePerGas,
maxPriorityFeePerGas,
chainId: await this.client.getChainId(),
chainId: await this.ethClient.getChainId(),
};
console.log("TxData:", transactionDataWithGasLimit);
return FeeMarketEIP1559Transaction.fromTxData(transactionDataWithGasLimit);
}

private reconstructSignature = (
transaction: FeeMarketEIP1559Transaction,
big_r: string,
big_s: string
): FeeMarketEIP1559Transaction => {
const r = Buffer.from(big_r.substring(2), "hex");
const s = Buffer.from(big_s, "hex");

const candidates = [0n, 1n].map((v) => transaction.addSignature(v, r, s));
const signature = candidates.find(
(c) =>
c.getSenderAddress().toString().toLowerCase() ===
this.sender.toLowerCase()
);

if (!signature) {
throw new Error("Signature is not valid");
}

return signature;
};

/**
* Relays signed transaction to Etherem mempool for execution.
* @param signedTx - Signed Ethereum transaction.
* @returns Transaction Hash of relayed transaction.
*/
private async relaySignedTransaction(
signedTx: FeeMarketEIP1559Transaction
): Promise<Hash> {
const serializedTx = bytesToHex(signedTx.serialize()) as Hex;
const txHash = await this.ethClient.sendRawTransaction({
serializedTransaction: serializedTx,
});
console.log(`Transaction Confirmed: ${this.scanUrl}/tx/${txHash}`);
return txHash;
}
}
43 changes: 17 additions & 26 deletions src/mpcContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,12 @@ import {
} from "./utils/kdf";
import { NO_DEPOSIT, nearAccountFromEnv, TGAS } from "./chains/near";
import BN from "bn.js";
import { NearSignPayload } from "./types";

interface ChangeMethodArgs<T> {
args: T;
gas: BN;
attachedDeposit: BN;
}

interface SignArgs {
path: string;
payload: number[];
}

interface SignResult {
big_r: string;
big_s: string;
}
import {
ChangeMethodArgs,
MPCSignature,
NearContractFunctionPayload,
SignArgs,
} from "./types";

interface MultichainContractInterface extends Contract {
// Define the signature for the `public_key` view method
Expand All @@ -33,6 +22,10 @@ interface MultichainContractInterface extends Contract {
sign: (args: ChangeMethodArgs<SignArgs>) => Promise<[string, string]>;
}

/**
* High-level interface for the Near MPC-Recovery Contract
* located in: https://github.com/near/mpc-recovery
*/
export class MultichainContract {
contract: MultichainContractInterface;

Expand Down Expand Up @@ -65,24 +58,22 @@ export class MultichainContract {
};

requestSignature = async (
payload: number[],
path: string,
signArgs: SignArgs,
gas?: BN
): Promise<SignResult> => {
): Promise<MPCSignature> => {
const [big_r, big_s] = await this.contract.sign({
args: { path, payload },
args: signArgs,
// Default of 200 TGAS
gas: gas || TGAS.muln(200),
attachedDeposit: new BN(NO_DEPOSIT),
});
return { big_r, big_s };
};

buildSignatureRequestTx = async (
payload: number[],
path: string,
encodeSignatureRequestTx = async (
signArgs: SignArgs,
gas?: BN
): Promise<NearSignPayload> => {
): Promise<NearContractFunctionPayload> => {
return {
signerId: this.contract.account.accountId,
receiverId: this.contract.contractId,
Expand All @@ -91,7 +82,7 @@ export class MultichainContract {
type: "FunctionCall",
params: {
methodName: "sign",
args: { path, payload },
args: signArgs,
gas: (gas || TGAS.muln(200)).toString(),
deposit: NO_DEPOSIT,
},
Expand Down
52 changes: 50 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { FeeMarketEIP1559Transaction } from "@ethereumjs/tx";
import { Address, Hex } from "viem";
import { MultichainContract } from "./mpcContract";
import { FunctionCallAction } from "@near-wallet-selector/core";
import BN from "bn.js";

export interface BaseTx {
/// Recipient of the transaction
Expand Down Expand Up @@ -40,13 +41,60 @@ export interface GasPrices {
maxPriorityFeePerGas: bigint;
}

/// Near Contract Type for change methods
export interface ChangeMethodArgs<T> {
/// Change method function agruments.
args: T;
/// GasLimit on transaction execution.
gas: BN;
/// Deposit (i.e. payable amount) to attach to transaction.
attachedDeposit: BN;
}

/**
* Arguments required for signature request from MPC Contract
* cf. https://github.com/near/mpc-recovery/blob/ac040bcbb31ba9362a6641a5899647105a53ee4a/contract/src/lib.rs#L297-L320
*/
export interface SignArgs {
/// Derivation Path of for ETH account associated with Near AccountId
path: string;
/// Serialized Ethereum Transaction Bytes.
payload: number[];
/// version number associated with derived ETH Address (must be increasing).
key_version: number;
}

export interface TxPayload {
/// Deserialized Ethereum Transaction.
transaction: FeeMarketEIP1559Transaction;
payload: number[];
/// Arguments required by Near MPC Contract signature request.
signArgs: SignArgs;
}

export interface NearSignPayload {
export interface NearContractFunctionPayload {
/// Signer of function call.
signerId: string;
/// Transaction Recipient (a Near ContractId).
receiverId: string;
/// Function call actions.
actions: Array<FunctionCallAction>;
}

/**
* Result Type of MPC contract signature request.
* Representing Affine Points on eliptic curve.
*/
export interface MPCSignature {
big_r: string;
big_s: string;
}

/**
* Sufficient data required to construct a signed Ethereum Transaction.
*/
export interface TransactionWithSignature {
/// Unsigned Ethereum transaction data.
transaction: FeeMarketEIP1559Transaction;
/// Representation of the transaction's signature.
signature: MPCSignature;
}

0 comments on commit bf31a15

Please sign in to comment.