@appliedblockchain/silentdatarollup-ethers-provider-fireblocks
Version:
Ethers.js provider for Silent Data with Fireblocks integration
359 lines (355 loc) • 12.5 kB
JavaScript
// 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
};