mainnet-js 2.6.7 → 2.7.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.
Files changed (98) hide show
  1. package/dist/index.html +1 -1
  2. package/dist/{mainnet-2.6.7.js → mainnet-2.7.0.js} +446 -156
  3. package/dist/module/cli.js +0 -4
  4. package/dist/module/cli.js.map +1 -1
  5. package/dist/module/index.d.ts +1 -3
  6. package/dist/module/index.d.ts.map +1 -1
  7. package/dist/module/index.js +1 -3
  8. package/dist/module/index.js.map +1 -1
  9. package/dist/module/network/ElectrumNetworkProvider.d.ts +21 -27
  10. package/dist/module/network/ElectrumNetworkProvider.d.ts.map +1 -1
  11. package/dist/module/network/ElectrumNetworkProvider.js +92 -102
  12. package/dist/module/network/ElectrumNetworkProvider.js.map +1 -1
  13. package/dist/module/network/NetworkProvider.d.ts +9 -22
  14. package/dist/module/network/NetworkProvider.d.ts.map +1 -1
  15. package/dist/module/network/constant.d.ts +0 -21
  16. package/dist/module/network/constant.d.ts.map +1 -1
  17. package/dist/module/network/constant.js +0 -21
  18. package/dist/module/network/constant.js.map +1 -1
  19. package/dist/module/network/default.d.ts +2 -2
  20. package/dist/module/network/default.d.ts.map +1 -1
  21. package/dist/module/network/default.js +15 -45
  22. package/dist/module/network/default.js.map +1 -1
  23. package/dist/module/network/interface.d.ts +2 -8
  24. package/dist/module/network/interface.d.ts.map +1 -1
  25. package/dist/module/network/util.d.ts.map +1 -1
  26. package/dist/module/network/util.js +4 -5
  27. package/dist/module/network/util.js.map +1 -1
  28. package/dist/module/rate/ExchangeRate.js +2 -1
  29. package/dist/module/rate/ExchangeRate.js.map +1 -1
  30. package/dist/module/transaction/Wif.d.ts.map +1 -1
  31. package/dist/module/transaction/Wif.js +1 -1
  32. package/dist/module/transaction/Wif.js.map +1 -1
  33. package/dist/module/wallet/Base.d.ts +282 -88
  34. package/dist/module/wallet/Base.d.ts.map +1 -1
  35. package/dist/module/wallet/Base.js +1058 -215
  36. package/dist/module/wallet/Base.js.map +1 -1
  37. package/dist/module/wallet/Util.d.ts +7 -54
  38. package/dist/module/wallet/Util.d.ts.map +1 -1
  39. package/dist/module/wallet/Util.js +12 -79
  40. package/dist/module/wallet/Util.js.map +1 -1
  41. package/dist/module/wallet/Wif.d.ts +46 -251
  42. package/dist/module/wallet/Wif.d.ts.map +1 -1
  43. package/dist/module/wallet/Wif.js +126 -1026
  44. package/dist/module/wallet/Wif.js.map +1 -1
  45. package/dist/module/wallet/createWallet.d.ts +2 -1
  46. package/dist/module/wallet/createWallet.d.ts.map +1 -1
  47. package/dist/module/wallet/createWallet.js +2 -3
  48. package/dist/module/wallet/createWallet.js.map +1 -1
  49. package/dist/module/wallet/interface.d.ts +2 -4
  50. package/dist/module/wallet/interface.d.ts.map +1 -1
  51. package/dist/tsconfig.tsbuildinfo +1 -1
  52. package/package.json +6 -12
  53. package/src/cli.ts +0 -4
  54. package/src/index.ts +1 -5
  55. package/src/network/ElectrumNetworkProvider.ts +133 -188
  56. package/src/network/NetworkProvider.ts +9 -30
  57. package/src/network/Rpc.test.ts +14 -5
  58. package/src/network/constant.ts +0 -23
  59. package/src/network/default.ts +26 -66
  60. package/src/network/electrum.test.ts +2 -4
  61. package/src/network/interface.ts +2 -9
  62. package/src/network/util.ts +6 -7
  63. package/src/rate/ExchangeRate.test.ts +1 -1
  64. package/src/rate/ExchangeRate.ts +2 -1
  65. package/{polyfill/json.js → src/test/json.test.ts} +7 -1
  66. package/src/transaction/Wif.ts +2 -1
  67. package/src/wallet/Base.ts +1520 -273
  68. package/src/wallet/Cashtokens.test.headless.js +1 -1
  69. package/src/wallet/Cashtokens.test.ts +7 -8
  70. package/src/wallet/Util.ts +20 -102
  71. package/src/wallet/Wif.bip39.test.ts +3 -3
  72. package/src/wallet/Wif.test.ts +31 -25
  73. package/src/wallet/Wif.ts +174 -1493
  74. package/src/wallet/Wif.watchOnly.test.ts +5 -5
  75. package/src/wallet/createWallet.ts +11 -10
  76. package/src/wallet/interface.ts +3 -4
  77. package/webpack.config.cjs +4 -55
  78. package/dist/module/qr/Qr.d.ts +0 -9
  79. package/dist/module/qr/Qr.d.ts.map +0 -1
  80. package/dist/module/qr/Qr.js +0 -22
  81. package/dist/module/qr/Qr.js.map +0 -1
  82. package/dist/module/qr/interface.d.ts +0 -6
  83. package/dist/module/qr/interface.d.ts.map +0 -1
  84. package/dist/module/qr/interface.js +0 -2
  85. package/dist/module/qr/interface.js.map +0 -1
  86. package/dist/module/util/eventsource.d.ts +0 -3
  87. package/dist/module/util/eventsource.d.ts.map +0 -1
  88. package/dist/module/util/eventsource.js +0 -11
  89. package/dist/module/util/eventsource.js.map +0 -1
  90. package/polyfill/README.md +0 -1
  91. package/polyfill/eventsource.js +0 -6
  92. package/polyfill/support/types.js +0 -286
  93. package/polyfill/util.cjs +0 -249
  94. package/src/network/default.test.ts +0 -37
  95. package/src/qr/Qr.test.ts +0 -14
  96. package/src/qr/Qr.ts +0 -24
  97. package/src/qr/interface.ts +0 -5
  98. package/src/util/eventsource.ts +0 -12
@@ -1,38 +1,142 @@
1
- // import { getStorageProvider } from "../db/getStorageProvider.js";
2
- import { MnemonicI, WalletI } from "./interface.js";
3
- import { NetworkType } from "../enum.js";
4
- import { StorageProvider } from "../db/index.js";
5
- import { getRuntimePlatform } from "../util/getRuntimePlatform.js";
6
- import { qrAddress } from "../qr/Qr.js";
7
- import { ImageI } from "../qr/interface.js";
8
- import { WalletTypeEnum } from "./enum.js";
9
- import NetworkProvider from "../network/NetworkProvider.js";
1
+ import {
2
+ binToHex,
3
+ CashAddressNetworkPrefix,
4
+ CashAddressType,
5
+ decodeCashAddress,
6
+ encodeCashAddress,
7
+ IdentitySnapshot,
8
+ } from "@bitauth/libauth";
9
+ import { DUST_UTXO_THRESHOLD } from "../constant.js";
10
+ import StorageProvider from "../db/StorageProvider.js";
11
+ import {
12
+ NetworkEnum,
13
+ networkPrefixMap,
14
+ NetworkType,
15
+ prefixFromNetworkMap,
16
+ UnitEnum,
17
+ } from "../enum.js";
18
+ import { getAddressHistory } from "../history/electrumTransformer.js";
19
+ import { TransactionHistoryItem } from "../history/interface.js";
20
+ import { HexHeaderI, NFTCapability, TxI, UtxoI } from "../interface.js";
21
+ import { SignedMessage } from "../message/signed.js";
22
+ import { getNetworkProvider } from "../network/default.js";
23
+ import ElectrumNetworkProvider from "../network/ElectrumNetworkProvider.js";
24
+ import { getRelayFeeCache } from "../network/getRelayFeeCache.js";
25
+ import { ElectrumRawTransaction } from "../network/interface.js";
26
+ import {
27
+ buildEncodedTransaction,
28
+ getFeeAmount,
29
+ getFeeAmountSimple,
30
+ getSuitableUtxos,
31
+ } from "../transaction/Wif.js";
32
+ import {
33
+ balanceFromSatoshi,
34
+ BalanceResponse,
35
+ balanceResponseFromSatoshi,
36
+ } from "../util/balanceObjectFromSatoshi.js";
37
+ import { checkUtxos } from "../util/checkUtxos.js";
38
+ import { derivePrefix } from "../util/derivePublicKeyHash.js";
39
+ import {
40
+ amountInSatoshi,
41
+ asSendRequestObject,
42
+ deriveTokenaddr,
43
+ getRuntimePlatform,
44
+ hexToBin,
45
+ sumTokenAmounts,
46
+ sumUtxoValue,
47
+ toTokenaddr,
48
+ } from "../util/index.js";
49
+ import { sanitizeUnit } from "../util/sanitizeUnit.js";
50
+ import { sumSendRequestAmounts } from "../util/sumSendRequestAmounts.js";
51
+ import { BCMR } from "./Bcmr.js";
52
+ import { FeePaidByEnum, WalletTypeEnum } from "./enum.js";
53
+ import {
54
+ CancelFn,
55
+ SendRequestOptionsI,
56
+ WaitForTransactionOptions,
57
+ WaitForTransactionResponse,
58
+ WalletI,
59
+ WalletInfoI,
60
+ } from "./interface.js";
61
+ import {
62
+ fromUtxoId,
63
+ OpReturnData,
64
+ SendRequest,
65
+ SendRequestArray,
66
+ SendRequestType,
67
+ SendResponse,
68
+ TokenBurnRequest,
69
+ TokenGenesisRequest,
70
+ TokenMintRequest,
71
+ TokenSendRequest,
72
+ } from "./model.js";
73
+ import { Util } from "./Util.js";
74
+ import { Wallet } from "./Wif.js";
75
+
76
+ const placeholderPrivateKey =
77
+ "0000000000000000000000000000000000000000000000000000000000000001";
10
78
 
11
79
  /**
12
80
  * A class to hold features used by all wallets
13
81
  * @class BaseWallet
14
82
  */
