-
Notifications
You must be signed in to change notification settings - Fork 1.3k
/
wallet.ts
283 lines (236 loc) · 7.94 KB
/
wallet.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
import type { BytesLike } from '@ethersproject/bytes';
import { NativeAssetId } from '@fuel-ts/constants';
import { hashMessage, hashTransaction } from '@fuel-ts/hasher';
import { HDWallet } from '@fuel-ts/hdwallet';
import { AbstractWallet } from '@fuel-ts/interfaces';
import type { BigNumberish } from '@fuel-ts/math';
import { Mnemonic } from '@fuel-ts/mnemonic';
import { ScriptTransactionRequest, transactionRequestify, Provider } from '@fuel-ts/providers';
import type {
TransactionRequest,
TransactionResponse,
Coin,
TransactionRequestLike,
CoinQuantityLike,
CoinQuantity,
CallResult,
} from '@fuel-ts/providers';
import { Signer } from '@fuel-ts/signer';
import type { GenerateOptions } from './types/GenerateOptions';
// TODO: import using .env file
const FUEL_NETWORK_URL = 'http://127.0.0.1:4000/graphql';
export default class Wallet extends AbstractWallet {
/* default HDWallet path */
static defaultPath = "m/44'/1179993420'/0'/0/0";
provider: Provider;
readonly signer: () => Signer;
constructor(privateKey: BytesLike, provider: string | Provider = FUEL_NETWORK_URL) {
super();
const signer = new Signer(privateKey);
this.signer = () => signer;
this.provider = this.connect(provider);
}
get address(): string {
return this.signer().address;
}
get privateKey(): string {
return this.signer().privateKey;
}
get publicKey(): string {
return this.signer().publicKey;
}
/**
* Change provider connection
*/
connect(provider: string | Provider) {
if (!provider) {
throw new Error('Provider is required');
} else if (typeof provider === 'string') {
this.provider = new Provider(provider);
} else {
this.provider = provider;
}
return this.provider;
}
/**
* Sign message with wallet instance privateKey
*
* @param message - Message
* @returns string - Signature a ECDSA 64 bytes
*/
signMessage(message: string): string {
return this.signer().sign(hashMessage(message));
}
/**
* Sign transaction with wallet instance privateKey
*
* @param transactionRequestLike - TransactionRequestLike
* @returns string - Signature a ECDSA 64 bytes
*/
signTransaction(transactionRequestLike: TransactionRequestLike): string {
const transactionRequest = transactionRequestify(transactionRequestLike);
const hashedTransaction = hashTransaction(transactionRequest);
const signature = this.signer().sign(hashedTransaction);
return signature;
}
populateTransactionWitnessesSignature(transactionRequestLike: TransactionRequestLike) {
const transactionRequest = transactionRequestify(transactionRequestLike);
const witnessIndex = transactionRequest.getCoinInputWitnessIndexByOwner(this.address);
if (typeof witnessIndex === 'number') {
const signedTransaction = this.signTransaction(transactionRequest);
transactionRequest.updateWitness(witnessIndex, signedTransaction);
}
return transactionRequest;
}
/**
* Returns coins satisfying the spend query.
*/
async getCoinsToSpend(quantities: CoinQuantityLike[]): Promise<Coin[]> {
return this.provider.getCoinsToSpend(this.address, quantities);
}
/**
* Gets coins owned by the wallet address.
*/
async getCoins(): Promise<Coin[]> {
const coins = [];
const pageSize = 9999;
let cursor;
// eslint-disable-next-line no-unreachable-loop
for (;;) {
const pageCoins = await this.provider.getCoins(this.address, undefined, {
first: pageSize,
after: cursor,
});
coins.push(...pageCoins);
const hasNextPage = pageCoins.length >= pageSize;
if (!hasNextPage) {
break;
}
// TODO: implement pagination
throw new Error(`Wallets with more than ${pageSize} coins are not yet supported`);
}
return coins;
}
/**
* Gets balance for the given asset.
*/
async getBalance(assetId: BytesLike = NativeAssetId): Promise<bigint> {
const amount = await this.provider.getBalance(this.address, assetId);
return amount;
}
/**
* Gets balances.
*/
async getBalances(): Promise<CoinQuantity[]> {
const balances = [];
const pageSize = 9999;
let cursor;
// eslint-disable-next-line no-unreachable-loop
for (;;) {
const pageBalances = await this.provider.getBalances(this.address, {
first: pageSize,
after: cursor,
});
balances.push(...pageBalances);
const hasNextPage = pageBalances.length >= pageSize;
if (!hasNextPage) {
break;
}
// TODO: implement pagination
throw new Error(`Wallets with more than ${pageSize} balances are not yet supported`);
}
return balances;
}
/**
* Adds coins to the transaction enough to fund it.
*/
async fund<T extends TransactionRequest>(request: T): Promise<void> {
const feeAmount = request.calculateFee();
const coins = await this.getCoinsToSpend([[feeAmount, NativeAssetId]]);
request.addCoins(coins);
}
/**
* Returns coins satisfying the spend query.
*/
async transfer(
/** Address of the destination */
destination: BytesLike,
/** Amount of coins */
amount: BigNumberish,
/** Asset ID of coins */
assetId: BytesLike = NativeAssetId,
/** Tx Params */
txParams: Pick<TransactionRequestLike, 'gasLimit' | 'gasPrice' | 'bytePrice' | 'maturity'> = {}
): Promise<TransactionResponse> {
const params = { gasLimit: 10000, ...txParams };
const request = new ScriptTransactionRequest(params);
request.addCoinOutput(destination, amount, assetId);
const feeAmount = request.calculateFee();
const coins = await this.getCoinsToSpend([
[amount, assetId],
[feeAmount, NativeAssetId],
]);
request.addCoins(coins);
return this.sendTransaction(request);
}
/**
* Populates witnesses signature and send it to the network using `provider.sendTransaction`.
*
* @param transactionRequest - TransactionRequest
* @returns TransactionResponse
*/
async sendTransaction(
transactionRequestLike: TransactionRequestLike
): Promise<TransactionResponse> {
const transactionRequest = transactionRequestify(transactionRequestLike);
return this.provider.sendTransaction(
this.populateTransactionWitnessesSignature(transactionRequest)
);
}
/**
* Populates witnesses signature and send a call it to the network using `provider.call`.
*
* @param transactionRequest - TransactionRequest
* @returns CallResult
*/
async simulateTransaction(transactionRequestLike: TransactionRequestLike): Promise<CallResult> {
const transactionRequest = transactionRequestify(transactionRequestLike);
return this.provider.call(this.populateTransactionWitnessesSignature(transactionRequest), {
utxoValidation: true,
});
}
/**
* Generate a new Wallet with a random keyPair
*
* @param options - GenerateOptions
* @returns wallet - Wallet instance
*/
static generate(generateOptions?: GenerateOptions) {
const privateKey = Signer.generatePrivateKey(generateOptions?.entropy);
return new Wallet(privateKey, generateOptions?.provider);
}
/**
* Create wallet from a seed
*/
static fromSeed(seed: string, path?: string): Wallet {
const hdWallet = HDWallet.fromSeed(seed);
const childWallet = hdWallet.derivePath(path || Wallet.defaultPath);
return new Wallet(<string>childWallet.privateKey);
}
/**
* Create wallet from mnemonic phrase
*/
static fromMnemonic(mnemonic: string, path?: string, passphrase?: BytesLike): Wallet {
const seed = Mnemonic.mnemonicToSeed(mnemonic, passphrase);
const hdWallet = HDWallet.fromSeed(seed);
const childWallet = hdWallet.derivePath(path || Wallet.defaultPath);
return new Wallet(<string>childWallet.privateKey);
}
/**
* Create wallet from extended key
*/
static fromExtendedKey(extendedKey: string): Wallet {
const hdWallet = HDWallet.fromExtendedKey(extendedKey);
return new Wallet(<string>hdWallet.privateKey);
}
}