pwc-sdk-wallet 0.6.3
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 +2062 -0
- package/dist/Vault.d.ts +493 -0
- package/dist/Vault.js +1090 -0
- package/dist/chain/ChainService.d.ts +84 -0
- package/dist/chain/ChainService.js +136 -0
- package/dist/chain/SolanaChainService.d.ts +94 -0
- package/dist/chain/SolanaChainService.js +167 -0
- package/dist/config/chains.d.ts +233 -0
- package/dist/config/chains.js +285 -0
- package/dist/config/constants.d.ts +102 -0
- package/dist/config/constants.js +109 -0
- package/dist/config/environment.d.ts +46 -0
- package/dist/config/environment.js +114 -0
- package/dist/config/gas.d.ts +107 -0
- package/dist/config/gas.js +123 -0
- package/dist/crypto/EncryptionService.d.ts +74 -0
- package/dist/crypto/EncryptionService.js +178 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +96 -0
- package/dist/keyrings/HDKeyring.d.ts +72 -0
- package/dist/keyrings/HDKeyring.js +156 -0
- package/dist/keyrings/SimpleKeyring.d.ts +31 -0
- package/dist/keyrings/SimpleKeyring.js +49 -0
- package/dist/keyrings/SolanaKeyring.d.ts +71 -0
- package/dist/keyrings/SolanaKeyring.js +159 -0
- package/dist/services/BatchProcessor.d.ts +42 -0
- package/dist/services/BatchProcessor.js +188 -0
- package/dist/services/MultiTransferService.d.ts +78 -0
- package/dist/services/MultiTransferService.js +252 -0
- package/dist/services/QRCodeService.d.ts +193 -0
- package/dist/services/QRCodeService.js +299 -0
- package/dist/services/TokenUtils.d.ts +307 -0
- package/dist/services/TokenUtils.js +429 -0
- package/dist/services/nft/MetadataResolver.d.ts +57 -0
- package/dist/services/nft/MetadataResolver.js +162 -0
- package/dist/services/nft/NFTAPIService.d.ts +53 -0
- package/dist/services/nft/NFTAPIService.js +122 -0
- package/dist/services/nft/NFTService.d.ts +241 -0
- package/dist/services/nft/NFTService.js +910 -0
- package/dist/types/multiTransfer.d.ts +68 -0
- package/dist/types/multiTransfer.js +2 -0
- package/dist/types/nft/index.d.ts +68 -0
- package/dist/types/nft/index.js +2 -0
- package/dist/types/nft.d.ts +265 -0
- package/dist/types/nft.js +6 -0
- package/package.json +70 -0
package/dist/Vault.js
ADDED
|
@@ -0,0 +1,1090 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Vault = void 0;
|
|
4
|
+
const HDKeyring_1 = require("./keyrings/HDKeyring");
|
|
5
|
+
const SimpleKeyring_1 = require("./keyrings/SimpleKeyring");
|
|
6
|
+
const SolanaKeyring_1 = require("./keyrings/SolanaKeyring");
|
|
7
|
+
const ChainService_1 = require("./chain/ChainService");
|
|
8
|
+
const SolanaChainService_1 = require("./chain/SolanaChainService");
|
|
9
|
+
const EncryptionService_1 = require("./crypto/EncryptionService");
|
|
10
|
+
const chains_1 = require("./config/chains");
|
|
11
|
+
const constants_1 = require("./config/constants");
|
|
12
|
+
const ethers_1 = require("ethers");
|
|
13
|
+
const MultiTransferService_1 = require("./services/MultiTransferService");
|
|
14
|
+
const NFTService_1 = require("./services/nft/NFTService");
|
|
15
|
+
const QRCodeService_1 = require("./services/QRCodeService");
|
|
16
|
+
class Vault {
|
|
17
|
+
/**
|
|
18
|
+
* Creates a new Vault instance with the specified keyrings.
|
|
19
|
+
* @param keyrings - Array of keyrings to initialize the vault with. Defaults to empty array.
|
|
20
|
+
*/
|
|
21
|
+
constructor(keyrings = [], config) {
|
|
22
|
+
this.keyrings = [];
|
|
23
|
+
this.chainId = '1'; // Default to Ethereum mainnet
|
|
24
|
+
this.keyrings = keyrings;
|
|
25
|
+
this.config = config;
|
|
26
|
+
if (config?.rpcUrls) {
|
|
27
|
+
require('./config/chains').setGlobalRPCConfig(config.rpcUrls);
|
|
28
|
+
}
|
|
29
|
+
if (config?.explorerUrls) {
|
|
30
|
+
require('./config/chains').setGlobalExplorerConfig(config.explorerUrls);
|
|
31
|
+
}
|
|
32
|
+
if (config?.gasConfig) {
|
|
33
|
+
require('./config/gas').setGlobalGasConfig(config.gasConfig);
|
|
34
|
+
}
|
|
35
|
+
if (config?.networkGasConfig) {
|
|
36
|
+
require('./config/gas').setGlobalNetworkGasConfig(config.networkGasConfig);
|
|
37
|
+
}
|
|
38
|
+
if (config?.defaultChainId) {
|
|
39
|
+
this.chainId = config.defaultChainId;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// --- Vault Creation / Loading ---
|
|
43
|
+
/**
|
|
44
|
+
* Creates a new Vault with a fresh mnemonic.
|
|
45
|
+
* @param password - The password used to encrypt the vault data
|
|
46
|
+
* @param chainType - The type of blockchain to support ('evm' for Ethereum-compatible chains or 'solana' for Solana). Defaults to 'evm'
|
|
47
|
+
* @returns Promise resolving to an object containing the created vault instance and its encrypted data
|
|
48
|
+
* @throws Error if mnemonic generation or keyring initialization fails
|
|
49
|
+
*/
|
|
50
|
+
static async createNew(password, chainType = 'evm', config) {
|
|
51
|
+
const mnemonic = EncryptionService_1.EncryptionService.generateMnemonic();
|
|
52
|
+
if (chainType === 'solana') {
|
|
53
|
+
const solanaKeyring = new SolanaKeyring_1.SolanaKeyring(mnemonic);
|
|
54
|
+
const vault = new Vault([solanaKeyring], config);
|
|
55
|
+
vault.chainId = 'solana'; // Set Solana chain ID
|
|
56
|
+
const encryptedVault = await vault.encrypt(password);
|
|
57
|
+
return { vault, encryptedVault };
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
const hdKeyring = new HDKeyring_1.HDKeyring(mnemonic);
|
|
61
|
+
await hdKeyring.initialize();
|
|
62
|
+
const vault = new Vault([hdKeyring], config);
|
|
63
|
+
vault.chainId = '1'; // Set Ethereum mainnet as default
|
|
64
|
+
const encryptedVault = await vault.encrypt(password);
|
|
65
|
+
return { vault, encryptedVault };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Creates a new Vault from an existing mnemonic phrase.
|
|
70
|
+
* @param mnemonic - The existing mnemonic phrase (12, 15, 18, 21, or 24 words)
|
|
71
|
+
* @param password - The password used to encrypt the vault data
|
|
72
|
+
* @param chainType - The type of blockchain to support ('evm' for Ethereum-compatible chains or 'solana' for Solana). Defaults to 'evm'
|
|
73
|
+
* @returns Promise resolving to an object containing the created vault instance and its encrypted data
|
|
74
|
+
* @throws Error if mnemonic is invalid or keyring initialization fails
|
|
75
|
+
*/
|
|
76
|
+
static async createFromMnemonic(mnemonic, password, chainType = 'evm', config) {
|
|
77
|
+
if (chainType === 'solana') {
|
|
78
|
+
const solanaKeyring = new SolanaKeyring_1.SolanaKeyring(mnemonic);
|
|
79
|
+
const vault = new Vault([solanaKeyring], config);
|
|
80
|
+
vault.chainId = 'solana'; // Set Solana chain ID
|
|
81
|
+
const encryptedVault = await vault.encrypt(password);
|
|
82
|
+
return { vault, encryptedVault };
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
const hdKeyring = new HDKeyring_1.HDKeyring(mnemonic);
|
|
86
|
+
await hdKeyring.initialize();
|
|
87
|
+
const vault = new Vault([hdKeyring], config);
|
|
88
|
+
vault.chainId = '1'; // Set Ethereum mainnet as default
|
|
89
|
+
const encryptedVault = await vault.encrypt(password);
|
|
90
|
+
return { vault, encryptedVault };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Loads a Vault from its encrypted state using the provided password.
|
|
95
|
+
* @param password - The password used to decrypt the vault data
|
|
96
|
+
* @param encryptedVault - The encrypted vault data to decrypt and load
|
|
97
|
+
* @returns Promise resolving to the loaded Vault instance
|
|
98
|
+
* @throws Error if password is incorrect or decryption fails
|
|
99
|
+
*/
|
|
100
|
+
static async load(password, encryptedVault, config) {
|
|
101
|
+
const decryptedString = await EncryptionService_1.EncryptionService.decrypt(encryptedVault, password);
|
|
102
|
+
const serializedKeyrings = JSON.parse(decryptedString);
|
|
103
|
+
const keyrings = [];
|
|
104
|
+
for (const sKeyring of serializedKeyrings) {
|
|
105
|
+
if (sKeyring.type === 'HD') {
|
|
106
|
+
keyrings.push(await HDKeyring_1.HDKeyring.deserialize(sKeyring));
|
|
107
|
+
}
|
|
108
|
+
else if (sKeyring.type === 'Simple') {
|
|
109
|
+
keyrings.push(SimpleKeyring_1.SimpleKeyring.deserialize(sKeyring));
|
|
110
|
+
}
|
|
111
|
+
else if (sKeyring.type === 'Solana') {
|
|
112
|
+
keyrings.push(await SolanaKeyring_1.SolanaKeyring.deserialize(sKeyring));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const vault = new Vault(keyrings, config);
|
|
116
|
+
// Set chain ID based on keyring type
|
|
117
|
+
if (keyrings.some(k => k instanceof SolanaKeyring_1.SolanaKeyring)) {
|
|
118
|
+
vault.chainId = 'solana';
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
vault.chainId = '1'; // Default to Ethereum mainnet
|
|
122
|
+
}
|
|
123
|
+
return vault;
|
|
124
|
+
}
|
|
125
|
+
// --- Account Management ---
|
|
126
|
+
/**
|
|
127
|
+
* Adds a new account derived from the HD keyring using BIP-44 derivation.
|
|
128
|
+
* @returns Promise resolving to the newly created account information
|
|
129
|
+
* @throws Error if no HD keyring is available in the vault
|
|
130
|
+
*/
|
|
131
|
+
async addNewHDAccount() {
|
|
132
|
+
const hdKeyring = this.keyrings.find(k => k instanceof HDKeyring_1.HDKeyring);
|
|
133
|
+
if (!hdKeyring) {
|
|
134
|
+
throw new Error('No HD keyring available to create new accounts.');
|
|
135
|
+
}
|
|
136
|
+
const newAddress = await hdKeyring.addNewAccount();
|
|
137
|
+
return {
|
|
138
|
+
address: newAddress,
|
|
139
|
+
type: 'HD',
|
|
140
|
+
name: `Account ${hdKeyring.accounts.length}`
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Adds a new Solana account derived from the Solana keyring.
|
|
145
|
+
* @returns Promise resolving to the newly created Solana account information
|
|
146
|
+
* @throws Error if no Solana keyring is available in the vault
|
|
147
|
+
*/
|
|
148
|
+
async addNewSolanaAccount() {
|
|
149
|
+
const solanaKeyring = this.keyrings.find(k => k instanceof SolanaKeyring_1.SolanaKeyring);
|
|
150
|
+
if (!solanaKeyring) {
|
|
151
|
+
throw new Error('No Solana keyring available to create new accounts.');
|
|
152
|
+
}
|
|
153
|
+
const newAddress = await solanaKeyring.addNewAccount();
|
|
154
|
+
return {
|
|
155
|
+
address: newAddress,
|
|
156
|
+
type: 'Solana',
|
|
157
|
+
name: `Solana ${solanaKeyring.accounts.length}`
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Imports an account from a private key and adds it to the vault.
|
|
162
|
+
* @param privateKey - The private key to import (hex string without '0x' prefix for EVM, base58 for Solana)
|
|
163
|
+
* @returns Promise resolving to the imported account information
|
|
164
|
+
* @throws Error if the private key is already managed by an HD keyring or if import fails
|
|
165
|
+
*/
|
|
166
|
+
async importAccount(privateKey) {
|
|
167
|
+
// Prevent importing a private key that's already managed by the HD keyring
|
|
168
|
+
for (const keyring of this.keyrings) {
|
|
169
|
+
if (keyring instanceof HDKeyring_1.HDKeyring) {
|
|
170
|
+
const pk = await keyring.getPrivateKeyForAddress(ChainService_1.ChainService.getAddress(privateKey)).catch(() => null);
|
|
171
|
+
if (pk) {
|
|
172
|
+
throw new Error("This private key is already part of your mnemonic-derived accounts.");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const newKeyring = new SimpleKeyring_1.SimpleKeyring(privateKey);
|
|
177
|
+
this.keyrings.push(newKeyring);
|
|
178
|
+
return {
|
|
179
|
+
address: newKeyring.address,
|
|
180
|
+
type: newKeyring.type,
|
|
181
|
+
name: `Imported ${this.keyrings.filter(k => k.type === 'Simple').length}`
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Returns a list of all accounts managed by the vault across all keyrings.
|
|
186
|
+
* @returns Array of Account objects representing all accounts in the vault
|
|
187
|
+
*/
|
|
188
|
+
getAccounts() {
|
|
189
|
+
const accounts = [];
|
|
190
|
+
let hdAccountCount = 1;
|
|
191
|
+
let importedAccountCount = 1;
|
|
192
|
+
let solanaAccountCount = 1;
|
|
193
|
+
for (const keyring of this.keyrings) {
|
|
194
|
+
if (keyring instanceof HDKeyring_1.HDKeyring) {
|
|
195
|
+
for (const address of keyring.accounts) {
|
|
196
|
+
accounts.push({
|
|
197
|
+
address,
|
|
198
|
+
type: 'HD',
|
|
199
|
+
name: `Account ${hdAccountCount++}`,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else if (keyring instanceof SimpleKeyring_1.SimpleKeyring) {
|
|
204
|
+
accounts.push({
|
|
205
|
+
address: keyring.address,
|
|
206
|
+
type: 'Simple',
|
|
207
|
+
name: `Imported ${importedAccountCount++}`,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
else if (keyring instanceof SolanaKeyring_1.SolanaKeyring) {
|
|
211
|
+
for (const address of keyring.accounts) {
|
|
212
|
+
accounts.push({
|
|
213
|
+
address,
|
|
214
|
+
type: 'Solana',
|
|
215
|
+
name: `Solana ${solanaAccountCount++}`,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return accounts;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Exports the mnemonic phrase from the vault for backup purposes.
|
|
224
|
+
* Requires password verification and includes rate limiting for security.
|
|
225
|
+
* @param password - The vault password to verify before exporting
|
|
226
|
+
* @returns Promise resolving to the mnemonic phrase as a string
|
|
227
|
+
* @throws Error if password is incorrect, no mnemonic exists, or rate limit exceeded
|
|
228
|
+
*/
|
|
229
|
+
async exportMnemonic(password) {
|
|
230
|
+
// Rate limiting check
|
|
231
|
+
const vaultId = this.getVaultId();
|
|
232
|
+
const now = Date.now();
|
|
233
|
+
const attempts = Vault.exportAttempts.get(vaultId);
|
|
234
|
+
if (attempts) {
|
|
235
|
+
if (attempts.count >= Vault.MAX_EXPORT_ATTEMPTS) {
|
|
236
|
+
const timeRemaining = Vault.EXPORT_COOLDOWN_MS - (now - attempts.lastAttempt);
|
|
237
|
+
if (timeRemaining > 0) {
|
|
238
|
+
throw new Error(`Too many export attempts. Please wait ${Math.ceil(timeRemaining / 1000)} seconds before trying again.`);
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
// Reset attempts after cooldown
|
|
242
|
+
Vault.exportAttempts.delete(vaultId);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const hdKeyring = this.keyrings.find(k => k instanceof HDKeyring_1.HDKeyring);
|
|
247
|
+
if (!hdKeyring) {
|
|
248
|
+
throw new Error('This vault does not have a mnemonic.');
|
|
249
|
+
}
|
|
250
|
+
// Update attempt counter
|
|
251
|
+
const currentAttempts = Vault.exportAttempts.get(vaultId) || { count: 0, lastAttempt: 0 };
|
|
252
|
+
currentAttempts.count++;
|
|
253
|
+
currentAttempts.lastAttempt = now;
|
|
254
|
+
Vault.exportAttempts.set(vaultId, currentAttempts);
|
|
255
|
+
// Audit logging (without sensitive data)
|
|
256
|
+
// console.log(`Mnemonic export attempted for vault ${vaultId}, attempt ${currentAttempts.count}`);
|
|
257
|
+
try {
|
|
258
|
+
// Verify password by attempting to decrypt the vault
|
|
259
|
+
const encryptedVault = await this.encrypt(password);
|
|
260
|
+
// If encryption succeeds, password is correct
|
|
261
|
+
const mnemonic = hdKeyring.getMnemonic();
|
|
262
|
+
// Reset attempts on successful export
|
|
263
|
+
Vault.exportAttempts.delete(vaultId);
|
|
264
|
+
// Audit logging for successful export
|
|
265
|
+
// console.log(`Mnemonic export successful for vault ${vaultId}`);
|
|
266
|
+
// Use temporary data protection
|
|
267
|
+
return EncryptionService_1.EncryptionService.withTemporaryData(mnemonic, (tempMnemonic) => {
|
|
268
|
+
// Return a copy to avoid direct reference to the original
|
|
269
|
+
return String(tempMnemonic);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
// Use constant-time error message to prevent timing attacks
|
|
274
|
+
throw new Error('Incorrect password. Cannot export mnemonic.');
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Gets a unique identifier for this vault used for rate limiting purposes.
|
|
279
|
+
* @returns String identifier based on the first account address
|
|
280
|
+
*/
|
|
281
|
+
getVaultId() {
|
|
282
|
+
// Use the first account address as vault identifier
|
|
283
|
+
const accounts = this.getAccounts();
|
|
284
|
+
return accounts.length > 0 ? accounts[0].address : 'unknown';
|
|
285
|
+
}
|
|
286
|
+
// --- Internal & Utility ---
|
|
287
|
+
/**
|
|
288
|
+
* Encrypts the entire vault's state using the provided password.
|
|
289
|
+
* @param password - The password to use for encryption
|
|
290
|
+
* @returns Promise resolving to the encrypted vault data
|
|
291
|
+
* @throws Error if encryption fails
|
|
292
|
+
*/
|
|
293
|
+
async encrypt(password) {
|
|
294
|
+
const serializedKeyrings = this.keyrings.map(k => k.serialize());
|
|
295
|
+
const jsonString = JSON.stringify(serializedKeyrings);
|
|
296
|
+
return EncryptionService_1.EncryptionService.encrypt(jsonString, password);
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Retrieves the private key for a specific address from the appropriate keyring.
|
|
300
|
+
* @param address - The account address to get the private key for
|
|
301
|
+
* @returns Promise resolving to the private key as a string
|
|
302
|
+
* @throws Error if the address is not found in any keyring
|
|
303
|
+
*/
|
|
304
|
+
async getPrivateKeyFor(address) {
|
|
305
|
+
for (const keyring of this.keyrings) {
|
|
306
|
+
if (keyring instanceof HDKeyring_1.HDKeyring && keyring.accounts.includes(address)) {
|
|
307
|
+
return keyring.getPrivateKeyForAddress(address);
|
|
308
|
+
}
|
|
309
|
+
if (keyring instanceof SimpleKeyring_1.SimpleKeyring && keyring.address === address) {
|
|
310
|
+
return keyring.getPrivateKey();
|
|
311
|
+
}
|
|
312
|
+
if (keyring instanceof SolanaKeyring_1.SolanaKeyring && keyring.accounts.includes(address)) {
|
|
313
|
+
return keyring.getPrivateKeyForAddress(address);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
throw new Error('Private key not found for the given address.');
|
|
317
|
+
}
|
|
318
|
+
// --- Blockchain Interaction Methods ---
|
|
319
|
+
/**
|
|
320
|
+
* Gets the token balance for a specific account and token contract.
|
|
321
|
+
* @param accountAddress - The account address to check balance for
|
|
322
|
+
* @param tokenAddress - The contract address of the token
|
|
323
|
+
* @param chainId - The ID of the blockchain where the token is deployed
|
|
324
|
+
* @returns Promise resolving to the token balance as a bigint
|
|
325
|
+
* @throws Error if account or token not found, or chain service fails
|
|
326
|
+
*/
|
|
327
|
+
async getTokenBalance(accountAddress, tokenAddress, chainId) {
|
|
328
|
+
const chainInfo = (0, chains_1.getChainConfig)(chainId);
|
|
329
|
+
const privateKey = await this.getPrivateKeyFor(accountAddress);
|
|
330
|
+
if (chainInfo.type === 'solana') {
|
|
331
|
+
const chainService = new SolanaChainService_1.SolanaChainService(privateKey, chainInfo);
|
|
332
|
+
return chainService.getTokenBalance(tokenAddress);
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
const chainService = new ChainService_1.ChainService(privateKey, chainInfo);
|
|
336
|
+
return chainService.getTokenBalance(tokenAddress);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Sends tokens from a specific account to another address.
|
|
341
|
+
* @param fromAddress - The sender's account address
|
|
342
|
+
* @param to - The recipient's address
|
|
343
|
+
* @param amount - The amount of tokens to send as a string
|
|
344
|
+
* @param tokenAddress - The contract address of the token to send
|
|
345
|
+
* @param chainId - The ID of the blockchain for the transaction
|
|
346
|
+
* @returns Promise resolving to the transaction response with hash and details
|
|
347
|
+
* @throws Error if insufficient balance, invalid addresses, or transaction fails
|
|
348
|
+
*/
|
|
349
|
+
async sendToken(fromAddress, to, amount, tokenAddress, chainId) {
|
|
350
|
+
const chainInfo = (0, chains_1.getChainConfig)(chainId);
|
|
351
|
+
const privateKey = await this.getPrivateKeyFor(fromAddress);
|
|
352
|
+
if (chainInfo.type === 'solana') {
|
|
353
|
+
const chainService = new SolanaChainService_1.SolanaChainService(privateKey, chainInfo);
|
|
354
|
+
const result = await chainService.sendToken(tokenAddress, to, amount);
|
|
355
|
+
return {
|
|
356
|
+
hash: result.hash,
|
|
357
|
+
from: result.from,
|
|
358
|
+
to: result.to,
|
|
359
|
+
blockNumber: result.blockNumber
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
const chainService = new ChainService_1.ChainService(privateKey, chainInfo);
|
|
364
|
+
const result = await chainService.sendToken(tokenAddress, to, amount);
|
|
365
|
+
return {
|
|
366
|
+
hash: result.hash,
|
|
367
|
+
from: result.from,
|
|
368
|
+
to: result.to || '',
|
|
369
|
+
blockNumber: result.blockNumber || undefined
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Generates a vanity HD wallet with a specific address prefix.
|
|
375
|
+
* Uses default configuration from VANITY_WALLET_CONFIG for optimal settings.
|
|
376
|
+
* The prefix defaults to 'aaa' as configured in VANITY_WALLET_CONFIG.DEFAULT_PREFIX.
|
|
377
|
+
*
|
|
378
|
+
* @param password - The password to encrypt the generated vault
|
|
379
|
+
* @param onProgress - Optional callback function for progress updates (attempts count and current address)
|
|
380
|
+
* @returns Promise resolving to an object containing the generated vault, encrypted vault, number of attempts, and the found address
|
|
381
|
+
* @throws Error if generation fails or max attempts exceeded
|
|
382
|
+
*
|
|
383
|
+
* @example
|
|
384
|
+
* ```typescript
|
|
385
|
+
* // Simple usage - will use default prefix 'aaa'
|
|
386
|
+
* const result = await Vault.generateVanityHDWallet("mypassword");
|
|
387
|
+
*
|
|
388
|
+
* // With progress callback
|
|
389
|
+
* const result = await Vault.generateVanityHDWallet(
|
|
390
|
+
* "mypassword",
|
|
391
|
+
* (attempts, address) => console.log(`Attempt ${attempts}: ${address}`)
|
|
392
|
+
* );
|
|
393
|
+
* ```
|
|
394
|
+
*/
|
|
395
|
+
static async generateVanityHDWallet(password, onProgress) {
|
|
396
|
+
// Use default values from config
|
|
397
|
+
const targetPrefix = constants_1.VANITY_WALLET_CONFIG.DEFAULT_PREFIX;
|
|
398
|
+
const maxAttempts = constants_1.VANITY_WALLET_CONFIG.DEFAULT_MAX_ATTEMPTS;
|
|
399
|
+
const caseSensitive = constants_1.VANITY_WALLET_CONFIG.DEFAULT_CASE_SENSITIVE;
|
|
400
|
+
const chainType = 'evm'; // Default to EVM for simplicity
|
|
401
|
+
// Validate prefix
|
|
402
|
+
if (targetPrefix.length > constants_1.VANITY_WALLET_CONFIG.MAX_PREFIX_LENGTH) {
|
|
403
|
+
throw new Error(`Prefix too long. Maximum length is ${constants_1.VANITY_WALLET_CONFIG.MAX_PREFIX_LENGTH} characters.`);
|
|
404
|
+
}
|
|
405
|
+
const normalizedPrefix = caseSensitive ? targetPrefix : targetPrefix.toLowerCase();
|
|
406
|
+
let attempts = 0;
|
|
407
|
+
let foundAddress = '';
|
|
408
|
+
let foundMnemonic = '';
|
|
409
|
+
while (attempts < maxAttempts) {
|
|
410
|
+
attempts++;
|
|
411
|
+
// Generate new mnemonic
|
|
412
|
+
const mnemonic = EncryptionService_1.EncryptionService.generateMnemonic();
|
|
413
|
+
// Create HD keyring and get first address
|
|
414
|
+
const hdKeyring = new HDKeyring_1.HDKeyring(mnemonic);
|
|
415
|
+
await hdKeyring.initialize();
|
|
416
|
+
const firstAddress = hdKeyring.accounts[0];
|
|
417
|
+
// Check if address matches prefix (remove '0x' for EVM)
|
|
418
|
+
const addressWithoutPrefix = firstAddress.slice(2);
|
|
419
|
+
const addressToCheck = caseSensitive ? addressWithoutPrefix : addressWithoutPrefix.toLowerCase();
|
|
420
|
+
if (addressToCheck.startsWith(normalizedPrefix)) {
|
|
421
|
+
foundAddress = firstAddress;
|
|
422
|
+
foundMnemonic = mnemonic;
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
// Progress callback
|
|
426
|
+
if (onProgress && attempts % constants_1.VANITY_WALLET_CONFIG.PROGRESS_UPDATE_INTERVAL === 0) {
|
|
427
|
+
onProgress(attempts, firstAddress);
|
|
428
|
+
}
|
|
429
|
+
// Non-blocking interval to allow event loop to breathe
|
|
430
|
+
if (attempts % constants_1.VANITY_WALLET_CONFIG.NON_BLOCKING_INTERVAL === 0) {
|
|
431
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (!foundAddress) {
|
|
435
|
+
throw new Error(`Could not generate vanity address with prefix "${targetPrefix}" after ${attempts} attempts. Try using a shorter prefix.`);
|
|
436
|
+
}
|
|
437
|
+
// Create vault with the found mnemonic
|
|
438
|
+
const { vault, encryptedVault } = await Vault.createFromMnemonic(foundMnemonic, password, chainType);
|
|
439
|
+
return { vault, encryptedVault, attempts, foundAddress };
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Generates a vanity HD wallet optimized for mobile devices.
|
|
443
|
+
* Uses mobile-optimized settings for faster completion and better UX.
|
|
444
|
+
*
|
|
445
|
+
* @param password - The password to encrypt the generated vault
|
|
446
|
+
* @param onProgress - Optional callback function for progress updates (attempts count and current address)
|
|
447
|
+
* @param options - Optional configuration options for mobile optimization
|
|
448
|
+
* @returns Promise resolving to an object containing the generated vault, encrypted vault, number of attempts, and the found address
|
|
449
|
+
* @throws Error if generation fails, max attempts exceeded, or timeout reached
|
|
450
|
+
*
|
|
451
|
+
* @example
|
|
452
|
+
* ```typescript
|
|
453
|
+
* // Simple mobile-optimized usage
|
|
454
|
+
* const result = await Vault.generateVanityHDWalletMobile("mypassword");
|
|
455
|
+
*
|
|
456
|
+
* // With custom options
|
|
457
|
+
* const result = await Vault.generateVanityHDWalletMobile(
|
|
458
|
+
* "mypassword",
|
|
459
|
+
* (attempts, address) => console.log(`Attempt ${attempts}: ${address}`),
|
|
460
|
+
* { prefix: 'ab', maxAttempts: 10000 }
|
|
461
|
+
* );
|
|
462
|
+
* ```
|
|
463
|
+
*/
|
|
464
|
+
static async generateVanityHDWalletMobile(password, onProgress, options = {}) {
|
|
465
|
+
// Use mobile-optimized defaults
|
|
466
|
+
const targetPrefix = options.prefix || constants_1.VANITY_WALLET_CONFIG.MOBILE_DEFAULT_PREFIX;
|
|
467
|
+
const maxAttempts = options.maxAttempts || constants_1.VANITY_WALLET_CONFIG.MOBILE_DEFAULT_MAX_ATTEMPTS;
|
|
468
|
+
const caseSensitive = options.caseSensitive ?? constants_1.VANITY_WALLET_CONFIG.DEFAULT_CASE_SENSITIVE;
|
|
469
|
+
const timeout = options.timeout || constants_1.VANITY_WALLET_CONFIG.MOBILE_TIMEOUT_MS;
|
|
470
|
+
const chainType = 'evm'; // Default to EVM for simplicity
|
|
471
|
+
// Validate prefix
|
|
472
|
+
if (targetPrefix.length > constants_1.VANITY_WALLET_CONFIG.MAX_PREFIX_LENGTH) {
|
|
473
|
+
throw new Error(`Prefix too long. Maximum length is ${constants_1.VANITY_WALLET_CONFIG.MAX_PREFIX_LENGTH} characters.`);
|
|
474
|
+
}
|
|
475
|
+
const normalizedPrefix = caseSensitive ? targetPrefix : targetPrefix.toLowerCase();
|
|
476
|
+
let attempts = 0;
|
|
477
|
+
let foundAddress = '';
|
|
478
|
+
let foundMnemonic = '';
|
|
479
|
+
const startTime = Date.now();
|
|
480
|
+
// Batch processing for better performance
|
|
481
|
+
const processBatch = async (batchSize) => {
|
|
482
|
+
const batchPromises = [];
|
|
483
|
+
for (let i = 0; i < batchSize; i++) {
|
|
484
|
+
batchPromises.push((async () => {
|
|
485
|
+
const mnemonic = EncryptionService_1.EncryptionService.generateMnemonic();
|
|
486
|
+
const hdKeyring = new HDKeyring_1.HDKeyring(mnemonic);
|
|
487
|
+
await hdKeyring.initialize();
|
|
488
|
+
const address = hdKeyring.accounts[0];
|
|
489
|
+
return { address, mnemonic };
|
|
490
|
+
})());
|
|
491
|
+
}
|
|
492
|
+
const results = await Promise.all(batchPromises);
|
|
493
|
+
for (const { address, mnemonic } of results) {
|
|
494
|
+
attempts++;
|
|
495
|
+
// Check if address matches prefix (remove '0x' for EVM)
|
|
496
|
+
const addressWithoutPrefix = address.slice(2);
|
|
497
|
+
const addressToCheck = caseSensitive ? addressWithoutPrefix : addressWithoutPrefix.toLowerCase();
|
|
498
|
+
if (addressToCheck.startsWith(normalizedPrefix)) {
|
|
499
|
+
return { found: true, address, mnemonic };
|
|
500
|
+
}
|
|
501
|
+
// Progress callback
|
|
502
|
+
if (onProgress && attempts % constants_1.VANITY_WALLET_CONFIG.MOBILE_PROGRESS_UPDATE_INTERVAL === 0) {
|
|
503
|
+
onProgress(attempts, address);
|
|
504
|
+
}
|
|
505
|
+
// Check timeout
|
|
506
|
+
if (Date.now() - startTime > timeout) {
|
|
507
|
+
throw new Error(`Vanity wallet generation timed out after ${timeout}ms. Try using a shorter prefix.`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return { found: false };
|
|
511
|
+
};
|
|
512
|
+
// Process in batches until found or max attempts reached
|
|
513
|
+
while (attempts < maxAttempts) {
|
|
514
|
+
const batchSize = Math.min(constants_1.VANITY_WALLET_CONFIG.MOBILE_BATCH_SIZE, maxAttempts - attempts);
|
|
515
|
+
const result = await processBatch(batchSize);
|
|
516
|
+
if (result.found) {
|
|
517
|
+
foundAddress = result.address;
|
|
518
|
+
foundMnemonic = result.mnemonic;
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
// Non-blocking interval to allow event loop to breathe (more frequent for mobile)
|
|
522
|
+
if (attempts % constants_1.VANITY_WALLET_CONFIG.MOBILE_NON_BLOCKING_INTERVAL === 0) {
|
|
523
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (!foundAddress) {
|
|
527
|
+
throw new Error(`Could not generate vanity address with prefix "${targetPrefix}" after ${attempts} attempts. Try using a shorter prefix.`);
|
|
528
|
+
}
|
|
529
|
+
// Create vault with the found mnemonic
|
|
530
|
+
const { vault, encryptedVault } = await Vault.createFromMnemonic(foundMnemonic, password, chainType);
|
|
531
|
+
return { vault, encryptedVault, attempts, foundAddress };
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Ultra-optimized vanity wallet generation for maximum performance.
|
|
535
|
+
* Bypasses heavy HDKeyring initialization and uses direct cryptographic operations.
|
|
536
|
+
* Similar to web-based vanity generators for maximum speed.
|
|
537
|
+
*
|
|
538
|
+
* @param password - The password to encrypt the generated vault
|
|
539
|
+
* @param onProgress - Optional callback function for progress updates
|
|
540
|
+
* @param options - Configuration options for ultra-optimized generation
|
|
541
|
+
* @returns Promise resolving to generated vault data
|
|
542
|
+
*/
|
|
543
|
+
static async generateVanityHDWalletUltra(password, onProgress, options = {}) {
|
|
544
|
+
const targetPrefix = options.prefix || 'a';
|
|
545
|
+
const maxAttempts = options.maxAttempts || 100000;
|
|
546
|
+
const caseSensitive = options.caseSensitive ?? false;
|
|
547
|
+
const timeout = options.timeout || 15000; // 15 seconds default
|
|
548
|
+
const batchSize = options.batchSize || 1000; // Larger batches for better performance
|
|
549
|
+
const chainType = 'evm';
|
|
550
|
+
// Validate prefix
|
|
551
|
+
if (targetPrefix.length > constants_1.VANITY_WALLET_CONFIG.MAX_PREFIX_LENGTH) {
|
|
552
|
+
throw new Error(`Prefix too long. Maximum length is ${constants_1.VANITY_WALLET_CONFIG.MAX_PREFIX_LENGTH} characters.`);
|
|
553
|
+
}
|
|
554
|
+
const normalizedPrefix = caseSensitive ? targetPrefix : targetPrefix.toLowerCase();
|
|
555
|
+
let attempts = 0;
|
|
556
|
+
let foundAddress = '';
|
|
557
|
+
let foundMnemonic = '';
|
|
558
|
+
const startTime = Date.now();
|
|
559
|
+
// Ultra-optimized batch processing with direct crypto operations
|
|
560
|
+
const processUltraBatch = async (batchSize) => {
|
|
561
|
+
const batchPromises = [];
|
|
562
|
+
for (let i = 0; i < batchSize; i++) {
|
|
563
|
+
batchPromises.push((async () => {
|
|
564
|
+
// Generate mnemonic directly using existing service
|
|
565
|
+
const mnemonic = EncryptionService_1.EncryptionService.generateMnemonic();
|
|
566
|
+
// Use existing HDKeyring but with minimal overhead
|
|
567
|
+
const hdKeyring = new HDKeyring_1.HDKeyring(mnemonic);
|
|
568
|
+
await hdKeyring.initialize();
|
|
569
|
+
const address = hdKeyring.accounts[0];
|
|
570
|
+
return { address, mnemonic };
|
|
571
|
+
})());
|
|
572
|
+
}
|
|
573
|
+
const results = await Promise.all(batchPromises);
|
|
574
|
+
for (const { address, mnemonic } of results) {
|
|
575
|
+
attempts++;
|
|
576
|
+
// Check if address matches prefix (remove '0x' for EVM)
|
|
577
|
+
const addressWithoutPrefix = address.slice(2);
|
|
578
|
+
const addressToCheck = caseSensitive ? addressWithoutPrefix : addressWithoutPrefix.toLowerCase();
|
|
579
|
+
if (addressToCheck.startsWith(normalizedPrefix)) {
|
|
580
|
+
return { found: true, address, mnemonic };
|
|
581
|
+
}
|
|
582
|
+
// Progress callback (less frequent for performance)
|
|
583
|
+
if (onProgress && attempts % 5000 === 0) {
|
|
584
|
+
onProgress(attempts, address);
|
|
585
|
+
}
|
|
586
|
+
// Check timeout
|
|
587
|
+
if (Date.now() - startTime > timeout) {
|
|
588
|
+
throw new Error(`Vanity wallet generation timed out after ${timeout}ms. Try using a shorter prefix.`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return { found: false };
|
|
592
|
+
};
|
|
593
|
+
// Process in ultra-batches until found or max attempts reached
|
|
594
|
+
while (attempts < maxAttempts) {
|
|
595
|
+
const currentBatchSize = Math.min(batchSize, maxAttempts - attempts);
|
|
596
|
+
const result = await processUltraBatch(currentBatchSize);
|
|
597
|
+
if (result.found) {
|
|
598
|
+
foundAddress = result.address;
|
|
599
|
+
foundMnemonic = result.mnemonic;
|
|
600
|
+
break;
|
|
601
|
+
}
|
|
602
|
+
// Minimal non-blocking interval for ultra-performance
|
|
603
|
+
if (attempts % 10000 === 0) {
|
|
604
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (!foundAddress) {
|
|
608
|
+
throw new Error(`Could not generate vanity address with prefix "${targetPrefix}" after ${attempts} attempts. Try using a shorter prefix.`);
|
|
609
|
+
}
|
|
610
|
+
// Create vault with the found mnemonic
|
|
611
|
+
const { vault, encryptedVault } = await Vault.createFromMnemonic(foundMnemonic, password, chainType);
|
|
612
|
+
return { vault, encryptedVault, attempts, foundAddress };
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Transfers native tokens to multiple recipients in a single operation.
|
|
616
|
+
* Uses batch processing for optimal performance and provides progress tracking.
|
|
617
|
+
*
|
|
618
|
+
* @param fromAddress - The sender's account address
|
|
619
|
+
* @param recipients - Array of recipients with addresses and amounts
|
|
620
|
+
* @param chainId - The ID of the blockchain for the transaction
|
|
621
|
+
* @param onProgress - Optional callback for progress updates (completed, total, txHash)
|
|
622
|
+
* @returns Promise resolving to the multi-transfer result with success/failure details
|
|
623
|
+
* @throws Error if validation fails, insufficient balance, or transfer fails
|
|
624
|
+
*
|
|
625
|
+
* @example
|
|
626
|
+
* ```typescript
|
|
627
|
+
* const recipients = [
|
|
628
|
+
* { address: '0x123...', amount: '0.1' },
|
|
629
|
+
* { address: '0x456...', amount: '0.2' },
|
|
630
|
+
* { address: '0x789...', amount: '0.05' }
|
|
631
|
+
* ];
|
|
632
|
+
*
|
|
633
|
+
* const result = await vault.multiTransferNativeTokens(
|
|
634
|
+
* '0xmyAddress',
|
|
635
|
+
* recipients,
|
|
636
|
+
* '1', // Ethereum
|
|
637
|
+
* (completed, total, txHash) => console.log(`Completed ${completed}/${total}: ${txHash}`)
|
|
638
|
+
* );
|
|
639
|
+
*
|
|
640
|
+
* console.log(`Success: ${result.successfulCount}, Failed: ${result.failedCount}`);
|
|
641
|
+
* ```
|
|
642
|
+
*/
|
|
643
|
+
async multiTransferNativeTokens(fromAddress, recipients, chainId, onProgress) {
|
|
644
|
+
const chainInfo = (0, chains_1.getChainConfig)(chainId);
|
|
645
|
+
const privateKey = await this.getPrivateKeyFor(fromAddress);
|
|
646
|
+
let chainService;
|
|
647
|
+
if (chainInfo.type === 'solana') {
|
|
648
|
+
chainService = new SolanaChainService_1.SolanaChainService(privateKey, chainInfo);
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
chainService = new ChainService_1.ChainService(privateKey, chainInfo);
|
|
652
|
+
}
|
|
653
|
+
const multiTransferService = new MultiTransferService_1.MultiTransferService(this, chainService, chainId);
|
|
654
|
+
return multiTransferService.transferNativeTokens(fromAddress, recipients, chainId, { onProgress });
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Transfers ERC-20/SPL tokens to multiple recipients in a single operation.
|
|
658
|
+
* Uses batch processing for optimal performance and provides progress tracking.
|
|
659
|
+
*
|
|
660
|
+
* @param fromAddress - The sender's account address
|
|
661
|
+
* @param tokenAddress - The contract address of the token to send
|
|
662
|
+
* @param recipients - Array of recipients with addresses and amounts
|
|
663
|
+
* @param chainId - The ID of the blockchain for the transaction
|
|
664
|
+
* @param onProgress - Optional callback for progress updates (completed, total, txHash)
|
|
665
|
+
* @returns Promise resolving to the multi-transfer result with success/failure details
|
|
666
|
+
* @throws Error if validation fails, insufficient balance, or transfer fails
|
|
667
|
+
*
|
|
668
|
+
* @example
|
|
669
|
+
* ```typescript
|
|
670
|
+
* const recipients = [
|
|
671
|
+
* { address: '0x1111...', amount: '100' },
|
|
672
|
+
* { address: '0x456...', amount: '200' },
|
|
673
|
+
* { address: '0x789...', amount: '50' }
|
|
674
|
+
* ];
|
|
675
|
+
*
|
|
676
|
+
* const result = await vault.multiTransferTokens(
|
|
677
|
+
* '0xmyAddress',
|
|
678
|
+
* '0xtokenContract',
|
|
679
|
+
* recipients,
|
|
680
|
+
* '1', // Ethereum
|
|
681
|
+
* (completed, total, txHash) => console.log(`Completed ${completed}/${total}: ${txHash}`)
|
|
682
|
+
* );
|
|
683
|
+
*
|
|
684
|
+
* console.log(`Success: ${result.successfulCount}, Failed: ${result.failedCount}`);
|
|
685
|
+
* ```
|
|
686
|
+
*/
|
|
687
|
+
async multiTransferTokens(fromAddress, tokenAddress, recipients, chainId, onProgress) {
|
|
688
|
+
const chainInfo = (0, chains_1.getChainConfig)(chainId);
|
|
689
|
+
const privateKey = await this.getPrivateKeyFor(fromAddress);
|
|
690
|
+
let chainService;
|
|
691
|
+
if (chainInfo.type === 'solana') {
|
|
692
|
+
chainService = new SolanaChainService_1.SolanaChainService(privateKey, chainInfo);
|
|
693
|
+
}
|
|
694
|
+
else {
|
|
695
|
+
chainService = new ChainService_1.ChainService(privateKey, chainInfo);
|
|
696
|
+
}
|
|
697
|
+
const multiTransferService = new MultiTransferService_1.MultiTransferService(this, chainService, chainId);
|
|
698
|
+
return multiTransferService.transferTokens(fromAddress, tokenAddress, recipients, chainId, { onProgress });
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Estimates gas cost for a native token transfer.
|
|
702
|
+
* Useful for displaying gas costs to users before sending transactions.
|
|
703
|
+
*
|
|
704
|
+
* @param fromAddress - The sender's account address
|
|
705
|
+
* @param to - The recipient's address
|
|
706
|
+
* @param amount - The amount to send as a string (e.g., "0.1")
|
|
707
|
+
* @param chainId - The ID of the blockchain for the transaction
|
|
708
|
+
* @returns Promise resolving to the estimated gas cost as a bigint
|
|
709
|
+
* @throws Error if estimation fails or addresses are invalid
|
|
710
|
+
*
|
|
711
|
+
* @example
|
|
712
|
+
* ```typescript
|
|
713
|
+
* const gasEstimate = await vault.estimateNativeTransferGas(
|
|
714
|
+
* '0x1234...',
|
|
715
|
+
* '0x5678...',
|
|
716
|
+
* '0.01', // 0.01 ETH
|
|
717
|
+
* '1' // Ethereum
|
|
718
|
+
* );
|
|
719
|
+
*
|
|
720
|
+
* console.log('Estimated gas:', gasEstimate.toString());
|
|
721
|
+
* console.log('Estimated cost (in ETH):', ethers.formatEther(gasEstimate * gasPrice));
|
|
722
|
+
* ```
|
|
723
|
+
*/
|
|
724
|
+
async estimateNativeTransferGas(fromAddress, to, amount, chainId) {
|
|
725
|
+
const chainInfo = (0, chains_1.getChainConfig)(chainId);
|
|
726
|
+
const privateKey = await this.getPrivateKeyFor(fromAddress);
|
|
727
|
+
if (chainInfo.type === 'solana') {
|
|
728
|
+
const chainService = new SolanaChainService_1.SolanaChainService(privateKey, chainInfo);
|
|
729
|
+
return chainService.estimateTransactionFee();
|
|
730
|
+
}
|
|
731
|
+
else {
|
|
732
|
+
const chainService = new ChainService_1.ChainService(privateKey, chainInfo);
|
|
733
|
+
const tx = {
|
|
734
|
+
to: to,
|
|
735
|
+
value: ethers_1.ethers.parseEther(amount)
|
|
736
|
+
};
|
|
737
|
+
return chainService.estimateGas(tx);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Estimates gas cost for an ERC-20/SPL token transfer.
|
|
742
|
+
* Useful for displaying gas costs to users before sending token transactions.
|
|
743
|
+
*
|
|
744
|
+
* @param fromAddress - The sender's account address
|
|
745
|
+
* @param tokenAddress - The contract address of the token
|
|
746
|
+
* @param to - The recipient's address
|
|
747
|
+
* @param amount - The amount of tokens to send as a string
|
|
748
|
+
* @param chainId - The ID of the blockchain for the transaction
|
|
749
|
+
* @returns Promise resolving to the estimated gas cost as a bigint
|
|
750
|
+
* @throws Error if estimation fails, token contract is invalid, or addresses are invalid
|
|
751
|
+
*
|
|
752
|
+
* @example
|
|
753
|
+
* ```typescript
|
|
754
|
+
* const gasEstimate = await vault.estimateTokenTransferGas(
|
|
755
|
+
* '0x1234...',
|
|
756
|
+
* '0xA0b86a33E6441b8c4C8C8C8C8C8C8C8C8C8C8C8C', // USDC
|
|
757
|
+
* '0x5678...',
|
|
758
|
+
* '100', // 100 USDC
|
|
759
|
+
* '1' // Ethereum
|
|
760
|
+
* );
|
|
761
|
+
*
|
|
762
|
+
* console.log('Estimated gas for token transfer:', gasEstimate.toString());
|
|
763
|
+
* ```
|
|
764
|
+
*/
|
|
765
|
+
async estimateTokenTransferGas(fromAddress, tokenAddress, to, amount, chainId) {
|
|
766
|
+
const chainInfo = (0, chains_1.getChainConfig)(chainId);
|
|
767
|
+
const privateKey = await this.getPrivateKeyFor(fromAddress);
|
|
768
|
+
if (chainInfo.type === 'solana') {
|
|
769
|
+
const chainService = new SolanaChainService_1.SolanaChainService(privateKey, chainInfo);
|
|
770
|
+
return chainService.estimateTransactionFee();
|
|
771
|
+
}
|
|
772
|
+
else {
|
|
773
|
+
const chainService = new ChainService_1.ChainService(privateKey, chainInfo);
|
|
774
|
+
// Get token info to parse amount correctly
|
|
775
|
+
const tokenInfo = await chainService.getTokenInfo(tokenAddress);
|
|
776
|
+
const parsedAmount = ethers_1.ethers.parseUnits(amount, tokenInfo.decimals);
|
|
777
|
+
// Create contract instance for gas estimation
|
|
778
|
+
const contract = new ethers_1.ethers.Contract(tokenAddress, [
|
|
779
|
+
"function transfer(address to, uint amount) returns (bool)"
|
|
780
|
+
], chainService['wallet'].provider);
|
|
781
|
+
// Estimate gas for token transfer
|
|
782
|
+
return contract.transfer.estimateGas(to, parsedAmount);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Estimates gas cost for multi-transfer operations.
|
|
787
|
+
* Useful for displaying total gas costs before executing batch transfers.
|
|
788
|
+
*
|
|
789
|
+
* @param fromAddress - The sender's account address
|
|
790
|
+
* @param recipients - Array of recipients with addresses and amounts
|
|
791
|
+
* @param chainId - The ID of the blockchain for the transaction
|
|
792
|
+
* @param isNative - Whether estimating for native tokens (true) or ERC-20/SPL tokens (false)
|
|
793
|
+
* @param tokenAddress - Token contract address (required if isNative is false)
|
|
794
|
+
* @returns Promise resolving to the estimated total gas cost as a bigint
|
|
795
|
+
* @throws Error if estimation fails or parameters are invalid
|
|
796
|
+
*
|
|
797
|
+
* @example
|
|
798
|
+
* ```typescript
|
|
799
|
+
* const recipients = [
|
|
800
|
+
* { address: '0x1111...', amount: '0.01' },
|
|
801
|
+
* { address: '0x2222...', amount: '0.02' },
|
|
802
|
+
* { address: '0x3333...', amount: '0.005' }
|
|
803
|
+
* ];
|
|
804
|
+
*
|
|
805
|
+
* // Estimate for native token multi-transfer
|
|
806
|
+
* const nativeGasEstimate = await vault.estimateMultiTransferGas(
|
|
807
|
+
* '0x1234...',
|
|
808
|
+
* recipients,
|
|
809
|
+
* '1', // Ethereum
|
|
810
|
+
* true // Native tokens
|
|
811
|
+
* );
|
|
812
|
+
*
|
|
813
|
+
* // Estimate for token multi-transfer
|
|
814
|
+
* const tokenGasEstimate = await vault.estimateMultiTransferGas(
|
|
815
|
+
* '0x1234...',
|
|
816
|
+
* recipients,
|
|
817
|
+
* '1', // Ethereum
|
|
818
|
+
* false, // ERC-20 tokens
|
|
819
|
+
* '0xA0b86a33E6441b8c4C8C8C8C8C8C8C8C8C8C8C8C' // USDC
|
|
820
|
+
* );
|
|
821
|
+
*
|
|
822
|
+
* console.log('Native multi-transfer gas:', nativeGasEstimate.toString());
|
|
823
|
+
* console.log('Token multi-transfer gas:', tokenGasEstimate.toString());
|
|
824
|
+
* ```
|
|
825
|
+
*/
|
|
826
|
+
async estimateMultiTransferGas(fromAddress, recipients, chainId, isNative = true, tokenAddress) {
|
|
827
|
+
const chainInfo = (0, chains_1.getChainConfig)(chainId);
|
|
828
|
+
const privateKey = await this.getPrivateKeyFor(fromAddress);
|
|
829
|
+
let chainService;
|
|
830
|
+
if (chainInfo.type === 'solana') {
|
|
831
|
+
chainService = new SolanaChainService_1.SolanaChainService(privateKey, chainInfo);
|
|
832
|
+
}
|
|
833
|
+
else {
|
|
834
|
+
chainService = new ChainService_1.ChainService(privateKey, chainInfo);
|
|
835
|
+
}
|
|
836
|
+
const multiTransferService = new MultiTransferService_1.MultiTransferService(this, chainService, chainId);
|
|
837
|
+
return multiTransferService.estimateGasCost(recipients, chainId, isNative, tokenAddress);
|
|
838
|
+
}
|
|
839
|
+
// --- NFT Methods ---
|
|
840
|
+
/**
|
|
841
|
+
* Gets all NFTs owned by an address across multiple collections.
|
|
842
|
+
* Pure blockchain implementation - reads from smart contracts.
|
|
843
|
+
*
|
|
844
|
+
* @param address - Wallet address to check
|
|
845
|
+
* @param contractAddresses - Array of NFT contract addresses to check
|
|
846
|
+
* @param options - NFT query options
|
|
847
|
+
* @returns Promise resolving to array of NFT details
|
|
848
|
+
* @throws Error if any contract query fails
|
|
849
|
+
*
|
|
850
|
+
* @example
|
|
851
|
+
* ```typescript
|
|
852
|
+
* const collections = [
|
|
853
|
+
* '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D', // BAYC
|
|
854
|
+
* '0x60E4d786628Fea6478F785A6d7e704777c86a7c6' // MAYC
|
|
855
|
+
* ];
|
|
856
|
+
*
|
|
857
|
+
* const nfts = await vault.getOwnedNFTs(
|
|
858
|
+
* '0x1234...', // Wallet address
|
|
859
|
+
* collections,
|
|
860
|
+
* { includeMetadata: true }
|
|
861
|
+
* );
|
|
862
|
+
*
|
|
863
|
+
* console.log(`Found ${nfts.length} owned NFTs`);
|
|
864
|
+
* nfts.forEach(nft => {
|
|
865
|
+
* console.log(`${nft.name} from ${nft.contractAddress}`);
|
|
866
|
+
* });
|
|
867
|
+
* ```
|
|
868
|
+
*/
|
|
869
|
+
async getOwnedNFTs(address, contractAddresses, options = {}) {
|
|
870
|
+
const nftService = new NFTService_1.NFTService(this.chainId, this, address);
|
|
871
|
+
const nfts = [];
|
|
872
|
+
for (const contractAddress of contractAddresses) {
|
|
873
|
+
try {
|
|
874
|
+
const balance = await nftService.getNFTBalance(address, contractAddress);
|
|
875
|
+
for (const tokenId of balance.tokenIds) {
|
|
876
|
+
try {
|
|
877
|
+
const nftDetail = await nftService.getNFTDetail(contractAddress, tokenId, options);
|
|
878
|
+
nfts.push(nftDetail);
|
|
879
|
+
}
|
|
880
|
+
catch (error) {
|
|
881
|
+
console.warn(`Failed to get NFT detail for ${contractAddress}:${tokenId}:`, error);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
catch (error) {
|
|
886
|
+
console.warn(`Failed to get balance for contract ${contractAddress}:`, error);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return nfts;
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Transfers an NFT from one address to another.
|
|
893
|
+
* Pure blockchain implementation using smart contract calls.
|
|
894
|
+
*
|
|
895
|
+
* @param fromAddress - Sender's address
|
|
896
|
+
* @param toAddress - Recipient's address
|
|
897
|
+
* @param contractAddress - NFT contract address
|
|
898
|
+
* @param tokenId - Token ID to transfer
|
|
899
|
+
* @returns Promise resolving to transaction response
|
|
900
|
+
* @throws Error if transfer fails, insufficient permissions, or NFT not owned
|
|
901
|
+
*
|
|
902
|
+
* @example
|
|
903
|
+
* ```typescript
|
|
904
|
+
* const result = await vault.transferNFT(
|
|
905
|
+
* '0x1234...', // From address
|
|
906
|
+
* '0x5678...', // To address
|
|
907
|
+
* '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D', // BAYC contract
|
|
908
|
+
* '1234' // Token ID
|
|
909
|
+
* );
|
|
910
|
+
*
|
|
911
|
+
* console.log('Transfer successful:', result.hash);
|
|
912
|
+
* ```
|
|
913
|
+
*/
|
|
914
|
+
async transferNFT(fromAddress, toAddress, contractAddress, tokenId) {
|
|
915
|
+
const chainInfo = (0, chains_1.getChainConfig)(this.chainId);
|
|
916
|
+
const privateKey = await this.getPrivateKeyFor(fromAddress);
|
|
917
|
+
if (chainInfo.type === 'solana') {
|
|
918
|
+
const chainService = new SolanaChainService_1.SolanaChainService(privateKey, chainInfo);
|
|
919
|
+
// Solana NFT transfer implementation would go here
|
|
920
|
+
throw new Error('Solana NFT transfer not yet implemented');
|
|
921
|
+
}
|
|
922
|
+
else {
|
|
923
|
+
const chainService = new ChainService_1.ChainService(privateKey, chainInfo);
|
|
924
|
+
// Create ERC-721 contract instance
|
|
925
|
+
const contract = new ethers_1.ethers.Contract(contractAddress, [
|
|
926
|
+
"function transferFrom(address from, address to, uint256 tokenId)",
|
|
927
|
+
"function ownerOf(uint256 tokenId) view returns (address)"
|
|
928
|
+
], chainService['wallet']);
|
|
929
|
+
// Verify ownership
|
|
930
|
+
const owner = await contract.ownerOf(tokenId);
|
|
931
|
+
if (owner.toLowerCase() !== fromAddress.toLowerCase()) {
|
|
932
|
+
throw new Error('NFT is not owned by the specified address');
|
|
933
|
+
}
|
|
934
|
+
// Transfer NFT
|
|
935
|
+
const tx = await contract.transferFrom(fromAddress, toAddress, tokenId);
|
|
936
|
+
const receipt = await tx.wait();
|
|
937
|
+
return {
|
|
938
|
+
hash: receipt.hash,
|
|
939
|
+
from: fromAddress,
|
|
940
|
+
to: toAddress,
|
|
941
|
+
blockNumber: receipt.blockNumber
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
// --- QR Code Methods ---
|
|
946
|
+
/**
|
|
947
|
+
* Imports wallet from QR code data.
|
|
948
|
+
* @param qrString - QR code string to parse
|
|
949
|
+
* @param password - Password to decrypt the mnemonic
|
|
950
|
+
* @returns Promise resolving to imported account information
|
|
951
|
+
* @throws Error if QR data is invalid or import fails
|
|
952
|
+
*
|
|
953
|
+
* @example
|
|
954
|
+
* ```typescript
|
|
955
|
+
* // Import wallet from scanned QR code
|
|
956
|
+
* const accounts = await vault.importFromQR(qrString, 'my-password');
|
|
957
|
+
* console.log('Imported accounts:', accounts);
|
|
958
|
+
* ```
|
|
959
|
+
*/
|
|
960
|
+
async importFromQR(qrString, password) {
|
|
961
|
+
try {
|
|
962
|
+
const importData = QRCodeService_1.QRCodeService.extractWalletImportData(qrString);
|
|
963
|
+
// Decrypt the mnemonic
|
|
964
|
+
const mnemonic = await EncryptionService_1.EncryptionService.decrypt(importData.encryptedMnemonic, password);
|
|
965
|
+
// Create new keyring from mnemonic
|
|
966
|
+
let newKeyring;
|
|
967
|
+
if (importData.chainType === 'solana') {
|
|
968
|
+
newKeyring = new SolanaKeyring_1.SolanaKeyring(mnemonic);
|
|
969
|
+
}
|
|
970
|
+
else {
|
|
971
|
+
newKeyring = new HDKeyring_1.HDKeyring(mnemonic);
|
|
972
|
+
await newKeyring.initialize();
|
|
973
|
+
}
|
|
974
|
+
// Add the keyring to vault
|
|
975
|
+
this.keyrings.push(newKeyring);
|
|
976
|
+
// Update chain ID if needed
|
|
977
|
+
if (importData.chainType === 'solana' && this.chainId !== 'solana') {
|
|
978
|
+
this.chainId = 'solana';
|
|
979
|
+
}
|
|
980
|
+
// Return imported accounts
|
|
981
|
+
const accounts = [];
|
|
982
|
+
for (let i = 0; i < Math.min(importData.accountCount, newKeyring.accounts.length); i++) {
|
|
983
|
+
accounts.push({
|
|
984
|
+
address: newKeyring.accounts[i],
|
|
985
|
+
type: newKeyring.type,
|
|
986
|
+
name: `${newKeyring.type} ${i + 1}`
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
return accounts;
|
|
990
|
+
}
|
|
991
|
+
catch (error) {
|
|
992
|
+
throw new Error(`Failed to import wallet from QR: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Processes transaction from QR code data.
|
|
997
|
+
* @param qrString - QR code string to parse
|
|
998
|
+
* @param fromAddress - Sender's address
|
|
999
|
+
* @returns Promise resolving to transaction response
|
|
1000
|
+
* @throws Error if QR data is invalid or transaction fails
|
|
1001
|
+
*
|
|
1002
|
+
* @example
|
|
1003
|
+
* ```typescript
|
|
1004
|
+
* // Process transaction from scanned QR code
|
|
1005
|
+
* const result = await vault.processTransactionFromQR(qrString, '0x1234...');
|
|
1006
|
+
* console.log('Transaction hash:', result.hash);
|
|
1007
|
+
* ```
|
|
1008
|
+
*/
|
|
1009
|
+
async processTransactionFromQR(qrString, fromAddress) {
|
|
1010
|
+
try {
|
|
1011
|
+
const txData = QRCodeService_1.QRCodeService.extractTransactionData(qrString);
|
|
1012
|
+
// Validate sender address
|
|
1013
|
+
const accounts = this.getAccounts();
|
|
1014
|
+
const senderAccount = accounts.find(acc => acc.address.toLowerCase() === fromAddress.toLowerCase());
|
|
1015
|
+
if (!senderAccount) {
|
|
1016
|
+
throw new Error('Sender address not found in wallet accounts');
|
|
1017
|
+
}
|
|
1018
|
+
// Execute transaction based on type
|
|
1019
|
+
switch (txData.txType) {
|
|
1020
|
+
case 'native':
|
|
1021
|
+
return await this.sendNativeToken(fromAddress, txData.to, txData.amount, txData.chainId);
|
|
1022
|
+
case 'token':
|
|
1023
|
+
if (!txData.tokenAddress) {
|
|
1024
|
+
throw new Error('Token address required for token transfer');
|
|
1025
|
+
}
|
|
1026
|
+
return await this.sendToken(fromAddress, txData.to, txData.amount, txData.tokenAddress, txData.chainId);
|
|
1027
|
+
case 'nft':
|
|
1028
|
+
if (!txData.tokenAddress || !txData.tokenId) {
|
|
1029
|
+
throw new Error('Token address and token ID required for NFT transfer');
|
|
1030
|
+
}
|
|
1031
|
+
return await this.transferNFT(fromAddress, txData.to, txData.tokenAddress, txData.tokenId);
|
|
1032
|
+
default:
|
|
1033
|
+
throw new Error(`Unsupported transaction type: ${txData.txType}`);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
catch (error) {
|
|
1037
|
+
throw new Error(`Failed to process transaction from QR: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Sends native tokens (ETH, BNB, MATIC, etc.) from a specific account to another address.
|
|
1042
|
+
* @param fromAddress - The sender's account address
|
|
1043
|
+
* @param to - The recipient's address
|
|
1044
|
+
* @param amount - The amount to send as a string (e.g., "0.1")
|
|
1045
|
+
* @param chainId - The ID of the blockchain for the transaction
|
|
1046
|
+
* @returns Promise resolving to the transaction response
|
|
1047
|
+
* @throws Error if insufficient balance, invalid addresses, or transaction fails
|
|
1048
|
+
*
|
|
1049
|
+
* @example
|
|
1050
|
+
* ```typescript
|
|
1051
|
+
* // Send 0.1 ETH on Ethereum mainnet
|
|
1052
|
+
* const tx = await vault.sendNativeToken(from, to, '0.1', '1');
|
|
1053
|
+
* console.log('Transaction hash:', tx.hash);
|
|
1054
|
+
* ```
|
|
1055
|
+
*/
|
|
1056
|
+
async sendNativeToken(fromAddress, to, amount, chainId) {
|
|
1057
|
+
const chainInfo = (0, chains_1.getChainConfig)(chainId);
|
|
1058
|
+
const privateKey = await this.getPrivateKeyFor(fromAddress);
|
|
1059
|
+
if (chainInfo.type === 'solana') {
|
|
1060
|
+
const chainService = new SolanaChainService_1.SolanaChainService(privateKey, chainInfo);
|
|
1061
|
+
const result = await chainService.sendTransaction(to, amount);
|
|
1062
|
+
return {
|
|
1063
|
+
hash: result.hash,
|
|
1064
|
+
from: result.from,
|
|
1065
|
+
to: result.to,
|
|
1066
|
+
blockNumber: result.blockNumber || undefined
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
else {
|
|
1070
|
+
const chainService = new ChainService_1.ChainService(privateKey, chainInfo);
|
|
1071
|
+
// Create transaction request for native token transfer
|
|
1072
|
+
const txRequest = {
|
|
1073
|
+
to: to,
|
|
1074
|
+
value: ethers_1.ethers.parseEther(amount)
|
|
1075
|
+
};
|
|
1076
|
+
const result = await chainService.sendTransaction(txRequest);
|
|
1077
|
+
return {
|
|
1078
|
+
hash: result.hash,
|
|
1079
|
+
from: fromAddress,
|
|
1080
|
+
to: to,
|
|
1081
|
+
blockNumber: result.blockNumber || undefined
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
exports.Vault = Vault;
|
|
1087
|
+
// Rate limiting for export attempts
|
|
1088
|
+
Vault.exportAttempts = new Map();
|
|
1089
|
+
Vault.MAX_EXPORT_ATTEMPTS = 5;
|
|
1090
|
+
Vault.EXPORT_COOLDOWN_MS = 30000; // 30 seconds
|