15
83
  export class BaseWallet implements WalletI {
16
- provider?: NetworkProvider;
17
- derivationPath: string = "m/44'/0'/0'/0/0";
18
- parentDerivationPath: string = "m/44'/0'/0'";
19
- parentXPubKey?: string;
20
- mnemonic?: string;
21
- address?: string;
22
- privateKey?: any;
23
- publicKey?: any;
24
84
  public static StorageProvider?: typeof StorageProvider;
25
- isTestnet: boolean;
26
- name: string;
85
+
86
+ provider: ElectrumNetworkProvider;
27
87
  network: NetworkType;
28
88
  walletType: WalletTypeEnum;
89
+ _slpSemiAware: boolean = false; // a flag which requires an utxo to have more than 546 sats to be spendable and counted in the balance
90
+ publicKeyHash!: Uint8Array;
91
+ cashaddr!: string;
92
+ tokenaddr!: string;
93
+ isTestnet: boolean;
94
+ name: string = "";
95
+ _util?: Util;
96
+
97
+ public get networkPrefix(): CashAddressNetworkPrefix {
98
+ return prefixFromNetworkMap[this.network];
99
+ }
100
+
101
+ // interface to util functions. see Util.ts
102
+ public get util() {
103
+ if (!this._util) {
104
+ this._util = new Util(this.network);
105
+ }
106
+
107
+ return this._util;
108
+ }
109
+
110
+ // interface to util util. see Util.Util
111
+ public static get util() {
112
+ return new this().util;
113
+ }
114
+
115
+ // Return wallet info
116
+ public getInfo(): WalletInfoI {
117
+ return {
118
+ cashaddr: this.cashaddr,
119
+ tokenaddr: this.tokenaddr,
120
+ isTestnet: this.isTestnet,
121
+ name: this.name,
122
+ network: this.network as any,
123
+ publicKeyHash: binToHex(this.publicKeyHash),
124
+ walletId: this.toString(),
125
+ walletDbEntry: this.toDbString(),
126
+ };
127
+ }
128
+
129
+ public slpSemiAware(value: boolean = true): this {
130
+ this._slpSemiAware = value;
131
+ return this;
132
+ }
29
133
 
30
134
  //#region Accessors
31
135
  protected getNetworkProvider(
32
136
  // @ts-ignore
33
137
  network: NetworkType = NetworkType.Mainnet
34
138
  ): any {
35
- throw Error("getNetworkProvider called on base wallet");
139
+ return getNetworkProvider(network);
36
140
  }
37
141
 
38
142
  /**
@@ -45,109 +149,33 @@ export class BaseWallet implements WalletI {
45
149
  * @returns The deposit address as a string
46
150
  */
47
151
  public getDepositAddress(): string {
48
- return this.address!;
152
+ return this.cashaddr;
49
153
  }
50
154
 
51
155
  /**
52
- * getDepositQr - get an address qrcode, encoded for display on the web
53
- *
54
- * a high-level function
55
- *
56
- * @see {@link https://rest-unstable.mainnet.cash/api-docs/#/wallet/depositQr|/wallet/deposit_qr} for REST endpoint
156
+ * getTokenDepositAddress - get a cashtoken aware wallet deposit address
57
157
  *
58
- * @returns The qrcode for the address
158
+ * @returns The cashtoken aware deposit address as a string
59
159
  */
60
- public getDepositQr(): ImageI {
61
- return qrAddress(this.getDepositAddress() as string);
62
- }
63
-
64
- // Get mnemonic and derivation path for wallet
65
- public getSeed(): MnemonicI {
66
- if (!this.mnemonic) {
67
- throw Error("Wallet mnemonic seed phrase not set");
68
- }
69
- if (!this.derivationPath) {
70
- throw Error("Wallet derivation path not set");
71
- }
72
- return {
73
- seed: this.mnemonic,
74
- derivationPath: this.derivationPath,
75
- parentDerivationPath: this.parentDerivationPath,
76
- };
160
+ public getTokenDepositAddress(): string {
161
+ return this.tokenaddr;
77
162
  }
78
163
  //#endregion Accessors
79
164
 
80
165
  //#region Constructors and Statics
81
166
  /**
82
167
  * constructor for a new wallet
83
- * @param {string} name name of the wallet
84
168
  * @param network network for wallet
85
169
  *
86
170
  * @throws {Error} if called on BaseWallet
87
171
  */
88
- constructor(
89
- name: string = "",
90
- network = NetworkType.Mainnet,
91
- walletType = WalletTypeEnum.Seed
92
- ) {
93
- this.name = name;
172
+ constructor(network = NetworkType.Mainnet) {
94
173
  this.network = network;
95
- this.walletType = walletType;
174
+ this.walletType = WalletTypeEnum.Watch;
96
175
  this.provider = this.getNetworkProvider(this.network);
97
176
  this.isTestnet = this.network === NetworkType.Mainnet ? false : true;
98
177
  }
99
178
 
100
- /**
101
- * fromId - create a wallet from encoded walletId string
102
- *
103
- * @param walletId walletId options to steer the creation process
104
- *
105
- * @returns wallet instantiated accordingly to the walletId rules
106
- */
107
- public static async fromId<T extends typeof BaseWallet>(
108
- this: T,
109
- walletId: string
110
- ): Promise<InstanceType<T>> {
111
- return new this().fromId(walletId) as InstanceType<T>;
112
- }
113
-
114
- /**
115
- * fromSeed - create a wallet using the seed phrase and derivation path
116
- *
117
- * unless specified the derivation path m/44'/245'/0'/0/0 will be userd
118
- * this derivation path is standard for Electron Cash SLP and other SLP enabled wallets
119
- *
120
- * @param seed BIP39 12 word seed phrase
121
- * @param derivationPath BIP44 HD wallet derivation path to get a single the private key from hierarchy
122
- *
123
- * @returns instantiated wallet
124
- */
125
- public static async fromSeed<T extends typeof BaseWallet>(
126
- this: T,
127
- seed: string,
128
- derivationPath?: string
129
- ): Promise<InstanceType<T>> {
130
- return new this().fromSeed(seed, derivationPath) as InstanceType<T>;
131
- }
132
-
133
- /**
134
- * newRandom - create a random wallet
135
- *
136
- * if `name` parameter is specified, the wallet will also be persisted to DB
137
- *
138
- * @param name user friendly wallet alias
139
- * @param dbName name under which the wallet will be stored in the database
140
- *
141
- * @returns instantiated wallet
142
- */
143
- public static async newRandom<T extends typeof BaseWallet>(
144
- this: T,
145
- name: string = "",
146
- dbName?: string
147
- ): Promise<InstanceType<T>> {
148
- return new this().newRandom(name, dbName) as InstanceType<T>;
149
- }
150
-
151
179
  /**
152
180
  * watchOnly - create a watch-only wallet
153
181
  *
@@ -164,141 +192,8 @@ export class BaseWallet implements WalletI {
164
192
  ) {
165
193
  return new this().watchOnly(address) as InstanceType<T>;
166
194
  }
167
-
168
- /**
169
- * named - create a named wallet
170
- *
171
- * @param name user friendly wallet alias
172
- * @param dbName name under which the wallet will be stored in the database
173
- * @param force force recreate wallet in the database if a record already exist
174
- *
175
- * @returns instantiated wallet
176
- */
177
- public static async named<T extends typeof BaseWallet>(
178
- this: T,
179
- name: string,
180
- dbName?: string,
181
- force?: boolean
182
- ): Promise<InstanceType<T>> {
183
- return new this().named(name, dbName, force) as InstanceType<T>;
184
- }
185
-
186
- /**
187
- * replaceNamed - replace (recover) named wallet with a new walletId
188
- *
189
- * If wallet with a provided name does not exist yet, it will be created with a `walletId` supplied
190
- * If wallet exists it will be overwritten without exception
191
- *
192
- * @param name user friendly wallet alias
193
- * @param walletId walletId options to steer the creation process
194
- * @param dbName name under which the wallet will be stored in the database
195
- *
196
- * @returns instantiated wallet
197
- */
198
- public static async replaceNamed<T extends typeof BaseWallet>(
199
- this: T,
200
- name: string,
201
- walletId: string,
202
- dbName?: string
203
- ): Promise<InstanceType<T>> {
204
- return new this().replaceNamed(name, walletId, dbName) as InstanceType<T>;
205
- }
206
-
207
- /**
208
- * namedExists - check if a named wallet already exists
209
- *
210
- * @param name user friendly wallet alias
211
- * @param dbName name under which the wallet will be stored in the database
212
- *
213
- * @returns boolean
214
- */
215
- public static async namedExists(
216
- name: string,
217
- dbName?: string
218
- ): Promise<boolean> {
219
- return new this().namedExists(name, dbName);
220
- }
221
195
  //#endregion Constructors
222
196
 
223
- //#region Protected implementations
224
- /**
225
- * generate creates a new wallet
226
- * @throws {Error} if called on BaseWallet
227
- */
228
- protected generate(): Promise<this> {
229
- throw Error("generate called on base wallet");
230
- }
231
-
232
- /**
233
- * fromId - creates a wallet from serialized string
234
- *
235
- * @throws {Error} if called on BaseWallet
236
- */
237
- protected fromId(walletId: string): Promise<this> {
238
- let [walletType, networkGiven, arg1, arg2]: string[] = walletId.split(":");
239
-
240
- if (this.network != networkGiven) {
241
- throw Error(`Network prefix ${networkGiven} to a ${this.network} wallet`);
242
- }
243
- switch (walletType) {
244
- case "watch":
245
- if (arg2) {
246
- // watch:testnet:bchtest:qq1234567
247
- return this.watchOnly(`${arg1}:${arg2}`);
248
- }
249
- // watch:testnet:qq1234567
250
- return this.watchOnly(`${arg1}`);
251
-
252
- case "named":
253
- if (arg2) {
254
- // named:testnet:wallet_1:my_database
255
- return this.named(arg1, arg2);
256
- } else {
257
- // named:testnet:wallet_1
258
- return this.named(arg1);
259
- }
260
-
261
- case "seed":
262
- if (arg2) {
263
- // seed:testnet:table later ... stove kitten pluck:m/44'/0'/0'/0/0
264
- return this.fromSeed(arg1, arg2);
265
- }
266
- // seed:testnet:table later ... stove kitten pluck
267
- return this.fromSeed(arg1);
268
- default:
269
- throw Error(`Unknown wallet type '${walletType}'`);
270
- }
271
- }
272
-
273
- // Initialize wallet from a mnemonic phrase
274
- protected async fromSeed(
275
- // @ts-ignore
276
- mnemonic: string,
277
- // @ts-ignore
278
- derivationPath?: string
279
- ): Promise<this> {
280
- throw Error("fromSeed called on base wallet");
281
- }
282
-
283
- /**
284
- * newRandom (internal) if the wallet is named, get or create it; otherwise create a random
285
- * unnamed wallet
286
- * @param {string} name name of the wallet
287
- * @param {string} dbName database name the wallet is stored in
288
- */
289
- protected async newRandom(name: string, dbName?: string): Promise<this> {
290
- if (name.length > 0) {
291
- return this.named(name, dbName);
292
- } else {
293
- return this.generate();
294
- }
295
- }
296
-
297
- // @ts-ignore
298
- protected async watchOnly(address: string): Promise<this> {
299
- throw Error("fromSeed called on base wallet");
300
- }
301
-
302
197
  /**
303
198
  * named (internal) get a named wallet from the database or create a new one.
304
199
  * Note: this function should behave identically if
@@ -320,13 +215,13 @@ export class BaseWallet implements WalletI {
320
215
  }
321
216
  _checkContextSafety(this);
322
217
  this.name = name;
323
- dbName = dbName ? dbName : (this.network as string);
324
- let db = getStorageProvider(dbName);
218
+ dbName = dbName ? dbName : prefixFromNetworkMap[this.network];
219
+ const db = getStorageProvider(dbName);
325
220
 
326
221
  // If there is a database, force saving or error
327
222
  if (db) {
328
223
  await db.init();
329
- let savedWalletRecord = await db.getWallet(name);
224
+ const savedWalletRecord = await db.getWallet(name);
330
225
  if (savedWalletRecord) {
331
226
  await db.close();
332
227
  if (forceNew) {
@@ -334,11 +229,11 @@ export class BaseWallet implements WalletI {
334
229
  `A wallet with the name ${name} already exists in ${dbName}`
335
230
  );
336
231
  }
337
- let recoveredWallet = await this.fromId(savedWalletRecord.wallet);
232
+ const recoveredWallet = await this.fromId(savedWalletRecord.wallet);
338
233
  recoveredWallet.name = savedWalletRecord.name;
339
234
  return recoveredWallet;
340
235
  } else {
341
- let wallet = await this.generate();
236
+ const wallet = await this.generate();
342
237
  await db.addWallet(wallet.name, wallet.toDbString());
343
238
  await db.close();
344
239
  return wallet;
@@ -372,7 +267,7 @@ export class BaseWallet implements WalletI {
372
267
  }
373
268
  _checkContextSafety(this);
374
269
  this.name = name;
375
- dbName = dbName ? dbName : (this.network as string);
270
+ dbName = dbName ? dbName : prefixFromNetworkMap[this.network];
376
271
  let db = getStorageProvider(dbName);
377
272
 
378
273
  if (db) {
@@ -407,7 +302,7 @@ export class BaseWallet implements WalletI {
407
302
  throw Error("Named wallets must have a non-empty name");
408
303
  }
409
304
  _checkContextSafety(this);
410
- dbName = dbName ? dbName : (this.network as string);
305
+ dbName = dbName ? dbName : prefixFromNetworkMap[this.network];
411
306
  let db = getStorageProvider(dbName);
412
307
 
413
308
  if (db) {
@@ -421,7 +316,10 @@ export class BaseWallet implements WalletI {
421
316
  );
422
317
  }
423
318
  }
424
- //#endregion Protected implementations
319
+
320
+ protected async generate(): Promise<this> {
321
+ return this;
322
+ }
425
323
 
426
324
  //#region Serialization
427
325
  /**
@@ -430,13 +328,7 @@ export class BaseWallet implements WalletI {
430
328
  * @throws {Error} if called on BaseWallet
431
329
  */
432
330
  public toDbString(): string {
433
- if (this.walletType == WalletTypeEnum.Seed) {
434
- return `${this.walletType}:${this.network}:${this.mnemonic}:${this.derivationPath}`;
435
- } else if (this.walletType == WalletTypeEnum.Watch) {
436
- return `${this.walletType}:${this.network}:${this.getDepositAddress()}`;
437
- }
438
-
439
- return "";
331
+ return `${this.walletType}:${this.network}:${this.getDepositAddress()}`;
440
332
  }
441
333
 
442
334
  // Returns the serialized wallet as a string
@@ -444,39 +336,1394 @@ export class BaseWallet implements WalletI {
444
336
  // In all other cases, the a named wallet is deserialized from the database
445
337
  // by the name key
446
338
  public toString() {
447
- if (this.name) {
448
- return `named:${this.network}:${this.name}`;
449
- } else if (this.walletType == WalletTypeEnum.Seed) {
450
- return `${this.walletType}:${this.network}:${this.mnemonic}:${this.derivationPath}`;
451
- } else if (this.walletType == WalletTypeEnum.Watch) {
452
- return `${this.walletType}:${this.network}:${this.getDepositAddress()}`;
339
+ return `${this.walletType}:${this.network}:${this.getDepositAddress()}`;
340
+ }
341
+ //#endregion Serialization
342
+
343
+ /**
344
+ * explorerUrl Web url to a transaction on a block explorer
345
+ *
346
+ * @param txId transaction Id
347
+ * @returns Url string
348
+ */
349
+ public explorerUrl(txId: string) {
350
+ const explorerUrlMap = {
351
+ mainnet: "https://blockchair.com/bitcoin-cash/transaction/",
352
+ testnet: "https://www.blockchain.com/bch-testnet/tx/",
353
+ regtest: "",
354
+ };
355
+
356
+ return explorerUrlMap[this.network] + txId;
357
+ }
358
+
359
+ // returns the public key hash for an address
360
+ public getPublicKeyHash(hex = false): string | Uint8Array {
361
+ if (this.publicKeyHash) {
362
+ return hex ? binToHex(this.publicKeyHash) : this.publicKeyHash;
363
+ } else {
364
+ throw Error(
365
+ "The public key hash for this wallet is not known. If this wallet was created from the constructor directly, calling the deriveInfo() function may help. "
366
+ );
453
367
  }
368
+ }
454
369
 
455
- return "";
370
+ /**
371
+ * fromCashaddr - create a watch-only wallet in the network derived from the address
372
+ *
373
+ * such kind of wallet does not have a private key and is unable to spend any funds
374
+ * however it still allows to use many utility functions such as getting and watching balance, etc.
375
+ *
376
+ * @param address cashaddress of a wallet
377
+ *
378
+ * @returns instantiated wallet
379
+ */
380
+ public static async fromCashaddr<T extends typeof BaseWallet>(
381
+ this: T,
382
+ address: string
383
+ ): Promise<InstanceType<T>> {
384
+ const prefix = derivePrefix(address);
385
+ const networkType = networkPrefixMap[prefix] as NetworkType;
386
+ return new this(networkType).watchOnly(address) as InstanceType<T>;
456
387
  }
457
- //#endregion Serialization
458
388
 
459
- //#region Funds
460
- // @ts-ignore
461
- public async getBalance(rawUnit?: any): Promise<any> {
462
- throw Error("sendMax called on base wallet");
389
+ /**
390
+ * fromTokenaddr - create a watch-only wallet in the network derived from the address
391
+ *
392
+ * such kind of wallet does not have a private key and is unable to spend any funds
393
+ * however it still allows to use many utility functions such as getting and watching balance, etc.
394
+ *
395
+ * @param address token aware cashaddress of a wallet
396
+ *
397
+ * @returns instantiated wallet
398
+ */
399
+ public static async fromTokenaddr<T extends typeof BaseWallet>(
400
+ this: T,
401
+ address: string
402
+ ): Promise<InstanceType<T>> {
403
+ const prefix = derivePrefix(address);
404
+ const networkType = networkPrefixMap[prefix] as NetworkType;
405
+ return new this(networkType).watchOnly(address) as InstanceType<T>;
406
+ }
407
+
408
+ /**
409
+ * named - create a named wallet
410
+ *
411
+ * @param name user friendly wallet alias
412
+ * @param dbName name under which the wallet will be stored in the database
413
+ * @param force force recreate wallet in the database if a record already exist
414
+ *
415
+ * @returns instantiated wallet
416
+ */
417
+ public static async named<T extends typeof Wallet>(
418
+ this: T,
419
+ name: string,
420
+ dbName?: string,
421
+ force?: boolean
422
+ ): Promise<InstanceType<T>> {
423
+ return new this().named(name, dbName, force) as InstanceType<T>;
463
424
  }
464
425
 
465
- // @ts-ignore
466
- public async getMaxAmountToSend(params?: any): Promise<any> {
467
- throw Error("getMaxAmountToSend called on base wallet");
426
+ /**
427
+ * replaceNamed - replace (recover) named wallet with a new walletId
428
+ *
429
+ * If wallet with a provided name does not exist yet, it will be created with a `walletId` supplied
430
+ * If wallet exists it will be overwritten without exception
431
+ *
432
+ * @param name user friendly wallet alias
433
+ * @param walletId walletId options to steer the creation process
434
+ * @param dbName name under which the wallet will be stored in the database
435
+ *
436
+ * @returns instantiated wallet
437
+ */
438
+ public static async replaceNamed<T extends typeof Wallet>(
439
+ this: T,
440
+ name: string,
441
+ walletId: string,
442
+ dbName?: string
443
+ ): Promise<InstanceType<T>> {
444
+ return new this().replaceNamed(name, walletId, dbName) as InstanceType<T>;
468
445
  }
469
446
 
470
- // @ts-ignore
471
- public async send(requests: any, options?: any): Promise<any> {
472
- throw Error("send called on base wallet");
447
+ /**
448
+ * namedExists - check if a named wallet already exists
449
+ *
450
+ * @param name user friendly wallet alias
451
+ * @param dbName name under which the wallet will be stored in the database
452
+ *
453
+ * @returns boolean
454
+ */
455
+ public static async namedExists(
456
+ name: string,
457
+ dbName?: string
458
+ ): Promise<boolean> {
459
+ return new this().namedExists(name, dbName);
473
460
  }
474
461
 
475
- // @ts-ignore
476
- public async sendMax(address: string, options?: any): Promise<any> {
477
- throw Error("sendMax called on base wallet");
462
+ protected fromId(walletId: string): Promise<this> {
463
+ const [walletType, networkGiven, arg1, arg2]: string[] =
464
+ walletId.split(":");
465
+
466
+ if (walletType !== WalletTypeEnum.Watch) {
467
+ throw Error(
468
+ `fromId called on a ${walletType} wallet, expected a ${WalletTypeEnum.Watch} wallet`
469
+ );
470
+ }
471
+
472
+ if (this.network != networkGiven) {
473
+ throw Error(`Network prefix ${networkGiven} to a ${this.network} wallet`);
474
+ }
475
+
476
+ if (arg2) {
477
+ return this.watchOnly(`${arg1}:${arg2}`);
478
+ }
479
+
480
+ return this.watchOnly(arg1);
481
+ }
482
+
483
+ // Initialize a watch only wallet from a cash addr
484
+ protected async watchOnly(address: string): Promise<this> {
485
+ this.walletType = WalletTypeEnum.Watch;
486
+ const addressComponents = address.split(":");
487
+ let addressPrefix: string;
488
+ let addressBase: string;
489
+ if (addressComponents.length === 1) {
490
+ addressBase = addressComponents.shift() as string;
491
+ addressPrefix = derivePrefix(addressBase);
492
+ } else {
493
+ addressPrefix = addressComponents.shift() as string;
494
+ addressBase = addressComponents.shift() as string;
495
+ if (addressPrefix in networkPrefixMap) {
496
+ if (networkPrefixMap[addressPrefix] !== this.network) {
497
+ throw Error(
498
+ `a ${addressPrefix} address cannot be watched from a ${this.network} Wallet`
499
+ );
500
+ }
501
+ }
502
+ }
503
+
504
+ const prefixedAddress = `${addressPrefix}:${addressBase}`;
505
+
506
+ // check if a token aware address was provided
507
+ const addressData = decodeCashAddress(prefixedAddress);
508
+ if (typeof addressData === "string") throw addressData;
509
+
510
+ this.publicKeyHash = addressData.payload;
511
+
512
+ let nonTokenAwareType = addressData.type;
513
+ if (nonTokenAwareType == CashAddressType.p2pkhWithTokens)
514
+ nonTokenAwareType = CashAddressType.p2pkh;
515
+ if (nonTokenAwareType == CashAddressType.p2shWithTokens)
516
+ nonTokenAwareType = CashAddressType.p2sh;
517
+ if (nonTokenAwareType == CashAddressType.p2pkh)
518
+ this.publicKeyHash = addressData.payload;
519
+
520
+ this.cashaddr = encodeCashAddress({
521
+ prefix: addressData.prefix as CashAddressNetworkPrefix,
522
+ type: nonTokenAwareType,
523
+ payload: addressData.payload,
524
+ }).address;
525
+ this.tokenaddr = deriveTokenaddr(addressData.payload, this.networkPrefix);
526
+
527
+ return this;
528
+ }
529
+ //#region Funds
530
+ /**
531
+ * utxos Get unspent outputs for the wallet
532
+ *
533
+ */
534
+ public async getUtxos() {
535
+ if (!this.cashaddr) {
536
+ throw Error("Attempted to get utxos without an address");
537
+ }
538
+ return await this.getAddressUtxos(this.cashaddr);
539
+ }
540
+
541
+ // gets wallet balance in sats, bch and currency
542
+ public async getBalance(
543
+ rawUnit?: string,
544
+ priceCache = true
545
+ ): Promise<BalanceResponse | number> {
546
+ if (rawUnit) {
547
+ const unit = sanitizeUnit(rawUnit);
548
+ return await balanceFromSatoshi(
549
+ await this.getBalanceFromProvider(),
550
+ unit,
551
+ priceCache
552
+ );
553
+ } else {
554
+ return await balanceResponseFromSatoshi(
555
+ await this.getBalanceFromProvider(),
556
+ priceCache
557
+ );
558
+ }
559
+ }
560
+
561
+ // Gets balance by summing value in all utxos in stats
562
+ public async getBalanceFromUtxos(): Promise<number> {
563
+ const utxos = (await this.getAddressUtxos(this.cashaddr)).filter(
564
+ (val) => val.token === undefined
565
+ );
566
+ return sumUtxoValue(utxos);
567
+ }
568
+
569
+ // Gets balance from fulcrum
570
+ public async getBalanceFromProvider(): Promise<number> {
571
+ // Fulcrum reports balance of all utxos, including tokens, which is undesirable
572
+ // // TODO not sure why getting the balance from a provider doesn't work
573
+ // if (this._slpAware || this._slpSemiAware) {
574
+ // return await this.getBalanceFromUtxos();
575
+ // } else {
576
+ // return await this.provider.getBalance(this.cashaddr);
577
+ // }
578
+
579
+ // FIXME
580
+ return this.getBalanceFromUtxos();
581
+ }
582
+
583
+ public async getAddressUtxos(address?: string): Promise<UtxoI[]> {
584
+ if (!address) {
585
+ address = this.cashaddr;
586
+ }
587
+
588
+ if (this._slpSemiAware) {
589
+ const bchUtxos: UtxoI[] = await this.provider.getUtxos(address);
590
+ return bchUtxos.filter(
591
+ (bchutxo) => bchutxo.satoshis > DUST_UTXO_THRESHOLD
592
+ );
593
+ } else {
594
+ return await this.provider.getUtxos(address);
595
+ }
596
+ }
597
+
598
+ // watching for any transaction hash of this wallet
599
+ public async watchAddress(
600
+ callback: (txHash: string) => void
601
+ ): Promise<CancelFn> {
602
+ return this.provider.watchAddress(this.getDepositAddress(), callback);
603
+ }
604
+
605
+ // watching for any transaction of this wallet
606
+ public async watchAddressTransactions(
607
+ callback: (tx: ElectrumRawTransaction) => void
608
+ ): Promise<CancelFn> {
609
+ return this.provider.watchAddressTransactions(
610
+ this.getDepositAddress(),
611
+ callback
612
+ );
613
+ }
614
+
615
+ // watching for cashtoken transaction of this wallet
616
+ public async watchAddressTokenTransactions(
617
+ callback: (tx: ElectrumRawTransaction) => void
618
+ ): Promise<CancelFn> {
619
+ return this.provider.watchAddressTokenTransactions(
620
+ this.getDepositAddress(),
621
+ callback
622
+ );
623
+ }
624
+
625
+ // sets up a callback to be called upon wallet's balance change
626
+ // can be cancelled by calling the function returned from this one
627
+ public async watchBalance(
628
+ callback: (balance: BalanceResponse) => void
629
+ ): Promise<CancelFn> {
630
+ return this.provider.watchAddressStatus(
631
+ this.getDepositAddress(),
632
+ async (_status: string) => {
633
+ const balance = (await this.getBalance()) as BalanceResponse;
634
+ callback(balance);
635
+ }
636
+ );
637
+ }
638
+
639
+ // sets up a callback to be called upon wallet's BCH or USD balance change
640
+ // if BCH balance does not change, the callback will be triggered every
641
+ // @param `usdPriceRefreshInterval` milliseconds by polling for new BCH USD price
642
+ // Since we want to be most sensitive to usd value change, we do not use the cached exchange rates
643
+ // can be cancelled by calling the function returned from this one
644
+ public async watchBalanceUsd(
645
+ callback: (balance: BalanceResponse) => void,
646
+ usdPriceRefreshInterval = 30000
647
+ ): Promise<CancelFn> {
648
+ let usdPrice = -1;
649
+
650
+ const _callback = async () => {
651
+ const balance = (await this.getBalance(
652
+ undefined,
653
+ false
654
+ )) as BalanceResponse;
655
+ if (usdPrice !== balance.usd!) {
656
+ usdPrice = balance.usd;
657
+ callback(balance);
658
+ }
659
+ };
660
+
661
+ const watchCancel = await this.provider.watchAddressStatus(
662
+ this.getDepositAddress(),
663
+ _callback
664
+ );
665
+ const interval = setInterval(_callback, usdPriceRefreshInterval);
666
+
667
+ return async () => {
668
+ await watchCancel?.();
669
+ clearInterval(interval);
670
+ };
671
+ }
672
+
673
+ // waits for address balance to be greater than or equal to the target value
674
+ // this call halts the execution
675
+ public async waitForBalance(
676
+ value: number,
677
+ rawUnit: UnitEnum = UnitEnum.BCH
678
+ ): Promise<BalanceResponse> {
679
+ return new Promise(async (resolve) => {
680
+ let watchCancel: CancelFn;
681
+ watchCancel = await this.watchBalance(
682
+ async (balance: BalanceResponse) => {
683
+ const satoshiBalance = await amountInSatoshi(value, rawUnit);
684
+ if (balance.sat! >= satoshiBalance) {
685
+ await watchCancel?.();
686
+ resolve(balance);
687
+ }
688
+ }
689
+ );
690
+ });
691
+ }
692
+
693
+ // sets up a callback to be called upon wallet's token balance change
694
+ // can be cancelled by calling the function returned from this one
695
+ public async watchTokenBalance(
696
+ tokenId: string,
697
+ callback: (balance: bigint) => void
698
+ ): Promise<CancelFn> {
699
+ let previous: bigint | undefined = undefined;
700
+ return await this.provider.watchAddressStatus(
701
+ this.getDepositAddress(),
702
+ async (_status: string) => {
703
+ const balance = await this.getTokenBalance(tokenId);
704
+ if (previous != balance) {
705
+ callback(balance);
706
+ }
707
+ previous = balance;
708
+ }
709
+ );
710
+ }
711
+
712
+ // waits for address token balance to be greater than or equal to the target amount
713
+ // this call halts the execution
714
+ public async waitForTokenBalance(
715
+ tokenId: string,
716
+ amount: bigint
717
+ ): Promise<bigint> {
718
+ return new Promise(async (resolve) => {
719
+ let watchCancel: CancelFn;
720
+ watchCancel = await this.watchTokenBalance(
721
+ tokenId,
722
+ async (balance: bigint) => {
723
+ if (balance >= amount) {
724
+ await watchCancel?.();
725
+ resolve(balance);
726
+ }
727
+ }
728
+ );
729
+ });
730
+ }
731
+
732
+ public async getTokenInfo(
733
+ tokenId: string
734
+ ): Promise<IdentitySnapshot | undefined> {
735
+ return BCMR.getTokenInfo(tokenId);
736
+ }
737
+
738
+ protected async _getMaxAmountToSend(
739
+ params: {
740
+ outputCount?: number;
741
+ options?: SendRequestOptionsI;
742
+ privateKey?: Uint8Array;
743
+ } = {
744
+ outputCount: 1,
745
+ options: {},
746
+ }
747
+ ): Promise<{ value: number; utxos: UtxoI[] }> {
748
+ if (!params.privateKey && params.options?.buildUnsigned !== true) {
749
+ throw Error("Couldn't get network or private key for wallet.");
750
+ }
751
+ if (!this.cashaddr) {
752
+ throw Error("attempted to send without a cashaddr");
753
+ }
754
+
755
+ if (params.options && params.options.slpSemiAware) {
756
+ this._slpSemiAware = true;
757
+ }
758
+
759
+ let feePaidBy;
760
+ if (params.options && params.options.feePaidBy) {
761
+ feePaidBy = params.options.feePaidBy;
762
+ } else {
763
+ feePaidBy = FeePaidByEnum.change;
764
+ }
765
+
766
+ // get inputs
767
+ let utxos: UtxoI[];
768
+ if (params.options && params.options.utxoIds) {
769
+ utxos = await checkUtxos(
770
+ params.options.utxoIds.map((utxoId: UtxoI | string) =>
771
+ typeof utxoId === "string" ? fromUtxoId(utxoId) : utxoId
772
+ ),
773
+ this as any
774
+ );
775
+ } else {
776
+ utxos = (await this.getAddressUtxos(this.cashaddr)).filter(
777
+ (utxo) => !utxo.token
778
+ );
779
+ }
780
+
781
+ // Get current height to assure recently mined coins are not spent.
782
+ const bestHeight = await this.provider.getBlockHeight();
783
+
784
+ // simulate outputs using the sender's address
785
+ const sendRequest = new SendRequest({
786
+ cashaddr: this.cashaddr,
787
+ value: 100,
788
+ unit: "sat",
789
+ });
790
+ const sendRequests = Array(params.outputCount)
791
+ .fill(0)
792
+ .map(() => sendRequest);
793
+
794
+ const fundingUtxos = await getSuitableUtxos(
795
+ utxos,
796
+ undefined,
797
+ bestHeight,
798
+ feePaidBy,
799
+ sendRequests
800
+ );
801
+ const relayFeePerByteInSatoshi = await getRelayFeeCache(this.provider);
802
+ const fee = await getFeeAmountSimple({
803
+ utxos: fundingUtxos,
804
+ sendRequests: sendRequests,
805
+ privateKey: params.privateKey ?? hexToBin(placeholderPrivateKey),
806
+ sourceAddress: this.cashaddr,
807
+ relayFeePerByteInSatoshi: relayFeePerByteInSatoshi,
808
+ feePaidBy: feePaidBy,
809
+ });
810
+ const spendableAmount = sumUtxoValue(fundingUtxos);
811
+
812
+ let result = spendableAmount - fee;
813
+ if (result < 0) {
814
+ result = 0;
815
+ }
816
+
817
+ return { value: result, utxos: fundingUtxos };
818
+ }
819
+
820
+ public async getMaxAmountToSend(
821
+ params: {
822
+ outputCount?: number;
823
+ options?: SendRequestOptionsI;
824
+ } = {
825
+ outputCount: 1,
826
+ options: {},
827
+ }
828
+ ): Promise<BalanceResponse> {
829
+ const { value: result } = await this._getMaxAmountToSend(params);
830
+
831
+ return await balanceResponseFromSatoshi(result);
832
+ }
833
+
834
+ /**
835
+ * send Send some amount to an address
836
+ * this function processes the send requests, encodes the transaction, sends it to the network
837
+ * @returns (depending on the options parameter) the transaction id, new address balance and a link to the transaction on the blockchain explorer
838
+ *
839
+ * This is a first class function with REST analog, maintainers should strive to keep backward-compatibility
840
+ *
841
+ */
842
+ public async send(
843
+ requests:
844
+ | SendRequest
845
+ | TokenSendRequest
846
+ | OpReturnData
847
+ | Array<SendRequest | TokenSendRequest | OpReturnData>
848
+ | SendRequestArray[],
849
+ options?: SendRequestOptionsI
850
+ ): Promise<SendResponse> {
851
+ const { encodedTransaction, tokenIds, sourceOutputs } =
852
+ await this.encodeTransaction(requests, undefined, options);
853
+
854
+ const resp = new SendResponse({});
855
+ resp.tokenIds = tokenIds;
856
+
857
+ if (options?.buildUnsigned !== true) {
858
+ const txId = await this.submitTransaction(
859
+ encodedTransaction,
860
+ options?.awaitTransactionPropagation === undefined ||
861
+ options?.awaitTransactionPropagation === true
862
+ );
863
+
864
+ resp.txId = txId;
865
+ resp.explorerUrl = this.explorerUrl(resp.txId);
866
+
867
+ if (
868
+ options?.queryBalance === undefined ||
869
+ options?.queryBalance === true
870
+ ) {
871
+ resp.balance = (await this.getBalance()) as BalanceResponse;
872
+ }
873
+ } else {
874
+ resp.unsignedTransaction = binToHex(encodedTransaction);
875
+ resp.sourceOutputs = sourceOutputs;
876
+ }
877
+
878
+ return resp;
879
+ }
880
+
881
+ /**
882
+ * sendMax Send all available funds to a destination cash address
883
+ *
884
+ * @param {string} cashaddr destination cash address
885
+ * @param {SendRequestOptionsI} options Options of the send requests
886
+ *
887
+ * @returns (depending on the options parameter) the transaction id, new address balance and a link to the transaction on the blockchain explorer
888
+ */
889
+ public async sendMax(
890
+ cashaddr: string,
891
+ options?: SendRequestOptionsI
892
+ ): Promise<SendResponse> {
893
+ return await this.sendMaxRaw(cashaddr, options);
894
+ }
895
+
896
+ /**
897
+ * sendMaxRaw (internal) Send all available funds to a destination cash address
898
+ *
899
+ * @param {string} cashaddr destination cash address
900
+ * @param {SendRequestOptionsI} options Options of the send requests
901
+ *
902
+ * @returns the transaction id sent to the network
903
+ */
904
+ protected async sendMaxRaw(
905
+ cashaddr: string,
906
+ options?: SendRequestOptionsI,
907
+ privateKey?: Uint8Array
908
+ ): Promise<SendResponse> {
909
+ const { value: maxSpendableAmount, utxos } = await this._getMaxAmountToSend(
910
+ {
911
+ outputCount: 1,
912
+ options: options,
913
+ privateKey: privateKey,
914
+ }
915
+ );
916
+
917
+ if (!options) {
918
+ options = {};
919
+ }
920
+
921
+ options.utxoIds = utxos;
922
+
923
+ const sendRequest = new SendRequest({
924
+ cashaddr: cashaddr,
925
+ value: maxSpendableAmount,
926
+ unit: "sat",
927
+ });
928
+
929
+ const { encodedTransaction, tokenIds, sourceOutputs } =
930
+ await this.encodeTransaction([sendRequest], true, options, privateKey);
931
+
932
+ const resp = new SendResponse({});
933
+ resp.tokenIds = tokenIds;
934
+
935
+ if (options?.buildUnsigned !== true) {
936
+ const txId = await this.submitTransaction(
937
+ encodedTransaction,
938
+ options?.awaitTransactionPropagation === undefined ||
939
+ options?.awaitTransactionPropagation === true
940
+ );
941
+
942
+ resp.txId = txId;
943
+ resp.explorerUrl = this.explorerUrl(resp.txId);
944
+
945
+ if (
946
+ options?.queryBalance === undefined ||
947
+ options?.queryBalance === true
948
+ ) {
949
+ resp.balance = (await this.getBalance()) as BalanceResponse;
950
+ }
951
+ } else {
952
+ resp.unsignedTransaction = binToHex(encodedTransaction);
953
+ resp.sourceOutputs = sourceOutputs;
954
+ }
955
+
956
+ return resp;
957
+ }
958
+
959
+ /**
960
+ * encodeTransaction Encode and sign a transaction given a list of sendRequests, options and estimate fees.
961
+ * @param {SendRequest[]} sendRequests SendRequests
962
+ * @param {boolean} discardChange=false
963
+ * @param {SendRequestOptionsI} options Options of the send requests
964
+ */
965
+ protected async encodeTransaction(
966
+ requests:
967
+ | SendRequest
968
+ | TokenSendRequest
969
+ | OpReturnData
970
+ | Array<SendRequest | TokenSendRequest | OpReturnData>
971
+ | SendRequestArray[],
972
+ discardChange: boolean = false,
973
+ options?: SendRequestOptionsI,
974
+ privateKey?: Uint8Array
975
+ ) {
976
+ let sendRequests = asSendRequestObject(requests);
977
+
978
+ if (!privateKey && options?.buildUnsigned !== true) {
979
+ throw new Error(`Missing private key`);
980
+ }
981
+
982
+ if (options && options.slpSemiAware) {
983
+ this._slpSemiAware = true;
984
+ }
985
+
986
+ let feePaidBy;
987
+ if (options && options.feePaidBy) {
988
+ feePaidBy = options.feePaidBy;
989
+ } else {
990
+ feePaidBy = FeePaidByEnum.change;
991
+ }
992
+
993
+ let changeAddress;
994
+ if (options && options.changeAddress) {
995
+ changeAddress = options.changeAddress;
996
+ } else {
997
+ changeAddress = this.cashaddr;
998
+ }
999
+
1000
+ let checkTokenQuantities: boolean = true;
1001
+ if (options && options.checkTokenQuantities === false) {
1002
+ checkTokenQuantities = false;
1003
+ }
1004
+
1005
+ // get inputs from options or query all inputs
1006
+ let utxos: UtxoI[];
1007
+ if (options && options.utxoIds) {
1008
+ utxos = await checkUtxos(
1009
+ options.utxoIds.map((utxoId: UtxoI | string) =>
1010
+ typeof utxoId === "string" ? fromUtxoId(utxoId) : utxoId
1011
+ ),
1012
+ this as any
1013
+ );
1014
+ } else {
1015
+ utxos = await this.getAddressUtxos(this.cashaddr);
1016
+ }
1017
+
1018
+ // filter out token utxos if there are no token requests
1019
+ if (
1020
+ checkTokenQuantities &&
1021
+ !sendRequests.some((val) => val instanceof TokenSendRequest)
1022
+ ) {
1023
+ utxos = utxos.filter((val) => !val.token);
1024
+ }
1025
+
1026
+ const addTokenChangeOutputs = (
1027
+ inputs: UtxoI[],
1028
+ outputs: SendRequestType[]
1029
+ ) => {
1030
+ // Allow for implicit token burn if the total amount sent is less than user had
1031
+ // allow for token genesis, creating more tokens than we had before (0)
1032
+ if (!checkTokenQuantities) {
1033
+ return;
1034
+ }
1035
+ const allTokenInputs = inputs.filter((val) => val.token);
1036
+ const allTokenOutputs = outputs.filter(
1037
+ (val) => val instanceof TokenSendRequest
1038
+ ) as TokenSendRequest[];
1039
+ const tokenIds = allTokenOutputs
1040
+ .map((val) => val.tokenId)
1041
+ .filter((val, idx, arr) => arr.indexOf(val) === idx);
1042
+ for (let tokenId of tokenIds) {
1043
+ const tokenInputs = allTokenInputs.filter(
1044
+ (val) => val.token?.tokenId === tokenId
1045
+ );
1046
+ const inputAmountSum = tokenInputs.reduce(
1047
+ (prev, cur) => prev + cur.token!.amount,
1048
+ 0n
1049
+ );
1050
+ const tokenOutputs = allTokenOutputs.filter(
1051
+ (val) => val.tokenId === tokenId
1052
+ );
1053
+ const outputAmountSum = tokenOutputs.reduce(
1054
+ (prev, cur) => prev + cur.amount,
1055
+ 0n
1056
+ );
1057
+
1058
+ const diff = inputAmountSum - outputAmountSum;
1059
+ if (diff < 0) {
1060
+ throw new Error("Not enough token amount to send");
1061
+ }
1062
+ if (diff >= 0) {
1063
+ let available = 0n;
1064
+ let change = 0n;
1065
+ const ensureUtxos: UtxoI[] = [];
1066
+ for (const token of tokenInputs.filter((val) => val.token?.amount)) {
1067
+ ensureUtxos.push(token);
1068
+ available += token.token!.amount;
1069
+ if (available >= outputAmountSum) {
1070
+ change = available - outputAmountSum;
1071
+ //break;
1072
+ }
1073
+ }
1074
+ if (ensureUtxos.length) {
1075
+ if (!options) {
1076
+ options = {};
1077
+ }
1078
+ options!.ensureUtxos = [
1079
+ ...(options.ensureUtxos ?? []),
1080
+ ...ensureUtxos,
1081
+ ].filter(
1082
+ (val, index, array) =>
1083
+ array.findIndex(
1084
+ (other) => other.txid === val.txid && other.vout === val.vout
1085
+ ) === index
1086
+ );
1087
+ }
1088
+ if (change > 0) {
1089
+ outputs.push(
1090
+ new TokenSendRequest({
1091
+ cashaddr: toTokenaddr(changeAddress) || this.tokenaddr,
1092
+ amount: change,
1093
+ tokenId: tokenId,
1094
+ commitment: tokenOutputs[0].commitment,
1095
+ capability: tokenOutputs[0].capability,
1096
+ value: tokenOutputs[0].value,
1097
+ })
1098
+ );
1099
+ }
1100
+ }
1101
+ }
1102
+ };
1103
+ addTokenChangeOutputs(utxos, sendRequests);
1104
+
1105
+ const bestHeight = await this.provider.getBlockHeight();
1106
+ const spendAmount = await sumSendRequestAmounts(sendRequests);
1107
+
1108
+ if (utxos.length === 0) {
1109
+ throw Error("There were no Unspent Outputs");
1110
+ }
1111
+ if (typeof spendAmount !== "bigint") {
1112
+ throw Error("Couldn't get spend amount when building transaction");
1113
+ }
1114
+
1115
+ const relayFeePerByteInSatoshi = await getRelayFeeCache(this.provider);
1116
+ const feeEstimate = await getFeeAmountSimple({
1117
+ utxos: utxos,
1118
+ sendRequests: sendRequests,
1119
+ privateKey: privateKey ?? hexToBin(placeholderPrivateKey),
1120
+ sourceAddress: this.cashaddr,
1121
+ relayFeePerByteInSatoshi: relayFeePerByteInSatoshi,
1122
+ feePaidBy: feePaidBy,
1123
+ });
1124
+
1125
+ const fundingUtxos = await getSuitableUtxos(
1126
+ utxos,
1127
+ BigInt(spendAmount) + BigInt(Math.ceil(feeEstimate)),
1128
+ bestHeight,
1129
+ feePaidBy,
1130
+ sendRequests,
1131
+ options?.ensureUtxos || [],
1132
+ options?.tokenOperation
1133
+ );
1134
+ if (fundingUtxos.length === 0) {
1135
+ throw Error(
1136
+ "The available inputs couldn't satisfy the request with fees"
1137
+ );
1138
+ }
1139
+ const fee = await getFeeAmount({
1140
+ utxos: fundingUtxos,
1141
+ sendRequests: sendRequests,
1142
+ privateKey: privateKey ?? hexToBin(placeholderPrivateKey),
1143
+ sourceAddress: this.cashaddr,
1144
+ relayFeePerByteInSatoshi: relayFeePerByteInSatoshi,
1145
+ feePaidBy: feePaidBy,
1146
+ });
1147
+ const { encodedTransaction, sourceOutputs } = await buildEncodedTransaction(
1148
+ {
1149
+ inputs: fundingUtxos,
1150
+ outputs: sendRequests,
1151
+ signingKey: privateKey ?? hexToBin(placeholderPrivateKey),
1152
+ sourceAddress: this.cashaddr,
1153
+ fee,
1154
+ discardChange,
1155
+ feePaidBy,
1156
+ changeAddress,
1157
+ buildUnsigned: options?.buildUnsigned === true,
1158
+ }
1159
+ );
1160
+
1161
+ const tokenIds = [
1162
+ ...fundingUtxos
1163
+ .filter((val) => val.token?.tokenId)
1164
+ .map((val) => val.token!.tokenId),
1165
+ ...sendRequests
1166
+ .filter((val) => val instanceof TokenSendRequest)
1167
+ .map((val) => (val as TokenSendRequest).tokenId),
1168
+ ].filter((value, index, array) => array.indexOf(value) === index);
1169
+
1170
+ return { encodedTransaction, tokenIds, sourceOutputs };
1171
+ }
1172
+
1173
+ // Submit a raw transaction
1174
+ public async submitTransaction(
1175
+ transaction: Uint8Array,
1176
+ awaitPropagation: boolean = true
1177
+ ): Promise<string> {
1178
+ if (!this.provider) {
1179
+ throw Error("Wallet network provider was not initialized");
1180
+ }
1181
+ let rawTransaction = binToHex(transaction);
1182
+ return await this.provider.sendRawTransaction(
1183
+ rawTransaction,
1184
+ awaitPropagation
1185
+ );
1186
+ }
1187
+
1188
+ // gets transaction history of this wallet
1189
+ public async getRawHistory(
1190
+ fromHeight: number = 0,
1191
+ toHeight: number = -1
1192
+ ): Promise<TxI[]> {
1193
+ return await this.provider.getHistory(this.cashaddr, fromHeight, toHeight);
1194
+ }
1195
+
1196
+ /**
1197
+ * getHistory gets transaction history of this wallet with most data decoded and ready to present to user
1198
+ * @note balance calculations are valid only if querying to the blockchain tip (`toHeight` === -1, `count` === -1)
1199
+ * @note this method is heavy on network calls, if invoked in browser use of cache is advised, @see `Config.UseLocalStorageCache`
1200
+ * @note this method tries to recreate the history tab view of Electron Cash wallet, however, it may not be 100% accurate if the tnransaction value changes are the same in the same block (ordering)
1201
+ *
1202
+ * @param unit optional, BCH or currency unit to present balance and balance changes. If unit is currency like USD or EUR, balances will be subject to possible rounding errors. Default 0
1203
+ * @param fromHeight optional, if set, history will be limited. Default 0
1204
+ * @param toHeight optional, if set, history will be limited. Default -1, meaning that all history items will be returned, including mempool
1205
+ * @param start optional, if set, the result set will be paginated with offset `start`
1206
+ * @param count optional, if set, the result set will be paginated with `count`. Default -1, meaning that all history items will be returned
1207
+ *
1208
+ * @returns an array of transaction history items, with input values and addresses encoded in cashaddress format. @see `TransactionHistoryItem` type
1209
+ */
1210
+ public async getHistory({
1211
+ unit = "sat",
1212
+ fromHeight = 0,
1213
+ toHeight = -1,
1214
+ start = 0,
1215
+ count = -1,
1216
+ }: {
1217
+ unit?: UnitEnum;
1218
+ fromHeight?: number;
1219
+ toHeight?: number;
1220
+ start?: number;
1221
+ count?: number;
1222
+ }): Promise<TransactionHistoryItem[]> {
1223
+ return getAddressHistory({
1224
+ address: this.cashaddr,
1225
+ provider: this.provider,
1226
+ unit,
1227
+ fromHeight,
1228
+ toHeight,
1229
+ start,
1230
+ count,
1231
+ });
1232
+ }
1233
+
1234
+ // gets last transaction of this wallet
1235
+ public async getLastTransaction(
1236
+ confirmedOnly: boolean = false
1237
+ ): Promise<ElectrumRawTransaction | null> {
1238
+ let history: TxI[] = await this.getRawHistory();
1239
+ if (confirmedOnly) {
1240
+ history = history.filter((val) => val.height > 0);
1241
+ }
1242
+
1243
+ if (!history.length) {
1244
+ return null;
1245
+ }
1246
+
1247
+ const [lastTx] = history.slice(-1);
1248
+ return this.provider.getRawTransactionObject(lastTx.tx_hash);
1249
+ }
1250
+
1251
+ // waits for next transaction, program execution is halted
1252
+ public async waitForTransaction(
1253
+ options: WaitForTransactionOptions = {
1254
+ getTransactionInfo: true,
1255
+ getBalance: false,
1256
+ txHash: undefined,
1257
+ }
1258
+ ): Promise<WaitForTransactionResponse> {
1259
+ if (options.getTransactionInfo === undefined) {
1260
+ options.getTransactionInfo = true;
1261
+ }
1262
+
1263
+ return new Promise(async (resolve) => {
1264
+ let txHashSeen = false;
1265
+
1266
+ const makeResponse = async (txHash?: string) => {
1267
+ const response = <WaitForTransactionResponse>{};
1268
+ const promises: any[] = [undefined, undefined];
1269
+
1270
+ if (options.getBalance === true) {
1271
+ promises[0] = this.getBalance();
1272
+ }
1273
+
1274
+ if (options.getTransactionInfo === true) {
1275
+ if (!txHash) {
1276
+ promises[1] = this.getLastTransaction();
1277
+ } else {
1278
+ promises[1] = this.provider.getRawTransactionObject(txHash);
1279
+ }
1280
+ }
1281
+
1282
+ const result = await Promise.all(promises);
1283
+ response.balance = result[0];
1284
+ response.transactionInfo = result[1];
1285
+
1286
+ return response;
1287
+ };
1288
+
1289
+ // waiting for a specific transaction to propagate
1290
+ if (options.txHash) {
1291
+ let cancel: CancelFn;
1292
+
1293
+ const waitForTransactionCallback = async (data) => {
1294
+ if (data && data[0] === options.txHash! && data[1] !== null) {
1295
+ txHashSeen = true;
1296
+ await cancel?.();
1297
+
1298
+ resolve(makeResponse(options.txHash!));
1299
+ }
1300
+ };
1301
+
1302
+ cancel = await this.provider.subscribeToTransaction(
1303
+ options.txHash,
1304
+ waitForTransactionCallback
1305
+ );
1306
+ return;
1307
+ }
1308
+
1309
+ // waiting for any address transaction
1310
+ let watchCancel: CancelFn;
1311
+ let initialResponseSeen = false;
1312
+ watchCancel = await this.provider.watchAddressStatus(
1313
+ this.getDepositAddress(),
1314
+ async (_status) => {
1315
+ if (initialResponseSeen) {
1316
+ await watchCancel?.();
1317
+ resolve(makeResponse());
1318
+ return;
1319
+ }
1320
+
1321
+ initialResponseSeen = true;
1322
+ }
1323
+ );
1324
+ });
1325
+ }
1326
+
1327
+ /**
1328
+ * watchBlocks Watch network blocks
1329
+ *
1330
+ * @param callback callback with a block header object
1331
+ * @param skipCurrentHeight if set, the notification about current block will not arrive
1332
+ *
1333
+ * @returns a function which will cancel watching upon evaluation
1334
+ */
1335
+ public async watchBlocks(
1336
+ callback: (header: HexHeaderI) => void,
1337
+ skipCurrentHeight: boolean = true
1338
+ ): Promise<CancelFn> {
1339
+ return this.provider.watchBlocks(callback, skipCurrentHeight);
1340
+ }
1341
+
1342
+ /**
1343
+ * waitForBlock Wait for a network block
1344
+ *
1345
+ * @param height if specified waits for this exact blockchain height, otherwise resolves with the next block
1346
+ *
1347
+ */
1348
+ public async waitForBlock(height?: number): Promise<HexHeaderI> {
1349
+ return this.provider.waitForBlock(height);
1350
+ }
1351
+
1352
+ //#endregion Funds
1353
+
1354
+ // Convenience wrapper to verify interface
1355
+ public async verify(message: string, sig: string, publicKey?: Uint8Array) {
1356
+ return await new SignedMessage().verify(
1357
+ message,
1358
+ sig,
1359
+ this.cashaddr,
1360
+ publicKey
1361
+ );
1362
+ }
1363
+
1364
+ //#region Cashtokens
1365
+ /**
1366
+ * Create new cashtoken, both funglible and/or non-fungible (NFT)
1367
+ * Refer to spec https://github.com/bitjson/cashtokens
1368
+ * @param {number} genesisRequest.amount amount of *fungible* tokens to create
1369
+ * @param {NFTCapability?} genesisRequest.capability capability of new NFT
1370
+ * @param {string?} genesisRequest.commitment NFT commitment message
1371
+ * @param {string?} genesisRequest.cashaddr cash address to send the created token UTXO to; if undefined will default to your address
1372
+ * @param {number?} genesisRequest.value satoshi value to send alongside with tokens; if undefined will default to 1000 satoshi
1373
+ * @param {SendRequestType | SendRequestType[]} sendRequests single or an array of extra send requests (OP_RETURN, value transfer, etc.) to include in genesis transaction
1374
+ * @param {SendRequestOptionsI} options Options of the send requests
1375
+ */
1376
+ public async tokenGenesis(
1377
+ genesisRequest: TokenGenesisRequest,
1378
+ sendRequests: SendRequestType | SendRequestType[] = [],
1379
+ options?: SendRequestOptionsI
1380
+ ): Promise<SendResponse> {
1381
+ if (!Array.isArray(sendRequests)) {
1382
+ sendRequests = [sendRequests];
1383
+ }
1384
+
1385
+ let utxos: UtxoI[];
1386
+ if (options && options.utxoIds) {
1387
+ utxos = await checkUtxos(
1388
+ options.utxoIds.map((utxoId: UtxoI | string) =>
1389
+ typeof utxoId === "string" ? fromUtxoId(utxoId) : utxoId
1390
+ ),
1391
+ this as any
1392
+ );
1393
+ } else {
1394
+ utxos = await this.getAddressUtxos(this.cashaddr);
1395
+ }
1396
+
1397
+ const genesisInputs = utxos.filter((val) => val.vout === 0 && !val.token);
1398
+ if (genesisInputs.length === 0) {
1399
+ throw new Error(
1400
+ "No suitable inputs with vout=0 available for new token genesis"
1401
+ );
1402
+ }
1403
+
1404
+ const genesisSendRequest = new TokenSendRequest({
1405
+ cashaddr: genesisRequest.cashaddr || this.tokenaddr,
1406
+ amount: genesisRequest.amount,
1407
+ value: genesisRequest.value || 1000,
1408
+ capability: genesisRequest.capability,
1409
+ commitment: genesisRequest.commitment,
1410
+ tokenId: genesisInputs[0].txid,
1411
+ });
1412
+
1413
+ return this.send([genesisSendRequest, ...(sendRequests as any)], {
1414
+ ...options,
1415
+ utxoIds: utxos,
1416
+ ensureUtxos: [genesisInputs[0]],
1417
+ checkTokenQuantities: false,
1418
+ queryBalance: false,
1419
+ tokenOperation: "genesis",
1420
+ });
1421
+ }
1422
+
1423
+ /**
1424
+ * Mint new NFT cashtokens using an existing minting token
1425
+ * Refer to spec https://github.com/bitjson/cashtokens
1426
+ * @param {string} tokenId tokenId of an NFT to mint
1427
+ * @param {TokenMintRequest | TokenMintRequest[]} mintRequests mint requests with new token properties and recipients
1428
+ * @param {NFTCapability?} mintRequest.capability capability of new NFT
1429
+ * @param {string?} mintRequest.commitment NFT commitment message
1430
+ * @param {string?} mintRequest.cashaddr cash address to send the created token UTXO to; if undefined will default to your address
1431
+ * @param {number?} mintRequest.value satoshi value to send alongside with tokens; if undefined will default to 1000 satoshi
1432
+ * @param {boolean?} deductTokenAmount if minting token contains fungible amount, deduct from it by amount of minted tokens
1433
+ * @param {SendRequestOptionsI} options Options of the send requests
1434
+ */
1435
+ public async tokenMint(
1436
+ tokenId: string,
1437
+ mintRequests: TokenMintRequest | Array<TokenMintRequest>,
1438
+ deductTokenAmount: boolean = false,
1439
+ options?: SendRequestOptionsI
1440
+ ): Promise<SendResponse> {
1441
+ if (tokenId?.length !== 64) {
1442
+ throw Error(`Invalid tokenId supplied: ${tokenId}`);
1443
+ }
1444
+
1445
+ if (!Array.isArray(mintRequests)) {
1446
+ mintRequests = [mintRequests];
1447
+ }
1448
+
1449
+ const utxos = await this.getAddressUtxos(this.cashaddr);
1450
+ const nftUtxos = utxos.filter(
1451
+ (val) =>
1452
+ val.token?.tokenId === tokenId &&
1453
+ val.token?.capability === NFTCapability.minting
1454
+ );
1455
+ if (!nftUtxos.length) {
1456
+ throw new Error(
1457
+ "You do not have any token UTXOs with minting capability for specified tokenId"
1458
+ );
1459
+ }
1460
+ const newAmount =
1461
+ deductTokenAmount && nftUtxos[0].token!.amount > 0
1462
+ ? nftUtxos[0].token!.amount - BigInt(mintRequests.length)
1463
+ : nftUtxos[0].token!.amount;
1464
+ const safeNewAmount = newAmount < 0n ? 0n : newAmount;
1465
+ const mintingInput = new TokenSendRequest({
1466
+ cashaddr: this.tokenaddr,
1467
+ tokenId: tokenId,
1468
+ capability: nftUtxos[0].token!.capability,
1469
+ commitment: nftUtxos[0].token!.commitment,
1470
+ amount: safeNewAmount,
1471
+ value: nftUtxos[0].satoshis,
1472
+ });
1473
+ return this.send(
1474
+ [
1475
+ mintingInput,
1476
+ ...mintRequests.map(
1477
+ (val) =>
1478
+ new TokenSendRequest({
1479
+ cashaddr: val.cashaddr || this.tokenaddr,
1480
+ amount: 0,
1481
+ tokenId: tokenId,
1482
+ value: val.value,
1483
+ capability: val.capability,
1484
+ commitment: val.commitment,
1485
+ })
1486
+ ),
1487
+ ],
1488
+ {
1489
+ ...options,
1490
+ ensureUtxos: [nftUtxos[0]],
1491
+ checkTokenQuantities: false,
1492
+ queryBalance: false,
1493
+ tokenOperation: "mint",
1494
+ }
1495
+ );
1496
+ }
1497
+
1498
+ /**
1499
+ * Perform an explicit token burning by spending a token utxo to an OP_RETURN
1500
+ *
1501
+ * Behaves differently for fungible and non-fungible tokens:
1502
+ * * NFTs are always "destroyed"
1503
+ * * FTs' amount is reduced by the amount specified, if 0 FT amount is left and no NFT present, the token is "destroyed"
1504
+ *
1505
+ * Refer to spec https://github.com/bitjson/cashtokens
1506
+ * @param {string} burnRequest.tokenId tokenId of a token to burn
1507
+ * @param {NFTCapability} burnRequest.capability capability of the NFT token to select, optional
1508
+ * @param {string?} burnRequest.commitment commitment of the NFT token to select, optional
1509
+ * @param {number?} burnRequest.amount amount of fungible tokens to burn, optional
1510
+ * @param {string?} burnRequest.cashaddr address to return token and satoshi change to
1511
+ * @param {string?} message optional message to include in OP_RETURN
1512
+ * @param {SendRequestOptionsI} options Options of the send requests
1513
+ */
1514
+ public async tokenBurn(
1515
+ burnRequest: TokenBurnRequest,
1516
+ message?: string,
1517
+ options?: SendRequestOptionsI
1518
+ ): Promise<SendResponse> {
1519
+ if (burnRequest.tokenId?.length !== 64) {
1520
+ throw Error(`Invalid tokenId supplied: ${burnRequest.tokenId}`);
1521
+ }
1522
+
1523
+ const utxos = await this.getAddressUtxos(this.cashaddr);
1524
+ const tokenUtxos = utxos.filter(
1525
+ (val) =>
1526
+ val.token?.tokenId === burnRequest.tokenId &&
1527
+ val.token?.capability === burnRequest.capability &&
1528
+ val.token?.commitment === burnRequest.commitment
1529
+ );
1530
+
1531
+ if (!tokenUtxos.length) {
1532
+ throw new Error("You do not have suitable token UTXOs to perform burn");
1533
+ }
1534
+
1535
+ const totalFungibleAmount = tokenUtxos.reduce(
1536
+ (prev, cur) => prev + (cur.token?.amount || 0n),
1537
+ 0n
1538
+ );
1539
+ let fungibleBurnAmount =
1540
+ burnRequest.amount && burnRequest.amount > 0 ? burnRequest.amount! : 0n;
1541
+ fungibleBurnAmount = BigInt(fungibleBurnAmount);
1542
+ const hasNFT = burnRequest.capability || burnRequest.commitment;
1543
+
1544
+ let utxoIds: UtxoI[] = [];
1545
+ let changeSendRequests: TokenSendRequest[];
1546
+ if (hasNFT) {
1547
+ // does not have FT tokens, let us destroy the token completely
1548
+ if (totalFungibleAmount === 0n) {
1549
+ changeSendRequests = [];
1550
+ utxoIds.push(tokenUtxos[0]);
1551
+ } else {
1552
+ // add utxos to spend from
1553
+ let available = 0n;
1554
+ for (const token of tokenUtxos.filter((val) => val.token?.amount)) {
1555
+ utxoIds.push(token);
1556
+ available += token.token!.amount;
1557
+ if (available >= fungibleBurnAmount) {
1558
+ break;
1559
+ }
1560
+ }
1561
+
1562
+ // if there are FT, reduce their amount
1563
+ const newAmount = totalFungibleAmount - fungibleBurnAmount;
1564
+ const safeNewAmount = newAmount < 0n ? 0n : newAmount;
1565
+ changeSendRequests = [
1566
+ new TokenSendRequest({
1567
+ cashaddr: burnRequest.cashaddr || this.tokenaddr,
1568
+ tokenId: burnRequest.tokenId,
1569
+ capability: burnRequest.capability,
1570
+ commitment: burnRequest.commitment,
1571
+ amount: safeNewAmount,
1572
+ value: tokenUtxos[0].satoshis,
1573
+ }),
1574
+ ];
1575
+ }
1576
+ } else {
1577
+ // if we are burning last fungible tokens, let us destroy the token completely
1578
+ if (totalFungibleAmount === fungibleBurnAmount) {
1579
+ changeSendRequests = [];
1580
+ utxoIds.push(...tokenUtxos);
1581
+ } else {
1582
+ // add utxos to spend from
1583
+ let available = 0n;
1584
+ for (const token of tokenUtxos.filter((val) => val.token?.amount)) {
1585
+ utxoIds.push(token);
1586
+ available += token.token!.amount;
1587
+ if (available >= fungibleBurnAmount) {
1588
+ break;
1589
+ }
1590
+ }
1591
+
1592
+ // reduce the FT amount
1593
+ const newAmount = available - fungibleBurnAmount;
1594
+ const safeNewAmount = newAmount < 0n ? 0n : newAmount;
1595
+ changeSendRequests = [
1596
+ new TokenSendRequest({
1597
+ cashaddr: burnRequest.cashaddr || this.tokenaddr,
1598
+ tokenId: burnRequest.tokenId,
1599
+ amount: safeNewAmount,
1600
+ value: tokenUtxos.reduce((a, c) => a + c.satoshis, 0),
1601
+ }),
1602
+ ];
1603
+ }
1604
+ }
1605
+
1606
+ const opReturn = OpReturnData.fromString(message || "");
1607
+ return this.send([opReturn, ...changeSendRequests], {
1608
+ ...options,
1609
+ checkTokenQuantities: false,
1610
+ queryBalance: false,
1611
+ ensureUtxos: utxoIds.length > 0 ? utxoIds : undefined,
1612
+ tokenOperation: "burn",
1613
+ });
1614
+ }
1615
+
1616
+ /**
1617
+ * getTokenUtxos Get unspent token outputs for the wallet
1618
+ * will return utxos only for the specified token if `tokenId` provided
1619
+ * @param {string?} tokenId tokenId (category) to filter utxos by, if not set will return utxos from all tokens
1620
+ * @returns {UtxoI[]} token utxos
1621
+ */
1622
+ public async getTokenUtxos(tokenId?: string): Promise<UtxoI[]> {
1623
+ const utxos = await this.getAddressUtxos(this.cashaddr);
1624
+ return utxos.filter((val) =>
1625
+ tokenId ? val.token?.tokenId === tokenId : val.token
1626
+ );
1627
+ }
1628
+
1629
+ /**
1630
+ * getTokenBalance Gets fungible token balance
1631
+ * for NFT token balance see @ref getNftTokenBalance
1632
+ * @param {string} tokenId tokenId to get balance for
1633
+ * @returns {bigint} fungible token balance
1634
+ */
1635
+ public async getTokenBalance(tokenId: string): Promise<bigint> {
1636
+ const utxos = (await this.getTokenUtxos(tokenId)).filter(
1637
+ (val) => val.token?.amount
1638
+ );
1639
+ return sumTokenAmounts(utxos, tokenId);
1640
+ }
1641
+
1642
+ /**
1643
+ * getNftTokenBalance Gets non-fungible token (NFT) balance for a particular tokenId
1644
+ * disregards fungible token balances
1645
+ * for fungible token balance see @ref getTokenBalance
1646
+ * @param {string} tokenId tokenId to get balance for
1647
+ * @returns {number} non-fungible token balance
1648
+ */
1649
+ public async getNftTokenBalance(tokenId: string): Promise<number> {
1650
+ const utxos = (await this.getTokenUtxos(tokenId)).filter(
1651
+ (val) => val.token?.commitment !== undefined
1652
+ );
1653
+ return utxos.length;
1654
+ }
1655
+
1656
+ /**
1657
+ * getAllTokenBalances Gets all fungible token balances in this wallet
1658
+ * @returns {Object} a map [tokenId => balance] for all tokens in this wallet
1659
+ */
1660
+ public async getAllTokenBalances(): Promise<{ [tokenId: string]: bigint }> {
1661
+ const result = {};
1662
+ const utxos = (await this.getTokenUtxos()).filter(
1663
+ (val) => val.token?.amount
1664
+ );
1665
+ for (const utxo of utxos) {
1666
+ if (!result[utxo.token!.tokenId]) {
1667
+ result[utxo.token!.tokenId] = 0n;
1668
+ }
1669
+ result[utxo.token!.tokenId] += utxo.token!.amount;
1670
+ }
1671
+ return result;
1672
+ }
1673
+
1674
+ /**
1675
+ * getAllNftTokenBalances Gets all non-fungible token (NFT) balances in this wallet
1676
+ * @returns {Object} a map [tokenId => balance] for all NFTs in this wallet
1677
+ */
1678
+ public async getAllNftTokenBalances(): Promise<{
1679
+ [tokenId: string]: number;
1680
+ }> {
1681
+ const result = {};
1682
+ const utxos = (await this.getTokenUtxos()).filter(
1683
+ (val) => val.token?.commitment !== undefined
1684
+ );
1685
+ for (const utxo of utxos) {
1686
+ if (!result[utxo.token!.tokenId]) {
1687
+ result[utxo.token!.tokenId] = 0;
1688
+ }
1689
+ result[utxo.token!.tokenId] += 1;
1690
+ }
1691
+ return result;
1692
+ }
1693
+ //#endregion Cashtokens
1694
+ }
1695
+
1696
+ /**
1697
+ * Class to manage a mainnet watch wallet.
1698
+ */
1699
+ export class WatchWallet extends BaseWallet {
1700
+ static networkPrefix = CashAddressNetworkPrefix.mainnet;
1701
+ static walletType = WalletTypeEnum.Watch;
1702
+ constructor() {
1703
+ super(NetworkType.Mainnet);
1704
+ }
1705
+ }
1706
+
1707
+ /**
1708
+ * Class to manage a testnet watch wallet.
1709
+ */
1710
+ export class TestNetWatchWallet extends BaseWallet {
1711
+ static networkPrefix = CashAddressNetworkPrefix.testnet;
1712
+ static walletType = WalletTypeEnum.Watch;
1713
+ constructor() {
1714
+ super(NetworkType.Testnet);
1715
+ }
1716
+ }
1717
+
1718
+ /**
1719
+ * Class to manage a regtest watch wallet.
1720
+ */
1721
+ export class RegTestWatchWallet extends BaseWallet {
1722
+ static networkPrefix = CashAddressNetworkPrefix.regtest;
1723
+ static walletType = WalletTypeEnum.Watch;
1724
+ constructor() {
1725
+ super(NetworkType.Regtest);
478
1726
  }
479
- //#endregion Funds
480
1727
  }
481
1728
 
482
1729
  /**