diff --git a/src/chain-adapters/Bitcoin/Bitcoin.ts b/src/chain-adapters/Bitcoin/Bitcoin.ts index 713ef48..90812cd 100644 --- a/src/chain-adapters/Bitcoin/Bitcoin.ts +++ b/src/chain-adapters/Bitcoin/Bitcoin.ts @@ -10,7 +10,7 @@ import type { } from '@chain-adapters/Bitcoin/types' import { parseBTCNetwork } from '@chain-adapters/Bitcoin/utils' import { ChainAdapter } from '@chain-adapters/ChainAdapter' -import type { BaseChainSignatureContract } from '@contracts/ChainSignatureContract' +import type { ChainSignatureContract } from '@contracts/ChainSignatureContract' import type { HashToSign, RSVSignature, UncompressedPubKeySEC1 } from '@types' import { cryptography } from '@utils' @@ -26,7 +26,7 @@ export class Bitcoin extends ChainAdapter< private readonly network: BTCNetworkIds private readonly btcRpcAdapter: BTCRpcAdapter - private readonly contract: BaseChainSignatureContract + private readonly contract: ChainSignatureContract /** * Creates a new Bitcoin chain instance @@ -41,7 +41,7 @@ export class Bitcoin extends ChainAdapter< btcRpcAdapter, }: { network: BTCNetworkIds - contract: BaseChainSignatureContract + contract: ChainSignatureContract btcRpcAdapter: BTCRpcAdapter }) { super() @@ -230,7 +230,6 @@ export class Bitcoin extends ChainAdapter< const psbt = await this.createPSBT({ transactionRequest, }) - // We can't double sign a PSBT, therefore we serialize the payload before to return it const psbtHex = psbt.toHex() diff --git a/src/chain-adapters/Cosmos/Cosmos.ts b/src/chain-adapters/Cosmos/Cosmos.ts index c103d7c..bc32eaa 100644 --- a/src/chain-adapters/Cosmos/Cosmos.ts +++ b/src/chain-adapters/Cosmos/Cosmos.ts @@ -23,7 +23,7 @@ import type { BalanceResponse, } from '@chain-adapters/Cosmos/types' import { fetchChainInfo } from '@chain-adapters/Cosmos/utils' -import type { BaseChainSignatureContract } from '@contracts/ChainSignatureContract' +import type { ChainSignatureContract } from '@contracts/ChainSignatureContract' import type { HashToSign, RSVSignature, UncompressedPubKeySEC1 } from '@types' import { cryptography } from '@utils' @@ -37,7 +37,7 @@ export class Cosmos extends ChainAdapter< > { private readonly registry: Registry private readonly chainId: CosmosNetworkIds - private readonly contract: BaseChainSignatureContract + private readonly contract: ChainSignatureContract private readonly endpoints?: { rpcUrl?: string restUrl?: string @@ -57,7 +57,7 @@ export class Cosmos extends ChainAdapter< contract, endpoints, }: { - contract: BaseChainSignatureContract + contract: ChainSignatureContract chainId: CosmosNetworkIds endpoints?: { rpcUrl?: string diff --git a/src/chain-adapters/EVM/EVM.ts b/src/chain-adapters/EVM/EVM.ts index 8956922..df68910 100644 --- a/src/chain-adapters/EVM/EVM.ts +++ b/src/chain-adapters/EVM/EVM.ts @@ -30,7 +30,7 @@ import type { UserOperationV7, } from '@chain-adapters/EVM/types' import { fetchEVMFeeProperties } from '@chain-adapters/EVM/utils' -import type { BaseChainSignatureContract } from '@contracts/ChainSignatureContract' +import type { ChainSignatureContract } from '@contracts/ChainSignatureContract' import type { HashToSign, RSVSignature } from '@types' /** @@ -42,7 +42,7 @@ export class EVM extends ChainAdapter< EVMUnsignedTransaction > { private readonly client: PublicClient - private readonly contract: BaseChainSignatureContract + private readonly contract: ChainSignatureContract /** * Creates a new EVM chain instance @@ -55,7 +55,7 @@ export class EVM extends ChainAdapter< contract, }: { publicClient: PublicClient - contract: BaseChainSignatureContract + contract: ChainSignatureContract }) { super() @@ -244,11 +244,11 @@ export class EVM extends ChainAdapter< userOp.paymaster && isAddress(userOp.paymaster) ? concat([ - userOp.paymaster, - pad(userOp.paymasterVerificationGasLimit, { size: 16 }), - pad(userOp.paymasterPostOpGasLimit, { size: 16 }), - userOp.paymasterData, - ]) + userOp.paymaster, + pad(userOp.paymasterVerificationGasLimit, { size: 16 }), + pad(userOp.paymasterPostOpGasLimit, { size: 16 }), + userOp.paymasterData, + ]) : 'paymasterAndData' in userOp ? userOp.paymasterAndData : '0x' @@ -325,7 +325,7 @@ export class EVM extends ChainAdapter< const hash = await this.client.sendRawTransaction({ serializedTransaction: txSerialized as `0x${string}`, }) - return { hash: hash } + return { hash } } catch (error) { console.error('Transaction broadcast failed:', error) throw new Error('Failed to broadcast transaction.') diff --git a/src/chain-adapters/Solana/Solana.ts b/src/chain-adapters/Solana/Solana.ts index 097cdfe..4836a03 100644 --- a/src/chain-adapters/Solana/Solana.ts +++ b/src/chain-adapters/Solana/Solana.ts @@ -2,7 +2,7 @@ import type { Connection as SolanaConnection } from '@solana/web3.js' import { PublicKey, Transaction, SystemProgram } from '@solana/web3.js' import type BN from 'bn.js' -import type { BaseChainSignatureContract } from '@contracts/ChainSignatureContract' +import type { ChainSignatureContract } from '@contracts/ChainSignatureContract' import type { HashToSign, Signature } from '@types' import { ChainAdapter } from '../ChainAdapter' @@ -22,11 +22,11 @@ export class Solana extends ChainAdapter< SolanaUnsignedTransaction > { private readonly connection: SolanaConnection - private readonly contract: BaseChainSignatureContract + private readonly contract: ChainSignatureContract constructor(args: { solanaConnection: SolanaConnection - contract: BaseChainSignatureContract + contract: ChainSignatureContract }) { super() this.connection = args.solanaConnection diff --git a/src/contracts/ChainSignatureContract.ts b/src/contracts/ChainSignatureContract.ts index c9f4abe..8ca76ca 100644 --- a/src/contracts/ChainSignatureContract.ts +++ b/src/contracts/ChainSignatureContract.ts @@ -1,73 +1,158 @@ -import type BN from 'bn.js' +import { type CodeResult } from '@near-js/types' +import { type Transaction } from '@near-wallet-selector/core' +import { + najToUncompressedPubKeySEC1, + uint8ArrayToHex, +} from '@utils/cryptography' +import { providers } from 'near-api-js' -import type { - RSVSignature, - UncompressedPubKeySEC1, - Ed25519PubKey, - DerivedPublicKeyArgs, -} from '../types' +import { + type RSVSignature, + type UncompressedPubKeySEC1, + type NajPublicKey, + type MPCSignature, +} from '@types' -export interface ArgsEd25519 extends DerivedPublicKeyArgs { - IsEd25519: boolean +import { NEAR_MAX_GAS } from './constants' +import { responseToMpcSignature } from './transaction' +import { type NearNetworkIds } from './types' + +interface ViewMethodParams { + contractId: string + method: string + args?: Record } -export interface SignArgs { - /** The payload to sign as an array of 32 bytes */ - payload: number[] - /** The derivation path for key generation */ +export type HashToSign = number[] + +export interface SignArgs { + payloads: HashToSign[] path: string - /** Version of the key to use */ - key_version: number + keyType: 'Eddsa' | 'Ecdsa' + signerAccount: { + accountId: string + signAndSendTransactions: (transactions: { + transactions: Transaction[] + }) => Promise + } } -/** - * Base contract interface required for compatibility with ChainAdapter instances like EVM and Bitcoin. - * - * See {@link EVM} and {@link Bitcoin} for example implementations. - */ -export abstract class BaseChainSignatureContract { - /** - * Gets the current signature deposit required by the contract. - * This deposit amount helps manage network congestion. - * - * @returns Promise resolving to the required deposit amount as a BigNumber - */ - abstract getCurrentSignatureDeposit(): Promise - - /** - * Gets the derived public key for a given path and predecessor. - * - * @param args - Arguments for key derivation - * @param args.path - The path to use derive the key - * @param args.predecessor - The id/address of the account requesting signature - * @param args.IsEd25519 - Flag indicating if the key is Ed25519 - * @returns Promise resolving to the derived SEC1 uncompressed public key - */ - // abstract getDerivedPublicKey(args: ArgsEd25519): Promise - abstract getDerivedPublicKey( - args: DerivedPublicKeyArgs | ArgsEd25519 - ): Promise -} +export class ChainSignatureContract { + private readonly contractId: string + private readonly networkId: NearNetworkIds + private readonly provider: providers.FailoverRpcProvider + + constructor({ + contractId, + networkId, + fallbackRpcUrls, + }: { + contractId: string + networkId: NearNetworkIds + fallbackRpcUrls?: string[] + }) { + this.contractId = contractId + this.networkId = networkId + + const rpcProviderUrls = + fallbackRpcUrls && fallbackRpcUrls.length > 0 + ? fallbackRpcUrls + : [`https://rpc.${this.networkId}.near.org`] + + this.provider = new providers.FailoverRpcProvider( + rpcProviderUrls.map((url) => new providers.JsonRpcProvider({ url })) + ) + } + + private async viewFunction({ + contractId, + method, + args = {}, + }: ViewMethodParams): Promise { + const res = await this.provider.query({ + request_type: 'call_function', + account_id: contractId, + method_name: method, + args_base64: Buffer.from(JSON.stringify(args)).toString('base64'), + finality: 'optimistic', + }) + + return JSON.parse(Buffer.from(res.result).toString()) + } + + getCurrentSignatureDeposit(): number { + return 1 + } + + async sign({ + payloads, + path, + keyType, + signerAccount, + }: SignArgs): Promise { + const transactions = payloads.map((payload) => ({ + signerId: signerAccount.accountId, + receiverId: this.contractId, + actions: [ + { + type: 'FunctionCall' as const, + params: { + methodName: 'sign', + args: { + request: { + payload_v2: { [keyType]: uint8ArrayToHex(payload) }, + path, + domain_id: keyType === 'Eddsa' ? 1 : 0, + }, + }, + gas: NEAR_MAX_GAS.toString(), + deposit: '1', + }, + }, + ], + })) + + const sentTxs = (await signerAccount.signAndSendTransactions({ + transactions, + })) as MPCSignature[] + + const rsvSignatures = sentTxs.map((tx) => + responseToMpcSignature({ signature: tx }) + ) + + return rsvSignatures as RSVSignature[] + } + + async getPublicKey(): Promise { + const najPubKey = await this.viewFunction({ + contractId: this.contractId, + method: 'public_key', + }) + return najToUncompressedPubKeySEC1(najPubKey as NajPublicKey) + } + + async getDerivedPublicKey(args: { + path: string + predecessor: string + IsEd25519?: boolean + }): Promise { + if (args.IsEd25519) { + return (await this.viewFunction({ + contractId: this.contractId, + method: 'derived_public_key', + args: { + path: args.path, + predecessor: args.predecessor, + domain_id: 1, + }, + })) as `Ed25519:${string}` + } -/** - * Full contract interface that extends BaseChainSignatureContract to provide all Sig Network Smart Contract capabilities. - */ -export abstract class ChainSignatureContract extends BaseChainSignatureContract { - /** - * Signs a payload using Sig Network MPC. - * - * @param args - Arguments for the signing operation - * @param args.payload - The data to sign as an array of 32 bytes - * @param args.path - The string path to use derive the key - * @param args.key_version - Version of the key to use - * @returns Promise resolving to the RSV signature - */ - abstract sign(args: SignArgs & Record): Promise - - /** - * Gets the public key associated with this contract instance. - * - * @returns Promise resolving to the SEC1 uncompressed public key - */ - abstract getPublicKey(): Promise + const najPubKey = (await this.viewFunction({ + contractId: this.contractId, + method: 'derived_public_key', + args, + })) as NajPublicKey + return najToUncompressedPubKeySEC1(najPubKey) + } } diff --git a/src/contracts/near/account.ts b/src/contracts/account.ts similarity index 93% rename from src/contracts/near/account.ts rename to src/contracts/account.ts index 2a78cbb..7d758e7 100644 --- a/src/contracts/near/account.ts +++ b/src/contracts/account.ts @@ -2,7 +2,7 @@ import { Account, Connection } from '@near-js/accounts' import { KeyPair } from '@near-js/crypto' import { InMemoryKeyStore } from '@near-js/keystores' -import { DONT_CARE_ACCOUNT_ID } from '@contracts/near/constants' +import { DONT_CARE_ACCOUNT_ID } from '@contracts/constants' type SetConnectionArgs = | { diff --git a/src/contracts/near/constants.ts b/src/contracts/constants.ts similarity index 100% rename from src/contracts/near/constants.ts rename to src/contracts/constants.ts diff --git a/src/contracts/evm/ChainSignaturesContract.ts b/src/contracts/evm/ChainSignaturesContract.ts deleted file mode 100644 index 1848fae..0000000 --- a/src/contracts/evm/ChainSignaturesContract.ts +++ /dev/null @@ -1,326 +0,0 @@ -import { najToUncompressedPubKeySEC1 } from '@utils/cryptography' -import { getRootPublicKey } from '@utils/publicKey' -import BN from 'bn.js' -import { withRetry, type PublicClient, type WalletClient, type Hex } from 'viem' - -import { CHAINS, KDF_CHAIN_IDS } from '@constants' -import { ChainSignatureContract as AbstractChainSignatureContract } from '@contracts/ChainSignatureContract' -import type { SignArgs } from '@contracts/ChainSignatureContract' -import type { - NajPublicKey, - RSVSignature, - ChainSigEvmMpcSignature, - UncompressedPubKeySEC1, -} from '@types' -import { cryptography } from '@utils' - -import { abi } from './ChainSignaturesContractABI' -import { - SignatureNotFoundError, - SignatureContractError, - SigningError, -} from './errors' -import type { - RequestIdArgs, - SignOptions, - SignRequest, - SignatureErrorData, -} from './types' -import { getRequestId } from './utils' - -/** - * Implementation of the ChainSignatureContract for EVM chains. - * - * When signing data, the contract emits a SignatureRequested event with a requestId. - * This requestId is used to track the signature request and retrieve the signature - * once it's available. The sign method handles this process automatically by polling - * for the signature using the requestId. - */ -export class ChainSignatureContract extends AbstractChainSignatureContract { - private readonly publicClient: PublicClient - private readonly walletClient: WalletClient - private readonly contractAddress: Hex - private readonly rootPublicKey: NajPublicKey - - /** - * Creates a new instance of the ChainSignatureContract for EVM chains. - * - * @param args - Configuration options for the contract - * @param args.publicClient - A Viem PublicClient instance for reading from the blockchain - * @param args.walletClient - A Viem WalletClient instance for sending transactions - * @param args.contractAddress - The address of the deployed ChainSignatures contract (e.g. `0x857ED3A242B59cC24144814a0DF41C397a3811E6`) - * @param args.rootPublicKey - Optional root public key. If not provided, it will be derived from the contract address - */ - constructor(args: { - publicClient: PublicClient - walletClient: WalletClient - contractAddress: Hex - rootPublicKey?: NajPublicKey - }) { - super() - this.publicClient = args.publicClient - this.walletClient = args.walletClient - this.contractAddress = args.contractAddress - - const rootPublicKey = - args.rootPublicKey || - getRootPublicKey(this.contractAddress, CHAINS.ETHEREUM) - - if (!rootPublicKey) { - throw new Error( - `Invalid public key, please provide a valid root public key or contract address` - ) - } - - this.rootPublicKey = rootPublicKey - } - - async getCurrentSignatureDeposit(): Promise { - const deposit = (await this.publicClient.readContract({ - address: this.contractAddress, - abi, - functionName: 'getSignatureDeposit', - })) as bigint - - return new BN(deposit.toString()) - } - - async getDerivedPublicKey(args: { - path: string - predecessor: string - }): Promise { - if ('IsEd25519' in args && args.IsEd25519) { - throw new Error('Ed25519 not supported on EVM chains') - } - const pubKey = cryptography.deriveChildPublicKey( - await this.getPublicKey(), - args.predecessor.toLowerCase(), - args.path, - KDF_CHAIN_IDS.ETHEREUM - ) - - return pubKey - } - - async getPublicKey(): Promise { - return najToUncompressedPubKeySEC1(this.rootPublicKey) - } - - async getLatestKeyVersion(): Promise { - const version = (await this.publicClient.readContract({ - address: this.contractAddress, - abi, - functionName: 'latestKeyVersion', - })) as bigint - - return Number(version) - } - - /** - * Sends a transaction to the contract to request a signature, then - * polls for the signature result. If the signature is not found within the retry - * parameters, it will throw an error. - */ - async sign( - args: SignArgs, - options: SignOptions = { - sign: { - algo: '', - dest: '', - params: '', - }, - retry: { - delay: 5000, - retryCount: 12, - }, - } - ): Promise { - if (!this.walletClient?.account) { - throw new Error('Wallet client required for signing operations') - } - - const request: SignRequest = { - payload: `0x${Buffer.from(args.payload).toString('hex')}`, - path: args.path, - keyVersion: args.key_version, - algo: options.sign.algo ?? '', - dest: options.sign.dest ?? '', - params: options.sign.params ?? '', - } - - const requestId = this.getRequestId({ - ...request, - address: this.walletClient.account.address, - chainId: this.publicClient.chain?.id - ? BigInt(this.publicClient.chain.id) - : 0n, - }) - - const hash = await this.walletClient.writeContract({ - address: this.contractAddress, - abi, - chain: this.publicClient.chain, - account: this.walletClient.account, - functionName: 'sign', - args: [request], - value: BigInt((await this.getCurrentSignatureDeposit()).toString()), - }) - - const receipt = await this.publicClient.waitForTransactionReceipt({ hash }) - - try { - const result = await withRetry( - async () => { - const result = await this.getSignatureFromEvents( - requestId, - receipt.blockNumber - ) - - // TODO: Validate if this is the signature corresponding to the transaction as anybody can call respond on the contract - - if (result) { - return result - } else { - throw new Error('Signature not found yet') - } - }, - { - delay: options.retry.delay, - retryCount: options.retry.retryCount, - shouldRetry: ({ count, error }) => { - // TODO: Should be enabled only on debug mode - console.log( - `Retrying get signature: ${count}/${options.retry.retryCount}` - ) - return error.message === 'Signature not found yet' - }, - } - ) - - if (result) { - return result - } else { - const errorData = await this.getErrorFromEvents( - requestId, - receipt.blockNumber - ) - if (errorData) { - throw new SignatureContractError(errorData.error, requestId, receipt) - } else { - throw new SignatureNotFoundError(requestId, receipt) - } - } - } catch (error) { - if ( - error instanceof SignatureNotFoundError || - error instanceof SignatureContractError - ) { - throw error - } else { - throw new SigningError( - requestId, - receipt, - error instanceof Error ? error : undefined - ) - } - } - } - - /** - * Generates the request ID for a signature request allowing to track the response. - * - * @param request - The signature request object containing: - * @param request.address - The contract/wallet address calling the signing contract - * @param request.payload - The data payload to be signed as a hex string - * @param request.path - The derivation path for the key - * @param request.keyVersion - The version of the key to use - * @param request.chainId - The chain ID as a bigint - * @param request.algo - The signing algorithm to use - * @param request.dest - The destination for the signature - * @param request.params - Additional parameters for the signing process - * @returns A hex string representing the unique request ID - * - * @example - * ```typescript - * const requestId = ChainSignatureContract.getRequestId({ - * address: walletClient.account.address, - * payload: payload: `0x${Buffer.from(args.payload).toString('hex')}`,, - * path: '', - * keyVersion: 0, - * chainId: 1n, - * algo: '', - * dest: '', - * params: '' - * }); - * console.log(requestId); // 0x... - * ``` - */ - getRequestId(request: RequestIdArgs): Hex { - return getRequestId(request) - } - - async getErrorFromEvents( - requestId: Hex, - fromBlock: bigint - ): Promise { - const errorLogs = await this.publicClient.getContractEvents({ - address: this.contractAddress, - abi, - eventName: 'SignatureError', - args: { - requestId, - }, - fromBlock, - toBlock: 'latest', - }) - - if (errorLogs.length > 0) { - const { args: errorData } = errorLogs[ - errorLogs.length - 1 - ] as unknown as { - args: SignatureErrorData - } - - return errorData - } - - return undefined - } - - /** - * Searches for SignatureResponded events that match the given requestId. - * It works in conjunction with the getRequestId method which generates the unique - * identifier for a signature request. - * - * @param requestId - The identifier for the signature request - * @param fromBlock - The block number to start searching from - * @returns The RSV signature if found, undefined otherwise - */ - async getSignatureFromEvents( - requestId: Hex, - fromBlock: bigint - ): Promise { - const logs = await this.publicClient.getContractEvents({ - address: this.contractAddress, - abi, - eventName: 'SignatureResponded', - args: { - requestId, - }, - fromBlock, - toBlock: 'latest', - }) - - if (logs.length > 0) { - const { args: signatureData } = logs[logs.length - 1] as unknown as { - args: { - signature: ChainSigEvmMpcSignature - } - } - - return cryptography.toRSV(signatureData.signature) - } - - return undefined - } -} diff --git a/src/contracts/evm/ChainSignaturesContractABI.ts b/src/contracts/evm/ChainSignaturesContractABI.ts deleted file mode 100644 index 68a26c2..0000000 --- a/src/contracts/evm/ChainSignaturesContractABI.ts +++ /dev/null @@ -1,359 +0,0 @@ -export const abi = [ - { - inputs: [ - { internalType: 'address', name: '_mpc_network', type: 'address' }, - { internalType: 'uint256', name: '_signatureDeposit', type: 'uint256' }, - ], - stateMutability: 'nonpayable', - type: 'constructor', - }, - { inputs: [], name: 'AccessControlBadConfirmation', type: 'error' }, - { - inputs: [ - { internalType: 'address', name: 'account', type: 'address' }, - { internalType: 'bytes32', name: 'neededRole', type: 'bytes32' }, - ], - name: 'AccessControlUnauthorizedAccount', - type: 'error', - }, - { - anonymous: false, - inputs: [ - { indexed: true, internalType: 'bytes32', name: 'role', type: 'bytes32' }, - { - indexed: true, - internalType: 'bytes32', - name: 'previousAdminRole', - type: 'bytes32', - }, - { - indexed: true, - internalType: 'bytes32', - name: 'newAdminRole', - type: 'bytes32', - }, - ], - name: 'RoleAdminChanged', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { indexed: true, internalType: 'bytes32', name: 'role', type: 'bytes32' }, - { - indexed: true, - internalType: 'address', - name: 'account', - type: 'address', - }, - { - indexed: true, - internalType: 'address', - name: 'sender', - type: 'address', - }, - ], - name: 'RoleGranted', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { indexed: true, internalType: 'bytes32', name: 'role', type: 'bytes32' }, - { - indexed: true, - internalType: 'address', - name: 'account', - type: 'address', - }, - { - indexed: true, - internalType: 'address', - name: 'sender', - type: 'address', - }, - ], - name: 'RoleRevoked', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'bytes32', - name: 'requestId', - type: 'bytes32', - }, - { - indexed: false, - internalType: 'address', - name: 'responder', - type: 'address', - }, - { indexed: false, internalType: 'string', name: 'error', type: 'string' }, - ], - name: 'SignatureError', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: 'address', - name: 'sender', - type: 'address', - }, - { - indexed: false, - internalType: 'bytes32', - name: 'payload', - type: 'bytes32', - }, - { - indexed: false, - internalType: 'uint32', - name: 'keyVersion', - type: 'uint32', - }, - { - indexed: false, - internalType: 'uint256', - name: 'deposit', - type: 'uint256', - }, - { - indexed: false, - internalType: 'uint256', - name: 'chainId', - type: 'uint256', - }, - { indexed: false, internalType: 'string', name: 'path', type: 'string' }, - { indexed: false, internalType: 'string', name: 'algo', type: 'string' }, - { indexed: false, internalType: 'string', name: 'dest', type: 'string' }, - { - indexed: false, - internalType: 'string', - name: 'params', - type: 'string', - }, - ], - name: 'SignatureRequested', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'bytes32', - name: 'requestId', - type: 'bytes32', - }, - { - indexed: false, - internalType: 'address', - name: 'responder', - type: 'address', - }, - { - components: [ - { - components: [ - { internalType: 'uint256', name: 'x', type: 'uint256' }, - { internalType: 'uint256', name: 'y', type: 'uint256' }, - ], - internalType: 'struct ChainSignatures.AffinePoint', - name: 'bigR', - type: 'tuple', - }, - { internalType: 'uint256', name: 's', type: 'uint256' }, - { internalType: 'uint8', name: 'recoveryId', type: 'uint8' }, - ], - indexed: false, - internalType: 'struct ChainSignatures.Signature', - name: 'signature', - type: 'tuple', - }, - ], - name: 'SignatureResponded', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'owner', - type: 'address', - }, - { - indexed: false, - internalType: 'uint256', - name: 'amount', - type: 'uint256', - }, - ], - name: 'Withdraw', - type: 'event', - }, - { - inputs: [], - name: 'DEFAULT_ADMIN_ROLE', - outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [{ internalType: 'bytes32', name: 'role', type: 'bytes32' }], - name: 'getRoleAdmin', - outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'getSignatureDeposit', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { internalType: 'bytes32', name: 'role', type: 'bytes32' }, - { internalType: 'address', name: 'account', type: 'address' }, - ], - name: 'grantRole', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { internalType: 'bytes32', name: 'role', type: 'bytes32' }, - { internalType: 'address', name: 'account', type: 'address' }, - ], - name: 'hasRole', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { internalType: 'bytes32', name: 'role', type: 'bytes32' }, - { internalType: 'address', name: 'callerConfirmation', type: 'address' }, - ], - name: 'renounceRole', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - components: [ - { internalType: 'bytes32', name: 'requestId', type: 'bytes32' }, - { - components: [ - { - components: [ - { internalType: 'uint256', name: 'x', type: 'uint256' }, - { internalType: 'uint256', name: 'y', type: 'uint256' }, - ], - internalType: 'struct ChainSignatures.AffinePoint', - name: 'bigR', - type: 'tuple', - }, - { internalType: 'uint256', name: 's', type: 'uint256' }, - { internalType: 'uint8', name: 'recoveryId', type: 'uint8' }, - ], - internalType: 'struct ChainSignatures.Signature', - name: 'signature', - type: 'tuple', - }, - ], - internalType: 'struct ChainSignatures.Response[]', - name: '_responses', - type: 'tuple[]', - }, - ], - name: 'respond', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - components: [ - { internalType: 'bytes32', name: 'requestId', type: 'bytes32' }, - { internalType: 'string', name: 'errorMessage', type: 'string' }, - ], - internalType: 'struct ChainSignatures.ErrorResponse[]', - name: '_errors', - type: 'tuple[]', - }, - ], - name: 'respondError', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { internalType: 'bytes32', name: 'role', type: 'bytes32' }, - { internalType: 'address', name: 'account', type: 'address' }, - ], - name: 'revokeRole', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [{ internalType: 'uint256', name: '_amount', type: 'uint256' }], - name: 'setSignatureDeposit', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - components: [ - { internalType: 'bytes32', name: 'payload', type: 'bytes32' }, - { internalType: 'string', name: 'path', type: 'string' }, - { internalType: 'uint32', name: 'keyVersion', type: 'uint32' }, - { internalType: 'string', name: 'algo', type: 'string' }, - { internalType: 'string', name: 'dest', type: 'string' }, - { internalType: 'string', name: 'params', type: 'string' }, - ], - internalType: 'struct ChainSignatures.SignRequest', - name: '_request', - type: 'tuple', - }, - ], - name: 'sign', - outputs: [], - stateMutability: 'payable', - type: 'function', - }, - { - inputs: [{ internalType: 'bytes4', name: 'interfaceId', type: 'bytes4' }], - name: 'supportsInterface', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { internalType: 'uint256', name: '_amount', type: 'uint256' }, - { internalType: 'address', name: '_receiver', type: 'address' }, - ], - name: 'withdraw', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, -] diff --git a/src/contracts/evm/errors.ts b/src/contracts/evm/errors.ts deleted file mode 100644 index 19a59a6..0000000 --- a/src/contracts/evm/errors.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { TransactionReceipt } from 'viem' - -export class ChainSignatureError extends Error { - requestId: `0x${string}` - receipt: TransactionReceipt - - constructor( - message: string, - requestId: `0x${string}`, - receipt: TransactionReceipt - ) { - super(message) - this.name = 'ChainSignatureError' - this.requestId = requestId - this.receipt = receipt - } -} - -export class SignatureNotFoundError extends ChainSignatureError { - constructor(requestId: `0x${string}`, receipt: TransactionReceipt) { - super('Signature not found after maximum retries', requestId, receipt) - this.name = 'SignatureNotFoundError' - } -} - -export class SignatureContractError extends ChainSignatureError { - errorCode: string - - constructor( - errorCode: string, - requestId: `0x${string}`, - receipt: TransactionReceipt - ) { - super(`Signature error: ${errorCode}`, requestId, receipt) - this.name = 'SignatureContractError' - this.errorCode = errorCode - } -} - -export class SigningError extends ChainSignatureError { - originalError?: Error - - constructor( - requestId: `0x${string}`, - receipt: TransactionReceipt, - originalError?: Error - ) { - super('Error signing request', requestId, receipt) - this.name = 'SigningError' - this.originalError = originalError - } -} diff --git a/src/contracts/evm/index.ts b/src/contracts/evm/index.ts deleted file mode 100644 index 0238782..0000000 --- a/src/contracts/evm/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import * as ChainSignaturesContractABI from './ChainSignaturesContractABI' -import * as errors from './errors' -export * from './ChainSignaturesContract' - -const utils = { - ChainSignaturesContractABI, - errors, -} - -export { utils } diff --git a/src/contracts/evm/types.ts b/src/contracts/evm/types.ts deleted file mode 100644 index 507a60b..0000000 --- a/src/contracts/evm/types.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { Address, Hex } from 'viem' - -export interface SignOptions { - sign: { - algo?: string - dest?: string - params?: string - } - retry: { - delay?: number - retryCount?: number - } -} - -export interface SignatureErrorData { - requestId: string - responder: string - error: string -} - -export interface SignRequest { - payload: Hex - path: string - keyVersion: number - algo: string - dest: string - params: string -} - -export interface RequestIdArgs { - address: Address - payload: Hex - path: string - keyVersion: number - chainId: bigint - algo: string - dest: string - params: string -} diff --git a/src/contracts/evm/utils.ts b/src/contracts/evm/utils.ts deleted file mode 100644 index 5612e57..0000000 --- a/src/contracts/evm/utils.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { encodeAbiParameters, keccak256 } from 'viem' -import * as chains from 'viem/chains' -import type { Chain } from 'viem/chains' - -import { type RequestIdArgs } from './types' - -export const getChain = (chainId: number): Chain => { - for (const chain of Object.values(chains)) { - if (chain.id === chainId) { - return chain - } - } - throw new Error('Chain not found') -} - -export const getRequestId = (request: RequestIdArgs): `0x${string}` => { - const encoded = encodeAbiParameters( - [ - { type: 'address' }, - { type: 'bytes' }, - { type: 'string' }, - { type: 'uint32' }, - { type: 'uint256' }, - { type: 'string' }, - { type: 'string' }, - { type: 'string' }, - ], - [ - request.address, - request.payload, - request.path, - Number(request.keyVersion), - request.chainId, - request.algo, - request.dest, - request.params, - ] - ) - - return keccak256(encoded) -} diff --git a/src/contracts/index.ts b/src/contracts/index.ts index 7325617..4ebf5bd 100644 --- a/src/contracts/index.ts +++ b/src/contracts/index.ts @@ -1,4 +1,11 @@ -export { ChainSignatureContract, type SignArgs } from './ChainSignatureContract' +// TODO: fix signAndSend +// import * as signAndSend from './signAndSend' +import * as transaction from './transaction' +export * from './ChainSignatureContract' -export * as near from './near' -export * as evm from './evm' +const utils = { + transaction, + // signAndSend, +} + +export { utils } diff --git a/src/contracts/near/ChainSignatureContract.ts b/src/contracts/near/ChainSignatureContract.ts deleted file mode 100644 index 3834883..0000000 --- a/src/contracts/near/ChainSignatureContract.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { Contract } from '@near-js/accounts' -import { KeyPair } from '@near-js/crypto' -import { actionCreators } from '@near-js/transactions' -import { najToUncompressedPubKeySEC1 } from '@utils/cryptography' -import { getRootPublicKey } from '@utils/publicKey' -import BN from 'bn.js' - -import { CHAINS, KDF_CHAIN_IDS } from '@constants' -import { ChainSignatureContract as AbstractChainSignatureContract } from '@contracts/ChainSignatureContract' -import type { SignArgs } from '@contracts/ChainSignatureContract' -import { getNearAccount } from '@contracts/near/account' -import { DONT_CARE_ACCOUNT_ID, NEAR_MAX_GAS } from '@contracts/near/constants' -import { - responseToMpcSignature, - type SendTransactionOptions, - sendTransactionUntil, -} from '@contracts/near/transaction' -import { - type NearNetworkIds, - type ChainSignatureContractIds, -} from '@contracts/near/types' -import type { RSVSignature, UncompressedPubKeySEC1, NajPublicKey } from '@types' -import { cryptography } from '@utils' - -type NearContract = Contract & { - public_key: () => Promise - experimental_signature_deposit: () => Promise - derived_public_key: (args: { - path: string - predecessor: string - domain_id?: number - }) => Promise -} - -interface ChainSignatureContractArgs { - networkId: NearNetworkIds - contractId: ChainSignatureContractIds - accountId?: string - keypair?: KeyPair - rootPublicKey?: NajPublicKey - sendTransactionOptions?: SendTransactionOptions -} - -/** - * Implementation of the ChainSignatureContract for NEAR chains. - * - * This class provides an interface to interact with the ChainSignatures contract - * deployed on NEAR. It supports both view methods (which don't require authentication) - * and change methods (which require a valid NEAR account and keypair). - * - * @extends AbstractChainSignatureContract - */ -export class ChainSignatureContract extends AbstractChainSignatureContract { - private readonly networkId: NearNetworkIds - private readonly contractId: ChainSignatureContractIds - private readonly accountId: string - private readonly keypair: KeyPair - private readonly rootPublicKey?: NajPublicKey - private readonly sendTransactionOptions?: SendTransactionOptions - /** - * Creates a new instance of the ChainSignatureContract for NEAR chains. - * - * @param args - Configuration options for the contract - * @param args.networkId - The NEAR network ID (e.g. 'testnet', 'mainnet') - * @param args.contractId - The contract ID of the deployed ChainSignatures contract - * @param args.accountId - Optional NEAR account ID for signing transactions. Required for change methods. - * @param args.keypair - Optional NEAR KeyPair for signing transactions. Required for change methods. - * @param args.rootPublicKey - Optional root public key for the contract. If not provided, it will be derived from the contract ID. - * @param args.sendTransactionOptions - Optional configuration for transaction sending behavior. - */ - constructor({ - networkId, - contractId, - accountId = DONT_CARE_ACCOUNT_ID, - keypair = KeyPair.fromRandom('ed25519'), - rootPublicKey, - sendTransactionOptions, - }: ChainSignatureContractArgs) { - super() - - this.networkId = networkId - this.contractId = contractId - this.accountId = accountId - this.keypair = keypair - this.sendTransactionOptions = sendTransactionOptions - - this.rootPublicKey = - rootPublicKey || getRootPublicKey(this.contractId, CHAINS.NEAR) - } - - private async getContract(): Promise { - const account = await getNearAccount({ - networkId: this.networkId, - accountId: this.accountId, - keypair: this.keypair, - }) - - return new Contract(account, this.contractId, { - viewMethods: [ - 'public_key', - 'experimental_signature_deposit', - 'derived_public_key', - ], - // Change methods use the sendTransactionUntil because the internal retry of the Contract class - // throws on NodeJs. - changeMethods: [], - useLocalViewExecution: false, - }) as unknown as NearContract - } - - async getCurrentSignatureDeposit(): Promise { - const contract = await this.getContract() - return new BN( - (await contract.experimental_signature_deposit()).toLocaleString( - 'fullwide', - { - useGrouping: false, - } - ) - ) - } - - async getDerivedPublicKey(args: { - path: string - predecessor: string - IsEd25519?: boolean - }): Promise { - if (args.IsEd25519) { - const contract = await this.getContract() - return (await contract.derived_public_key({ - path: args.path, - predecessor: args.predecessor, - domain_id: 1, - })) as `Ed25519:${string}` - } - - if (this.rootPublicKey) { - const pubKey = cryptography.deriveChildPublicKey( - await this.getPublicKey(), - args.predecessor.toLowerCase(), - args.path, - KDF_CHAIN_IDS.NEAR - ) - return pubKey - } else { - // Support for legacy contract - const contract = await this.getContract() - const najPubKey = await contract.derived_public_key(args) - return najToUncompressedPubKeySEC1(najPubKey as NajPublicKey) - } - } - - async getPublicKey(): Promise { - if (this.rootPublicKey) { - return najToUncompressedPubKeySEC1(this.rootPublicKey) - } else { - // Support for legacy contract - const contract = await this.getContract() - const najPubKey = await contract.public_key() - return najToUncompressedPubKeySEC1(najPubKey) - } - } - - async sign( - args: SignArgs, - options?: { - nonce?: number - } - ): Promise { - this.requireAccount() - - const deposit = await this.getCurrentSignatureDeposit() - - const result = await sendTransactionUntil({ - accountId: this.accountId, - keypair: this.keypair, - networkId: this.networkId, - receiverId: this.contractId, - actions: [ - actionCreators.functionCall( - 'sign', - { request: args }, - BigInt(NEAR_MAX_GAS.toString()), - BigInt(deposit.toString()) - ), - ], - nonce: options?.nonce, - options: this.sendTransactionOptions, - }) - - const signature = responseToMpcSignature({ response: result }) - - if (!signature) { - throw new Error('Transaction failed') - } - - return signature - } - - private requireAccount(): void { - if (this.accountId === DONT_CARE_ACCOUNT_ID) { - throw new Error( - 'A valid account ID and keypair are required for change methods. Please instantiate a new contract with valid credentials.' - ) - } - } -} diff --git a/src/contracts/near/index.ts b/src/contracts/near/index.ts deleted file mode 100644 index 255a187..0000000 --- a/src/contracts/near/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import * as signAndSend from './signAndSend' -import * as transaction from './transaction' -export * from './ChainSignatureContract' - -const utils = { - transaction, - signAndSend, -} - -export { utils } diff --git a/src/contracts/near/signAndSend/index.ts b/src/contracts/signAndSend/index.ts similarity index 100% rename from src/contracts/near/signAndSend/index.ts rename to src/contracts/signAndSend/index.ts diff --git a/src/contracts/near/signAndSend/keypair.ts b/src/contracts/signAndSend/keypair.ts similarity index 96% rename from src/contracts/near/signAndSend/keypair.ts rename to src/contracts/signAndSend/keypair.ts index 7e3c4ba..ae43852 100644 --- a/src/contracts/near/signAndSend/keypair.ts +++ b/src/contracts/signAndSend/keypair.ts @@ -2,14 +2,14 @@ import { type KeyPair } from '@near-js/crypto' import * as chainAdapters from '@chain-adapters' import { BTCRpcAdapters } from '@chain-adapters/Bitcoin/BTCRpcAdapter' -import { getNearAccount } from '@contracts/near/account' -import { ChainSignatureContract } from '@contracts/near/ChainSignatureContract' +import { getNearAccount } from '@contracts/account' +import { ChainSignatureContract } from '@contracts/ChainSignatureContract' import { type Response, type BitcoinRequest, type CosmosRequest, type EVMRequest, -} from '@contracts/near/types' +} from '@contracts/types' import { createPublicClient, http } from 'viem'; export const EVMTransaction = async ( diff --git a/src/contracts/near/transaction.ts b/src/contracts/transaction.ts similarity index 69% rename from src/contracts/near/transaction.ts rename to src/contracts/transaction.ts index 25eb23a..fc93fab 100644 --- a/src/contracts/near/transaction.ts +++ b/src/contracts/transaction.ts @@ -2,79 +2,36 @@ import { InMemoryKeyStore } from '@near-js/keystores' import type { Action as TransactionAction } from '@near-js/transactions' import type { TxExecutionStatus } from '@near-js/types' import type { - Action as WalletAction, FinalExecutionOutcome, NetworkId, } from '@near-wallet-selector/core' -import BN from 'bn.js' import { transactions, utils as nearUtils, connect, type KeyPair, } from 'near-api-js' -import { getTransactionLastResult } from 'near-api-js/lib/providers' import { withRetry } from 'viem' -import { ChainSignatureContract } from '@contracts/near/ChainSignatureContract' -import { NEAR_MAX_GAS } from '@contracts/near/constants' -import { type ChainSignatureContractIds } from '@contracts/near/types' import { type RSVSignature, - type KeyDerivationPath, type MPCSignature, - type HashToSign, + type Ed25519Signature, } from '@types' import { cryptography } from '@utils' -export const mpcPayloadsToChainSigTransaction = async ({ - networkId, - contractId, - hashesToSign, - path, -}: { - networkId: NetworkId - contractId: ChainSignatureContractIds - hashesToSign: HashToSign[] - path: KeyDerivationPath -}): Promise<{ - receiverId: string - actions: WalletAction[] -}> => { - const contract = new ChainSignatureContract({ - networkId, - contractId, - }) - - const currentContractFee = await contract.getCurrentSignatureDeposit() - - return { - receiverId: contractId, - actions: hashesToSign.map((payload) => ({ - type: 'FunctionCall', - params: { - methodName: 'sign', - args: { - request: { - payload: Array.from(payload), - path, - key_version: 0, - }, - }, - gas: NEAR_MAX_GAS.div(new BN(hashesToSign.length)).toString(), - deposit: currentContractFee?.toString() || '1', - }, - })), - } -} - export const responseToMpcSignature = ({ - response, + signature, }: { - response: FinalExecutionOutcome -}): RSVSignature | undefined => { - const signature = getTransactionLastResult(response) as MPCSignature - + signature: MPCSignature +}): RSVSignature | Ed25519Signature | undefined => { + if ( + 'scheme' in signature && + signature.scheme === 'Ed25519' && + 'signature' in signature + ) { + return signature as Ed25519Signature + } if (signature) { return cryptography.toRSV(signature) } else { diff --git a/src/contracts/near/types.ts b/src/contracts/types.ts similarity index 97% rename from src/contracts/near/types.ts rename to src/contracts/types.ts index 214900c..0592bf3 100644 --- a/src/contracts/near/types.ts +++ b/src/contracts/types.ts @@ -7,7 +7,6 @@ import type { CosmosTransactionRequest, } from '@chain-adapters/Cosmos/types' import { type EVMTransactionRequest } from '@chain-adapters/EVM/types' -import type { KeyDerivationPath } from '@types' export type ChainSignatureContractIds = string diff --git a/src/types.ts b/src/types.ts index 87276fd..340b3fb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,4 @@ -import { type SignArgs } from '@contracts/ChainSignatureContract' - -export type HashToSign = SignArgs['payload'] +export { type HashToSign } from '@contracts/ChainSignatureContract' type Base58String = string @@ -22,9 +20,15 @@ export interface Signature { signature: number[] } +type Scheme = 'secp256k1' | 'ed25519' | 'Ed25519' | 'Secp256k1' + export interface KeyDerivationPath { index: number - scheme: 'secp256k1' | 'ed25519' + scheme: Scheme +} + +export interface Ed25519Signature { + signature: number[] } export interface RSVSignature { @@ -59,3 +63,5 @@ export type MPCSignature = | NearNearMpcSignature | ChainSigNearMpcSignature | ChainSigEvmMpcSignature + | { scheme: Scheme } + | Ed25519Signature diff --git a/src/utils/cryptography.ts b/src/utils/cryptography.ts index b44fbb4..7144bc5 100644 --- a/src/utils/cryptography.ts +++ b/src/utils/cryptography.ts @@ -139,3 +139,15 @@ export function deriveChildPublicKey( return `04${newX}${newY}` } + +/** + * Converts a Uint8Array to a hexadecimal string. + * + * @param uint8Array - The Uint8Array to convert. + * @returns The hexadecimal string representation of the Uint8Array. + */ +export const uint8ArrayToHex = (uint8Array: number[]): string => { + return Array.from(uint8Array) + .map((byte) => byte.toString(16).padStart(2, '0')) + .join('') +}