gun-eth 1.2.2 → 1.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. package/README.md +101 -21
  2. package/index.js +270 -1
  3. package/package.json +1 -1
  4. package/gun-eth.js +0 -270
package/README.md CHANGED
@@ -4,43 +4,123 @@
4
4
 
5
5
  gun-eth is a plugin for GunDB that integrates Ethereum and Web3 functionality. This plugin extends GunDB's capabilities by allowing interaction with the Ethereum blockchain and providing cryptographic and signature management features.
6
6
 
7
- ## Key Features
7
+ SHINE Smart Contract deployed on Optimism Sepolia: [0x43D838b683F772F08f321E5FA265ad3e333BE9C2](https://sepolia-optimism.etherscan.io/address/0x43D838b683F772F08f321E5FA265ad3e333BE9C2)
8
8
 
9
- - Ethereum signature verification
10
- - Signature-based password generation
11
- - Signature creation using Ethereum providers (e.g., MetaMask)
9
+ Currently, the contract is deployed only on Optimism Sepolia. In the future, it will be deployed on multiple chains.
12
10
 
13
- ## Example
11
+ ### Key Features
14
12
 
15
- [gun-eth dapp](https://gun-eth.vercel.app/)
13
+ - **Ethereum Signature Verification**: Verify Ethereum signatures for messages.
14
+ - **Password Generation**: Generate secure passwords from Ethereum signatures.
15
+ - **Signature Creation**: Create Ethereum signatures for messages.
16
+ - **Encrypted Key Pair Management**: Create, store, and retrieve encrypted key pairs.
17
+ - **SHINE Implementation**: Implement the SHINE for data verification on the blockchain.
16
18
 
17
- ## Installation
19
+ ### How to install
18
20
 
19
21
  ```bash
20
22
  npm install gun-eth
21
23
  ```
22
24
 
23
- ## Usage
24
-
25
- ```javascript
26
- const Gun = require("gun");
27
- require("gun-eth");
25
+ ```bash
26
+ import gun from "gun";
27
+ import "gun-eth";
28
28
 
29
- const gun = Gun();
29
+ const gun = gun();
30
30
 
31
- // Now you can use the new features
31
+ await gun.generatePassword("YOUR_SIGNATURE");
32
32
  ```
33
33
 
34
- ## Main Functions
34
+ ### How to use
35
+
36
+ Learn more about Gun.js [here](https://gun.eco/docs/Getting-Started).
37
+
38
+ Learn more about plugin implementation [here](https://github.com/amark/gun/wiki/Adding-Methods-to-the-Gun-Chain#abstraction-layers).
39
+
40
+ ### Core Functions
41
+
42
+ - `verifySignature(message, signature)`: Verifies an Ethereum signature for a given message.
43
+
44
+ ```js
45
+ const recoveredAddress = await gun.verifySignature(message, signature);
46
+ ```
47
+
48
+ - `generatePassword(signature)`: Generates a password from an Ethereum signature.
49
+
50
+ ```js
51
+ const password = gun.generatePassword(signature);
52
+ ```
53
+
54
+ - `createSignature(message)`: Creates an Ethereum signature for a message.
55
+
56
+ ```js
57
+ const signature = await gun.createSignature(message);
58
+ ```
59
+
60
+ - `createAndStoreEncryptedPair(address, signature)`: Creates and stores an encrypted key pair.
35
61
 
36
- - `gun.verifySignature(message, signature)`: Verifies an Ethereum signature
37
- - `gun.generatePassword(signature)`: Generates a password based on a signature
38
- - `gun.createSignature(message)`: Creates a signature using an Ethereum provider
62
+ ```js
63
+ await gun.createAndStoreEncryptedPair(address, signature);
64
+ ```
65
+
66
+ - `getAndDecryptPair(address, signature)`: Retrieves and decrypts a stored key pair.
67
+
68
+ ```js
69
+ const decryptedPair = await gun.getAndDecryptPair(address, signature);
70
+ ```
71
+
72
+ - `shine(chain, nodeId, data, callback)`: Implements SHINE for data verification and storage on the blockchain.
73
+ ```js
74
+ gun.shine("optimismSepolia", nodeId, data, callback);
75
+ ```
76
+
77
+ ### SHINE
78
+
79
+ SHINE (Secure Hash Integrity Network Ethereum) provides a mechanism for verifying data integrity using Ethereum and Gun.js.
80
+
81
+ 1. **Data Storage**: When saving data, a content hash is generated and stored in both Gun.js and on the Ethereum blockchain.
82
+ 2. **Data Verification**: To verify data, the stored hash is compared with a hash generated from the data retrieved from Gun.js.
83
+ 3. **Blockchain Interaction**: The plugin interacts with an Ethereum smart contract to store and verify data hashes.
84
+
85
+ ### Usage Examples
86
+
87
+ #### Verifying Data by NodeId
88
+
89
+ ```js
90
+ const nodeId = "your-node-id-here";
91
+ gun.shine("optimismSepolia", nodeId, null, (ack) => {
92
+ if (ack.ok) {
93
+ console.log("Data verified on blockchain", ack);
94
+ console.log("Timestamp:", ack.timestamp);
95
+ console.log("Updater:", ack.updater);
96
+ console.log("Latest Record:", ack.latestRecord);
97
+ } else {
98
+ console.log("Data not verified or not found", ack);
99
+ }
100
+ });
101
+ ```
102
+
103
+ #### Storing New Data
104
+
105
+ ```js
106
+ const data = { message: "Hello, blockchain!" };
107
+ gun.shine("optimismSepolia", null, data, (ack) => {
108
+ if (ack.ok) {
109
+ console.log("Data stored on Gun.js and blockchain", ack);
110
+ console.log("New Node ID:", ack.nodeId);
111
+ console.log("Transaction Hash:", ack.txHash);
112
+ } else {
113
+ console.log("Error storing data", ack);
114
+ }
115
+ });
116
+ ```
39
117
 
40
- ## Dependencies
118
+ ### Security Considerations
41
119
 
42
- - gun: ^0.2020.1239
43
- - ethers: ^6.0.0
120
+ - Use a secure Ethereum provider (e.g., MetaMask) when interacting with functions that require signatures.
121
+ - Generated passwords and key pairs are sensitive. Handle them carefully and avoid exposing them.
122
+ - Keep Gun.js and Ethereum dependencies up to date for security.
123
+ - Be aware of gas costs associated with blockchain interactions when using SHINE.
44
124
 
45
125
  ## Contributing
46
126
 
package/index.js CHANGED
@@ -1 +1,270 @@
1
- import "./gun-eth.js";
1
+ /* const Gun = require("gun/gun");
2
+ const SEA = require("gun/sea");
3
+ const ethers = require("ethers");
4
+ const SHINE = require("./SHINE.json"); */
5
+
6
+ import Gun from "gun";
7
+ import SEA from "gun/sea";
8
+ import { ethers } from "ethers";
9
+
10
+ import SHINE from "./SHINE.json";
11
+
12
+ const SHINE_ABI = SHINE.abi;
13
+ const SHINE_OPTIMISM_SEPOLIA = SHINE.address;
14
+
15
+ let SHINE_CONTRACT_ADDRESS;
16
+
17
+ // Aggiungi il metodo alla catena di Gun
18
+ Gun.chain.verifySignature = async function (message, signature) {
19
+ try {
20
+ const recoveredAddress = ethers.verifyMessage(message, signature);
21
+ return recoveredAddress;
22
+ } catch (error) {
23
+ console.error("Errore durante la verifica della firma:", error);
24
+ return null;
25
+ }
26
+ };
27
+
28
+ Gun.chain.generatePassword = function (signature) {
29
+ try {
30
+ // Usa SHA-256 per derivare una password dalla firma
31
+ const hexSignature = ethers.hexlify(signature);
32
+ const hash = ethers.keccak256(hexSignature);
33
+
34
+ console.log("Password generata:", hash);
35
+ return hash;
36
+ } catch (error) {
37
+ console.error("Errore nella generazione della password:", error);
38
+ return null;
39
+ }
40
+ };
41
+
42
+ Gun.chain.createSignature = async function (message) {
43
+ try {
44
+ // Controlla se window.ethereum è disponibile (metamask o altro provider)
45
+ if (typeof window.ethereum !== "undefined") {
46
+ await window.ethereum.request({ method: "eth_requestAccounts" });
47
+ const provider = new ethers.BrowserProvider(window.ethereum);
48
+ const signer = await provider.getSigner();
49
+ const signature = await signer.signMessage(message);
50
+ console.log("Firma creata:", signature);
51
+ return signature;
52
+ } else {
53
+ throw new Error("Provider Ethereum non trovato");
54
+ }
55
+ } catch (error) {
56
+ console.error("Errore durante la creazione della firma:", error);
57
+ return null;
58
+ }
59
+ };
60
+
61
+ Gun.chain.createAndStoreEncryptedPair = async function (address, signature) {
62
+ try {
63
+ const gun = this;
64
+ const pair = await SEA.pair();
65
+ const encryptedPair = await SEA.encrypt(JSON.stringify(pair), signature);
66
+
67
+ await gun.get("users").get(address).put({ encryptedPair });
68
+ console.log("Pair crittografato e archiviato per:", address);
69
+ } catch (error) {
70
+ console.error(
71
+ "Errore durante la creazione e l'archiviazione del pair crittografato:",
72
+ error
73
+ );
74
+ }
75
+ };
76
+
77
+ Gun.chain.getAndDecryptPair = async function (address, signature) {
78
+ try {
79
+ const gun = this;
80
+ const encryptedData = await gun
81
+ .get("users")
82
+ .get(address)
83
+ .get("encryptedPair")
84
+ .then();
85
+ if (!encryptedData) {
86
+ throw new Error("Nessun dato crittografato trovato per questo indirizzo");
87
+ }
88
+
89
+ const decryptedPair = await SEA.decrypt(encryptedData, signature);
90
+
91
+ console.log(decryptedPair);
92
+ return decryptedPair;
93
+ } catch (error) {
94
+ console.error(
95
+ "Errore durante il recupero e la decrittazione del pair:",
96
+ error
97
+ );
98
+ return null;
99
+ }
100
+ };
101
+
102
+ Gun.chain.shine = function (chain, nodeId, data, callback) {
103
+ console.log("SHINE plugin called with:", { chain, nodeId, data });
104
+
105
+ if (typeof callback !== "function") {
106
+ console.error("Callback must be a function");
107
+ return this;
108
+ }
109
+
110
+ const gun = this;
111
+
112
+ // Seleziona l'indirizzo basato sulla catena
113
+ if (chain === "optimismSepolia") {
114
+ SHINE_CONTRACT_ADDRESS = SHINE_OPTIMISM_SEPOLIA;
115
+ } else {
116
+ throw new Error("Chain not supported");
117
+ }
118
+
119
+ // Funzione per ottenere il signer
120
+ const getSigner = async () => {
121
+ if (typeof window.ethereum !== "undefined") {
122
+ await window.ethereum.request({ method: "eth_requestAccounts" });
123
+ const provider = new ethers.BrowserProvider(window.ethereum);
124
+ return provider.getSigner();
125
+ } else {
126
+ throw new Error("Ethereum provider not found");
127
+ }
128
+ };
129
+
130
+ // Funzione per verificare on-chain
131
+ const verifyOnChain = async (nodeId, contentHash) => {
132
+ console.log("Verifying on chain:", { nodeId, contentHash });
133
+ const signer = await getSigner();
134
+ const contract = new ethers.Contract(
135
+ SHINE_CONTRACT_ADDRESS,
136
+ SHINE_ABI,
137
+ signer
138
+ );
139
+ const [isValid, timestamp, updater] = await contract.verifyData(
140
+ ethers.toUtf8Bytes(nodeId),
141
+ contentHash
142
+ );
143
+ console.log("Verification result:", { isValid, timestamp, updater });
144
+ return { isValid, timestamp, updater };
145
+ };
146
+
147
+ // Funzione per scrivere on-chain
148
+ const writeOnChain = async (nodeId, contentHash) => {
149
+ console.log("Writing on chain:", { nodeId, contentHash });
150
+ const signer = await getSigner();
151
+ const contract = new ethers.Contract(
152
+ SHINE_CONTRACT_ADDRESS,
153
+ SHINE_ABI,
154
+ signer
155
+ );
156
+ const tx = await contract.updateData(
157
+ ethers.toUtf8Bytes(nodeId),
158
+ contentHash
159
+ );
160
+ console.log("Transaction sent:", tx.hash);
161
+ const receipt = await tx.wait();
162
+ console.log("Transaction confirmed:", receipt);
163
+ return tx;
164
+ };
165
+
166
+ // Nuova funzione per ottenere l'ultimo record dalla blockchain
167
+ const getLatestRecord = async (nodeId) => {
168
+ const signer = await getSigner();
169
+ const contract = new ethers.Contract(
170
+ SHINE_CONTRACT_ADDRESS,
171
+ SHINE_ABI,
172
+ signer
173
+ );
174
+ const [contentHash, timestamp, updater] = await contract.getLatestRecord(
175
+ ethers.toUtf8Bytes(nodeId)
176
+ );
177
+ console.log("Latest record from blockchain:", {
178
+ nodeId,
179
+ contentHash,
180
+ timestamp,
181
+ updater,
182
+ });
183
+ return { contentHash, timestamp, updater };
184
+ };
185
+
186
+ // Processo SHINE
187
+ if (nodeId && !data) {
188
+ // Caso 1: Utente passa solo il nodo
189
+ gun.get(nodeId).once(async (existingData) => {
190
+ if (!existingData) {
191
+ if (callback) callback({ err: "Node not found in GunDB" });
192
+ return;
193
+ }
194
+
195
+ console.log("existingData", existingData);
196
+
197
+ // Usa il contentHash memorizzato invece di ricalcolarlo
198
+ const contentHash = existingData._contentHash;
199
+ console.log("contentHash", contentHash);
200
+
201
+ if (!contentHash) {
202
+ if (callback) callback({ err: "No content hash found for this node" });
203
+ return;
204
+ }
205
+
206
+ try {
207
+ const { isValid, timestamp, updater } = await verifyOnChain(
208
+ nodeId,
209
+ contentHash
210
+ );
211
+ const latestRecord = await getLatestRecord(nodeId);
212
+
213
+ if (isValid) {
214
+ if (callback)
215
+ callback({
216
+ ok: true,
217
+ message: "Data verified on blockchain",
218
+ timestamp,
219
+ updater,
220
+ latestRecord,
221
+ });
222
+ } else {
223
+ if (callback)
224
+ callback({
225
+ ok: false,
226
+ message: "Data not verified on blockchain",
227
+ latestRecord,
228
+ });
229
+ }
230
+ } catch (error) {
231
+ if (callback) callback({ err: error.message });
232
+ }
233
+ });
234
+ } else if (data && !nodeId) {
235
+ // Caso 2: Utente passa solo il testo (data)
236
+ const newNodeId = Gun.text.random();
237
+ const dataString = JSON.stringify(data);
238
+ const contentHash = ethers.keccak256(ethers.toUtf8Bytes(dataString));
239
+
240
+ gun
241
+ .get(newNodeId)
242
+ .put({ ...data, _contentHash: contentHash }, async (ack) => {
243
+ console.log("ack", ack);
244
+ if (ack.err) {
245
+ if (callback) callback({ err: "Error saving data to GunDB" });
246
+ return;
247
+ }
248
+
249
+ try {
250
+ const tx = await writeOnChain(newNodeId, contentHash);
251
+ if (callback)
252
+ callback({
253
+ ok: true,
254
+ message: "Data written to GunDB and blockchain",
255
+ nodeId: newNodeId,
256
+ txHash: tx.hash,
257
+ });
258
+ } catch (error) {
259
+ if (callback) callback({ err: error.message });
260
+ }
261
+ });
262
+ } else {
263
+ if (callback)
264
+ callback({
265
+ err: "Invalid input. Provide either nodeId or data, not both.",
266
+ });
267
+ }
268
+
269
+ return gun;
270
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gun-eth",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "description": "A GunDB plugin for Ethereum, and Web3",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/gun-eth.js DELETED
@@ -1,270 +0,0 @@
1
- /* const Gun = require("gun/gun");
2
- const SEA = require("gun/sea");
3
- const ethers = require("ethers");
4
- const SHINE = require("./SHINE.json"); */
5
-
6
- import Gun from "gun";
7
- import SEA from "gun/sea";
8
- import { ethers } from "ethers";
9
-
10
- import SHINE from "./SHINE.json";
11
-
12
- const SHINE_ABI = SHINE.abi;
13
- const SHINE_OPTIMISM_SEPOLIA = SHINE.address;
14
-
15
- let SHINE_CONTRACT_ADDRESS;
16
-
17
- // Aggiungi il metodo alla catena di Gun
18
- Gun.chain.verifySignature = async function (message, signature) {
19
- try {
20
- const recoveredAddress = ethers.verifyMessage(message, signature);
21
- return recoveredAddress;
22
- } catch (error) {
23
- console.error("Errore durante la verifica della firma:", error);
24
- return null;
25
- }
26
- };
27
-
28
- Gun.chain.generatePassword = function (signature) {
29
- try {
30
- // Usa SHA-256 per derivare una password dalla firma
31
- const hexSignature = ethers.hexlify(signature);
32
- const hash = ethers.keccak256(hexSignature);
33
-
34
- console.log("Password generata:", hash);
35
- return hash;
36
- } catch (error) {
37
- console.error("Errore nella generazione della password:", error);
38
- return null;
39
- }
40
- };
41
-
42
- Gun.chain.createSignature = async function (message) {
43
- try {
44
- // Controlla se window.ethereum è disponibile (metamask o altro provider)
45
- if (typeof window.ethereum !== "undefined") {
46
- await window.ethereum.request({ method: "eth_requestAccounts" });
47
- const provider = new ethers.BrowserProvider(window.ethereum);
48
- const signer = await provider.getSigner();
49
- const signature = await signer.signMessage(message);
50
- console.log("Firma creata:", signature);
51
- return signature;
52
- } else {
53
- throw new Error("Provider Ethereum non trovato");
54
- }
55
- } catch (error) {
56
- console.error("Errore durante la creazione della firma:", error);
57
- return null;
58
- }
59
- };
60
-
61
- Gun.chain.createAndStoreEncryptedPair = async function (address, signature) {
62
- try {
63
- const gun = this;
64
- const pair = await SEA.pair();
65
- const encryptedPair = await SEA.encrypt(JSON.stringify(pair), signature);
66
-
67
- await gun.get("users").get(address).put({ encryptedPair });
68
- console.log("Pair crittografato e archiviato per:", address);
69
- } catch (error) {
70
- console.error(
71
- "Errore durante la creazione e l'archiviazione del pair crittografato:",
72
- error
73
- );
74
- }
75
- };
76
-
77
- Gun.chain.getAndDecryptPair = async function (address, signature) {
78
- try {
79
- const gun = this;
80
- const encryptedData = await gun
81
- .get("users")
82
- .get(address)
83
- .get("encryptedPair")
84
- .then();
85
- if (!encryptedData) {
86
- throw new Error("Nessun dato crittografato trovato per questo indirizzo");
87
- }
88
-
89
- const decryptedPair = await SEA.decrypt(encryptedData, signature);
90
-
91
- console.log(decryptedPair);
92
- return decryptedPair;
93
- } catch (error) {
94
- console.error(
95
- "Errore durante il recupero e la decrittazione del pair:",
96
- error
97
- );
98
- return null;
99
- }
100
- };
101
-
102
- Gun.chain.shine = function (chain, nodeId, data, callback) {
103
- console.log("SHINE plugin called with:", { chain, nodeId, data });
104
-
105
- if (typeof callback !== "function") {
106
- console.error("Callback must be a function");
107
- return this;
108
- }
109
-
110
- const gun = this;
111
-
112
- // Seleziona l'indirizzo basato sulla catena
113
- if (chain === "optimismSepolia") {
114
- SHINE_CONTRACT_ADDRESS = SHINE_OPTIMISM_SEPOLIA;
115
- } else {
116
- throw new Error("Chain not supported");
117
- }
118
-
119
- // Funzione per ottenere il signer
120
- const getSigner = async () => {
121
- if (typeof window.ethereum !== "undefined") {
122
- await window.ethereum.request({ method: "eth_requestAccounts" });
123
- const provider = new ethers.BrowserProvider(window.ethereum);
124
- return provider.getSigner();
125
- } else {
126
- throw new Error("Ethereum provider not found");
127
- }
128
- };
129
-
130
- // Funzione per verificare on-chain
131
- const verifyOnChain = async (nodeId, contentHash) => {
132
- console.log("Verifying on chain:", { nodeId, contentHash });
133
- const signer = await getSigner();
134
- const contract = new ethers.Contract(
135
- SHINE_CONTRACT_ADDRESS,
136
- SHINE_ABI,
137
- signer
138
- );
139
- const [isValid, timestamp, updater] = await contract.verifyData(
140
- ethers.toUtf8Bytes(nodeId),
141
- contentHash
142
- );
143
- console.log("Verification result:", { isValid, timestamp, updater });
144
- return { isValid, timestamp, updater };
145
- };
146
-
147
- // Funzione per scrivere on-chain
148
- const writeOnChain = async (nodeId, contentHash) => {
149
- console.log("Writing on chain:", { nodeId, contentHash });
150
- const signer = await getSigner();
151
- const contract = new ethers.Contract(
152
- SHINE_CONTRACT_ADDRESS,
153
- SHINE_ABI,
154
- signer
155
- );
156
- const tx = await contract.updateData(
157
- ethers.toUtf8Bytes(nodeId),
158
- contentHash
159
- );
160
- console.log("Transaction sent:", tx.hash);
161
- const receipt = await tx.wait();
162
- console.log("Transaction confirmed:", receipt);
163
- return tx;
164
- };
165
-
166
- // Nuova funzione per ottenere l'ultimo record dalla blockchain
167
- const getLatestRecord = async (nodeId) => {
168
- const signer = await getSigner();
169
- const contract = new ethers.Contract(
170
- SHINE_CONTRACT_ADDRESS,
171
- SHINE_ABI,
172
- signer
173
- );
174
- const [contentHash, timestamp, updater] = await contract.getLatestRecord(
175
- ethers.toUtf8Bytes(nodeId)
176
- );
177
- console.log("Latest record from blockchain:", {
178
- nodeId,
179
- contentHash,
180
- timestamp,
181
- updater,
182
- });
183
- return { contentHash, timestamp, updater };
184
- };
185
-
186
- // Processo SHINE
187
- if (nodeId && !data) {
188
- // Caso 1: Utente passa solo il nodo
189
- gun.get(nodeId).once(async (existingData) => {
190
- if (!existingData) {
191
- if (callback) callback({ err: "Node not found in GunDB" });
192
- return;
193
- }
194
-
195
- console.log("existingData", existingData);
196
-
197
- // Usa il contentHash memorizzato invece di ricalcolarlo
198
- const contentHash = existingData._contentHash;
199
- console.log("contentHash", contentHash);
200
-
201
- if (!contentHash) {
202
- if (callback) callback({ err: "No content hash found for this node" });
203
- return;
204
- }
205
-
206
- try {
207
- const { isValid, timestamp, updater } = await verifyOnChain(
208
- nodeId,
209
- contentHash
210
- );
211
- const latestRecord = await getLatestRecord(nodeId);
212
-
213
- if (isValid) {
214
- if (callback)
215
- callback({
216
- ok: true,
217
- message: "Data verified on blockchain",
218
- timestamp,
219
- updater,
220
- latestRecord,
221
- });
222
- } else {
223
- if (callback)
224
- callback({
225
- ok: false,
226
- message: "Data not verified on blockchain",
227
- latestRecord,
228
- });
229
- }
230
- } catch (error) {
231
- if (callback) callback({ err: error.message });
232
- }
233
- });
234
- } else if (data && !nodeId) {
235
- // Caso 2: Utente passa solo il testo (data)
236
- const newNodeId = Gun.text.random();
237
- const dataString = JSON.stringify(data);
238
- const contentHash = ethers.keccak256(ethers.toUtf8Bytes(dataString));
239
-
240
- gun
241
- .get(newNodeId)
242
- .put({ ...data, _contentHash: contentHash }, async (ack) => {
243
- console.log("ack", ack);
244
- if (ack.err) {
245
- if (callback) callback({ err: "Error saving data to GunDB" });
246
- return;
247
- }
248
-
249
- try {
250
- const tx = await writeOnChain(newNodeId, contentHash);
251
- if (callback)
252
- callback({
253
- ok: true,
254
- message: "Data written to GunDB and blockchain",
255
- nodeId: newNodeId,
256
- txHash: tx.hash,
257
- });
258
- } catch (error) {
259
- if (callback) callback({ err: error.message });
260
- }
261
- });
262
- } else {
263
- if (callback)
264
- callback({
265
- err: "Invalid input. Provide either nodeId or data, not both.",
266
- });
267
- }
268
-
269
- return gun;
270
- };