@appliedblockchain/silentdatarollup-hardhat-plugin-fireblocks 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # Silent Data [Rollup] Providers - Hardhat Plugin Fireblocks Package
2
+
3
+ ## Table of Contents
4
+
5
+ - [Introduction](#introduction)
6
+ - [Prerequisites](#prerequisites)
7
+ - [Integration](#integration)
8
+ - [Hardhat Integration](#hardhat-integration)
9
+ - [Installing Hardhat Integration Dependencies](#installing-hardhat-integration-dependencies)
10
+ - [Hardhat Integration Example](#hardhat-integration-example)
11
+ - [Troubleshooting](#troubleshooting)
12
+ - [License](#license)
13
+ - [Additional Resources](#additional-resources)
14
+
15
+ ## Introduction
16
+
17
+ Custom providers for integrating Silent Data [Rollup] with Hardhat and Fireblocks.
18
+
19
+ ## Prerequisites
20
+
21
+ - Node.js (version 18 or higher)
22
+ - Hardhat v2
23
+ - npm
24
+ - Basic knowledge of Ethereum and smart contracts
25
+
26
+ ## Integration
27
+
28
+ ### Hardhat Integration
29
+
30
+ #### Installing Hardhat Integration Dependencies
31
+
32
+ ```bash
33
+ npm install @appliedblockchain/silentdatarollup-core @appliedblockchain/silentdatarollup-hardhat-plugin-fireblocks @nomicfoundation/hardhat-ignition-ethers@0.15.7
34
+ ```
35
+
36
+ #### Hardhat Integration Example
37
+
38
+ To integrate the Silent Data [Rollup] Provider with Hardhat, you need to configure your Silent Data network in the `hardhat.config.ts` file. Below is an example of how to set it up, and note that a `silentdata` property is needed on the network config to enable it. This property can be an empty object to apply defaults, or you can specify the configurations.
39
+
40
+ ```typescript
41
+ import {
42
+ ApiBaseUrl,
43
+ ASSETS,
44
+ ChainId,
45
+ } from '@fireblocks/fireblocks-web3-provider'
46
+ import '@nomicfoundation/hardhat-ignition-ethers'
47
+ import '@appliedblockchain/silentdatarollup-hardhat-plugin-fireblocks'
48
+ import { SilentDataSignatureTypeRollupProvider } from '@appliedblockchain/silentdatarollup-core'
49
+
50
+ const RPC_URL = 'SILENT_DATA_ROLLUP_RPC_URL'
51
+ const FIREBLOCKS_API_KEY = 'FIREBLOCKS_API_KEY'
52
+
53
+ const fireblocksConfig = {
54
+ privateKey: 'FIREBLOCKS_PATH_TO_PRIVATE_KEY',
55
+ apiKey: FIREBLOCKS_API_KEY,
56
+ assetId: ASSETS[ChainId.SEPOLIA].assetId,
57
+ vaultAccountIds: 'FIREBLOCKS_VAULT_ACCOUNT_ID', // Note: Currently, only one vault account can be passed to the configuration.
58
+ chainId: ChainId.SEPOLIA,
59
+ apiBaseUrl: ApiBaseUrl.Sandbox, // If using a sandbox workspace
60
+ rpcUrl: RPC_URL,
61
+ }
62
+
63
+ export default {
64
+ solidity: '0.8.21',
65
+ defaultNetwork: 'sdr',
66
+ networks: {
67
+ hardhat: {},
68
+ sdr: {
69
+ url: RPC_URL,
70
+ fireblocks: fireblocksConfig,
71
+ silentdata: {
72
+ authSignatureType: SignatureType.EIP712, // Optional, defaults to RAW
73
+ },
74
+ },
75
+ },
76
+ }
77
+ ```
78
+
79
+ Note: With the above configuration, you can deploy a contract using Hardhat Ignition. For a detailed example, including a sample contract and an Ignition module, please refer to the [Hardhat Ignition Getting Started Guide](https://hardhat.org/ignition/docs/getting-started).
80
+
81
+ To deploy your contract using Hardhat Ignition, run the following command:
82
+
83
+ ```bash
84
+ npx hardhat ignition deploy ignition/modules/Apollo.ts --network sdr
85
+ ```
86
+
87
+ ## Troubleshooting
88
+
89
+ If you encounter any issues, please check the following:
90
+
91
+ 1. Ensure you're using the correct RPC URL for your desired network.
92
+ 2. Verify that your private key is correctly set and has sufficient funds.
93
+ 3. Ensure that your token is still active on the SilentData AppChains dashboard.
94
+ 4. If using Fireblocks, validate your user and API keys.
95
+
96
+ ## License
97
+
98
+ This project is licensed under the [MIT License](LICENSE).
99
+
100
+ ## Additional Resources
101
+
102
+ - [Silent Data [Rollup] Documentation](https://docs.silentdata.com)
103
+ - [Fireblocks Hardhat Plugin](https://developers.fireblocks.com/reference/hardhat-plugin)
104
+ - [Fireblocks Developer Documentation](https://developers.fireblocks.com/api)
105
+ - [Fireblocks Web3 Provider](https://developers.fireblocks.com/reference/evm-web3-provider)
106
+ - [Hardhat Ignition](https://hardhat.org/hardhat-runner/plugins/nomiclabs-hardhat-ignition)
@@ -0,0 +1,16 @@
1
+ import { SignatureType } from '@appliedblockchain/silentdatarollup-core';
2
+
3
+ interface SilentdataNetworkConfig {
4
+ authSignatureType: SignatureType;
5
+ maxRetries?: number;
6
+ pollingInterval?: number;
7
+ }
8
+
9
+ declare module 'hardhat/types/config' {
10
+ interface HttpNetworkUserConfig {
11
+ silentdata?: SilentdataNetworkConfig;
12
+ }
13
+ interface HttpNetworkConfig {
14
+ silentdata?: SilentdataNetworkConfig;
15
+ }
16
+ }
@@ -0,0 +1,16 @@
1
+ import { SignatureType } from '@appliedblockchain/silentdatarollup-core';
2
+
3
+ interface SilentdataNetworkConfig {
4
+ authSignatureType: SignatureType;
5
+ maxRetries?: number;
6
+ pollingInterval?: number;
7
+ }
8
+
9
+ declare module 'hardhat/types/config' {
10
+ interface HttpNetworkUserConfig {
11
+ silentdata?: SilentdataNetworkConfig;
12
+ }
13
+ interface HttpNetworkConfig {
14
+ silentdata?: SilentdataNetworkConfig;
15
+ }
16
+ }
package/dist/index.js ADDED
@@ -0,0 +1,384 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (let key of __getOwnPropNames(from))
11
+ if (!__hasOwnProp.call(to, key) && key !== except)
12
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
+ }
14
+ return to;
15
+ };
16
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
+ // If the importer is in node compatibility mode or this is not an ESM
18
+ // file that has been converted to a CommonJS file using a Babel-
19
+ // compatible transform (i.e. "__esModule" has not been set), then set
20
+ // "default" to the CommonJS "module.exports" for node compatibility.
21
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
+ mod
23
+ ));
24
+
25
+ // src/type-extensions.ts
26
+ var import_config = require("hardhat/types/config");
27
+
28
+ // src/index.ts
29
+ var import_hardhat_fireblocks = require("@fireblocks/hardhat-fireblocks");
30
+ var import_debug2 = __toESM(require("debug"));
31
+ var import_config2 = require("hardhat/config");
32
+
33
+ // src/constants.ts
34
+ var import_silentdatarollup_core = require("@appliedblockchain/silentdatarollup-core");
35
+ var SIGN_RPC_METHODS = [...import_silentdatarollup_core.SIGN_RPC_METHODS, "eth_call"];
36
+ var DEBUG_NAMESPACE = "silentdata:fireblocks";
37
+ var DEFAULT_MAX_RETRIES = 25;
38
+ var DEFAULT_POLLING_INTERVAL = 2e3;
39
+
40
+ // src/provider.ts
41
+ var import_silentdatarollup_core2 = require("@appliedblockchain/silentdatarollup-core");
42
+ var import_fireblocks_web3_provider = require("@fireblocks/fireblocks-web3-provider");
43
+ var import_debug = __toESM(require("debug"));
44
+ var import_ethers = require("ethers");
45
+ var import_fireblocks_sdk = require("fireblocks-sdk");
46
+ var import_plugins = require("hardhat/plugins");
47
+ var log = (0, import_debug.default)(DEBUG_NAMESPACE);
48
+ var SilentDataFireblocksSigner = class extends import_plugins.ProviderWrapper {
49
+ constructor(provider, config) {
50
+ super(provider);
51
+ this.lastNonce = {};
52
+ log("SilentDataFireblocksSigner initialized");
53
+ const fireblocksWeb3Provider = provider._provider._wrappedProvider._wrappedProvider._fireblocksWeb3Provider;
54
+ this.setupInterceptor(fireblocksWeb3Provider);
55
+ this._fireblocksWeb3Provider = fireblocksWeb3Provider;
56
+ this.config = {
57
+ ...config,
58
+ authSignatureType: config?.authSignatureType ?? import_silentdatarollup_core2.SignatureType.Raw
59
+ };
60
+ this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
61
+ this.pollingInterval = config.pollingInterval ?? DEFAULT_POLLING_INTERVAL;
62
+ this.baseProvider = new import_silentdatarollup_core2.SilentDataRollupBase(config);
63
+ }
64
+ async request(args) {
65
+ log(".request()", JSON.stringify(args, null, 2));
66
+ const payload = {
67
+ jsonrpc: "2.0",
68
+ ...args,
69
+ id: Math.floor(Math.random() * 1e10)
70
+ };
71
+ if (args.method === "eth_sendTransaction") {
72
+ return this.sendTransaction(payload);
73
+ }
74
+ const result = await new Promise((resolve, reject) => {
75
+ ;
76
+ this._fireblocksWeb3Provider.send(
77
+ payload,
78
+ (error, response) => {
79
+ if (error) {
80
+ reject(error);
81
+ } else {
82
+ resolve(response.result);
83
+ }
84
+ }
85
+ );
86
+ });
87
+ return result;
88
+ }
89
+ send(method, params) {
90
+ log("Provider .send() method called:", method);
91
+ return this.request({ method, params });
92
+ }
93
+ sendAsync(payload, callback) {
94
+ log("Provider .sendAsync() method called:", payload.method);
95
+ this.request(payload).then(callback).catch(callback);
96
+ }
97
+ setupInterceptor(provider) {
98
+ log("Setting up silent data interceptor for Fireblocks");
99
+ const originalSend = provider.send;
100
+ provider.send = async (payload, callback) => {
101
+ ;
102
+ (async () => {
103
+ const requiresAuthHeaders = SIGN_RPC_METHODS.includes(payload.method);
104
+ if (payload.method === "eth_sendTransaction") {
105
+ try {
106
+ const result2 = await this.sendTransaction(payload);
107
+ callback(null, result2);
108
+ } catch (error) {
109
+ callback(error, null);
110
+ }
111
+ return;
112
+ }
113
+ log("Intercepted send method:", JSON.stringify(payload, null, 2));
114
+ if (requiresAuthHeaders) {
115
+ log("Request requires auth headers");
116
+ const clonedEthereum = new import_fireblocks_web3_provider.FireblocksWeb3Provider(
117
+ provider.config
118
+ );
119
+ const authHeaders = await this.getAuthHeaders(payload);
120
+ const allHeaders = [];
121
+ for (const [key, value] of Object.entries(authHeaders)) {
122
+ allHeaders.push({ name: key, value });
123
+ }
124
+ ;
125
+ clonedEthereum.headers = allHeaders;
126
+ log("Auth headers set for cloned FireblocksWeb3Provider provider");
127
+ return originalSend.call(clonedEthereum, payload, callback);
128
+ }
129
+ const result = await originalSend.call(provider, payload, callback);
130
+ return result;
131
+ })();
132
+ };
133
+ }
134
+ async signMessage(content, operation, type) {
135
+ const vaultAccountId = this._fireblocksWeb3Provider.vaultAccountIds[0]?.toString();
136
+ const transactionArguments = {
137
+ operation,
138
+ assetId: this._fireblocksWeb3Provider.assetId,
139
+ source: {
140
+ type: import_fireblocks_sdk.PeerType.VAULT_ACCOUNT,
141
+ id: vaultAccountId
142
+ },
143
+ extraParameters: {
144
+ rawMessageData: {
145
+ messages: [
146
+ {
147
+ content,
148
+ ...type ? { type } : {}
149
+ }
150
+ ]
151
+ }
152
+ }
153
+ };
154
+ log("Creating transaction", JSON.stringify(transactionArguments, null, 2));
155
+ const txInfo = await this._fireblocksWeb3Provider.createTransaction(transactionArguments);
156
+ const sig = txInfo.signedMessages[0].signature;
157
+ const v = 27 + sig.v;
158
+ return "0x" + sig.r + sig.s + v.toString(16);
159
+ }
160
+ async getAuthHeaders(payload) {
161
+ log("Getting auth headers for method:", JSON.stringify(payload, null, 2));
162
+ const requestId = this._wrappedProvider._provider._wrappedProvider._wrappedProvider._wrappedProvider._nextRequestId;
163
+ const rpcRequest = {
164
+ jsonrpc: "2.0",
165
+ ...payload,
166
+ id: requestId
167
+ };
168
+ const xTimestamp = (/* @__PURE__ */ new Date()).toISOString();
169
+ const headers = {
170
+ [import_silentdatarollup_core2.HEADER_TIMESTAMP]: xTimestamp
171
+ };
172
+ const signatureType = this.config?.authSignatureType ?? import_silentdatarollup_core2.SignatureType.Raw;
173
+ let content;
174
+ switch (signatureType) {
175
+ case import_silentdatarollup_core2.SignatureType.Raw:
176
+ const preparedMessage = this.baseProvider.prepareSignatureMessage(
177
+ rpcRequest,
178
+ xTimestamp
179
+ );
180
+ content = (0, import_ethers.hashMessage)(preparedMessage).slice(2);
181
+ break;
182
+ case import_silentdatarollup_core2.SignatureType.EIP712:
183
+ const types = (0, import_silentdatarollup_core2.getAuthEIP721Types)(payload);
184
+ const message = this.baseProvider.prepareSignatureTypedData(
185
+ payload,
186
+ xTimestamp
187
+ );
188
+ content = {
189
+ types: {
190
+ EIP712Domain: [
191
+ { name: "name", type: "string" },
192
+ { name: "version", type: "string" }
193
+ ],
194
+ ...types
195
+ },
196
+ primaryType: "Call",
197
+ domain: import_silentdatarollup_core2.eip721Domain,
198
+ message
199
+ };
200
+ break;
201
+ default:
202
+ throw new Error(`Unsupported signature type: ${signatureType}`);
203
+ }
204
+ const signature = await this.signMessage(
205
+ content,
206
+ signatureType === import_silentdatarollup_core2.SignatureType.Raw ? import_fireblocks_sdk.TransactionOperation.RAW : import_fireblocks_sdk.TransactionOperation.TYPED_MESSAGE,
207
+ signatureType
208
+ );
209
+ headers[signatureType === import_silentdatarollup_core2.SignatureType.Raw ? import_silentdatarollup_core2.HEADER_SIGNATURE : import_silentdatarollup_core2.HEADER_EIP712_SIGNATURE] = signature;
210
+ log("Auth headers generated successfully");
211
+ return headers;
212
+ }
213
+ /**
214
+ * Manages and returns the next available nonce for a given address.
215
+ *
216
+ * This method implements a local nonce management system to handle concurrent
217
+ * transactions and potential network delays. It's necessary because:
218
+ * 1. Multiple transactions can be initiated before earlier ones are confirmed.
219
+ * 2. We need to ensure each transaction uses a unique, incrementing nonce.
220
+ *
221
+ * The method works by:
222
+ * - Tracking the last used nonce for each address.
223
+ * - Comparing it with the current network nonce.
224
+ * - Always returning a nonce higher than both the network nonce and the last used nonce.
225
+ *
226
+ * This approach helps prevent nonce conflicts and ensures transactions can be
227
+ * sent in rapid succession without waiting for network confirmation.
228
+ *
229
+ * @param address - The Ethereum address for which to get the next nonce.
230
+ * @returns A Promise that resolves to the next available nonce as a number.
231
+ */
232
+ async getNextNonce(address) {
233
+ try {
234
+ log("Getting next nonce for address:", address);
235
+ const currentNonce = await this._wrappedProvider.request({
236
+ method: "eth_getTransactionCount",
237
+ params: [address, "latest"]
238
+ });
239
+ const currentNonceNumber = parseInt(currentNonce, 16);
240
+ log("Current nonce from provider:", currentNonceNumber);
241
+ this.lastNonce[address] = Math.max(
242
+ this.lastNonce[address] || 0,
243
+ currentNonceNumber
244
+ );
245
+ this.lastNonce[address]++;
246
+ const nextNonce = this.lastNonce[address] - 1;
247
+ log("Next nonce to be used:", nextNonce);
248
+ return nextNonce;
249
+ } catch (error) {
250
+ log("Error fetching nonce:", error);
251
+ throw new Error("Failed to get next nonce");
252
+ }
253
+ }
254
+ async getTransactionParams(payload) {
255
+ const from = payload.params[0].from;
256
+ const nonce = await this.getNextNonce(from);
257
+ log("Using nonce:", nonce);
258
+ const chainId = await this.getChainId();
259
+ log("Chain ID:", chainId);
260
+ const [maxPriorityFeePerGas, maxFeePerGas] = await this.getFeeData();
261
+ log("Max Priority Fee Per Gas:", maxPriorityFeePerGas.toString());
262
+ log("Max Fee Per Gas:", maxFeePerGas.toString());
263
+ const gasLimit = await this.estimateGasLimit(payload.params[0]);
264
+ log("Estimated gas limit:", gasLimit);
265
+ return {
266
+ type: 2,
267
+ chainId,
268
+ nonce,
269
+ to: payload.params[0].to,
270
+ maxFeePerGas: maxFeePerGas.toString(),
271
+ maxPriorityFeePerGas: maxPriorityFeePerGas.toString(),
272
+ gasLimit,
273
+ data: payload.params[0].data,
274
+ value: payload.params[0].value
275
+ };
276
+ }
277
+ async getChainId() {
278
+ const networkResult = await this._wrappedProvider.request({
279
+ method: "eth_chainId",
280
+ params: []
281
+ });
282
+ return parseInt(networkResult, 16);
283
+ }
284
+ async getFeeData() {
285
+ const [maxPriorityFeePerGasHex, baseFeePerGasHex] = await Promise.all([
286
+ this._wrappedProvider.request({
287
+ method: "eth_maxPriorityFeePerGas",
288
+ params: []
289
+ }),
290
+ this._wrappedProvider.request({
291
+ method: "eth_getBlockByNumber",
292
+ params: ["latest", false]
293
+ }).then((block) => block.baseFeePerGas)
294
+ ]);
295
+ const maxPriorityFeePerGas = BigInt(maxPriorityFeePerGasHex);
296
+ const baseFeePerGas = BigInt(baseFeePerGasHex);
297
+ const maxFeePerGas = maxPriorityFeePerGas + baseFeePerGas * BigInt(2);
298
+ return [maxPriorityFeePerGas, maxFeePerGas];
299
+ }
300
+ async estimateGasLimit(txParams) {
301
+ const gasLimitHex = await this._wrappedProvider.request({
302
+ method: "eth_estimateGas",
303
+ params: [txParams]
304
+ });
305
+ return parseInt(gasLimitHex, 16);
306
+ }
307
+ async waitForTransaction(txHash) {
308
+ for (let i = 0; i < this.maxRetries; i++) {
309
+ const receipt = await this._wrappedProvider.request({
310
+ method: "eth_getTransactionReceipt",
311
+ params: [txHash]
312
+ });
313
+ if (receipt) {
314
+ log("Transaction mined, receipt:", JSON.stringify(receipt, null, 2));
315
+ if (receipt.status !== "0x1") {
316
+ log("Transaction failed", receipt);
317
+ throw new Error("Transaction failed");
318
+ }
319
+ return receipt;
320
+ }
321
+ await new Promise((resolve) => setTimeout(resolve, this.pollingInterval));
322
+ }
323
+ throw new Error("Transaction was not mined within the expected timeframe");
324
+ }
325
+ async sendTransaction(payload) {
326
+ log("Starting sendTransaction");
327
+ if (payload.method !== "eth_sendTransaction") {
328
+ log("Not an eth_sendTransaction method, skipping");
329
+ throw new Error("Not an eth_sendTransaction method.");
330
+ }
331
+ const txParams = await this.getTransactionParams(payload);
332
+ log("Transaction params:", txParams);
333
+ const tx = import_ethers.Transaction.from(txParams);
334
+ const txHash = (0, import_ethers.keccak256)(tx.unsignedSerialized);
335
+ const txHashHex = (0, import_ethers.hexlify)(txHash).slice(2);
336
+ log("Transaction hash to sign:", txHashHex);
337
+ log("Creating signature");
338
+ const signature = await this.createPersonalSignature(
339
+ txHashHex,
340
+ import_fireblocks_sdk.TransactionOperation.RAW
341
+ );
342
+ log("Signature created:", signature);
343
+ tx.signature = signature;
344
+ log("Signature added to transaction");
345
+ log("Broadcasting transaction");
346
+ const signedTx = tx.serialized;
347
+ try {
348
+ const txHash2 = await this._wrappedProvider.request({
349
+ method: "eth_sendRawTransaction",
350
+ params: [signedTx]
351
+ });
352
+ log("Transaction broadcasted, hash:", txHash2);
353
+ await this.waitForTransaction(txHash2);
354
+ return txHash2;
355
+ } catch (error) {
356
+ log("Transaction broadcast failed:", error);
357
+ throw error;
358
+ }
359
+ }
360
+ async createPersonalSignature(content, operation, type) {
361
+ return this.signMessage(content, operation, type);
362
+ }
363
+ };
364
+
365
+ // src/index.ts
366
+ var log2 = (0, import_debug2.default)(DEBUG_NAMESPACE);
367
+ (0, import_config2.extendEnvironment)((hre) => {
368
+ const networkConfig = hre.network.config;
369
+ if (!networkConfig.silentdata) {
370
+ log2("SilentData configuration not found. Returning the original provider.");
371
+ return;
372
+ }
373
+ if (!networkConfig.fireblocks) {
374
+ log2("Fireblocks configuration not found. Returning the original provider.");
375
+ return;
376
+ }
377
+ log2("SilentData and Fireblocks configuration found. Extending provider...");
378
+ const wrappedProvider = new SilentDataFireblocksSigner(
379
+ hre.network.provider,
380
+ networkConfig.silentdata
381
+ );
382
+ log2("Wrapped provider created");
383
+ hre.network.provider = wrappedProvider;
384
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,371 @@
1
+ // src/type-extensions.ts
2
+ import "hardhat/types/config";
3
+
4
+ // src/index.ts
5
+ import "@fireblocks/hardhat-fireblocks";
6
+ import debug2 from "debug";
7
+ import { extendEnvironment } from "hardhat/config";
8
+
9
+ // src/constants.ts
10
+ import { SIGN_RPC_METHODS as CORE_SIGN_RPC_METHODS } from "@appliedblockchain/silentdatarollup-core";
11
+ var SIGN_RPC_METHODS = [...CORE_SIGN_RPC_METHODS, "eth_call"];
12
+ var DEBUG_NAMESPACE = "silentdata:fireblocks";
13
+ var DEFAULT_MAX_RETRIES = 25;
14
+ var DEFAULT_POLLING_INTERVAL = 2e3;
15
+
16
+ // src/provider.ts
17
+ import {
18
+ eip721Domain,
19
+ getAuthEIP721Types,
20
+ HEADER_EIP712_SIGNATURE,
21
+ HEADER_SIGNATURE,
22
+ HEADER_TIMESTAMP,
23
+ SignatureType,
24
+ SilentDataRollupBase
25
+ } from "@appliedblockchain/silentdatarollup-core";
26
+ import { FireblocksWeb3Provider } from "@fireblocks/fireblocks-web3-provider";
27
+ import debug from "debug";
28
+ import { hashMessage, hexlify, keccak256, Transaction } from "ethers";
29
+ import {
30
+ PeerType,
31
+ TransactionOperation
32
+ } from "fireblocks-sdk";
33
+ import { ProviderWrapper } from "hardhat/plugins";
34
+ var log = debug(DEBUG_NAMESPACE);
35
+ var SilentDataFireblocksSigner = class extends ProviderWrapper {
36
+ constructor(provider, config) {
37
+ super(provider);
38
+ this.lastNonce = {};
39
+ log("SilentDataFireblocksSigner initialized");
40
+ const fireblocksWeb3Provider = provider._provider._wrappedProvider._wrappedProvider._fireblocksWeb3Provider;
41
+ this.setupInterceptor(fireblocksWeb3Provider);
42
+ this._fireblocksWeb3Provider = fireblocksWeb3Provider;
43
+ this.config = {
44
+ ...config,
45
+ authSignatureType: config?.authSignatureType ?? SignatureType.Raw
46
+ };
47
+ this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
48
+ this.pollingInterval = config.pollingInterval ?? DEFAULT_POLLING_INTERVAL;
49
+ this.baseProvider = new SilentDataRollupBase(config);
50
+ }
51
+ async request(args) {
52
+ log(".request()", JSON.stringify(args, null, 2));
53
+ const payload = {
54
+ jsonrpc: "2.0",
55
+ ...args,
56
+ id: Math.floor(Math.random() * 1e10)
57
+ };
58
+ if (args.method === "eth_sendTransaction") {
59
+ return this.sendTransaction(payload);
60
+ }
61
+ const result = await new Promise((resolve, reject) => {
62
+ ;
63
+ this._fireblocksWeb3Provider.send(
64
+ payload,
65
+ (error, response) => {
66
+ if (error) {
67
+ reject(error);
68
+ } else {
69
+ resolve(response.result);
70
+ }
71
+ }
72
+ );
73
+ });
74
+ return result;
75
+ }
76
+ send(method, params) {
77
+ log("Provider .send() method called:", method);
78
+ return this.request({ method, params });
79
+ }
80
+ sendAsync(payload, callback) {
81
+ log("Provider .sendAsync() method called:", payload.method);
82
+ this.request(payload).then(callback).catch(callback);
83
+ }
84
+ setupInterceptor(provider) {
85
+ log("Setting up silent data interceptor for Fireblocks");
86
+ const originalSend = provider.send;
87
+ provider.send = async (payload, callback) => {
88
+ ;
89
+ (async () => {
90
+ const requiresAuthHeaders = SIGN_RPC_METHODS.includes(payload.method);
91
+ if (payload.method === "eth_sendTransaction") {
92
+ try {
93
+ const result2 = await this.sendTransaction(payload);
94
+ callback(null, result2);
95
+ } catch (error) {
96
+ callback(error, null);
97
+ }
98
+ return;
99
+ }
100
+ log("Intercepted send method:", JSON.stringify(payload, null, 2));
101
+ if (requiresAuthHeaders) {
102
+ log("Request requires auth headers");
103
+ const clonedEthereum = new FireblocksWeb3Provider(
104
+ provider.config
105
+ );
106
+ const authHeaders = await this.getAuthHeaders(payload);
107
+ const allHeaders = [];
108
+ for (const [key, value] of Object.entries(authHeaders)) {
109
+ allHeaders.push({ name: key, value });
110
+ }
111
+ ;
112
+ clonedEthereum.headers = allHeaders;
113
+ log("Auth headers set for cloned FireblocksWeb3Provider provider");
114
+ return originalSend.call(clonedEthereum, payload, callback);
115
+ }
116
+ const result = await originalSend.call(provider, payload, callback);
117
+ return result;
118
+ })();
119
+ };
120
+ }
121
+ async signMessage(content, operation, type) {
122
+ const vaultAccountId = this._fireblocksWeb3Provider.vaultAccountIds[0]?.toString();
123
+ const transactionArguments = {
124
+ operation,
125
+ assetId: this._fireblocksWeb3Provider.assetId,
126
+ source: {
127
+ type: PeerType.VAULT_ACCOUNT,
128
+ id: vaultAccountId
129
+ },
130
+ extraParameters: {
131
+ rawMessageData: {
132
+ messages: [
133
+ {
134
+ content,
135
+ ...type ? { type } : {}
136
+ }
137
+ ]
138
+ }
139
+ }
140
+ };
141
+ log("Creating transaction", JSON.stringify(transactionArguments, null, 2));
142
+ const txInfo = await this._fireblocksWeb3Provider.createTransaction(transactionArguments);
143
+ const sig = txInfo.signedMessages[0].signature;
144
+ const v = 27 + sig.v;
145
+ return "0x" + sig.r + sig.s + v.toString(16);
146
+ }
147
+ async getAuthHeaders(payload) {
148
+ log("Getting auth headers for method:", JSON.stringify(payload, null, 2));
149
+ const requestId = this._wrappedProvider._provider._wrappedProvider._wrappedProvider._wrappedProvider._nextRequestId;
150
+ const rpcRequest = {
151
+ jsonrpc: "2.0",
152
+ ...payload,
153
+ id: requestId
154
+ };
155
+ const xTimestamp = (/* @__PURE__ */ new Date()).toISOString();
156
+ const headers = {
157
+ [HEADER_TIMESTAMP]: xTimestamp
158
+ };
159
+ const signatureType = this.config?.authSignatureType ?? SignatureType.Raw;
160
+ let content;
161
+ switch (signatureType) {
162
+ case SignatureType.Raw:
163
+ const preparedMessage = this.baseProvider.prepareSignatureMessage(
164
+ rpcRequest,
165
+ xTimestamp
166
+ );
167
+ content = hashMessage(preparedMessage).slice(2);
168
+ break;
169
+ case SignatureType.EIP712:
170
+ const types = getAuthEIP721Types(payload);
171
+ const message = this.baseProvider.prepareSignatureTypedData(
172
+ payload,
173
+ xTimestamp
174
+ );
175
+ content = {
176
+ types: {
177
+ EIP712Domain: [
178
+ { name: "name", type: "string" },
179
+ { name: "version", type: "string" }
180
+ ],
181
+ ...types
182
+ },
183
+ primaryType: "Call",
184
+ domain: eip721Domain,
185
+ message
186
+ };
187
+ break;
188
+ default:
189
+ throw new Error(`Unsupported signature type: ${signatureType}`);
190
+ }
191
+ const signature = await this.signMessage(
192
+ content,
193
+ signatureType === SignatureType.Raw ? TransactionOperation.RAW : TransactionOperation.TYPED_MESSAGE,
194
+ signatureType
195
+ );
196
+ headers[signatureType === SignatureType.Raw ? HEADER_SIGNATURE : HEADER_EIP712_SIGNATURE] = signature;
197
+ log("Auth headers generated successfully");
198
+ return headers;
199
+ }
200
+ /**
201
+ * Manages and returns the next available nonce for a given address.
202
+ *
203
+ * This method implements a local nonce management system to handle concurrent
204
+ * transactions and potential network delays. It's necessary because:
205
+ * 1. Multiple transactions can be initiated before earlier ones are confirmed.
206
+ * 2. We need to ensure each transaction uses a unique, incrementing nonce.
207
+ *
208
+ * The method works by:
209
+ * - Tracking the last used nonce for each address.
210
+ * - Comparing it with the current network nonce.
211
+ * - Always returning a nonce higher than both the network nonce and the last used nonce.
212
+ *
213
+ * This approach helps prevent nonce conflicts and ensures transactions can be
214
+ * sent in rapid succession without waiting for network confirmation.
215
+ *
216
+ * @param address - The Ethereum address for which to get the next nonce.
217
+ * @returns A Promise that resolves to the next available nonce as a number.
218
+ */
219
+ async getNextNonce(address) {
220
+ try {
221
+ log("Getting next nonce for address:", address);
222
+ const currentNonce = await this._wrappedProvider.request({
223
+ method: "eth_getTransactionCount",
224
+ params: [address, "latest"]
225
+ });
226
+ const currentNonceNumber = parseInt(currentNonce, 16);
227
+ log("Current nonce from provider:", currentNonceNumber);
228
+ this.lastNonce[address] = Math.max(
229
+ this.lastNonce[address] || 0,
230
+ currentNonceNumber
231
+ );
232
+ this.lastNonce[address]++;
233
+ const nextNonce = this.lastNonce[address] - 1;
234
+ log("Next nonce to be used:", nextNonce);
235
+ return nextNonce;
236
+ } catch (error) {
237
+ log("Error fetching nonce:", error);
238
+ throw new Error("Failed to get next nonce");
239
+ }
240
+ }
241
+ async getTransactionParams(payload) {
242
+ const from = payload.params[0].from;
243
+ const nonce = await this.getNextNonce(from);
244
+ log("Using nonce:", nonce);
245
+ const chainId = await this.getChainId();
246
+ log("Chain ID:", chainId);
247
+ const [maxPriorityFeePerGas, maxFeePerGas] = await this.getFeeData();
248
+ log("Max Priority Fee Per Gas:", maxPriorityFeePerGas.toString());
249
+ log("Max Fee Per Gas:", maxFeePerGas.toString());
250
+ const gasLimit = await this.estimateGasLimit(payload.params[0]);
251
+ log("Estimated gas limit:", gasLimit);
252
+ return {
253
+ type: 2,
254
+ chainId,
255
+ nonce,
256
+ to: payload.params[0].to,
257
+ maxFeePerGas: maxFeePerGas.toString(),
258
+ maxPriorityFeePerGas: maxPriorityFeePerGas.toString(),
259
+ gasLimit,
260
+ data: payload.params[0].data,
261
+ value: payload.params[0].value
262
+ };
263
+ }
264
+ async getChainId() {
265
+ const networkResult = await this._wrappedProvider.request({
266
+ method: "eth_chainId",
267
+ params: []
268
+ });
269
+ return parseInt(networkResult, 16);
270
+ }
271
+ async getFeeData() {
272
+ const [maxPriorityFeePerGasHex, baseFeePerGasHex] = await Promise.all([
273
+ this._wrappedProvider.request({
274
+ method: "eth_maxPriorityFeePerGas",
275
+ params: []
276
+ }),
277
+ this._wrappedProvider.request({
278
+ method: "eth_getBlockByNumber",
279
+ params: ["latest", false]
280
+ }).then((block) => block.baseFeePerGas)
281
+ ]);
282
+ const maxPriorityFeePerGas = BigInt(maxPriorityFeePerGasHex);
283
+ const baseFeePerGas = BigInt(baseFeePerGasHex);
284
+ const maxFeePerGas = maxPriorityFeePerGas + baseFeePerGas * BigInt(2);
285
+ return [maxPriorityFeePerGas, maxFeePerGas];
286
+ }
287
+ async estimateGasLimit(txParams) {
288
+ const gasLimitHex = await this._wrappedProvider.request({
289
+ method: "eth_estimateGas",
290
+ params: [txParams]
291
+ });
292
+ return parseInt(gasLimitHex, 16);
293
+ }
294
+ async waitForTransaction(txHash) {
295
+ for (let i = 0; i < this.maxRetries; i++) {
296
+ const receipt = await this._wrappedProvider.request({
297
+ method: "eth_getTransactionReceipt",
298
+ params: [txHash]
299
+ });
300
+ if (receipt) {
301
+ log("Transaction mined, receipt:", JSON.stringify(receipt, null, 2));
302
+ if (receipt.status !== "0x1") {
303
+ log("Transaction failed", receipt);
304
+ throw new Error("Transaction failed");
305
+ }
306
+ return receipt;
307
+ }
308
+ await new Promise((resolve) => setTimeout(resolve, this.pollingInterval));
309
+ }
310
+ throw new Error("Transaction was not mined within the expected timeframe");
311
+ }
312
+ async sendTransaction(payload) {
313
+ log("Starting sendTransaction");
314
+ if (payload.method !== "eth_sendTransaction") {
315
+ log("Not an eth_sendTransaction method, skipping");
316
+ throw new Error("Not an eth_sendTransaction method.");
317
+ }
318
+ const txParams = await this.getTransactionParams(payload);
319
+ log("Transaction params:", txParams);
320
+ const tx = Transaction.from(txParams);
321
+ const txHash = keccak256(tx.unsignedSerialized);
322
+ const txHashHex = hexlify(txHash).slice(2);
323
+ log("Transaction hash to sign:", txHashHex);
324
+ log("Creating signature");
325
+ const signature = await this.createPersonalSignature(
326
+ txHashHex,
327
+ TransactionOperation.RAW
328
+ );
329
+ log("Signature created:", signature);
330
+ tx.signature = signature;
331
+ log("Signature added to transaction");
332
+ log("Broadcasting transaction");
333
+ const signedTx = tx.serialized;
334
+ try {
335
+ const txHash2 = await this._wrappedProvider.request({
336
+ method: "eth_sendRawTransaction",
337
+ params: [signedTx]
338
+ });
339
+ log("Transaction broadcasted, hash:", txHash2);
340
+ await this.waitForTransaction(txHash2);
341
+ return txHash2;
342
+ } catch (error) {
343
+ log("Transaction broadcast failed:", error);
344
+ throw error;
345
+ }
346
+ }
347
+ async createPersonalSignature(content, operation, type) {
348
+ return this.signMessage(content, operation, type);
349
+ }
350
+ };
351
+
352
+ // src/index.ts
353
+ var log2 = debug2(DEBUG_NAMESPACE);
354
+ extendEnvironment((hre) => {
355
+ const networkConfig = hre.network.config;
356
+ if (!networkConfig.silentdata) {
357
+ log2("SilentData configuration not found. Returning the original provider.");
358
+ return;
359
+ }
360
+ if (!networkConfig.fireblocks) {
361
+ log2("Fireblocks configuration not found. Returning the original provider.");
362
+ return;
363
+ }
364
+ log2("SilentData and Fireblocks configuration found. Extending provider...");
365
+ const wrappedProvider = new SilentDataFireblocksSigner(
366
+ hre.network.provider,
367
+ networkConfig.silentdata
368
+ );
369
+ log2("Wrapped provider created");
370
+ hre.network.provider = wrappedProvider;
371
+ });
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@appliedblockchain/silentdatarollup-hardhat-plugin-fireblocks",
3
+ "version": "1.0.0",
4
+ "description": "Hardhat plugin for Silent Data [Rollup] with Fireblocks integration",
5
+ "author": "Applied Blockchain",
6
+ "homepage": "https://github.com/appliedblockchain/silent-data-rollup-providers#readme",
7
+ "keywords": [
8
+ "ethereum",
9
+ "provider",
10
+ "silentdata",
11
+ "rollup",
12
+ "hardhat",
13
+ "fireblocks"
14
+ ],
15
+ "license": "MIT",
16
+ "repository": "https://github.com/appliedblockchain/silent-data-rollup-providers",
17
+ "main": "dist/index.js",
18
+ "module": "dist/index.mjs",
19
+ "types": "dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "import": "./dist/index.mjs",
23
+ "require": "./dist/index.js"
24
+ }
25
+ },
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "scripts": {
30
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
31
+ "check-exports": "attw --pack . --profile node16",
32
+ "prepack": "npm run build"
33
+ },
34
+ "dependencies": {
35
+ "@appliedblockchain/silentdatarollup-core": "1.0.0",
36
+ "@fireblocks/fireblocks-web3-provider": "1.3.8",
37
+ "@fireblocks/hardhat-fireblocks": "1.3.5",
38
+ "debug": "4.3.7",
39
+ "ethers": "6.13.2",
40
+ "fireblocks-sdk": "5.31.2"
41
+ },
42
+ "devDependencies": {
43
+ "@types/debug": "4.1.12",
44
+ "@types/node": "22.5.4",
45
+ "hardhat": "2.22.10",
46
+ "ts-node": "10.9.2",
47
+ "typescript": "5.6.2"
48
+ },
49
+ "peerDependencies": {
50
+ "@fireblocks/hardhat-fireblocks": "1.3.5",
51
+ "hardhat": "2.22.10"
52
+ },
53
+ "engines": {
54
+ "node": ">=18.0.0"
55
+ }
56
+ }