@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 +106 -0
- package/dist/index.d.mts +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +384 -0
- package/dist/index.mjs +371 -0
- package/package.json +56 -0
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)
|
package/dist/index.d.mts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|