UNPKG

@appliedblockchain/silentdatarollup-ethers-provider-fireblocks

Version:
359 lines (355 loc) 12.5 kB
// src/constants.ts import { DEBUG_NAMESPACE } from "@fireblocks/fireblocks-web3-provider"; var DEBUG_NAMESPACE_SILENTDATA_INTERCEPTOR = `${DEBUG_NAMESPACE}:silentdata-interceptor`; var EIP712Domain = [ { name: "name", type: "string" }, { name: "version", type: "string" }, { name: "chainId", type: "uint256" } ]; // src/provider.ts import { FireblocksWeb3Provider } from "@fireblocks/fireblocks-web3-provider"; import { DEBUG_NAMESPACE as DEBUG_NAMESPACE3, HEADER_DELEGATE, HEADER_DELEGATE_SIGNATURE, HEADER_EIP712_DELEGATE_SIGNATURE, isSignableContractCall, SIGN_RPC_METHODS, SignatureType as SignatureType2 } from "@appliedblockchain/silentdatarollup-core"; import debug2 from "debug"; import { BrowserProvider, hexlify, keccak256, Transaction } from "ethers"; import { TransactionOperation as TransactionOperation2 } from "fireblocks-sdk"; // src/Base.ts import { DEBUG_NAMESPACE as DEBUG_NAMESPACE2, delegateEIP712Types, eip712Domain, getAuthEIP712Types, SignatureType, SilentDataRollupBase } from "@appliedblockchain/silentdatarollup-core"; import debug from "debug"; import { hashMessage } from "ethers"; import { PeerType, TransactionOperation } from "fireblocks-sdk"; var log = debug(DEBUG_NAMESPACE2); var SilentDataRollupFireblocksBase = class extends SilentDataRollupBase { async createPersonalSignature(provider, content, operation, type) { log("Signing message. Operation:", { operation, type, content }); const vaultAccountId = provider.ethereum.vaultAccountIds[0]?.toString(); log("Using vault account ID:", vaultAccountId); const transactionArguments = { operation, assetId: provider.ethereum.assetId, source: { type: PeerType.VAULT_ACCOUNT, id: vaultAccountId }, extraParameters: { rawMessageData: { messages: [ { content, ...type === SignatureType.EIP712 ? { type } : {} } ] } } }; log("Creating transaction", JSON.stringify(transactionArguments, null, 2)); const txInfo = await provider.ethereum.createTransaction(transactionArguments); log("Transaction created. TxInfo:", txInfo); const sig = txInfo.signedMessages[0].signature; const v = 27 + sig.v; const signature = "0x" + sig.r + sig.s + v.toString(16); log("Signature generated:", signature); return signature; } async signDelegateHeader(provider, message, isSWC) { void isSWC; log("Signing raw delegate header. Message:", message); const hashedMessage = hashMessage(message).slice(2); return this.createPersonalSignature( provider, hashedMessage, TransactionOperation.RAW, SignatureType.Raw ); } async signTypedDelegateHeader(provider, chainId, message, isSWC) { void isSWC; log("Signing typed delegate header. Message:", message); const content = { types: { EIP712Domain, ...delegateEIP712Types }, primaryType: "Ticket", domain: { ...eip712Domain, chainId }, message }; log("EIP712 content created:", content); return this.createPersonalSignature( provider, content, TransactionOperation.TYPED_MESSAGE, SignatureType.EIP712 ); } async signAuthHeader(provider, payload, timestamp, chainId, isSWC) { void isSWC; log("Preparing raw message for signing"); const message = this.prepareMessage(chainId, payload, timestamp); log("Raw message:", message); let signature; if (this.config.delegate) { const delegateSigner = await this.getDelegateSigner(this); signature = await this.signMessage(delegateSigner, message); log("Raw signature generated by delegate signer:", signature); } else { log("Message to be hashed:", JSON.stringify(message)); const hashedMessage = hashMessage(message).slice(2); log("Hashed message:", hashedMessage); signature = await this.createPersonalSignature( provider, hashedMessage, TransactionOperation.RAW ); } log("Raw signature generated:", signature); return signature; } async signTypedAuthHeader(provider, payload, timestamp, chainId, isSWC) { void isSWC; const types = getAuthEIP712Types(payload); const message = this.prepareTypedData(payload, timestamp); const content = { types: { EIP712Domain, ...types }, primaryType: "Call", domain: { ...eip712Domain, chainId }, message }; log("EIP712 content created:", content); let signature; if (this.config.delegate) { const delegateSigner = await this.getDelegateSigner(this); const domain = { ...eip712Domain, chainId: chainId.toString() }; signature = await delegateSigner.signTypedData( domain, types, message ); } else { signature = await this.createPersonalSignature( provider, content, TransactionOperation.TYPED_MESSAGE, SignatureType.EIP712 ); } log("Typed signature generated:", signature); return signature; } }; // src/provider.ts var log2 = debug2(DEBUG_NAMESPACE3); var SilentDataRollupFireblocksProvider = class _SilentDataRollupFireblocksProvider extends BrowserProvider { constructor({ ethereum, network, options, config = {} }) { super(ethereum, network, options); this.lastNonce = {}; log2("Initializing SilentDataRollupFireblocksProvider"); this.setupInterceptor(ethereum, network, options); this.ethereum = ethereum; this.network = network; this._options = options; this.config = { ...config, authSignatureType: config?.authSignatureType ?? SignatureType2.Raw }; this.baseProvider = new SilentDataRollupFireblocksBase(config); } /** * Manages and returns the next available nonce for a given address. * * This method implements a local nonce management system to handle concurrent * transactions and potential network delays. It's necessary because: * 1. Multiple transactions can be initiated before earlier ones are confirmed. * 2. We need to ensure each transaction uses a unique, incrementing nonce. * * The method works by: * - Tracking the last used nonce for each address. * - Comparing it with the current network nonce. * - Always returning a nonce higher than both the network nonce and the last used nonce. * * This approach helps prevent nonce conflicts and ensures transactions can be * sent in rapid succession without waiting for network confirmation. * * @param address - The Ethereum address for which to get the next nonce. * @returns A Promise that resolves to the next available nonce as a number. */ async getNextNonce(address) { const currentNonce = await this.getTransactionCount(address); this.lastNonce[address] = this.lastNonce[address] || currentNonce; const nonce = Math.max(this.lastNonce[address], currentNonce); this.lastNonce[address] = nonce + 1; return nonce; } /** * Custom method to handle transaction creation, signing, and broadcasting. * * This method is necessary because: * 1. When using Fireblocks to sign a transaction with CONTRACT_CALL, * Fireblocks also broadcasts the transaction to the specified chain * on the Fireblocks provider configuration. * 2. We need to manually handle the transaction creation process instead of * delegating everything to Fireblocks, as we need to broadcast it to our * own nodes. * 3. When populating a transaction (e.g., getting the nonce), we need to make * requests with auth headers to our RPC. * * This custom implementation allows us to control the entire process, * from transaction creation to signing and broadcasting. It ensures that * the necessary authenticated requests are made when populating the transaction, * and that the final transaction is broadcast to our specific nodes. * * @param payload - The transaction payload to be sent * @returns The transaction hash */ async sendTransaction(payload) { log2("Starting signAndBroadcastTransaction"); if (payload.method !== "eth_sendTransaction") { log2("Not an eth_sendTransaction method, skipping"); throw new Error("Not an eth_sendTransaction method."); } const nonce = await this.getNextNonce(payload.params[0].from); const chainId = (await this.getNetwork()).chainId; const { maxFeePerGas, maxPriorityFeePerGas } = await this.getFeeData(); const gasLimit = await this.estimateGas(payload.params[0]); const txParams = { type: 2, chainId, nonce, to: payload.params[0].to, maxFeePerGas, maxPriorityFeePerGas, gasLimit, data: payload.params[0].data, value: payload.params[0].value }; log2("Transaction params:", txParams); const tx = Transaction.from(txParams); const txHash = keccak256(tx.unsignedSerialized); const txHashHex = hexlify(txHash).slice(2); log2("Transaction hash to sign:", txHashHex); log2("Creating signature"); const signature = await this.baseProvider.createPersonalSignature( this, txHashHex, TransactionOperation2.RAW ); log2("Signature created:", signature); tx.signature = signature; log2("Signature added to transaction"); log2("Broadcasting transaction"); const signedTx = tx.serialized; try { await this.broadcastTransaction(signedTx); log2("Transaction broadcasted"); } catch (error) { log2("Transaction broadcast failed"); throw error; } log2("Transaction hash:", tx.hash); return tx.hash; } setupInterceptor(ethereum, network, _options) { const originalSend = ethereum.send; const that = this; ethereum.send = async (payload, callback) => { ; (async () => { if (payload.method === "eth_sendTransaction") { return that.sendTransaction(payload); } const requiresAuthHeaders = SIGN_RPC_METHODS.includes(payload.method) || isSignableContractCall(payload, this.baseProvider.contracts) || this.config.alwaysSignEthCalls && payload.method === "eth_call"; const delegateHeaders = []; if (requiresAuthHeaders) { log2(`Intercepted request: ${JSON.stringify(payload, null, 2)}`); if (this.config.delegate) { const { [HEADER_DELEGATE]: xDelegate, [HEADER_DELEGATE_SIGNATURE]: xDelegateSignature, [HEADER_EIP712_DELEGATE_SIGNATURE]: xEip712DelegateSignature } = await this.baseProvider.getDelegateHeaders(this); delegateHeaders.push({ name: HEADER_DELEGATE, value: xDelegate }); if (xDelegateSignature) { delegateHeaders.push({ name: HEADER_DELEGATE_SIGNATURE, value: xDelegateSignature }); } if (xEip712DelegateSignature) { delegateHeaders.push({ name: HEADER_EIP712_DELEGATE_SIGNATURE, value: xEip712DelegateSignature }); } } const clonedEthereum = new FireblocksWeb3Provider( ethereum.config ); const authHeaders = await this.baseProvider.getAuthHeaders( this, payload ); const allHeaders = [...delegateHeaders]; for (const [key, value] of Object.entries(authHeaders)) { allHeaders.push({ name: key, value }); } ; clonedEthereum.headers = allHeaders; log2("Sending request with auth headers"); return originalSend.call(clonedEthereum, payload, callback); } const result = await originalSend.call(ethereum, payload, callback); return result; })(); }; } clone() { log2("Cloning SilentDataRollupFireblocksProvider"); const newFireblocksProvider = new FireblocksWeb3Provider( this.ethereum.config ); const clonedProvider = new _SilentDataRollupFireblocksProvider({ ethereum: newFireblocksProvider, network: this.network, options: this._options, config: this.config }); log2("SilentDataRollupFireblocksProvider cloned successfully"); return clonedProvider; } }; export { DEBUG_NAMESPACE_SILENTDATA_INTERCEPTOR, EIP712Domain, SilentDataRollupFireblocksProvider };