Skip to content

Commit

Permalink
Export Transaction Utils (#85)
Browse files Browse the repository at this point in the history
  • Loading branch information
bh2smith authored Aug 5, 2024
1 parent 17daa64 commit d729481
Show file tree
Hide file tree
Showing 11 changed files with 261 additions and 15 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ jobs:
- name: E2E Test
run: yarn && yarn test e2e
env:
NEAR_MULTICHAIN_CONTRACT: v2.multichain-mpc.testnet
NEAR_MULTICHAIN_CONTRACT: v1.signer-dev.testnet
NEAR_ACCOUNT_ID: ${{secrets.NEAR_ACCOUNT_ID}}
NEAR_ACCOUNT_PRIVATE_KEY: ${{secrets.NEAR_PK}}
8 changes: 5 additions & 3 deletions src/chains/ethereum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ export class NearEthAdapter {
): Promise<Hash> {
console.log("Creating Payload for sender:", this.address);
const { transaction, signArgs } = await this.createTxPayload(txData);
console.log("Requesting signature from Near...");
console.log(
`Requesting signature from Near at ${this.mpcContract.accountId}`
);
const signature = await this.mpcContract.requestSignature(
signArgs,
nearGas
Expand Down Expand Up @@ -244,12 +246,12 @@ export class NearEthAdapter {

/// Mintbase Wallet
async handleSessionRequest(
request: Web3WalletTypes.SessionRequest
request: Partial<Web3WalletTypes.SessionRequest>
): Promise<NearEthTxData> {
const {
chainId,
request: { method, params },
} = request.params;
} = request.params!;
console.log(`Session Request of type ${method} for chainId ${chainId}`);
const { evmMessage, payload, signatureRecoveryData } = await wcRouter(
method,
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export * from "./mpcContract";
export * from "./types/types";
export * from "./utils/signature";
export * from "./network";
export * from "./utils/transaction";

interface SetupConfig {
accountId: string;
Expand Down
4 changes: 4 additions & 0 deletions src/mpcContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ export class MultichainContract {
}) as MultichainContractInterface;
}

accountId(): string {
return this.contract.contractId;
}

deriveEthAddress = async (derivationPath: string): Promise<Address> => {
const rootPublicKey = await this.contract.public_key();

Expand Down
25 changes: 15 additions & 10 deletions src/utils/transaction.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
Hex,
PublicClient,
TransactionSerializable,
keccak256,
parseTransaction,
Expand All @@ -22,15 +23,19 @@ export function buildTxPayload(unsignedTxHash: `0x${string}`): number[] {

export async function populateTx(
tx: BaseTx,
from: Hex
from: Hex,
client?: PublicClient
): Promise<TransactionSerializable> {
const network = Network.fromChainId(tx.chainId);
const provider = client || Network.fromChainId(tx.chainId).client;
const chainId = await provider.getChainId();
if (chainId !== tx.chainId) {
// Can only happen when client is provided.
throw new Error(
`client chainId=${chainId} mismatch with tx.chainId=${tx.chainId}`
);
}
const transactionData = {
nonce:
tx.nonce ||
(await network.client.getTransactionCount({
address: from,
})),
nonce: tx.nonce ?? (await provider.getTransactionCount({ address: from })),
account: from,
to: tx.to,
value: tx.value ?? 0n,
Expand All @@ -39,15 +44,15 @@ export async function populateTx(
const [estimatedGas, { maxFeePerGas, maxPriorityFeePerGas }] =
await Promise.all([
// Only estimate gas if not provided.
tx.gas || network.client.estimateGas(transactionData),
network.client.estimateFeesPerGas(),
tx.gas || provider.estimateGas(transactionData),
provider.estimateFeesPerGas(),
]);
return {
...transactionData,
gas: estimatedGas,
maxFeePerGas,
maxPriorityFeePerGas,
chainId: network.chainId,
chainId,
};
}

Expand Down
34 changes: 34 additions & 0 deletions tests/unit/chains.near.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { KeyPair } from "near-api-js";
import {
nearAccountFromAccountId,
nearAccountFromKeyPair,
createNearAccount,
} from "../../src/";

const TESTNET_CONFIG = {
networkId: "testnet",
nodeUrl: "https://rpc.testnet.near.org",
};
describe("near", () => {
it("createNearAccount", async () => {
const id = "farmface.testnet";
const account = await createNearAccount(id, TESTNET_CONFIG);
expect(account.accountId).toBe(id);
});

it("nearAccountFromAccountId", async () => {
const id = "farmface.testnet";
const account = await nearAccountFromAccountId(id, TESTNET_CONFIG);
expect(account.accountId).toBe(id);
});

it("nearAccountFromKeyPair", async () => {
const id = "farmface.testnet";
const account = await nearAccountFromKeyPair({
accountId: id,
network: TESTNET_CONFIG,
keyPair: KeyPair.fromRandom("ed25519"),
});
expect(account.accountId).toBe(id);
});
});
88 changes: 88 additions & 0 deletions tests/unit/ethereum.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { zeroAddress } from "viem";
import { setupAdapter } from "../../src/";

const accountId = "farmface.testnet";
const network = {
networkId: "testnet",
nodeUrl: "https://rpc.testnet.near.org",
};

// disable logging on this file.
console.log = () => null;
describe("ethereum", () => {
it("adapter (read) methods", async () => {
const adapter = await setupAdapter({
accountId,
network,
mpcContractId: "v1.signer-prod.testnet",
derivationPath: "ethereum,1",
});
expect(await adapter.address).toBe(
"0xe09907d0a59bf84a68f5249ab328fc0ce0417a28"
);
expect(await adapter.getBalance(100)).toBe(0n);
expect(adapter.nearAccountId()).toBe(accountId);
const { transaction } = await adapter.createTxPayload({
to: zeroAddress,
chainId: 11155111,
});

const request = adapter.mpcSignRequest(transaction);
expect(request.actions.length).toEqual(1);

expect(() =>
adapter.handleSessionRequest({
params: {
chainId: "11155111",
request: { method: "poop", params: [] },
},
})
).rejects.toThrow("Unhandled session_request method: poop");

const ethSign = await adapter.handleSessionRequest({
params: {
chainId: "11155111",
request: {
method: "eth_sign",
params: [adapter.address, "0x"],
},
},
});

expect(ethSign).toStrictEqual({
nearPayload: {
signerId: "farmface.testnet",
receiverId: "v1.signer-prod.testnet",
actions: [
{
type: "FunctionCall",
params: {
methodName: "sign",
args: {
request: {
path: "ethereum,1",
payload: [
95, 53, 220, 233, 139, 164, 251, 162, 85, 48, 160, 38, 237,
128, 178, 206, 205, 170, 49, 9, 27, 164, 149, 139, 153, 181,
46, 161, 208, 104, 173, 173,
],
key_version: 0,
},
},
gas: "250000000000000",
deposit: "1",
},
},
],
},
evmMessage: "",
recoveryData: {
type: "eth_sign",
data: {
address: "0xe09907d0a59bf84a68f5249ab328fc0ce0417a28",
message: { raw: "0x" },
},
},
});
});
});
31 changes: 31 additions & 0 deletions tests/unit/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { setupAdapter } from "../../src/";
describe("index", () => {
it("setupAdapter", async () => {
const adapter = await setupAdapter({
accountId: "your-account.testnet",
network: {
networkId: "testnet",
nodeUrl: "https://rpc.testnet.near.org",
},
mpcContractId: "v1.signer-prod.testnet",
derivationPath: "ethereum,1",
});
expect(adapter.address).toBe("0x5898502fc8577c5a0ae0c6984bb33c394c11a0a5");
});

it("setupAdapter fails", async () => {
const adapter = await setupAdapter({
accountId: "your-account.testnet",
network: {
networkId: "testnet",
nodeUrl: "https://rpc.testnet.near.org",
},
mpcContractId: "v1.signer-prod.testnet",
derivationPath: "ethereum,1",
// KeyPair.fromRandom("ed25519").toString()
privateKey:
"ed25519:3UEFmgr6SdPJYekHgQgaLjbHeqHnJ5FmpdQ6NxD2u1618y3hom7KrDxFEZJixYGg9XBxtwrs4hxb2ChYBMf2bCMp",
});
expect(adapter.address).toBe("0x5898502fc8577c5a0ae0c6984bb33c394c11a0a5");
});
});
49 changes: 49 additions & 0 deletions tests/unit/mpcContract.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { createNearAccount, MultichainContract } from "../../src/";

const TESTNET_CONFIG = {
networkId: "testnet",
nodeUrl: "https://rpc.testnet.near.org",
};
const path = "derivationPath";
describe("mpcContract", () => {
it("Contract (Read) Methods", async () => {
const accountId = "farmface.testnet";
const contractId = "v1.signer-dev.testnet";
const account = await createNearAccount(accountId, TESTNET_CONFIG);
const mpc = new MultichainContract(account, contractId);
const ethAddress = await mpc.deriveEthAddress(path);
expect(mpc.accountId()).toEqual(contractId);
expect(ethAddress).toEqual("0xde886d5d90cf3ca465bcaf410fe2b460ec79a7d9");

const signArgs = {
payload: [1, 2],
path,
key_version: 0,
};
const expected = {
signerId: "farmface.testnet",
receiverId: "v1.signer-dev.testnet",
actions: [
{
type: "FunctionCall",
params: {
methodName: "sign",
args: {
request: {
payload: [1, 2],
path: "derivationPath",
key_version: 0,
},
},
gas: "250000000000000",
deposit: "1",
},
},
],
};
expect(mpc.encodeSignatureRequestTx(signArgs)).toEqual(expected);
// Set Gas:
expected.actions[0]!.params.gas = "1";
expect(mpc.encodeSignatureRequestTx(signArgs, 1n)).toEqual(expected);
});
});
16 changes: 16 additions & 0 deletions tests/unit/utils.signature.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
signatureFromOutcome,
signatureFromTxHash,
transformSignature,
} from "../../src/utils/signature";
Expand Down Expand Up @@ -48,4 +49,19 @@ describe("utility: get Signature", () => {
yParity: 1,
});
});

it("signatureForOutcome", () => {
// Outcome from: wkBWmTXoUgdm7RvdwNTUmtmDZLB14TfXMdyf57UXAaZ
const outcome = {
status: {
SuccessValue:
"eyJiaWdfciI6eyJhZmZpbmVfcG9pbnQiOiIwMkVBRDJCMUYwN0NDNDk4REIyNTU2MzE0QTZGNzdERkUzRUUzRDE0NTNCNkQ3OTJBNzcwOTE5MjRFNTFENEMyNDcifSwicyI6eyJzY2FsYXIiOiIxQTlGNjBDMkNDMjM5OEE1MDk3N0Q0Q0E5M0M0MDE2OEU4RjJDRTdBOUM5MEUzNzQ1MjJERjNDNzZDRjU0RjJFIn0sInJlY292ZXJ5X2lkIjoxfQ==",
},
};
expect(signatureFromOutcome(outcome)).toEqual({
r: "0xEAD2B1F07CC498DB2556314A6F77DFE3EE3D1453B6D792A77091924E51D4C247",
s: "0x1A9F60C2CC2398A50977D4CA93C40168E8F2CE7A9C90E374522DF3C76CF54F2E",
yParity: 1,
});
});
});
18 changes: 17 additions & 1 deletion tests/unit/utils.transaction.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { TransactionWithSignature } from "../../src";
import { zeroAddress } from "viem";
import { Network, TransactionWithSignature } from "../../src";
import {
buildTxPayload,
addSignature,
toPayload,
populateTx,
} from "../../src/utils/transaction";

describe("Transaction Builder Functions", () => {
Expand Down Expand Up @@ -38,4 +40,18 @@ describe("Transaction Builder Functions", () => {
"0x02f86b83aa36a780845974e6f084d0aa7af08094deadbeef0000000000000000000000000b00b1e50180c001a0ef532579e267c932b959a1adb9e455ac3c5397d0473471c4c3dd5d62fd4d7edea07c195e658c713d601d245311a259115bb91ec87c86acb07c03bd9c1936a6a9e8"
);
});

it("populateTx", async () => {
const baseTx = {
chainId: 11155111,
to: zeroAddress,
};
await expect(() =>
populateTx(baseTx, zeroAddress, Network.fromChainId(100).client)
).rejects.toThrow("client chainId=100 mismatch with tx.chainId=11155111");

const tx = await populateTx(baseTx, zeroAddress);
expect(tx.to).toEqual(zeroAddress);
expect(tx.value).toEqual(0n);
});
});

0 comments on commit d729481

Please sign in to comment.