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

MPC Contract Update #16

Merged
merged 2 commits into from
Mar 23, 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
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;
}
Comment on lines +49 to +60
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These public access methods were recently made while making this.sender private and the opensea script got botched because it still used this field.

Script was hotfixed here, but we should really run some CI to ensure that the examples directory is "sound"


/**
* 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;
}