@appliedblockchain/silentdatarollup-ethers-provider-fireblocks
Version:
Ethers.js provider for Silent Data with Fireblocks integration
375 lines (369 loc) • 15 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
DEBUG_NAMESPACE_SILENTDATA_INTERCEPTOR: () => DEBUG_NAMESPACE_SILENTDATA_INTERCEPTOR,
EIP712Domain: () => EIP712Domain,
SilentDataRollupFireblocksProvider: () => SilentDataRollupFireblocksProvider
});
module.exports = __toCommonJS(index_exports);
// src/constants.ts
var import_fireblocks_web3_provider = require("@fireblocks/fireblocks-web3-provider");
var DEBUG_NAMESPACE_SILENTDATA_INTERCEPTOR = `${import_fireblocks_web3_provider.DEBUG_NAMESPACE}:silentdata-interceptor`;
var EIP712Domain = [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
{ name: "chainId", type: "uint256" }
];
// src/provider.ts
var import_fireblocks_web3_provider2 = require("@fireblocks/fireblocks-web3-provider");
var import_silentdatarollup_core2 = require("@appliedblockchain/silentdatarollup-core");
var import_debug2 = __toESM(require("debug"));
var import_ethers2 = require("ethers");
var import_fireblocks_sdk2 = require("fireblocks-sdk");
// src/Base.ts
var import_silentdatarollup_core = require("@appliedblockchain/silentdatarollup-core");
var import_debug = __toESM(require("debug"));
var import_ethers = require("ethers");
var import_fireblocks_sdk = require("fireblocks-sdk");
var log = (0, import_debug.default)(import_silentdatarollup_core.DEBUG_NAMESPACE);
var SilentDataRollupFireblocksBase = class extends import_silentdatarollup_core.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: import_fireblocks_sdk.PeerType.VAULT_ACCOUNT,
id: vaultAccountId
},
extraParameters: {
rawMessageData: {
messages: [
{ content, ...type === import_silentdatarollup_core.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 = (0, import_ethers.hashMessage)(message).slice(2);
return this.createPersonalSignature(
provider,
hashedMessage,
import_fireblocks_sdk.TransactionOperation.RAW,
import_silentdatarollup_core.SignatureType.Raw
);
}
async signTypedDelegateHeader(provider, chainId, message, isSWC) {
void isSWC;
log("Signing typed delegate header. Message:", message);
const content = {
types: {
EIP712Domain,
...import_silentdatarollup_core.delegateEIP712Types
},
primaryType: "Ticket",
domain: { ...import_silentdatarollup_core.eip712Domain, chainId },
message
};
log("EIP712 content created:", content);
return this.createPersonalSignature(
provider,
content,
import_fireblocks_sdk.TransactionOperation.TYPED_MESSAGE,
import_silentdatarollup_core.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 = (0, import_ethers.hashMessage)(message).slice(2);
log("Hashed message:", hashedMessage);
signature = await this.createPersonalSignature(
provider,
hashedMessage,
import_fireblocks_sdk.TransactionOperation.RAW
);
}
log("Raw signature generated:", signature);
return signature;
}
async signTypedAuthHeader(provider, payload, timestamp, chainId, isSWC) {
void isSWC;
const types = (0, import_silentdatarollup_core.getAuthEIP712Types)(payload);
const message = this.prepareTypedData(payload, timestamp);
const content = {
types: {
EIP712Domain,
...types
},
primaryType: "Call",
domain: { ...import_silentdatarollup_core.eip712Domain, chainId },
message
};
log("EIP712 content created:", content);
let signature;
if (this.config.delegate) {
const delegateSigner = await this.getDelegateSigner(this);
const domain = { ...import_silentdatarollup_core.eip712Domain, chainId: chainId.toString() };
signature = await delegateSigner.signTypedData(
domain,
types,
message
);
} else {
signature = await this.createPersonalSignature(
provider,
content,
import_fireblocks_sdk.TransactionOperation.TYPED_MESSAGE,
import_silentdatarollup_core.SignatureType.EIP712
);
}
log("Typed signature generated:", signature);
return signature;
}
};
// src/provider.ts
var log2 = (0, import_debug2.default)(import_silentdatarollup_core2.DEBUG_NAMESPACE);
var SilentDataRollupFireblocksProvider = class _SilentDataRollupFireblocksProvider extends import_ethers2.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 ?? import_silentdatarollup_core2.SignatureType.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 = import_ethers2.Transaction.from(txParams);
const txHash = (0, import_ethers2.keccak256)(tx.unsignedSerialized);
const txHashHex = (0, import_ethers2.hexlify)(txHash).slice(2);
log2("Transaction hash to sign:", txHashHex);
log2("Creating signature");
const signature = await this.baseProvider.createPersonalSignature(
this,
txHashHex,
import_fireblocks_sdk2.TransactionOperation.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 = import_silentdatarollup_core2.SIGN_RPC_METHODS.includes(payload.method) || (0, import_silentdatarollup_core2.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 {
[import_silentdatarollup_core2.HEADER_DELEGATE]: xDelegate,
[import_silentdatarollup_core2.HEADER_DELEGATE_SIGNATURE]: xDelegateSignature,
[import_silentdatarollup_core2.HEADER_EIP712_DELEGATE_SIGNATURE]: xEip712DelegateSignature
} = await this.baseProvider.getDelegateHeaders(this);
delegateHeaders.push({ name: import_silentdatarollup_core2.HEADER_DELEGATE, value: xDelegate });
if (xDelegateSignature) {
delegateHeaders.push({
name: import_silentdatarollup_core2.HEADER_DELEGATE_SIGNATURE,
value: xDelegateSignature
});
}
if (xEip712DelegateSignature) {
delegateHeaders.push({
name: import_silentdatarollup_core2.HEADER_EIP712_DELEGATE_SIGNATURE,
value: xEip712DelegateSignature
});
}
}
const clonedEthereum = new import_fireblocks_web3_provider2.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 import_fireblocks_web3_provider2.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;
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
DEBUG_NAMESPACE_SILENTDATA_INTERCEPTOR,
EIP712Domain,
SilentDataRollupFireblocksProvider
});