UNPKG

@hyperlane-xyz/sdk

Version:

The official SDK for the Hyperlane Network

190 lines 7.38 kB
import { ethers } from 'ethers'; import { rootLogger } from '@hyperlane-xyz/utils'; import { TurnkeyClientManager, logTurnkeyError, validateTurnkeyActivityCompleted, } from '../turnkeyClient.js'; const logger = rootLogger.child({ module: 'sdk:turnkey-evm' }); /** * Turnkey signer for EVM transactions * Uses Turnkey's secure enclaves to sign transactions without exposing private keys * This is a custom ethers v5-compatible Signer that uses Turnkey SDK directly * Uses composition to access Turnkey functionality while extending ethers.Signer * * @example * ```typescript * const config: TurnkeyConfig = { * organizationId: 'your-org-id', * apiPublicKey: process.env.TURNKEY_API_PUBLIC_KEY, * apiPrivateKey: process.env.TURNKEY_API_PRIVATE_KEY, * privateKeyId: 'your-private-key-id', * publicKey: '0x...', // Ethereum address * }; * * const provider = new ethers.providers.JsonRpcProvider('...'); * const signer = new TurnkeyEvmSigner(config, provider); * * // Use with MultiProvider * multiProvider.setSigner('ethereum', signer); * ``` */ export class TurnkeyEvmSigner extends ethers.Signer { manager; address; provider; constructor(config, provider) { super(); this.manager = new TurnkeyClientManager(config); this.address = config.publicKey; this.provider = provider; logger.debug(`Initialized Turnkey EVM signer for key: ${this.address}`); } /** * Health check - delegates to manager */ async healthCheck() { return this.manager.healthCheck(); } /** * Get an ethers Signer connected to the provided provider * This returns a new instance with the provider connected */ async getSigner(provider) { logger.debug('Creating Turnkey EVM signer for transaction'); return this.connect(provider); } /** * Connect this signer to a provider (creates new instance with proper configuration) */ connect(provider) { return new TurnkeyEvmSigner(this.manager.getConfig(), provider); } /** * Get the address of this signer */ async getAddress() { return this.address; } /** * Sign a transaction using Turnkey */ async signTransaction(transaction) { if (!this.provider) { throw new Error('Provider required to sign transaction'); } logger.debug('Signing transaction with Turnkey', { to: transaction.to, value: transaction.value?.toString(), }); try { // Populate the transaction (fill in nonce, gasPrice, etc.) const populatedTx = await ethers.utils.resolveProperties(await this.populateTransaction(transaction)); // Remove 'from' field for serialization const { from: _, ...txToSerialize } = populatedTx; // For EIP-1559 transactions, explicitly set type: 2 and remove gasPrice if (txToSerialize.maxFeePerGas || txToSerialize.maxPriorityFeePerGas) { txToSerialize.type = 2; delete txToSerialize.gasPrice; } const unsignedTx = ethers.utils.serializeTransaction(txToSerialize); // Remove 0x prefix for Turnkey API (it expects raw hex) const unsignedTxHex = unsignedTx.startsWith('0x') ? unsignedTx.slice(2) : unsignedTx; // Sign using Turnkey's signTransaction API const { activity } = await this.manager.getClient().signTransaction({ signWith: this.address, type: 'TRANSACTION_TYPE_ETHEREUM', unsignedTransaction: unsignedTxHex, }); validateTurnkeyActivityCompleted(activity, 'Transaction signing'); const signedTx = activity.result?.signTransactionResult?.signedTransaction; if (!signedTx) { throw new Error('No signed transaction returned from Turnkey'); } logger.debug('Transaction signed successfully'); // Ensure the signed transaction has 0x prefix return signedTx.startsWith('0x') ? signedTx : `0x${signedTx}`; } catch (error) { logTurnkeyError('Failed to sign transaction with Turnkey', error); throw error; } } /** * Sign a message using Turnkey */ async signMessage(message) { logger.debug('Signing message with Turnkey'); try { const messageBytes = typeof message === 'string' ? ethers.utils.toUtf8Bytes(message) : message; const messageHash = ethers.utils.hashMessage(messageBytes); // Sign raw payload using Turnkey const { activity, r, s, v } = await this.manager .getClient() .signRawPayload({ signWith: this.address, payload: messageHash.slice(2), // Remove 0x prefix encoding: 'PAYLOAD_ENCODING_HEXADECIMAL', hashFunction: 'HASH_FUNCTION_NO_OP', }); validateTurnkeyActivityCompleted(activity, 'Message signing'); // Validate signature components if (!r || !s || !v) { throw new Error('Missing signature components from Turnkey'); } const hexPattern = /^0x[0-9a-fA-F]+$/; if (!hexPattern.test(r) || !hexPattern.test(s)) { throw new Error('Invalid signature format from Turnkey'); } const vNum = parseInt(v, 16); if (isNaN(vNum)) { throw new Error(`Invalid v value from Turnkey: ${v}`); } // Reconstruct the signature from r, s, v return ethers.utils.joinSignature({ r, s, v: vNum }); } catch (error) { logTurnkeyError('Failed to sign message with Turnkey', error); throw error; } } /** * Populate a transaction with default values (nonce, gas, etc.) */ async populateTransaction(transaction) { if (!this.provider) { throw new Error('Provider required to populate transaction'); } const tx = { ...transaction }; // Set from address if (!tx.from) { tx.from = this.address; } // Get nonce if not set if (tx.nonce == null) { tx.nonce = await this.provider.getTransactionCount(this.address, 'pending'); } // Get gas price if not set if (tx.gasPrice == null && tx.maxFeePerGas == null) { const feeData = await this.provider.getFeeData(); if (feeData.maxFeePerGas) { tx.maxFeePerGas = feeData.maxFeePerGas; tx.maxPriorityFeePerGas = feeData.maxPriorityFeePerGas || undefined; } else { tx.gasPrice = feeData.gasPrice || undefined; } } // Get chain ID if not set if (tx.chainId == null) { const network = await this.provider.getNetwork(); tx.chainId = network.chainId; } // Estimate gas if not set if (tx.gasLimit == null) { tx.gasLimit = await this.provider.estimateGas(tx); } return tx; } } //# sourceMappingURL=turnkey.js.map