From 9ea36c6b9e24f7920ef51c30086c9274e54573db Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Wed, 29 Jan 2025 15:34:24 +0100 Subject: [PATCH] TSDoc Migration --- src/beta.ts | 27 ++++- src/chains/ethereum.ts | 101 +++++++++------- src/chains/index.ts | 2 + src/chains/near.ts | 57 ++++----- src/index.ts | 87 +------------- src/mpcContract.ts | 88 +++++++++----- src/network/index.ts | 64 ++++++++-- src/setup.ts | 57 +++++++++ src/types/guards.ts | 75 +++++++++++- src/types/interfaces.ts | 247 ++++++++++++++++++--------------------- src/types/util.ts | 14 ++- src/utils/kdf.ts | 21 ++++ src/utils/mock-sign.ts | 52 +++++++-- src/utils/request.ts | 19 ++- src/utils/signature.ts | 42 ++++++- src/utils/transaction.ts | 49 +++++++- 16 files changed, 639 insertions(+), 363 deletions(-) create mode 100644 src/chains/index.ts create mode 100644 src/setup.ts diff --git a/src/beta.ts b/src/beta.ts index c507b85..f9b09d5 100644 --- a/src/beta.ts +++ b/src/beta.ts @@ -9,21 +9,39 @@ import { isSignMethod, NearEncodedSignRequest, signMethods } from "./types"; import { NearEthAdapter } from "./chains/ethereum"; import { requestRouter } from "./utils/request"; +/** + * Removes the EIP-155 prefix from an address string + * + * @param eip155Address - The EIP-155 formatted address + * @returns The address without the EIP-155 prefix + */ function stripEip155Prefix(eip155Address: string): string { return eip155Address.split(":").pop() ?? ""; } /** - * Features currently underdevelopment that will be migrated into the adapter class once refined. + * Features currently under development that will be migrated into the adapter class once refined. * These features are accessible through the adapter class as `adapter.beta.methodName(...)` */ export class Beta { adapter: NearEthAdapter; + /** + * Creates a new Beta instance + * + * @param adapter - The NearEthAdapter instance to use + */ constructor(adapter: NearEthAdapter) { this.adapter = adapter; } + /** + * Handles a WalletConnect session request by encoding it for NEAR + * + * @param request - The WalletConnect session request + * @returns The encoded request for NEAR + * @throws Error if the sign method is not supported + */ async handleSessionRequest( request: Partial ): Promise { @@ -52,6 +70,13 @@ export class Beta { }; } + /** + * Responds to a session request with a signature + * + * @param signature - The signature to respond with + * @param transaction - Optional transaction hex + * @returns The serialized signature or transaction hash + */ async respondSessionRequest( signature: Signature, transaction?: Hex diff --git a/src/chains/ethereum.ts b/src/chains/ethereum.ts index 82780fb..51e79a0 100644 --- a/src/chains/ethereum.ts +++ b/src/chains/ethereum.ts @@ -29,6 +29,9 @@ import { Beta } from "../beta"; import { requestRouter } from "../utils/request"; import { Account } from "near-api-js"; +/** + * Adapter class for interacting with Ethereum through NEAR MPC contract + */ export class NearEthAdapter { readonly mpcContract: IMpcContract; readonly address: Address; @@ -47,24 +50,28 @@ export class NearEthAdapter { } /** - * @returns Near Account linked to derived EVM account. + * Gets the NEAR Account linked to derived EVM account + * + * @returns The connected NEAR Account */ nearAccount(): Account { return this.mpcContract.connectedAccount; } /** - * @returns Near accountId linked to derived EVM account. + * Gets the NEAR account ID linked to derived EVM account + * + * @returns The connected NEAR account ID */ nearAccountId(): string { return this.mpcContract.connectedAccount.accountId; } /** - * Retrieves the balance of the Ethereum address associated with this adapter. + * Retrieves the balance of the Ethereum address associated with this adapter * - * @param {number} chainId - The chain ID of the Ethereum network to query. - * @returns {Promise} - A promise that resolves to the balance of the address in wei. + * @param chainId - The chain ID of the Ethereum network to query + * @returns The balance of the address in wei */ async getBalance(chainId: number): Promise { const network = Network.fromChainId(chainId); @@ -72,11 +79,12 @@ export class NearEthAdapter { } /** - * Constructs an EVM instance with the provided configuration. - * @param {AdapterParams} args - The configuration object for the Adapter instance. + * Constructs an EVM instance with the provided configuration + * + * @param args - The configuration object for the Adapter instance + * @returns A new NearEthAdapter instance */ static async fromConfig(args: AdapterParams): Promise { - // Sender is uniquely determined by the derivation path! const mpcContract = args.mpcContract; const derivationPath = args.derivationPath || "ethereum,1"; return new NearEthAdapter({ @@ -87,11 +95,12 @@ export class NearEthAdapter { } /** - * Constructs an EVM instance with the provided configuration. - * @param {AdapterParams} args - The configuration object for the Adapter instance. + * Creates a mocked EVM instance with the provided configuration + * + * @param args - The configuration object for the Adapter instance + * @returns A new mocked NearEthAdapter instance */ static async mocked(args: AdapterParams): Promise { - // Sender is uniquely determined by the derivation path! const mpcContract = args.mpcContract; const derivationPath = args.derivationPath || "ethereum,1"; return new NearEthAdapter({ @@ -104,11 +113,11 @@ export class NearEthAdapter { /** * 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. + * 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 {bigint} nearGas - manually specified gas to be sent with signature request. - * Note that the signature request is a recursive function. + * @param txData - Minimal transaction data to be signed by Near MPC and executed on EVM + * @param nearGas - Manually specified gas to be sent with signature request + * @returns The ethereum transaction hash */ async signAndSendTransaction( txData: BaseTx, @@ -125,11 +134,11 @@ export class NearEthAdapter { /** * 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. + * and prepares the signature request payload for the Near MPC Contract * - * @param {BaseTx} txData - Minimal transaction data to be signed by Near MPC and executed on EVM. - * @param {bigint} nearGas - manually specified gas to be sent with signature request. - * Note that the signature request is a recursive function. + * @param txData - Minimal transaction data to be signed by Near MPC and executed on EVM + * @param nearGas - Manually specified gas to be sent with signature request + * @returns The transaction and request payload */ async getSignatureRequestPayload( txData: BaseTx, @@ -149,10 +158,11 @@ export class NearEthAdapter { } /** - * Builds a Near Transaction Payload for Signing serialized EVM Transaction. - * @param {Hex} transaction RLP encoded (i.e. serialized) Ethereum Transaction - * @param nearGas optional gas parameter - * @returns {FunctionCallTransaction} Prepared Near Transaction with signerId as this.address + * Builds a Near Transaction Payload for Signing serialized EVM Transaction + * + * @param transaction - RLP encoded (i.e. serialized) Ethereum Transaction + * @param nearGas - Optional gas parameter + * @returns Prepared Near Transaction with signerId as this.address */ async mpcSignRequest( transaction: Hex, @@ -170,11 +180,10 @@ export class NearEthAdapter { /** * Builds a complete, unsigned transaction (with nonce, gas estimates, current prices) - * and payload bytes in preparation to be relayed to Near MPC contract. + * and payload bytes in preparation to be relayed to Near MPC contract * - * @param {BaseTx} tx - Minimal transaction data to be signed by Near MPC and executed on EVM. - * @param {number?} nonce - Optional transaction nonce. - * @returns Transaction and its bytes (the payload to be signed on Near). + * @param tx - Minimal transaction data to be signed by Near MPC and executed on EVM + * @returns Transaction and its bytes (the payload to be signed on Near) */ async createTxPayload(tx: BaseTx): Promise { const transaction = await this.buildTransaction(tx); @@ -187,17 +196,22 @@ export class NearEthAdapter { } /** - * Transforms minimal transaction request data into a fully populated EVM transaction. - * @param {BaseTx} tx - Minimal transaction request data - * @returns {Hex} serialized (aka RLP encoded) transaction. + * Transforms minimal transaction request data into a fully populated EVM transaction + * + * @param tx - Minimal transaction request data + * @returns Serialized (aka RLP encoded) transaction */ async buildTransaction(tx: BaseTx): Promise { const transaction = await populateTx(tx, this.address); return serializeTransaction(transaction); } - // Below code is inspired by https://github.com/Connor-ETHSeoul/near-viem - + /** + * Signs typed data according to EIP-712 + * + * @param typedData - The typed data to sign + * @returns The signature hash + */ async signTypedData< const typedData extends TypedData | Record, primaryType extends keyof typedData | "EIP712Domain" = keyof typedData, @@ -205,14 +219,21 @@ export class NearEthAdapter { return this.sign(hashTypedData(typedData)); } + /** + * Signs a message according to personal_sign + * + * @param message - The message to sign + * @returns The signature hash + */ async signMessage(message: SignableMessage): Promise { return this.sign(hashMessage(message)); } /** - * Requests signature from Near MPC Contract. - * @param msgHash - Message Hash to be signed. - * @returns Two different potential signatures for the hash (one of which is valid). + * Requests signature from Near MPC Contract + * + * @param msgHash - Message Hash to be signed + * @returns Two different potential signatures for the hash (one of which is valid) */ async sign(msgHash: `0x${string}` | Uint8Array): Promise { const signature = await this.mpcContract.requestSignature({ @@ -224,14 +245,10 @@ export class NearEthAdapter { } /** - * Encodes a signature request for submission to the Near-Ethereum transaction MPC contract. + * Encodes a signature request for submission to the Near-Ethereum transaction MPC contract * - * @async - * @function encodeSignRequest - * @param {SignRequestData} signRequest - The signature request data containing method, chain ID, and parameters. - * @returns {Promise} - * - Returns a promise that resolves to an object containing the encoded Near-Ethereum transaction data, - * the original EVM message, and recovery data necessary for verifying or reconstructing the signature. + * @param signRequest - The signature request data containing method, chain ID, and parameters + * @returns The encoded Near-Ethereum transaction data, original EVM message, and recovery data */ async encodeSignRequest( signRequest: SignRequestData diff --git a/src/chains/index.ts b/src/chains/index.ts new file mode 100644 index 0000000..6184b65 --- /dev/null +++ b/src/chains/index.ts @@ -0,0 +1,2 @@ +export * from "./ethereum"; +export * from "./near"; diff --git a/src/chains/near.ts b/src/chains/near.ts index dba69cd..91e24f2 100644 --- a/src/chains/near.ts +++ b/src/chains/near.ts @@ -1,18 +1,22 @@ import { keyStores, KeyPair, connect, Account } from "near-api-js"; import { NearConfig } from "near-api-js/lib/near"; +import { NearAccountConfig } from "../types/interfaces"; +/** Gas unit constant for NEAR transactions (1 TeraGas) */ export const TGAS = 1000000000000n; + +/** Default deposit value for NEAR transactions */ export const NO_DEPOSIT = "0"; +/** Valid NEAR network identifiers */ type NetworkId = "mainnet" | "testnet"; /** - * Extracts the network ID from a given NEAR account ID. - * If the account ID does not end with "near" or "testnet", it logs a warning. - * Defaults to "mainnet" if the network ID is not "testnet". + * Extracts the network ID from a given NEAR account ID * - * @param accountId - The NEAR account ID to extract the network ID from. - * @returns The network ID, either "mainnet" or "testnet". + * @param accountId - The NEAR account ID to analyze + * @returns The network ID ("mainnet" or "testnet") + * @remarks If the account ID doesn't end with "near" or "testnet", defaults to "mainnet" */ export function getNetworkId(accountId: string): NetworkId { const accountExt = accountId.split(".").pop() || ""; @@ -21,10 +25,10 @@ export function getNetworkId(accountId: string): NetworkId { } /** - * Generates a NEAR configuration object based on the provided network ID. + * Generates a NEAR configuration object for a specific network * - * @param networkId - The network ID, either "mainnet" or "testnet". - * @returns A NearConfig object containing the network ID and node URL. + * @param networkId - The target network identifier + * @returns Configuration object for NEAR connection */ export function configFromNetworkId(networkId: NetworkId): NearConfig { return { @@ -34,27 +38,24 @@ export function configFromNetworkId(networkId: NetworkId): NearConfig { } /** - * Loads Near Account from provided keyPair and accountId + * Creates a NEAR Account instance from provided credentials * - * @param keyPair {KeyPair} - * @param accountId {string} - * @param network {NearConfig} network settings - * @returns A Promise that resolves to a NEAR Account instance. + * @param config - Configuration containing account ID, network, and key pair + * @returns A NEAR Account instance */ -export const nearAccountFromKeyPair = async (config: { - keyPair: KeyPair; - accountId: string; - network: NearConfig; -}): Promise => { +export const nearAccountFromKeyPair = async ( + config: NearAccountConfig +): Promise => { return createNearAccount(config.accountId, config.network, config.keyPair); }; -/** Minimally sufficient Account instance to construct readonly MpcContract connection. - * Can't be used to change methods. +/** + * Creates a read-only NEAR Account instance from an account ID * - * @param accountId {string} - * @param network {NearConfig} network settings - * @returns A Promise that resolves to a NEAR Account instance. + * @param accountId - The NEAR account identifier + * @param network - The NEAR network configuration + * @returns A read-only NEAR Account instance + * @remarks This account cannot perform write operations */ export const nearAccountFromAccountId = async ( accountId: string, @@ -64,12 +65,12 @@ export const nearAccountFromAccountId = async ( }; /** - * Creates a NEAR account instance using the provided account ID, network configuration, and optional key pair. + * Creates a NEAR Account instance with optional write capabilities * - * @param accountId - The NEAR account ID. - * @param network - The NEAR network configuration. - * @param keyPair - (Optional) The key pair for the account. - * @returns A Promise that resolves to a NEAR Account instance. + * @param accountId - The NEAR account identifier + * @param network - The NEAR network configuration + * @param keyPair - Optional key pair for write access + * @returns A NEAR Account instance */ export const createNearAccount = async ( accountId: string, diff --git a/src/index.ts b/src/index.ts index b890ff7..25d7f4f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,87 +1,10 @@ -import { Account, KeyPair } from "near-api-js"; -import { NearEthAdapter } from "./chains/ethereum"; -import { MpcContract } from "./mpcContract"; -import { NearConfig } from "near-api-js/lib/near"; -import { - configFromNetworkId, - createNearAccount, - getNetworkId, -} from "./chains/near"; - -export * from "./chains/ethereum"; -export * from "./chains/near"; -export * from "./mpcContract"; +/// Directories +export * from "./chains"; export * from "./network"; export * from "./types"; export * from "./utils"; +/// Files +export * from "./mpcContract"; +export * from "./setup"; /// Beta features export * from "./beta"; - -type KeyPairString = `ed25519:${string}` | `secp256k1:${string}`; - -/** - * Configuration for setting up the adapter. - * - * @property {string} accountId - The NEAR account ID. - * @property {string} mpcContractId - The MPC contract ID. - * @property {NearConfig} [network] - (Optional) The NEAR network configuration. - * @property {string} [privateKey] - (Optional) The private key for the account. - * @property {string} [derivationPath] - (Optional) The derivation path for the Ethereum account. Defaults to "ethereum,1". - * @property {string} [rootPublicKey] - (Optional) The root public key for the account. If not available it will be fetched from the MPC contract. - */ -export interface SetupConfig { - accountId: string; - mpcContractId: string; - network?: NearConfig; - privateKey?: string; - derivationPath?: string; - rootPublicKey?: string; -} - -/** - * Sets up the NEAR-Ethereum adapter using the provided configuration. - * - * This function establishes a connection to the NEAR network using the given - * account details, configures the Multi-Party Computation (MPC) contract, and - * returns an instance of the NearEthAdapter. - * - * @param {SetupConfig} args - The configuration parameters for setting up the adapter. - * - * @returns {Promise} - A promise that resolves to an instance of NearEthAdapter configured with the provided settings. - * - * @throws {Error} - Throws an error if the `accountId` does not match the networkId of the provided or inferred `network`. - * @throws {Error} - Throws an error if there is a failure in creating a NEAR account. - */ -export async function setupAdapter(args: SetupConfig): Promise { - const { - accountId, - privateKey, - mpcContractId, - derivationPath = "ethereum,1", - } = args; - // Load near config from provided accountId if not provided - const accountNetwork = getNetworkId(accountId); - const config = args.network ?? configFromNetworkId(accountNetwork); - if (accountNetwork !== config.networkId) { - throw new Error( - `accountId ${accountId} doesn't match the networkId ${config.networkId}. Please ensure that your accountId is correct and corresponds to the intended network.` - ); - } - - let account: Account; - try { - account = await createNearAccount( - accountId, - config, - // Without private key, MPC contract connection is read-only. - privateKey ? KeyPair.fromString(privateKey as KeyPairString) : undefined - ); - } catch (error: unknown) { - console.error(`Failed to create NEAR account: ${error}`); - throw error; - } - return NearEthAdapter.fromConfig({ - mpcContract: new MpcContract(account, mpcContractId, args.rootPublicKey), - derivationPath: derivationPath, - }); -} diff --git a/src/mpcContract.ts b/src/mpcContract.ts index e2addce..16087f0 100644 --- a/src/mpcContract.ts +++ b/src/mpcContract.ts @@ -13,28 +13,26 @@ import { FinalExecutionOutcome } from "near-api-js/lib/providers"; /** * Near Contract Type for change methods. * - * @template T - The type of the arguments for the change method. - * @property {T} args - Change method function arguments. - * @property {string} gas - Gas limit on transaction execution. - * @property {Account} signerAccount - Account signing the call. - * @property {string} amount - Attached deposit (i.e., payable amount) to attach to the transaction. + * @typeParam T - The type of the arguments for the change method */ export interface ChangeMethodArgs { + /** Change method function arguments */ args: T; + /** Gas limit on transaction execution */ gas: string; + /** Account signing the call */ signerAccount: Account; + /** Attached deposit (i.e., payable amount) to attach to the transaction */ amount: string; } +/** Interface extending the base NEAR Contract with MPC-specific methods */ interface MpcContractInterface extends Contract { - /// Define the signature for the `public_key` view method. + /** Returns the public key */ public_key: () => Promise; - /// Returns required deposit based on current request queue. + /** Returns required deposit based on current request queue */ experimental_signature_deposit: () => Promise; - /// Some clown deployed one version of the contracts with this typo - experimantal_signature_deposit: () => Promise; - - /// Define the signature for the `sign` change method. + /** Signs a request using the MPC contract */ sign: ( args: ChangeMethodArgs<{ request: SignArgs }> ) => Promise; @@ -49,25 +47,39 @@ export class MpcContract implements IMpcContract { contract: MpcContractInterface; connectedAccount: Account; + /** + * Creates a new MPC Contract instance + * + * @param account - The NEAR account to use + * @param contractId - The contract ID + * @param rootPublicKey - Optional root public key + */ constructor(account: Account, contractId: string, rootPublicKey?: string) { this.connectedAccount = account; this.rootPublicKey = rootPublicKey; this.contract = new Contract(account.connection, contractId, { changeMethods: ["sign"], - viewMethods: [ - "public_key", - "experimental_signature_deposit", - "experimantal_signature_deposit", - ], + viewMethods: ["public_key", "experimental_signature_deposit"], useLocalViewExecution: false, }) as MpcContractInterface; } + /** + * Gets the contract ID + * + * @returns The contract ID + */ accountId(): string { return this.contract.contractId; } + /** + * Derives an Ethereum address from a derivation path + * + * @param derivationPath - The path to derive the address from + * @returns The derived Ethereum address + */ deriveEthAddress = async (derivationPath: string): Promise
=> { if (!this.rootPublicKey) { this.rootPublicKey = await this.contract.public_key(); @@ -82,6 +94,11 @@ export class MpcContract implements IMpcContract { return uncompressedHexPointToEvmAddress(publicKey); }; + /** + * Gets the required deposit for the signature + * + * @returns The required deposit amount as a string + */ getDeposit = async (): Promise => { const deposit = await this.contract.experimental_signature_deposit(); return BigInt( @@ -89,22 +106,29 @@ export class MpcContract implements IMpcContract { ).toString(); }; + /** + * Requests a signature from the MPC contract + * + * @param signArgs - The arguments for the signature request + * @param gas - Optional gas limit + * @returns The signature + */ requestSignature = async ( signArgs: SignArgs, gas?: bigint ): Promise => { - // near-api-js SUX so bad we can't configure this RPC timeout. - // const mpcSig = await this.contract.sign({ - // signerAccount: this.connectedAccount, - // args: { request: signArgs }, - // gas: gasOrDefault(gas), - // amount: await this.getDeposit(), - // }); const transaction = await this.encodeSignatureRequestTx(signArgs, gas); const outcome = await this.signAndSendSignRequest(transaction); return signatureFromOutcome(outcome); }; + /** + * Encodes a signature request into a transaction + * + * @param signArgs - The arguments for the signature request + * @param gas - Optional gas limit + * @returns The encoded transaction + */ async encodeSignatureRequestTx( signArgs: SignArgs, gas?: bigint @@ -126,6 +150,13 @@ export class MpcContract implements IMpcContract { }; } + /** + * Signs and sends a signature request + * + * @param transaction - The transaction to sign and send + * @param blockTimeout - Optional timeout in blocks + * @returns The execution outcome + */ async signAndSendSignRequest( transaction: FunctionCallTransaction<{ request: SignArgs }>, blockTimeout: number = 30 @@ -146,10 +177,7 @@ export class MpcContract implements IMpcContract { outcome.final_execution_status != "EXECUTED" && pings < blockTimeout ) { - // Sleep 1 second before next ping. await new Promise((resolve) => setTimeout(resolve, 1000)); - // txStatus times out when waiting for 'EXECUTED'. - // Instead we wait for an earlier status type, sleep between and keep pinging. outcome = await provider.txStatus(txHash, account.accountId, "INCLUDED"); pings += 1; } @@ -162,14 +190,20 @@ export class MpcContract implements IMpcContract { } } +/** + * Returns the gas value or a default if not provided + * + * @param gas - Optional gas value + * @returns The gas value as a string + */ function gasOrDefault(gas?: bigint): string { if (gas !== undefined) { return gas.toString(); } - // Default of 250 TGAS return (TGAS * 250n).toString(); } +/** Interface for MPC Contract implementation */ export interface IMpcContract { connectedAccount: Account; accountId(): string; diff --git a/src/network/index.ts b/src/network/index.ts index 2cc28e0..260dcca 100644 --- a/src/network/index.ts +++ b/src/network/index.ts @@ -2,34 +2,56 @@ import { Chain, createPublicClient, http, PublicClient } from "viem"; import * as chains from "viem/chains"; import { CHAIN_INFO } from "./constants"; -// Viem defaults are known to be unreliable. +/** Custom RPC endpoint overrides for specific chain IDs */ const rpcOverrides: { [key: number]: string } = { 43114: "https://rpc.ankr.com/avalanche", 11155111: "https://ethereum-sepolia-rpc.publicnode.com", }; -// We support all networks exported by viem +/** Map of all supported networks exported by viem */ const SUPPORTED_NETWORKS = createNetworkMap(Object.values(chains)); +/** Interface defining the required fields for a network configuration */ export interface NetworkFields { + /** Display name of the network */ name: string; + /** RPC endpoint URL */ rpcUrl: string; + /** Unique chain identifier */ chainId: number; + /** Block explorer URL */ scanUrl: string; + /** Network logo URL */ icon: string | undefined; + /** Whether this is a test network */ testnet: boolean; + /** Native currency information */ nativeCurrency: { + /** Number of decimal places */ decimals: number; + /** Full name of the currency */ name: string; + /** Currency symbol */ symbol: string; + /** Address of wrapped token contract */ wrappedAddress: string | undefined; - // This is often Network logo, but sometimes not (e.g. Gnosis Chain & xDai) + /** Currency logo URL (may differ from network icon) */ icon: string | undefined; }; } + +/** Interface defining optional configuration overrides for a Network instance */ +interface NetworkOptions { + /** Override the default RPC URL */ + rpcUrl?: string; + /** Override the default block explorer URL */ + scanUrl?: string; +} + /** - * Leveraging Network Data provided from through viem - * This class makes all relevant network fields accessible dynamically by chain ID. + * Network class that provides access to network-specific data and functionality + * Leverages network data provided through viem to make all relevant network fields + * accessible dynamically by chain ID. */ export class Network implements NetworkFields { name: string; @@ -47,6 +69,11 @@ export class Network implements NetworkFields { icon: string | undefined; }; + /** + * Creates a new Network instance + * + * @param fields - Network configuration fields + */ constructor({ name, rpcUrl, @@ -69,10 +96,15 @@ export class Network implements NetworkFields { this.icon = icon; } - static fromChainId( - chainId: number, - options: { rpcUrl?: string; scanUrl?: string } = {} - ): Network { + /** + * Creates a Network instance from a chain ID + * + * @param chainId - The chain ID to create the network for + * @param options - Optional configuration overrides + * @returns A new Network instance + * @throws Error if the chain ID is not supported + */ + static fromChainId(chainId: number, options: NetworkOptions = {}): Network { const networkFields = SUPPORTED_NETWORKS[chainId]; if (!networkFields) { throw new Error( @@ -91,9 +123,15 @@ export class Network implements NetworkFields { } } +/** Mapping of chain IDs to their network configurations */ type NetworkMap = { [key: number]: NetworkFields }; -/// Dynamically generate network map accessible by chainId. +/** + * Creates a map of network configurations indexed by chain ID + * + * @param supportedNetworks - Array of Chain objects from viem + * @returns A map of network configurations + */ function createNetworkMap(supportedNetworks: Chain[]): NetworkMap { const networkMap: NetworkMap = {}; supportedNetworks.forEach((network) => { @@ -117,6 +155,12 @@ function createNetworkMap(supportedNetworks: Chain[]): NetworkMap { return networkMap; } +/** + * Checks if a given chain ID corresponds to a test network + * + * @param chainId - The chain ID to check + * @returns True if the network is a testnet, false otherwise + */ export function isTestnet(chainId: number): boolean { return Network.fromChainId(chainId).testnet; } diff --git a/src/setup.ts b/src/setup.ts new file mode 100644 index 0000000..5a4a0bc --- /dev/null +++ b/src/setup.ts @@ -0,0 +1,57 @@ +import { Account, KeyPair } from "near-api-js"; +import { MpcContract } from "./mpcContract"; +import { + configFromNetworkId, + createNearAccount, + getNetworkId, + NearEthAdapter, +} from "./chains"; +import { isKeyPairString, SetupConfig } from "./types"; + +/** + * Sets up the NEAR-Ethereum adapter using the provided configuration + * + * This function establishes a connection to the NEAR network using the given + * account details, configures the Multi-Party Computation (MPC) contract, and + * returns an instance of the NearEthAdapter. + * + * @param args - The configuration parameters for setting up the adapter + * @returns An instance of NearEthAdapter configured with the provided settings + * @throws Error if the `accountId` does not match the networkId of the provided or inferred `network` + * @throws Error if there is a failure in creating a NEAR account + */ +export async function setupAdapter(args: SetupConfig): Promise { + const { + accountId, + privateKey, + mpcContractId, + derivationPath = "ethereum,1", + } = args; + // Load near config from provided accountId if not provided + const accountNetwork = getNetworkId(accountId); + const config = args.network ?? configFromNetworkId(accountNetwork); + if (accountNetwork !== config.networkId) { + throw new Error( + `accountId ${accountId} doesn't match the networkId ${config.networkId}. Please ensure that your accountId is correct and corresponds to the intended network.` + ); + } + + let account: Account; + try { + account = await createNearAccount( + accountId, + config, + // Without private key, MPC contract connection is read-only. + privateKey && isKeyPairString(privateKey) + ? KeyPair.fromString(privateKey) + : undefined + ); + } catch (error: unknown) { + console.error(`Failed to create NEAR account: ${error}`); + throw error; + } + return NearEthAdapter.fromConfig({ + mpcContract: new MpcContract(account, mpcContractId, args.rootPublicKey), + derivationPath: derivationPath, + }); +} diff --git a/src/types/guards.ts b/src/types/guards.ts index 1e2f8cf..9f9b17e 100644 --- a/src/types/guards.ts +++ b/src/types/guards.ts @@ -7,8 +7,19 @@ import { TransactionSerializable, TypedDataDomain, } from "viem"; -import { EIP712TypedData, SignMethod, TypedMessageTypes } from "."; +import { + EIP712TypedData, + KeyPairString, + SignMethod, + TypedMessageTypes, +} from "."; +/** + * Type guard to check if a value is a valid SignMethod + * + * @param method - The value to check + * @returns True if the value is a valid SignMethod + */ export function isSignMethod(method: unknown): method is SignMethod { return ( typeof method === "string" && @@ -22,6 +33,13 @@ export function isSignMethod(method: unknown): method is SignMethod { ); } +/** + * Type guard to check if a value is a valid TypedDataDomain + * Validates all optional properties according to EIP-712 specification + * + * @param domain - The value to check + * @returns True if the value matches TypedDataDomain structure + */ export const isTypedDataDomain = ( domain: unknown ): domain is TypedDataDomain => { @@ -55,6 +73,12 @@ export const isTypedDataDomain = ( }); }; +/** + * Type guard to check if a value matches the TypedMessageTypes structure + * + * @param types - The value to check + * @returns True if the value matches TypedMessageTypes structure + */ const isTypedMessageTypes = (types: unknown): types is TypedMessageTypes => { if (typeof types !== "object" || types === null) return false; @@ -74,6 +98,13 @@ const isTypedMessageTypes = (types: unknown): types is TypedMessageTypes => { }); }; +/** + * Type guard to check if a value is a valid EIP712TypedData + * Validates the structure according to EIP-712 specification + * + * @param obj - The value to check + * @returns True if the value matches EIP712TypedData structure + */ export const isEIP712TypedData = (obj: unknown): obj is EIP712TypedData => { if (typeof obj !== "object" || obj === null) return false; @@ -92,7 +123,13 @@ export const isEIP712TypedData = (obj: unknown): obj is EIP712TypedData => { ); }; -// Cheeky attempt to serialize. return true if successful! +/** + * Type guard to check if a value can be serialized as an Ethereum transaction + * Attempts to serialize the input and returns true if successful + * + * @param data - The value to check + * @returns True if the value can be serialized as a transaction + */ export function isTransactionSerializable( data: unknown ): data is TransactionSerializable { @@ -104,6 +141,13 @@ export function isTransactionSerializable( } } +/** + * Type guard to check if a value is a valid RLP-encoded transaction hex string + * Attempts to parse the input as a transaction and returns true if successful + * + * @param data - The value to check + * @returns True if the value is a valid RLP-encoded transaction hex + */ export function isRlpHex(data: unknown): data is Hex { try { parseTransaction(data as Hex); @@ -112,3 +156,30 @@ export function isRlpHex(data: unknown): data is Hex { return false; } } + +/** + * Type guard to check if a value is a valid NEAR key pair string + * + * @param value - The value to check + * @returns True if the value is a valid KeyPairString format + * @example + * ```ts + * isKeyPairString("ed25519:ABC123") // true + * isKeyPairString("secp256k1:DEF456") // true + * isKeyPairString("invalid:GHI789") // false + * isKeyPairString("ed25519") // false + * ``` + */ +export function isKeyPairString(value: unknown): value is KeyPairString { + if (typeof value !== "string") return false; + + const [prefix, key] = value.split(":"); + + // Check if we have both parts and the prefix is valid + if (!prefix || !key || !["ed25519", "secp256k1"].includes(prefix)) { + return false; + } + + // Check if the key part exists and is non-empty + return key.length > 0; +} diff --git a/src/types/interfaces.ts b/src/types/interfaces.ts index ff819a9..13a2738 100644 --- a/src/types/interfaces.ts +++ b/src/types/interfaces.ts @@ -1,3 +1,4 @@ +import { KeyPair } from "near-api-js"; import { IMpcContract } from "../mpcContract"; import { Address, @@ -8,10 +9,11 @@ import { TransactionSerializable, TypedDataDomain, } from "viem"; +import { NearConfig } from "near-api-js/lib/near"; /** - * Borrowed from @near-wallet-selector/core - * https://github.com/near/wallet-selector/blob/01081aefaa3c96ded9f83a23ecf0d210a4b64590/packages/core/src/lib/wallet/transactions.types.ts#L12 + * Borrowed from \@near-wallet-selector/core + * {@link https://github.com/near/wallet-selector/blob/01081aefaa3c96ded9f83a23ecf0f210a4b64590/packages/core/src/lib/wallet/transactions.types.ts#L12} */ export interface FunctionCallAction { type: "FunctionCall"; @@ -23,149 +25,136 @@ export interface FunctionCallAction { }; } -/** - * Represents the base transaction structure. - * - * @property {`0x${string}`} to - Recipient of the transaction. - * @property {bigint} [value] - ETH value of the transaction. - * @property {`0x${string}`} data - Call data of the transaction. - * @property {number} chainId - Integer ID of the network for the transaction. - * @property {number} [nonce] - Specified transaction nonce. - * @property {bigint} [gas] - Optional gas limit. - */ +/** Configuration for a NEAR account */ +export interface NearAccountConfig { + /** The key pair associated with the account */ + keyPair: KeyPair; + /** The NEAR account ID */ + accountId: string; + /** Network settings */ + network: NearConfig; +} + +/** Base transaction structure */ export interface BaseTx { + /** Recipient of the transaction */ to: `0x${string}`; + /** ETH value of the transaction */ value?: bigint; + /** Call data of the transaction */ data?: `0x${string}`; + /** Integer ID of the network for the transaction */ chainId: number; + /** Specified transaction nonce */ nonce?: number; + /** Optional gas limit */ gas?: bigint; } -/** - * Parameters for the adapter. - * - * @property {MpcContract} mpcContract - An instance of the NearMPC contract connected to the associated NEAR account. - * @property {string} [derivationPath] - Path used to generate ETH account from NEAR account (e.g., "ethereum,1"). - */ +/** Parameters for the adapter */ export interface AdapterParams { + /** An instance of the NearMPC contract connected to the associated NEAR account */ mpcContract: IMpcContract; + /** Path used to generate ETH account from NEAR account (e.g., "ethereum,1") */ derivationPath?: string; } /** * Represents a message that can be signed within an Ethereum Virtual Machine (EVM) context. * This can be a raw string, an EIP-712 typed data structure, or a serializable transaction. - * - * @typedef {string | EIP712TypedData | TransactionSerializable} EvmMessage */ export type EvmMessage = string | EIP712TypedData | TransactionSerializable; -/** - * Encapsulates a signature request for an Ethereum-based message. - * - * @interface EncodedSignRequest - * @property {EvmMessage} evmMessage - The message to be signed, which could be in plain string format, - * an EIP-712 typed data, or a serializable transaction. - * @property {Hash} hashToSign - A unique hash derived from `evmMessage` to identify the signature request. - */ +/** Encapsulates a signature request for an Ethereum-based message */ export interface EncodedSignRequest { + /** The message to be signed, which could be in plain string format, + * an EIP-712 typed data, or a serializable transaction */ evmMessage: EvmMessage; + /** A unique hash derived from `evmMessage` to identify the signature request */ hashToSign: Hash; } /** * Extends the `EncodedSignRequest` for use with NEAR protocol. * This structure contains an additional payload to facilitate transaction signing in NEAR. - * - * @interface NearEncodedSignRequest - * @extends EncodedSignRequest - * @property {FunctionCallTransaction<{ request: SignArgs }>} nearPayload - A NEAR-specific transaction payload, - * typically including a request with arguments - * for the function call. */ export interface NearEncodedSignRequest extends EncodedSignRequest { + /** A NEAR-specific transaction payload, typically including a request with arguments + * for the function call */ nearPayload: FunctionCallTransaction<{ request: SignArgs; }>; } -/** - * Represents the gas fees for an Ethereum transaction. - * - * @property {bigint} maxFeePerGas - The maximum fee per gas unit. - * @property {bigint} maxPriorityFeePerGas - The maximum priority fee per gas unit. - */ + +/** Represents the gas fees for an Ethereum transaction */ export interface GasFees { + /** The maximum fee per gas unit */ maxFeePerGas: bigint; + /** The maximum priority fee per gas unit */ maxPriorityFeePerGas: bigint; } /** * Arguments required for signature request from MPC Contract. - * cf. https://github.com/near/mpc/blob/48a572baab5904afe3cd62bd0da5a036db3a34b6/chain-signatures/contract/src/primitives.rs#L268 - * - * @property {string} path - Derivation path for ETH account associated with NEAR AccountId. - * @property {number[]} payload - Serialized Ethereum transaction bytes. - * @property {number} key_version - Version number associated with derived ETH address (must be increasing). + * {@link https://github.com/near/mpc/blob/48a572baab5904afe3cd62bd0da5a036db3a34b6/chain-signatures/contract/src/primitives.rs#L268} */ export interface SignArgs { + /** Derivation path 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; } -/** - * Represents the payload for a transaction. - * - * @property {Hex} transaction - Serialized Ethereum transaction. - * @property {SignArgs} signArgs - Arguments required by NEAR MPC Contract signature request. - */ +/** Represents the payload for a transaction */ export interface TxPayload { + /** Serialized Ethereum transaction */ transaction: Hex; + /** Arguments required by NEAR MPC Contract signature request */ signArgs: SignArgs; } -/** - * Represents a function call transaction. - * - * @template T - The type of the function call action arguments. - * @property {string} signerId - Signer of the function call. - * @property {string} receiverId - Transaction recipient (a NEAR ContractId). - * @property {Array>} actions - Function call actions. - */ +/** Represents a function call transaction */ export interface FunctionCallTransaction { + /** Signer of the function call */ signerId: string; + /** Transaction recipient (a NEAR ContractId) */ receiverId: string; + /** Function call actions */ actions: Array>; } /** * Result Type of MPC contract signature request. * Representing Affine Points on eliptic curve. - * Example: { - "big_r": { - "affine_point": "031F2CE94AF69DF45EC96D146DB2F6D35B8743FA2E21D2450070C5C339A4CD418B" - }, - "s": { "scalar": "5AE93A7C4138972B3FE8AEA1638190905C6DB5437BDE7274BEBFA41DDAF7E4F6" - }, - "recovery_id": 0 - } + * Example: + * ```json + * { + * "big_r": { + * "affine_point": "031F2CE94AF69DF45EC96D146DB2F6D35B8743FA2E21D2450070C5C339A4CD418B" + * }, + * "s": { + * "scalar": "5AE93A7C4138972B3FE8AEA1638190905C6DB5437BDE7274BEBFA41DDAF7E4F6" + * }, + * "recovery_id": 0 + * } + * ``` */ - export interface MPCSignature { + /** The R point of the signature */ big_r: { affine_point: string }; + /** The S value of the signature */ s: { scalar: string }; + /** The recovery ID */ recovery_id: number; } -/** - * Represents the data for a message. - * - * @property {Hex} address - The address associated with the message. - * @property {SignableMessage} message - The signable message. - */ +/** Represents the data for a message */ export interface MessageData { + /** The address associated with the message */ address: Hex; + /** The signable message */ message: SignableMessage; } @@ -173,91 +162,64 @@ export interface TypedDataTypes { name: string; type: string; } + export type TypedMessageTypes = { [key: string]: TypedDataTypes[]; }; -/** - * Represents the data for a typed message. - * - * @property {TypedDataDomain} domain - The domain of the message. - * @property {TypedMessageTypes} types - The types of the message. - * @property {Record} message - The message itself. - * @property {string} primaryType - The primary type of the message. - */ +/** Represents the data for a typed message */ export type EIP712TypedData = { + /** The domain of the message */ domain: TypedDataDomain; + /** The types of the message */ types: TypedMessageTypes; + /** The message itself */ message: Record; + /** The primary type of the message */ primaryType: string; }; -/** - * Sufficient data required to construct a signed Ethereum Transaction. - * - * @property {Hex} transaction - Unsigned Ethereum transaction data. - * @property {Signature} signature - Representation of the transaction's signature. - */ +/** Sufficient data required to construct a signed Ethereum Transaction */ export interface TransactionWithSignature { + /** Unsigned Ethereum transaction data */ transaction: Hex; + /** Representation of the transaction's signature */ signature: Signature; } -/// Below is hand-crafted types losely related to wallet connect - -/** - * Interface representing the parameters required for an Ethereum transaction. - * - * @property {Hex} from - The sender's Ethereum address in hexadecimal format. - * @property {Hex} to - The recipient's Ethereum address in hexadecimal format. - * @property {Hex} [gas] - Optional gas limit for the transaction in hexadecimal format. - * @property {Hex} [value] - Optional amount of Ether to send in hexadecimal format. - * @property {Hex} [data] - Optional data payload for the transaction in hexadecimal format, often used for contract interactions. */ +/** Interface representing the parameters required for an Ethereum transaction */ export interface EthTransactionParams { + /** The sender's Ethereum address in hexadecimal format */ from: Hex; + /** The recipient's Ethereum address in hexadecimal format */ to: Hex; + /** Optional gas limit for the transaction in hexadecimal format */ gas?: Hex; + /** Optional amount of Ether to send in hexadecimal format */ value?: Hex; + /** Optional data payload for the transaction in hexadecimal format, often used for contract interactions */ data?: Hex; } /** - * Type representing the parameters for a personal_sign request. - * - * @type {[Hex, Address]} - * @property {Hex} 0 - The message to be signed in hexadecimal format. - * @property {Address} 1 - The address of the signer in hexadecimal format. + * Parameters for a personal_sign request + * Tuple of [message: Hex, signer: Address] */ export type PersonalSignParams = [Hex, Address]; /** - * Type representing the parameters for an eth_sign request. - * - * @type {[Address, Hex]} - * @property {Address} 0 - The address of the signer in hexadecimal format. - * @property {Hex} 1 - The message to be signed in hexadecimal format. + * Parameters for an eth_sign request + * Tuple of [signer: Address, message: Hex] */ export type EthSignParams = [Address, Hex]; /** - * Type representing the parameters for signing complex structured data (like EIP-712). - * - * @type {[Hex, string]} - * @property {Hex} 0 - The address of the signer in hexadecimal format. - * @property {string} 1 - The structured data in JSON string format to be signed. + * Parameters for signing complex structured data (like EIP-712) + * Tuple of [signer: Hex, structuredData: string] */ export type TypedDataParams = [Hex, string]; -/** - * Type representing the possible request parameters for a signing session. - * - * @type {EthTransactionParams[] | Hex | PersonalSignParams | EthSignParams | TypedDataParams} - * @property {EthTransactionParams[]} - An array of Ethereum transaction parameters. - * @property {Hex} - A simple hexadecimal value representing RLP Encoded Ethereum Transaction. - * @property {PersonalSignParams} - Parameters for a personal sign request. - * @property {EthSignParams} - Parameters for an eth_sign request. - * @property {TypedDataParams} - Parameters for signing structured data. - */ +/** Type representing the possible request parameters for a signing session */ export type SessionRequestParams = | EthTransactionParams[] | Hex @@ -265,9 +227,7 @@ export type SessionRequestParams = | EthSignParams | TypedDataParams; -/** - * An array of supported signing methods. - */ +/** An array of supported signing methods */ export const signMethods = [ "eth_sign", "personal_sign", @@ -276,20 +236,35 @@ export const signMethods = [ "eth_signTypedData_v4", ] as const; -/** - * Type representing one of the supported signing methods. - */ +/** Type representing one of the supported signing methods */ export type SignMethod = (typeof signMethods)[number]; -/** - * Interface representing the data required for a signature request. - * - * @property {SignMethods} method - The signing method to be used. - * @property {number} chainId - The ID of the Ethereum chain where the transaction or signing is taking place. - * @property {SessionRequestParams} params - The parameters required for the signing request, which vary depending on the method. - */ +/** Interface representing the data required for a signature request */ export type SignRequestData = { + /** The signing method to be used */ method: SignMethod; + /** The ID of the Ethereum chain where the transaction or signing is taking place */ chainId: number; + /** The parameters required for the signing request, which vary depending on the method */ params: SessionRequestParams; }; +/** Template literal type for NEAR key pair strings */ +export type KeyPairString = `ed25519:${string}` | `secp256k1:${string}`; + +/** + * Configuration for setting up the adapter + */ +export interface SetupConfig { + /** The NEAR account ID */ + accountId: string; + /** The MPC contract ID */ + mpcContractId: string; + /** The NEAR network configuration */ + network?: NearConfig; + /** The private key for the account */ + privateKey?: string; + /** The derivation path for the Ethereum account. Defaults to "ethereum,1" */ + derivationPath?: string; + /** The root public key for the account. If not available it will be fetched from the MPC contract */ + rootPublicKey?: string; +} diff --git a/src/types/util.ts b/src/types/util.ts index 1727833..ac58ea8 100644 --- a/src/types/util.ts +++ b/src/types/util.ts @@ -4,9 +4,9 @@ import { FunctionCallAction } from "./interfaces"; /** * Converts a FunctionCallTransaction to an array of Action. * - * @template T - The type of the function call action arguments. - * @param {FunctionCallTransaction} transaction - The function call transaction to convert. - * @returns {Action[]} - An array of Action objects. + * @typeParam T - The type of the function call action arguments + * @param action - The function call transaction to convert + * @returns An array of Action objects */ export function convertToAction(action: FunctionCallAction): Action { return functionCall( @@ -18,10 +18,12 @@ export function convertToAction(action: FunctionCallAction): Action { } /** - * Converts a structure `T` into `object | Uint8Array`. + * Converts a structure `T` into `object | Uint8Array` * - * @param {T} input - The input structure to convert. - * @returns {object | Uint8Array} - The converted result. + * @typeParam T - The type of the input structure + * @param input - The input structure to convert + * @returns The converted result as either an object or Uint8Array + * @throws Error if conversion fails */ export function convertToCompatibleFormat(input: T): object | Uint8Array { try { diff --git a/src/utils/kdf.ts b/src/utils/kdf.ts index e4d43eb..4714ec3 100644 --- a/src/utils/kdf.ts +++ b/src/utils/kdf.ts @@ -3,6 +3,12 @@ import { ec as EC } from "elliptic"; import { Address, keccak256 } from "viem"; import { sha3_256 } from "js-sha3"; +/** + * Converts a NEAR account public key string to an uncompressed hex point + * + * @param najPublicKeyStr - The NEAR account public key string + * @returns Uncompressed hex point string prefixed with "04" + */ export function najPublicKeyStrToUncompressedHexPoint( najPublicKeyStr: string ): string { @@ -10,6 +16,14 @@ export function najPublicKeyStrToUncompressedHexPoint( return "04" + Buffer.from(decodedKey).toString("hex"); } +/** + * Derives a child public key using elliptic curve operations + * + * @param parentUncompressedPublicKeyHex - Parent public key as uncompressed hex + * @param signerId - The signer's identifier + * @param path - Optional derivation path (defaults to empty string) + * @returns Derived child public key as uncompressed hex string + */ export function deriveChildPublicKey( parentUncompressedPublicKeyHex: string, signerId: string, @@ -36,6 +50,13 @@ export function deriveChildPublicKey( return "04" + newX + newY; } +/** + * Converts an uncompressed hex point to an Ethereum address + * + * @param uncompressedHexPoint - The uncompressed hex point string + * @returns Ethereum address derived from the public key + * @remarks Takes the last 20 bytes of the keccak256 hash of the public key + */ export function uncompressedHexPointToEvmAddress( uncompressedHexPoint: string ): Address { diff --git a/src/utils/mock-sign.ts b/src/utils/mock-sign.ts index 5ce06e9..2b9df11 100644 --- a/src/utils/mock-sign.ts +++ b/src/utils/mock-sign.ts @@ -11,56 +11,77 @@ import { /** * Converts a raw hexadecimal signature into a structured Signature object - * @param hexSignature The raw hexadecimal signature (e.g., '0x...') + * + * @param hexSignature - The raw hexadecimal signature (e.g., '0x...') * @returns A structured Signature object with fields r, s, v, and yParity + * @throws Error if signature length is invalid */ function hexToSignature(hexSignature: Hex): Signature { - // Strip "0x" prefix if it exists const cleanedHex = hexSignature.slice(2); - // Ensure the signature is 65 bytes (130 hex characters) if (cleanedHex.length !== 130) { throw new Error( `Invalid hex signature length: ${cleanedHex.length}. Expected 130 characters (65 bytes).` ); } - // Extract the r, s, and v components from the hex signature - const v = BigInt(`0x${cleanedHex.slice(128, 130)}`); // Last byte (2 hex characters) + const v = BigInt(`0x${cleanedHex.slice(128, 130)}`); return { - r: `0x${cleanedHex.slice(0, 64)}`, // First 32 bytes (64 hex characters) - s: `0x${cleanedHex.slice(64, 128)}`, // Next 32 bytes (64 hex characters), + r: `0x${cleanedHex.slice(0, 64)}`, + s: `0x${cleanedHex.slice(64, 128)}`, v, - // Determine yParity based on v (27 or 28 maps to 0 or 1) yParity: v === 27n ? 0 : v === 28n ? 1 : undefined, }; } +/** Mock implementation of the MPC Contract interface for testing */ export class MockMpcContract implements IMpcContract { connectedAccount: Account; private ethAccount: PrivateKeyAccount; + /** + * Creates a new mock MPC contract instance + * + * @param account - The NEAR account to use + * @param privateKey - Optional private key (defaults to deterministic test key) + */ constructor(account: Account, privateKey?: Hex) { this.connectedAccount = account; this.ethAccount = privateKeyToAccount( privateKey || - // Known key from deterministic ganache client "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d" ); } + /** Gets the mock contract ID */ accountId(): string { return "mock-mpc.offline"; } + /** + * Returns the mock Ethereum address + * + * @returns The Ethereum address associated with the private key + */ deriveEthAddress = async (_unused?: string): Promise
=> { return this.ethAccount.address; }; + /** + * Returns a mock deposit amount + * + * @returns A constant deposit value of "1" + */ getDeposit = async (): Promise => { return "1"; }; + /** + * Signs a message using the mock private key + * + * @param signArgs - The signature request arguments + * @returns The signature + */ requestSignature = async ( signArgs: SignArgs, _gas?: bigint @@ -71,6 +92,13 @@ export class MockMpcContract implements IMpcContract { return hexToSignature(hexSignature); }; + /** + * Encodes a mock signature request transaction + * + * @param signArgs - The signature request arguments + * @param gas - Optional gas limit + * @returns The encoded transaction + */ async encodeSignatureRequestTx( signArgs: SignArgs, gas?: bigint @@ -93,6 +121,12 @@ export class MockMpcContract implements IMpcContract { } } +/** + * Creates a mock adapter instance for testing + * + * @param privateKey - Optional private key for the mock contract + * @returns A configured NearEthAdapter instance + */ export async function mockAdapter(privateKey?: Hex): Promise { const account = await nearAccountFromAccountId("mock-user.offline", { networkId: "testnet", diff --git a/src/utils/request.ts b/src/utils/request.ts index cd950bd..8897aaa 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -18,20 +18,15 @@ import { } from "../types"; /** - * Handles routing of signature requests based on the provided method, chain ID, and parameters. + * Routes signature requests to appropriate handlers based on method type * - * @async - * @function requestRouter - * @param {SignRequestData} params - An object containing the method, chain ID, and request parameters. - * @returns {Promise} - * - 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. + * @param request - The signature request data + * @returns Object containing the EVM message, payload hash, and recovery data */ -export async function requestRouter({ - method, - chainId, - params, -}: SignRequestData): Promise { +export async function requestRouter( + request: SignRequestData +): Promise { + const { method, chainId, params } = request; switch (method) { case "eth_sign": { const [_, messageHash] = params as EthSignParams; diff --git a/src/utils/signature.ts b/src/utils/signature.ts index d9afaf7..0fa76e6 100644 --- a/src/utils/signature.ts +++ b/src/utils/signature.ts @@ -5,18 +5,34 @@ import { FinalExecutionStatus, } from "near-api-js/lib/providers"; -// Basic structure of the JSON-RPC response +/** Basic structure of the JSON-RPC response */ export interface JSONRPCResponse { + /** JSON-RPC version */ jsonrpc: string; + /** Request identifier */ id: number | string | null; + /** Response result */ result?: T; + /** Error information if request failed */ error?: { + /** Error code */ code: number; + /** Error message */ message: string; + /** Additional error data */ data?: unknown; }; } +/** + * Retrieves a signature from a transaction hash + * + * @param nodeUrl - URL of the NEAR node + * @param txHash - Transaction hash to query + * @param accountId - Account ID used to determine shard for query (defaults to "non-empty") + * @returns The signature from the transaction + * @throws Error if HTTP request fails or response is invalid + */ export async function signatureFromTxHash( nodeUrl: string, txHash: string, @@ -66,6 +82,12 @@ export async function signatureFromTxHash( } } +/** + * Transforms an MPC signature into a standard Ethereum signature + * + * @param mpcSig - The MPC signature to transform + * @returns Standard Ethereum signature + */ export function transformSignature(mpcSig: MPCSignature): Signature { const { big_r, s, recovery_id } = mpcSig; return { @@ -75,6 +97,17 @@ export function transformSignature(mpcSig: MPCSignature): Signature { }; } +/** + * Extracts a signature from a transaction outcome + * + * @param outcome - Transaction outcome from NEAR API + * @returns The extracted signature + * @throws Error if signature is not found or is invalid + * @remarks + * Handles both standard and relayed signature requests. For relayed requests, + * extracts signature from receipts_outcome, taking the second occurrence as + * the first is nested inside `{ Ok: MPCSignature }`. + */ export function signatureFromOutcome( // The Partial object is intended to make up for the // difference between all the different near-api versions and wallet-selector bullshit @@ -118,7 +151,12 @@ export function signatureFromOutcome( } } -// type guard for MPCSignature object. +/** + * Type guard to check if an object is a valid MPC signature + * + * @param obj - The object to check + * @returns True if the object matches MPCSignature structure + */ function isMPCSignature(obj: unknown): obj is MPCSignature { return ( typeof obj === "object" && diff --git a/src/utils/transaction.ts b/src/utils/transaction.ts index d63db75..516fba0 100644 --- a/src/utils/transaction.ts +++ b/src/utils/transaction.ts @@ -13,6 +13,13 @@ import { import { BaseTx, TransactionWithSignature } from "../types"; import { Network } from "../network"; +/** + * Converts a message hash to a payload array + * + * @param msgHash - The message hash to convert + * @returns Array of numbers representing the payload + * @throws Error if the payload length is not 32 bytes + */ export function toPayload(msgHash: Hex | Uint8Array): number[] { const bytes = isBytes(msgHash) ? msgHash : toBytes(msgHash); if (bytes.length !== 32) { @@ -21,6 +28,13 @@ export function toPayload(msgHash: Hex | Uint8Array): number[] { return Array.from(bytes); } +/** + * Converts a payload array back to a hexadecimal string + * + * @param payload - The payload array to convert + * @returns Hexadecimal string representation + * @throws Error if the payload length is not 32 bytes + */ export function fromPayload(payload: number[]): Hex { if (payload.length !== 32) { throw new Error(`Payload must have 32 bytes: ${payload}`); @@ -29,10 +43,25 @@ export function fromPayload(payload: number[]): Hex { return toHex(new Uint8Array(payload)); } +/** + * Builds a transaction payload from a serialized transaction + * + * @param serializedTx - The serialized transaction + * @returns Array of numbers representing the transaction payload + */ export function buildTxPayload(serializedTx: `0x${string}`): number[] { return toPayload(keccak256(serializedTx)); } +/** + * Populates a transaction with necessary data + * + * @param tx - The base transaction data + * @param from - The sender's address + * @param client - Optional public client + * @returns Complete transaction data + * @throws Error if chain IDs don't match + */ export async function populateTx( tx: BaseTx, from: Hex, @@ -68,6 +97,12 @@ export async function populateTx( }; } +/** + * Adds a signature to a transaction + * + * @param params - Object containing transaction and signature + * @returns Serialized signed transaction + */ export function addSignature({ transaction, signature, @@ -81,9 +116,11 @@ export function addSignature({ } /** - * Relays signed transaction to Ethereum mem-pool for execution. - * @param serializedTransaction - Signed Ethereum transaction. - * @returns Transaction Hash of relayed transaction. + * Relays a signed transaction to the Ethereum mempool + * + * @param serializedTransaction - The signed transaction to relay + * @param wait - Whether to wait for confirmation + * @returns Transaction hash */ export async function relaySignedTransaction( serializedTransaction: Hex, @@ -105,10 +142,10 @@ export async function relaySignedTransaction( } /** - * Relays valid representation of signed transaction to Etherem mempool for execution. + * Broadcasts a signed transaction to the Ethereum mempool * - * @param {TransactionWithSignature} tx - Signed Ethereum transaction. - * @returns Hash of relayed transaction. + * @param tx - The signed transaction to broadcast + * @returns Transaction hash */ export async function broadcastSignedTransaction( tx: TransactionWithSignature