mainnet-js 2.6.7 → 2.7.1

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.1.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,17 +1,64 @@
1
- import { NetworkType } from "../enum.js";
2
- import { getRuntimePlatform } from "../util/getRuntimePlatform.js";
3
- import { qrAddress } from "../qr/Qr.js";
4
- import { WalletTypeEnum } from "./enum.js";
1
+ import { binToHex, CashAddressNetworkPrefix, CashAddressType, decodeCashAddress, encodeCashAddress, } from "@bitauth/libauth";
2
+ import { DUST_UTXO_THRESHOLD } from "../constant.js";
3
+ import { networkPrefixMap, NetworkType, prefixFromNetworkMap, UnitEnum, } from "../enum.js";
4
+ import { getAddressHistory } from "../history/electrumTransformer.js";
5
+ import { NFTCapability } from "../interface.js";
6
+ import { SignedMessage } from "../message/signed.js";
7
+ import { getNetworkProvider } from "../network/default.js";
8
+ import { getRelayFeeCache } from "../network/getRelayFeeCache.js";
9
+ import { buildEncodedTransaction, getFeeAmount, getFeeAmountSimple, getSuitableUtxos, } from "../transaction/Wif.js";
10
+ import { balanceFromSatoshi, balanceResponseFromSatoshi, } from "../util/balanceObjectFromSatoshi.js";
11
+ import { checkUtxos } from "../util/checkUtxos.js";
12
+ import { derivePrefix } from "../util/derivePublicKeyHash.js";
13
+ import { amountInSatoshi, asSendRequestObject, deriveTokenaddr, getRuntimePlatform, hexToBin, sumTokenAmounts, sumUtxoValue, toTokenaddr, } from "../util/index.js";
14
+ import { sanitizeUnit } from "../util/sanitizeUnit.js";
15
+ import { sumSendRequestAmounts } from "../util/sumSendRequestAmounts.js";
16
+ import { BCMR } from "./Bcmr.js";
17
+ import { FeePaidByEnum, WalletTypeEnum } from "./enum.js";
18
+ import { fromUtxoId, OpReturnData, SendRequest, SendResponse, TokenSendRequest, } from "./model.js";
19
+ import { Util } from "./Util.js";
20
+ const placeholderPrivateKey = "0000000000000000000000000000000000000000000000000000000000000001";
5
21
  /**
6
22
  * A class to hold features used by all wallets
7
23
  * @class BaseWallet
8
24
  */
9
25
  export class BaseWallet {
26
+ get networkPrefix() {
27
+ return prefixFromNetworkMap[this.network];
28
+ }
29
+ // interface to util functions. see Util.ts
30
+ get util() {
31
+ if (!this._util) {
32
+ this._util = new Util(this.network);
33
+ }
34
+ return this._util;
35
+ }
36
+ // interface to util util. see Util.Util
37
+ static get util() {
38
+ return new this().util;
39
+ }
40
+ // Return wallet info
41
+ getInfo() {
42
+ return {
43
+ cashaddr: this.cashaddr,
44
+ tokenaddr: this.tokenaddr,
45
+ isTestnet: this.isTestnet,
46
+ name: this.name,
47
+ network: this.network,
48
+ publicKeyHash: binToHex(this.publicKeyHash),
49
+ walletId: this.toString(),
50
+ walletDbEntry: this.toDbString(),
51
+ };
52
+ }
53
+ slpSemiAware(value = true) {
54
+ this._slpSemiAware = value;
55
+ return this;
56
+ }
10
57
  //#region Accessors
11
58
  getNetworkProvider(
12
59
  // @ts-ignore
13
60
  network = NetworkType.Mainnet) {
14
- throw Error("getNetworkProvider called on base wallet");
61
+ return getNetworkProvider(network);
15
62
  }
16
63
  /**
17
64
  * getDepositAddress - get a wallet deposit address
@@ -23,89 +70,32 @@ export class BaseWallet {
23
70
  * @returns The deposit address as a string
24
71
  */
25
72
  getDepositAddress() {
26
- return this.address;
73
+ return this.cashaddr;
27
74
  }
28
75
  /**
29
- * getDepositQr - get an address qrcode, encoded for display on the web
30
- *
31
- * a high-level function
76
+ * getTokenDepositAddress - get a cashtoken aware wallet deposit address
32
77
  *
33
- * @see {@link https://rest-unstable.mainnet.cash/api-docs/#/wallet/depositQr|/wallet/deposit_qr} for REST endpoint
34
- *
35
- * @returns The qrcode for the address
78
+ * @returns The cashtoken aware deposit address as a string
36
79
  */
37
- getDepositQr() {
38
- return qrAddress(this.getDepositAddress());
39
- }
40
- // Get mnemonic and derivation path for wallet
41
- getSeed() {
42
- if (!this.mnemonic) {
43
- throw Error("Wallet mnemonic seed phrase not set");
44
- }
45
- if (!this.derivationPath) {
46
- throw Error("Wallet derivation path not set");
47
- }
48
- return {
49
- seed: this.mnemonic,
50
- derivationPath: this.derivationPath,
51
- parentDerivationPath: this.parentDerivationPath,
52
- };
80
+ getTokenDepositAddress() {
81
+ return this.tokenaddr;
53
82
  }
54
83
  //#endregion Accessors
55
84
  //#region Constructors and Statics
56
85
  /**
57
86
  * constructor for a new wallet
58
- * @param {string} name name of the wallet
59
87
  * @param network network for wallet
60
88
  *
61
89
  * @throws {Error} if called on BaseWallet
62
90
  */
63
- constructor(name = "", network = NetworkType.Mainnet, walletType = WalletTypeEnum.Seed) {
64
- this.derivationPath = "m/44'/0'/0'/0/0";
65
- this.parentDerivationPath = "m/44'/0'/0'";
66
- this.name = name;
91
+ constructor(network = NetworkType.Mainnet) {
92
+ this._slpSemiAware = false; // a flag which requires an utxo to have more than 546 sats to be spendable and counted in the balance
93
+ this.name = "";
67
94
  this.network = network;
68
- this.walletType = walletType;
95
+ this.walletType = WalletTypeEnum.Watch;
69
96
  this.provider = this.getNetworkProvider(this.network);
70
97
  this.isTestnet = this.network === NetworkType.Mainnet ? false : true;
71
98
  }
72
- /**
73
- * fromId - create a wallet from encoded walletId string
74
- *
75
- * @param walletId walletId options to steer the creation process
76
- *
77
- * @returns wallet instantiated accordingly to the walletId rules
78
- */
79
- static async fromId(walletId) {
80
- return new this().fromId(walletId);
81
- }
82
- /**
83
- * fromSeed - create a wallet using the seed phrase and derivation path
84
- *
85
- * unless specified the derivation path m/44'/245'/0'/0/0 will be userd
86
- * this derivation path is standard for Electron Cash SLP and other SLP enabled wallets
87
- *
88
- * @param seed BIP39 12 word seed phrase
89
- * @param derivationPath BIP44 HD wallet derivation path to get a single the private key from hierarchy
90
- *
91
- * @returns instantiated wallet
92
- */
93
- static async fromSeed(seed, derivationPath) {
94
- return new this().fromSeed(seed, derivationPath);
95
- }
96
- /**
97
- * newRandom - create a random wallet
98
- *
99
- * if `name` parameter is specified, the wallet will also be persisted to DB
100
- *
101
- * @param name user friendly wallet alias
102
- * @param dbName name under which the wallet will be stored in the database
103
- *
104
- * @returns instantiated wallet
105
- */
106
- static async newRandom(name = "", dbName) {
107
- return new this().newRandom(name, dbName);
108
- }
109
99
  /**
110
100
  * watchOnly - create a watch-only wallet
111
101
  *
@@ -119,117 +109,7 @@ export class BaseWallet {
119
109
  static async watchOnly(address) {
120
110
  return new this().watchOnly(address);
121
111
  }
122
- /**
123
- * named - create a named wallet
124
- *
125
- * @param name user friendly wallet alias
126
- * @param dbName name under which the wallet will be stored in the database
127
- * @param force force recreate wallet in the database if a record already exist
128
- *
129
- * @returns instantiated wallet
130
- */
131
- static async named(name, dbName, force) {
132
- return new this().named(name, dbName, force);
133
- }
134
- /**
135
- * replaceNamed - replace (recover) named wallet with a new walletId
136
- *
137
- * If wallet with a provided name does not exist yet, it will be created with a `walletId` supplied
138
- * If wallet exists it will be overwritten without exception
139
- *
140
- * @param name user friendly wallet alias
141
- * @param walletId walletId options to steer the creation process
142
- * @param dbName name under which the wallet will be stored in the database
143
- *
144
- * @returns instantiated wallet
145
- */
146
- static async replaceNamed(name, walletId, dbName) {
147
- return new this().replaceNamed(name, walletId, dbName);
148
- }
149
- /**
150
- * namedExists - check if a named wallet already exists
151
- *
152
- * @param name user friendly wallet alias
153
- * @param dbName name under which the wallet will be stored in the database
154
- *
155
- * @returns boolean
156
- */
157
- static async namedExists(name, dbName) {
158
- return new this().namedExists(name, dbName);
159
- }
160
112
  //#endregion Constructors
161
- //#region Protected implementations
162
- /**
163
- * generate creates a new wallet
164
- * @throws {Error} if called on BaseWallet
165
- */
166
- generate() {
167
- throw Error("generate called on base wallet");
168
- }
169
- /**
170
- * fromId - creates a wallet from serialized string
171
- *
172
- * @throws {Error} if called on BaseWallet
173
- */
174
- fromId(walletId) {
175
- let [walletType, networkGiven, arg1, arg2] = walletId.split(":");
176
- if (this.network != networkGiven) {
177
- throw Error(`Network prefix ${networkGiven} to a ${this.network} wallet`);
178
- }
179
- switch (walletType) {
180
- case "watch":
181
- if (arg2) {
182
- // watch:testnet:bchtest:qq1234567
183
- return this.watchOnly(`${arg1}:${arg2}`);
184
- }
185
- // watch:testnet:qq1234567
186
- return this.watchOnly(`${arg1}`);
187
- case "named":
188
- if (arg2) {
189
- // named:testnet:wallet_1:my_database
190
- return this.named(arg1, arg2);
191
- }
192
- else {
193
- // named:testnet:wallet_1
194
- return this.named(arg1);
195
- }
196
- case "seed":
197
- if (arg2) {
198
- // seed:testnet:table later ... stove kitten pluck:m/44'/0'/0'/0/0
199
- return this.fromSeed(arg1, arg2);
200
- }
201
- // seed:testnet:table later ... stove kitten pluck
202
- return this.fromSeed(arg1);
203
- default:
204
- throw Error(`Unknown wallet type '${walletType}'`);
205
- }
206
- }
207
- // Initialize wallet from a mnemonic phrase
208
- async fromSeed(
209
- // @ts-ignore
210
- mnemonic,
211
- // @ts-ignore
212
- derivationPath) {
213
- throw Error("fromSeed called on base wallet");
214
- }
215
- /**
216
- * newRandom (internal) if the wallet is named, get or create it; otherwise create a random
217
- * unnamed wallet
218
- * @param {string} name name of the wallet
219
- * @param {string} dbName database name the wallet is stored in
220
- */
221
- async newRandom(name, dbName) {
222
- if (name.length > 0) {
223
- return this.named(name, dbName);
224
- }
225
- else {
226
- return this.generate();
227
- }
228
- }
229
- // @ts-ignore
230
- async watchOnly(address) {
231
- throw Error("fromSeed called on base wallet");
232
- }
233
113
  /**
234
114
  * named (internal) get a named wallet from the database or create a new one.
235
115
  * Note: this function should behave identically if
@@ -247,23 +127,23 @@ export class BaseWallet {
247
127
  }
248
128
  _checkContextSafety(this);
249
129
  this.name = name;
250
- dbName = dbName ? dbName : this.network;
251
- let db = getStorageProvider(dbName);
130
+ dbName = dbName ? dbName : prefixFromNetworkMap[this.network];
131
+ const db = getStorageProvider(dbName);
252
132
  // If there is a database, force saving or error
253
133
  if (db) {
254
134
  await db.init();
255
- let savedWalletRecord = await db.getWallet(name);
135
+ const savedWalletRecord = await db.getWallet(name);
256
136
  if (savedWalletRecord) {
257
137
  await db.close();
258
138
  if (forceNew) {
259
139
  throw Error(`A wallet with the name ${name} already exists in ${dbName}`);
260
140
  }
261
- let recoveredWallet = await this.fromId(savedWalletRecord.wallet);
141
+ const recoveredWallet = await this.fromId(savedWalletRecord.wallet);
262
142
  recoveredWallet.name = savedWalletRecord.name;
263
143
  return recoveredWallet;
264
144
  }
265
145
  else {
266
- let wallet = await this.generate();
146
+ const wallet = await this.generate();
267
147
  await db.addWallet(wallet.name, wallet.toDbString());
268
148
  await db.close();
269
149
  return wallet;
@@ -291,7 +171,7 @@ export class BaseWallet {
291
171
  }
292
172
  _checkContextSafety(this);
293
173
  this.name = name;
294
- dbName = dbName ? dbName : this.network;
174
+ dbName = dbName ? dbName : prefixFromNetworkMap[this.network];
295
175
  let db = getStorageProvider(dbName);
296
176
  if (db) {
297
177
  await db.init();
@@ -323,7 +203,7 @@ export class BaseWallet {
323
203
  throw Error("Named wallets must have a non-empty name");
324
204
  }
325
205
  _checkContextSafety(this);
326
- dbName = dbName ? dbName : this.network;
206
+ dbName = dbName ? dbName : prefixFromNetworkMap[this.network];
327
207
  let db = getStorageProvider(dbName);
328
208
  if (db) {
329
209
  await db.init();
@@ -335,7 +215,9 @@ export class BaseWallet {
335
215
  throw Error("No database was available or configured to store the named wallet.");
336
216
  }
337
217
  }
338
- //#endregion Protected implementations
218
+ async generate() {
219
+ return this;
220
+ }
339
221
  //#region Serialization
340
222
  /**
341
223
  * toDbString - store the serialized version of the wallet in the database, not just the name
@@ -343,47 +225,1008 @@ export class BaseWallet {
343
225
  * @throws {Error} if called on BaseWallet
344
226
  */
345
227
  toDbString() {
346
- if (this.walletType == WalletTypeEnum.Seed) {
347
- return `${this.walletType}:${this.network}:${this.mnemonic}:${this.derivationPath}`;
348
- }
349
- else if (this.walletType == WalletTypeEnum.Watch) {
350
- return `${this.walletType}:${this.network}:${this.getDepositAddress()}`;
351
- }
352
- return "";
228
+ return `${this.walletType}:${this.network}:${this.getDepositAddress()}`;
353
229
  }
354
230
  // Returns the serialized wallet as a string
355
231
  // If storing in a database, set asNamed to false to store secrets
356
232
  // In all other cases, the a named wallet is deserialized from the database
357
233
  // by the name key
358
234
  toString() {
359
- if (this.name) {
360
- return `named:${this.network}:${this.name}`;
235
+ return `${this.walletType}:${this.network}:${this.getDepositAddress()}`;
236
+ }
237
+ //#endregion Serialization
238
+ /**
239
+ * explorerUrl Web url to a transaction on a block explorer
240
+ *
241
+ * @param txId transaction Id
242
+ * @returns Url string
243
+ */
244
+ explorerUrl(txId) {
245
+ const explorerUrlMap = {
246
+ mainnet: "https://blockchair.com/bitcoin-cash/transaction/",
247
+ testnet: "https://www.blockchain.com/bch-testnet/tx/",
248
+ regtest: "",
249
+ };
250
+ return explorerUrlMap[this.network] + txId;
251
+ }
252
+ // returns the public key hash for an address
253
+ getPublicKeyHash(hex = false) {
254
+ if (this.publicKeyHash) {
255
+ return hex ? binToHex(this.publicKeyHash) : this.publicKeyHash;
361
256
  }
362
- else if (this.walletType == WalletTypeEnum.Seed) {
363
- return `${this.walletType}:${this.network}:${this.mnemonic}:${this.derivationPath}`;
257
+ else {
258
+ throw Error("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. ");
364
259
  }
365
- else if (this.walletType == WalletTypeEnum.Watch) {
366
- return `${this.walletType}:${this.network}:${this.getDepositAddress()}`;
260
+ }
261
+ /**
262
+ * fromCashaddr - create a watch-only wallet in the network derived from the address
263
+ *
264
+ * such kind of wallet does not have a private key and is unable to spend any funds
265
+ * however it still allows to use many utility functions such as getting and watching balance, etc.
266
+ *
267
+ * @param address cashaddress of a wallet
268
+ *
269
+ * @returns instantiated wallet
270
+ */
271
+ static async fromCashaddr(address) {
272
+ const prefix = derivePrefix(address);
273
+ const networkType = networkPrefixMap[prefix];
274
+ return new this(networkType).watchOnly(address);
275
+ }
276
+ /**
277
+ * fromTokenaddr - create a watch-only wallet in the network derived from the address
278
+ *
279
+ * such kind of wallet does not have a private key and is unable to spend any funds
280
+ * however it still allows to use many utility functions such as getting and watching balance, etc.
281
+ *
282
+ * @param address token aware cashaddress of a wallet
283
+ *
284
+ * @returns instantiated wallet
285
+ */
286
+ static async fromTokenaddr(address) {
287
+ const prefix = derivePrefix(address);
288
+ const networkType = networkPrefixMap[prefix];
289
+ return new this(networkType).watchOnly(address);
290
+ }
291
+ /**
292
+ * named - create a named wallet
293
+ *
294
+ * @param name user friendly wallet alias
295
+ * @param dbName name under which the wallet will be stored in the database
296
+ * @param force force recreate wallet in the database if a record already exist
297
+ *
298
+ * @returns instantiated wallet
299
+ */
300
+ static async named(name, dbName, force) {
301
+ return new this().named(name, dbName, force);
302
+ }
303
+ /**
304
+ * replaceNamed - replace (recover) named wallet with a new walletId
305
+ *
306
+ * If wallet with a provided name does not exist yet, it will be created with a `walletId` supplied
307
+ * If wallet exists it will be overwritten without exception
308
+ *
309
+ * @param name user friendly wallet alias
310
+ * @param walletId walletId options to steer the creation process
311
+ * @param dbName name under which the wallet will be stored in the database
312
+ *
313
+ * @returns instantiated wallet
314
+ */
315
+ static async replaceNamed(name, walletId, dbName) {
316
+ return new this().replaceNamed(name, walletId, dbName);
317
+ }
318
+ /**
319
+ * namedExists - check if a named wallet already exists
320
+ *
321
+ * @param name user friendly wallet alias
322
+ * @param dbName name under which the wallet will be stored in the database
323
+ *
324
+ * @returns boolean
325
+ */
326
+ static async namedExists(name, dbName) {
327
+ return new this().namedExists(name, dbName);
328
+ }
329
+ fromId(walletId) {
330
+ const [walletType, networkGiven, arg1, arg2] = walletId.split(":");
331
+ if (walletType !== WalletTypeEnum.Watch) {
332
+ throw Error(`fromId called on a ${walletType} wallet, expected a ${WalletTypeEnum.Watch} wallet`);
333
+ }
334
+ if (this.network != networkGiven) {
335
+ throw Error(`Network prefix ${networkGiven} to a ${this.network} wallet`);
367
336
  }
368
- return "";
337
+ if (arg2) {
338
+ return this.watchOnly(`${arg1}:${arg2}`);
339
+ }
340
+ return this.watchOnly(arg1);
341
+ }
342
+ // Initialize a watch only wallet from a cash addr
343
+ async watchOnly(address) {
344
+ this.walletType = WalletTypeEnum.Watch;
345
+ const addressComponents = address.split(":");
346
+ let addressPrefix;
347
+ let addressBase;
348
+ if (addressComponents.length === 1) {
349
+ addressBase = addressComponents.shift();
350
+ addressPrefix = derivePrefix(addressBase);
351
+ }
352
+ else {
353
+ addressPrefix = addressComponents.shift();
354
+ addressBase = addressComponents.shift();
355
+ if (addressPrefix in networkPrefixMap) {
356
+ if (networkPrefixMap[addressPrefix] !== this.network) {
357
+ throw Error(`a ${addressPrefix} address cannot be watched from a ${this.network} Wallet`);
358
+ }
359
+ }
360
+ }
361
+ const prefixedAddress = `${addressPrefix}:${addressBase}`;
362
+ // check if a token aware address was provided
363
+ const addressData = decodeCashAddress(prefixedAddress);
364
+ if (typeof addressData === "string")
365
+ throw addressData;
366
+ this.publicKeyHash = addressData.payload;
367
+ let nonTokenAwareType = addressData.type;
368
+ if (nonTokenAwareType == CashAddressType.p2pkhWithTokens)
369
+ nonTokenAwareType = CashAddressType.p2pkh;
370
+ if (nonTokenAwareType == CashAddressType.p2shWithTokens)
371
+ nonTokenAwareType = CashAddressType.p2sh;
372
+ if (nonTokenAwareType == CashAddressType.p2pkh)
373
+ this.publicKeyHash = addressData.payload;
374
+ this.cashaddr = encodeCashAddress({
375
+ prefix: addressData.prefix,
376
+ type: nonTokenAwareType,
377
+ payload: addressData.payload,
378
+ }).address;
379
+ this.tokenaddr = deriveTokenaddr(addressData.payload, this.networkPrefix);
380
+ return this;
369
381
  }
370
- //#endregion Serialization
371
382
  //#region Funds
372
- // @ts-ignore
373
- async getBalance(rawUnit) {
374
- throw Error("sendMax called on base wallet");
383
+ /**
384
+ * utxos Get unspent outputs for the wallet
385
+ *
386
+ */
387
+ async getUtxos() {
388
+ if (!this.cashaddr) {
389
+ throw Error("Attempted to get utxos without an address");
390
+ }
391
+ return await this.getAddressUtxos(this.cashaddr);
375
392
  }
376
- // @ts-ignore
377
- async getMaxAmountToSend(params) {
378
- throw Error("getMaxAmountToSend called on base wallet");
393
+ // gets wallet balance in sats, bch and currency
394
+ async getBalance(rawUnit, priceCache = true) {
395
+ if (rawUnit) {
396
+ const unit = sanitizeUnit(rawUnit);
397
+ return await balanceFromSatoshi(await this.getBalanceFromProvider(), unit, priceCache);
398
+ }
399
+ else {
400
+ return await balanceResponseFromSatoshi(await this.getBalanceFromProvider(), priceCache);
401
+ }
379
402
  }
380
- // @ts-ignore
403
+ // Gets balance by summing value in all utxos in stats
404
+ async getBalanceFromUtxos() {
405
+ const utxos = (await this.getAddressUtxos(this.cashaddr)).filter((val) => val.token === undefined);
406
+ return sumUtxoValue(utxos);
407
+ }
408
+ // Gets balance from fulcrum
409
+ async getBalanceFromProvider() {
410
+ // Fulcrum reports balance of all utxos, including tokens, which is undesirable
411
+ // // TODO not sure why getting the balance from a provider doesn't work
412
+ // if (this._slpAware || this._slpSemiAware) {
413
+ // return await this.getBalanceFromUtxos();
414
+ // } else {
415
+ // return await this.provider.getBalance(this.cashaddr);
416
+ // }
417
+ // FIXME
418
+ return this.getBalanceFromUtxos();
419
+ }
420
+ async getAddressUtxos(address) {
421
+ if (!address) {
422
+ address = this.cashaddr;
423
+ }
424
+ if (this._slpSemiAware) {
425
+ const bchUtxos = await this.provider.getUtxos(address);
426
+ return bchUtxos.filter((bchutxo) => bchutxo.satoshis > DUST_UTXO_THRESHOLD);
427
+ }
428
+ else {
429
+ return await this.provider.getUtxos(address);
430
+ }
431
+ }
432
+ // watching for any transaction hash of this wallet
433
+ async watchAddress(callback) {
434
+ return this.provider.watchAddress(this.getDepositAddress(), callback);
435
+ }
436
+ // watching for any transaction of this wallet
437
+ async watchAddressTransactions(callback) {
438
+ return this.provider.watchAddressTransactions(this.getDepositAddress(), callback);
439
+ }
440
+ // watching for cashtoken transaction of this wallet
441
+ async watchAddressTokenTransactions(callback) {
442
+ return this.provider.watchAddressTokenTransactions(this.getDepositAddress(), callback);
443
+ }
444
+ // sets up a callback to be called upon wallet's balance change
445
+ // can be cancelled by calling the function returned from this one
446
+ async watchBalance(callback) {
447
+ return this.provider.watchAddressStatus(this.getDepositAddress(), async (_status) => {
448
+ const balance = (await this.getBalance());
449
+ callback(balance);
450
+ });
451
+ }
452
+ // sets up a callback to be called upon wallet's BCH or USD balance change
453
+ // if BCH balance does not change, the callback will be triggered every
454
+ // @param `usdPriceRefreshInterval` milliseconds by polling for new BCH USD price
455
+ // Since we want to be most sensitive to usd value change, we do not use the cached exchange rates
456
+ // can be cancelled by calling the function returned from this one
457
+ async watchBalanceUsd(callback, usdPriceRefreshInterval = 30000) {
458
+ let usdPrice = -1;
459
+ const _callback = async () => {
460
+ const balance = (await this.getBalance(undefined, false));
461
+ if (usdPrice !== balance.usd) {
462
+ usdPrice = balance.usd;
463
+ callback(balance);
464
+ }
465
+ };
466
+ const watchCancel = await this.provider.watchAddressStatus(this.getDepositAddress(), _callback);
467
+ const interval = setInterval(_callback, usdPriceRefreshInterval);
468
+ return async () => {
469
+ await watchCancel?.();
470
+ clearInterval(interval);
471
+ };
472
+ }
473
+ // waits for address balance to be greater than or equal to the target value
474
+ // this call halts the execution
475
+ async waitForBalance(value, rawUnit = UnitEnum.BCH) {
476
+ return new Promise(async (resolve) => {
477
+ let watchCancel;
478
+ watchCancel = await this.watchBalance(async (balance) => {
479
+ const satoshiBalance = await amountInSatoshi(value, rawUnit);
480
+ if (balance.sat >= satoshiBalance) {
481
+ await watchCancel?.();
482
+ resolve(balance);
483
+ }
484
+ });
485
+ });
486
+ }
487
+ // sets up a callback to be called upon wallet's token balance change
488
+ // can be cancelled by calling the function returned from this one
489
+ async watchTokenBalance(tokenId, callback) {
490
+ let previous = undefined;
491
+ return await this.provider.watchAddressStatus(this.getDepositAddress(), async (_status) => {
492
+ const balance = await this.getTokenBalance(tokenId);
493
+ if (previous != balance) {
494
+ callback(balance);
495
+ }
496
+ previous = balance;
497
+ });
498
+ }
499
+ // waits for address token balance to be greater than or equal to the target amount
500
+ // this call halts the execution
501
+ async waitForTokenBalance(tokenId, amount) {
502
+ return new Promise(async (resolve) => {
503
+ let watchCancel;
504
+ watchCancel = await this.watchTokenBalance(tokenId, async (balance) => {
505
+ if (balance >= amount) {
506
+ await watchCancel?.();
507
+ resolve(balance);
508
+ }
509
+ });
510
+ });
511
+ }
512
+ async getTokenInfo(tokenId) {
513
+ return BCMR.getTokenInfo(tokenId);
514
+ }
515
+ async _getMaxAmountToSend(params = {
516
+ outputCount: 1,
517
+ options: {},
518
+ }) {
519
+ if (!params.privateKey && params.options?.buildUnsigned !== true) {
520
+ throw Error("Couldn't get network or private key for wallet.");
521
+ }
522
+ if (!this.cashaddr) {
523
+ throw Error("attempted to send without a cashaddr");
524
+ }
525
+ if (params.options && params.options.slpSemiAware) {
526
+ this._slpSemiAware = true;
527
+ }
528
+ let feePaidBy;
529
+ if (params.options && params.options.feePaidBy) {
530
+ feePaidBy = params.options.feePaidBy;
531
+ }
532
+ else {
533
+ feePaidBy = FeePaidByEnum.change;
534
+ }
535
+ // get inputs
536
+ let utxos;
537
+ if (params.options && params.options.utxoIds) {
538
+ utxos = await checkUtxos(params.options.utxoIds.map((utxoId) => typeof utxoId === "string" ? fromUtxoId(utxoId) : utxoId), this);
539
+ }
540
+ else {
541
+ utxos = (await this.getAddressUtxos(this.cashaddr)).filter((utxo) => !utxo.token);
542
+ }
543
+ // Get current height to assure recently mined coins are not spent.
544
+ const bestHeight = await this.provider.getBlockHeight();
545
+ // simulate outputs using the sender's address
546
+ const sendRequest = new SendRequest({
547
+ cashaddr: this.cashaddr,
548
+ value: 100,
549
+ unit: "sat",
550
+ });
551
+ const sendRequests = Array(params.outputCount)
552
+ .fill(0)
553
+ .map(() => sendRequest);
554
+ const fundingUtxos = await getSuitableUtxos(utxos, undefined, bestHeight, feePaidBy, sendRequests);
555
+ const relayFeePerByteInSatoshi = await getRelayFeeCache(this.provider);
556
+ const fee = await getFeeAmountSimple({
557
+ utxos: fundingUtxos,
558
+ sendRequests: sendRequests,
559
+ privateKey: params.privateKey ?? hexToBin(placeholderPrivateKey),
560
+ sourceAddress: this.cashaddr,
561
+ relayFeePerByteInSatoshi: relayFeePerByteInSatoshi,
562
+ feePaidBy: feePaidBy,
563
+ });
564
+ const spendableAmount = sumUtxoValue(fundingUtxos);
565
+ let result = spendableAmount - fee;
566
+ if (result < 0) {
567
+ result = 0;
568
+ }
569
+ return { value: result, utxos: fundingUtxos };
570
+ }
571
+ async getMaxAmountToSend(params = {
572
+ outputCount: 1,
573
+ options: {},
574
+ }) {
575
+ const { value: result } = await this._getMaxAmountToSend(params);
576
+ return await balanceResponseFromSatoshi(result);
577
+ }
578
+ /**
579
+ * send Send some amount to an address
580
+ * this function processes the send requests, encodes the transaction, sends it to the network
581
+ * @returns (depending on the options parameter) the transaction id, new address balance and a link to the transaction on the blockchain explorer
582
+ *
583
+ * This is a first class function with REST analog, maintainers should strive to keep backward-compatibility
584
+ *
585
+ */
381
586
  async send(requests, options) {
382
- throw Error("send called on base wallet");
587
+ const { encodedTransaction, tokenIds, sourceOutputs } = await this.encodeTransaction(requests, undefined, options);
588
+ const resp = new SendResponse({});
589
+ resp.tokenIds = tokenIds;
590
+ if (options?.buildUnsigned !== true) {
591
+ const txId = await this.submitTransaction(encodedTransaction, options?.awaitTransactionPropagation === undefined ||
592
+ options?.awaitTransactionPropagation === true);
593
+ resp.txId = txId;
594
+ resp.explorerUrl = this.explorerUrl(resp.txId);
595
+ if (options?.queryBalance === undefined ||
596
+ options?.queryBalance === true) {
597
+ resp.balance = (await this.getBalance());
598
+ }
599
+ }
600
+ else {
601
+ resp.unsignedTransaction = binToHex(encodedTransaction);
602
+ resp.sourceOutputs = sourceOutputs;
603
+ }
604
+ return resp;
383
605
  }
384
- // @ts-ignore
385
- async sendMax(address, options) {
386
- throw Error("sendMax called on base wallet");
606
+ /**
607
+ * sendMax Send all available funds to a destination cash address
608
+ *
609
+ * @param {string} cashaddr destination cash address
610
+ * @param {SendRequestOptionsI} options Options of the send requests
611
+ *
612
+ * @returns (depending on the options parameter) the transaction id, new address balance and a link to the transaction on the blockchain explorer
613
+ */
614
+ async sendMax(cashaddr, options) {
615
+ return await this.sendMaxRaw(cashaddr, options);
616
+ }
617
+ /**
618
+ * sendMaxRaw (internal) Send all available funds to a destination cash address
619
+ *
620
+ * @param {string} cashaddr destination cash address
621
+ * @param {SendRequestOptionsI} options Options of the send requests
622
+ *
623
+ * @returns the transaction id sent to the network
624
+ */
625
+ async sendMaxRaw(cashaddr, options, privateKey) {
626
+ const { value: maxSpendableAmount, utxos } = await this._getMaxAmountToSend({
627
+ outputCount: 1,
628
+ options: options,
629
+ privateKey: privateKey,
630
+ });
631
+ if (!options) {
632
+ options = {};
633
+ }
634
+ options.utxoIds = utxos;
635
+ const sendRequest = new SendRequest({
636
+ cashaddr: cashaddr,
637
+ value: maxSpendableAmount,
638
+ unit: "sat",
639
+ });
640
+ const { encodedTransaction, tokenIds, sourceOutputs } = await this.encodeTransaction([sendRequest], true, options, privateKey);
641
+ const resp = new SendResponse({});
642
+ resp.tokenIds = tokenIds;
643
+ if (options?.buildUnsigned !== true) {
644
+ const txId = await this.submitTransaction(encodedTransaction, options?.awaitTransactionPropagation === undefined ||
645
+ options?.awaitTransactionPropagation === true);
646
+ resp.txId = txId;
647
+ resp.explorerUrl = this.explorerUrl(resp.txId);
648
+ if (options?.queryBalance === undefined ||
649
+ options?.queryBalance === true) {
650
+ resp.balance = (await this.getBalance());
651
+ }
652
+ }
653
+ else {
654
+ resp.unsignedTransaction = binToHex(encodedTransaction);
655
+ resp.sourceOutputs = sourceOutputs;
656
+ }
657
+ return resp;
658
+ }
659
+ /**
660
+ * encodeTransaction Encode and sign a transaction given a list of sendRequests, options and estimate fees.
661
+ * @param {SendRequest[]} sendRequests SendRequests
662
+ * @param {boolean} discardChange=false
663
+ * @param {SendRequestOptionsI} options Options of the send requests
664
+ */
665
+ async encodeTransaction(requests, discardChange = false, options, privateKey) {
666
+ let sendRequests = asSendRequestObject(requests);
667
+ if (!privateKey && options?.buildUnsigned !== true) {
668
+ throw new Error(`Missing private key`);
669
+ }
670
+ if (options && options.slpSemiAware) {
671
+ this._slpSemiAware = true;
672
+ }
673
+ let feePaidBy;
674
+ if (options && options.feePaidBy) {
675
+ feePaidBy = options.feePaidBy;
676
+ }
677
+ else {
678
+ feePaidBy = FeePaidByEnum.change;
679
+ }
680
+ let changeAddress;
681
+ if (options && options.changeAddress) {
682
+ changeAddress = options.changeAddress;
683
+ }
684
+ else {
685
+ changeAddress = this.cashaddr;
686
+ }
687
+ let checkTokenQuantities = true;
688
+ if (options && options.checkTokenQuantities === false) {
689
+ checkTokenQuantities = false;
690
+ }
691
+ // get inputs from options or query all inputs
692
+ let utxos;
693
+ if (options && options.utxoIds) {
694
+ utxos = await checkUtxos(options.utxoIds.map((utxoId) => typeof utxoId === "string" ? fromUtxoId(utxoId) : utxoId), this);
695
+ }
696
+ else {
697
+ utxos = await this.getAddressUtxos(this.cashaddr);
698
+ }
699
+ // filter out token utxos if there are no token requests
700
+ if (checkTokenQuantities &&
701
+ !sendRequests.some((val) => val instanceof TokenSendRequest)) {
702
+ utxos = utxos.filter((val) => !val.token);
703
+ }
704
+ const addTokenChangeOutputs = (inputs, outputs) => {
705
+ // Allow for implicit token burn if the total amount sent is less than user had
706
+ // allow for token genesis, creating more tokens than we had before (0)
707
+ if (!checkTokenQuantities) {
708
+ return;
709
+ }
710
+ const allTokenInputs = inputs.filter((val) => val.token);
711
+ const allTokenOutputs = outputs.filter((val) => val instanceof TokenSendRequest);
712
+ const tokenIds = allTokenOutputs
713
+ .map((val) => val.tokenId)
714
+ .filter((val, idx, arr) => arr.indexOf(val) === idx);
715
+ for (let tokenId of tokenIds) {
716
+ const tokenInputs = allTokenInputs.filter((val) => val.token?.tokenId === tokenId);
717
+ const inputAmountSum = tokenInputs.reduce((prev, cur) => prev + cur.token.amount, 0n);
718
+ const tokenOutputs = allTokenOutputs.filter((val) => val.tokenId === tokenId);
719
+ const outputAmountSum = tokenOutputs.reduce((prev, cur) => prev + cur.amount, 0n);
720
+ const diff = inputAmountSum - outputAmountSum;
721
+ if (diff < 0) {
722
+ throw new Error("Not enough token amount to send");
723
+ }
724
+ if (diff >= 0) {
725
+ let available = 0n;
726
+ let change = 0n;
727
+ const ensureUtxos = [];
728
+ for (const token of tokenInputs.filter((val) => val.token?.amount)) {
729
+ ensureUtxos.push(token);
730
+ available += token.token.amount;
731
+ if (available >= outputAmountSum) {
732
+ change = available - outputAmountSum;
733
+ //break;
734
+ }
735
+ }
736
+ if (ensureUtxos.length) {
737
+ if (!options) {
738
+ options = {};
739
+ }
740
+ options.ensureUtxos = [
741
+ ...(options.ensureUtxos ?? []),
742
+ ...ensureUtxos,
743
+ ].filter((val, index, array) => array.findIndex((other) => other.txid === val.txid && other.vout === val.vout) === index);
744
+ }
745
+ if (change > 0) {
746
+ outputs.push(new TokenSendRequest({
747
+ cashaddr: toTokenaddr(changeAddress) || this.tokenaddr,
748
+ amount: change,
749
+ tokenId: tokenId,
750
+ commitment: tokenOutputs[0].commitment,
751
+ capability: tokenOutputs[0].capability,
752
+ value: tokenOutputs[0].value,
753
+ }));
754
+ }
755
+ }
756
+ }
757
+ };
758
+ addTokenChangeOutputs(utxos, sendRequests);
759
+ const bestHeight = await this.provider.getBlockHeight();
760
+ const spendAmount = await sumSendRequestAmounts(sendRequests);
761
+ if (utxos.length === 0) {
762
+ throw Error("There were no Unspent Outputs");
763
+ }
764
+ if (typeof spendAmount !== "bigint") {
765
+ throw Error("Couldn't get spend amount when building transaction");
766
+ }
767
+ const relayFeePerByteInSatoshi = await getRelayFeeCache(this.provider);
768
+ const feeEstimate = await getFeeAmountSimple({
769
+ utxos: utxos,
770
+ sendRequests: sendRequests,
771
+ privateKey: privateKey ?? hexToBin(placeholderPrivateKey),
772
+ sourceAddress: this.cashaddr,
773
+ relayFeePerByteInSatoshi: relayFeePerByteInSatoshi,
774
+ feePaidBy: feePaidBy,
775
+ });
776
+ const fundingUtxos = await getSuitableUtxos(utxos, BigInt(spendAmount) + BigInt(Math.ceil(feeEstimate)), bestHeight, feePaidBy, sendRequests, options?.ensureUtxos || [], options?.tokenOperation);
777
+ if (fundingUtxos.length === 0) {
778
+ throw Error("The available inputs couldn't satisfy the request with fees");
779
+ }
780
+ const fee = await getFeeAmount({
781
+ utxos: fundingUtxos,
782
+ sendRequests: sendRequests,
783
+ privateKey: privateKey ?? hexToBin(placeholderPrivateKey),
784
+ sourceAddress: this.cashaddr,
785
+ relayFeePerByteInSatoshi: relayFeePerByteInSatoshi,
786
+ feePaidBy: feePaidBy,
787
+ });
788
+ const { encodedTransaction, sourceOutputs } = await buildEncodedTransaction({
789
+ inputs: fundingUtxos,
790
+ outputs: sendRequests,
791
+ signingKey: privateKey ?? hexToBin(placeholderPrivateKey),
792
+ sourceAddress: this.cashaddr,
793
+ fee,
794
+ discardChange,
795
+ feePaidBy,
796
+ changeAddress,
797
+ buildUnsigned: options?.buildUnsigned === true,
798
+ });
799
+ const tokenIds = [
800
+ ...fundingUtxos
801
+ .filter((val) => val.token?.tokenId)
802
+ .map((val) => val.token.tokenId),
803
+ ...sendRequests
804
+ .filter((val) => val instanceof TokenSendRequest)
805
+ .map((val) => val.tokenId),
806
+ ].filter((value, index, array) => array.indexOf(value) === index);
807
+ return { encodedTransaction, tokenIds, sourceOutputs };
808
+ }
809
+ // Submit a raw transaction
810
+ async submitTransaction(transaction, awaitPropagation = true) {
811
+ if (!this.provider) {
812
+ throw Error("Wallet network provider was not initialized");
813
+ }
814
+ let rawTransaction = binToHex(transaction);
815
+ return await this.provider.sendRawTransaction(rawTransaction, awaitPropagation);
816
+ }
817
+ // gets transaction history of this wallet
818
+ async getRawHistory(fromHeight = 0, toHeight = -1) {
819
+ return await this.provider.getHistory(this.cashaddr, fromHeight, toHeight);
820
+ }
821
+ /**
822
+ * getHistory gets transaction history of this wallet with most data decoded and ready to present to user
823
+ * @note balance calculations are valid only if querying to the blockchain tip (`toHeight` === -1, `count` === -1)
824
+ * @note this method is heavy on network calls, if invoked in browser use of cache is advised, @see `Config.UseLocalStorageCache`
825
+ * @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)
826
+ *
827
+ * @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
828
+ * @param fromHeight optional, if set, history will be limited. Default 0
829
+ * @param toHeight optional, if set, history will be limited. Default -1, meaning that all history items will be returned, including mempool
830
+ * @param start optional, if set, the result set will be paginated with offset `start`
831
+ * @param count optional, if set, the result set will be paginated with `count`. Default -1, meaning that all history items will be returned
832
+ *
833
+ * @returns an array of transaction history items, with input values and addresses encoded in cashaddress format. @see `TransactionHistoryItem` type
834
+ */
835
+ async getHistory({ unit = "sat", fromHeight = 0, toHeight = -1, start = 0, count = -1, }) {
836
+ return getAddressHistory({
837
+ address: this.cashaddr,
838
+ provider: this.provider,
839
+ unit,
840
+ fromHeight,
841
+ toHeight,
842
+ start,
843
+ count,
844
+ });
845
+ }
846
+ // gets last transaction of this wallet
847
+ async getLastTransaction(confirmedOnly = false) {
848
+ let history = await this.getRawHistory();
849
+ if (confirmedOnly) {
850
+ history = history.filter((val) => val.height > 0);
851
+ }
852
+ if (!history.length) {
853
+ return null;
854
+ }
855
+ const [lastTx] = history.slice(-1);
856
+ return this.provider.getRawTransactionObject(lastTx.tx_hash);
857
+ }
858
+ // waits for next transaction, program execution is halted
859
+ async waitForTransaction(options = {
860
+ getTransactionInfo: true,
861
+ getBalance: false,
862
+ txHash: undefined,
863
+ }) {
864
+ if (options.getTransactionInfo === undefined) {
865
+ options.getTransactionInfo = true;
866
+ }
867
+ return new Promise(async (resolve) => {
868
+ let txHashSeen = false;
869
+ const makeResponse = async (txHash) => {
870
+ const response = {};
871
+ const promises = [undefined, undefined];
872
+ if (options.getBalance === true) {
873
+ promises[0] = this.getBalance();
874
+ }
875
+ if (options.getTransactionInfo === true) {
876
+ if (!txHash) {
877
+ promises[1] = this.getLastTransaction();
878
+ }
879
+ else {
880
+ promises[1] = this.provider.getRawTransactionObject(txHash);
881
+ }
882
+ }
883
+ const result = await Promise.all(promises);
884
+ response.balance = result[0];
885
+ response.transactionInfo = result[1];
886
+ return response;
887
+ };
888
+ // waiting for a specific transaction to propagate
889
+ if (options.txHash) {
890
+ let cancel;
891
+ const waitForTransactionCallback = async (data) => {
892
+ if (data && data[0] === options.txHash && data[1] !== null) {
893
+ txHashSeen = true;
894
+ await cancel?.();
895
+ resolve(makeResponse(options.txHash));
896
+ }
897
+ };
898
+ cancel = await this.provider.subscribeToTransaction(options.txHash, waitForTransactionCallback);
899
+ return;
900
+ }
901
+ // waiting for any address transaction
902
+ let watchCancel;
903
+ let initialResponseSeen = false;
904
+ watchCancel = await this.provider.watchAddressStatus(this.getDepositAddress(), async (_status) => {
905
+ if (initialResponseSeen) {
906
+ await watchCancel?.();
907
+ resolve(makeResponse());
908
+ return;
909
+ }
910
+ initialResponseSeen = true;
911
+ });
912
+ });
913
+ }
914
+ /**
915
+ * watchBlocks Watch network blocks
916
+ *
917
+ * @param callback callback with a block header object
918
+ * @param skipCurrentHeight if set, the notification about current block will not arrive
919
+ *
920
+ * @returns a function which will cancel watching upon evaluation
921
+ */
922
+ async watchBlocks(callback, skipCurrentHeight = true) {
923
+ return this.provider.watchBlocks(callback, skipCurrentHeight);
924
+ }
925
+ /**
926
+ * waitForBlock Wait for a network block
927
+ *
928
+ * @param height if specified waits for this exact blockchain height, otherwise resolves with the next block
929
+ *
930
+ */
931
+ async waitForBlock(height) {
932
+ return this.provider.waitForBlock(height);
933
+ }
934
+ //#endregion Funds
935
+ // Convenience wrapper to verify interface
936
+ async verify(message, sig, publicKey) {
937
+ return await new SignedMessage().verify(message, sig, this.cashaddr, publicKey);
938
+ }
939
+ //#region Cashtokens
940
+ /**
941
+ * Create new cashtoken, both funglible and/or non-fungible (NFT)
942
+ * Refer to spec https://github.com/bitjson/cashtokens
943
+ * @param {number} genesisRequest.amount amount of *fungible* tokens to create
944
+ * @param {NFTCapability?} genesisRequest.capability capability of new NFT
945
+ * @param {string?} genesisRequest.commitment NFT commitment message
946
+ * @param {string?} genesisRequest.cashaddr cash address to send the created token UTXO to; if undefined will default to your address
947
+ * @param {number?} genesisRequest.value satoshi value to send alongside with tokens; if undefined will default to 1000 satoshi
948
+ * @param {SendRequestType | SendRequestType[]} sendRequests single or an array of extra send requests (OP_RETURN, value transfer, etc.) to include in genesis transaction
949
+ * @param {SendRequestOptionsI} options Options of the send requests
950
+ */
951
+ async tokenGenesis(genesisRequest, sendRequests = [], options) {
952
+ if (!Array.isArray(sendRequests)) {
953
+ sendRequests = [sendRequests];
954
+ }
955
+ let utxos;
956
+ if (options && options.utxoIds) {
957
+ utxos = await checkUtxos(options.utxoIds.map((utxoId) => typeof utxoId === "string" ? fromUtxoId(utxoId) : utxoId), this);
958
+ }
959
+ else {
960
+ utxos = await this.getAddressUtxos(this.cashaddr);
961
+ }
962
+ const genesisInputs = utxos.filter((val) => val.vout === 0 && !val.token);
963
+ if (genesisInputs.length === 0) {
964
+ throw new Error("No suitable inputs with vout=0 available for new token genesis");
965
+ }
966
+ const genesisSendRequest = new TokenSendRequest({
967
+ cashaddr: genesisRequest.cashaddr || this.tokenaddr,
968
+ amount: genesisRequest.amount,
969
+ value: genesisRequest.value || 1000,
970
+ capability: genesisRequest.capability,
971
+ commitment: genesisRequest.commitment,
972
+ tokenId: genesisInputs[0].txid,
973
+ });
974
+ return this.send([genesisSendRequest, ...sendRequests], {
975
+ ...options,
976
+ utxoIds: utxos,
977
+ ensureUtxos: [genesisInputs[0]],
978
+ checkTokenQuantities: false,
979
+ queryBalance: false,
980
+ tokenOperation: "genesis",
981
+ });
982
+ }
983
+ /**
984
+ * Mint new NFT cashtokens using an existing minting token
985
+ * Refer to spec https://github.com/bitjson/cashtokens
986
+ * @param {string} tokenId tokenId of an NFT to mint
987
+ * @param {TokenMintRequest | TokenMintRequest[]} mintRequests mint requests with new token properties and recipients
988
+ * @param {NFTCapability?} mintRequest.capability capability of new NFT
989
+ * @param {string?} mintRequest.commitment NFT commitment message
990
+ * @param {string?} mintRequest.cashaddr cash address to send the created token UTXO to; if undefined will default to your address
991
+ * @param {number?} mintRequest.value satoshi value to send alongside with tokens; if undefined will default to 1000 satoshi
992
+ * @param {boolean?} deductTokenAmount if minting token contains fungible amount, deduct from it by amount of minted tokens
993
+ * @param {SendRequestOptionsI} options Options of the send requests
994
+ */
995
+ async tokenMint(tokenId, mintRequests, deductTokenAmount = false, options) {
996
+ if (tokenId?.length !== 64) {
997
+ throw Error(`Invalid tokenId supplied: ${tokenId}`);
998
+ }
999
+ if (!Array.isArray(mintRequests)) {
1000
+ mintRequests = [mintRequests];
1001
+ }
1002
+ const utxos = await this.getAddressUtxos(this.cashaddr);
1003
+ const nftUtxos = utxos.filter((val) => val.token?.tokenId === tokenId &&
1004
+ val.token?.capability === NFTCapability.minting);
1005
+ if (!nftUtxos.length) {
1006
+ throw new Error("You do not have any token UTXOs with minting capability for specified tokenId");
1007
+ }
1008
+ const newAmount = deductTokenAmount && nftUtxos[0].token.amount > 0
1009
+ ? nftUtxos[0].token.amount - BigInt(mintRequests.length)
1010
+ : nftUtxos[0].token.amount;
1011
+ const safeNewAmount = newAmount < 0n ? 0n : newAmount;
1012
+ const mintingInput = new TokenSendRequest({
1013
+ cashaddr: this.tokenaddr,
1014
+ tokenId: tokenId,
1015
+ capability: nftUtxos[0].token.capability,
1016
+ commitment: nftUtxos[0].token.commitment,
1017
+ amount: safeNewAmount,
1018
+ value: nftUtxos[0].satoshis,
1019
+ });
1020
+ return this.send([
1021
+ mintingInput,
1022
+ ...mintRequests.map((val) => new TokenSendRequest({
1023
+ cashaddr: val.cashaddr || this.tokenaddr,
1024
+ amount: 0,
1025
+ tokenId: tokenId,
1026
+ value: val.value,
1027
+ capability: val.capability,
1028
+ commitment: val.commitment,
1029
+ })),
1030
+ ], {
1031
+ ...options,
1032
+ ensureUtxos: [nftUtxos[0]],
1033
+ checkTokenQuantities: false,
1034
+ queryBalance: false,
1035
+ tokenOperation: "mint",
1036
+ });
1037
+ }
1038
+ /**
1039
+ * Perform an explicit token burning by spending a token utxo to an OP_RETURN
1040
+ *
1041
+ * Behaves differently for fungible and non-fungible tokens:
1042
+ * * NFTs are always "destroyed"
1043
+ * * FTs' amount is reduced by the amount specified, if 0 FT amount is left and no NFT present, the token is "destroyed"
1044
+ *
1045
+ * Refer to spec https://github.com/bitjson/cashtokens
1046
+ * @param {string} burnRequest.tokenId tokenId of a token to burn
1047
+ * @param {NFTCapability} burnRequest.capability capability of the NFT token to select, optional
1048
+ * @param {string?} burnRequest.commitment commitment of the NFT token to select, optional
1049
+ * @param {number?} burnRequest.amount amount of fungible tokens to burn, optional
1050
+ * @param {string?} burnRequest.cashaddr address to return token and satoshi change to
1051
+ * @param {string?} message optional message to include in OP_RETURN
1052
+ * @param {SendRequestOptionsI} options Options of the send requests
1053
+ */
1054
+ async tokenBurn(burnRequest, message, options) {
1055
+ if (burnRequest.tokenId?.length !== 64) {
1056
+ throw Error(`Invalid tokenId supplied: ${burnRequest.tokenId}`);
1057
+ }
1058
+ const utxos = await this.getAddressUtxos(this.cashaddr);
1059
+ const tokenUtxos = utxos.filter((val) => val.token?.tokenId === burnRequest.tokenId &&
1060
+ val.token?.capability === burnRequest.capability &&
1061
+ val.token?.commitment === burnRequest.commitment);
1062
+ if (!tokenUtxos.length) {
1063
+ throw new Error("You do not have suitable token UTXOs to perform burn");
1064
+ }
1065
+ const totalFungibleAmount = tokenUtxos.reduce((prev, cur) => prev + (cur.token?.amount || 0n), 0n);
1066
+ let fungibleBurnAmount = burnRequest.amount && burnRequest.amount > 0 ? burnRequest.amount : 0n;
1067
+ fungibleBurnAmount = BigInt(fungibleBurnAmount);
1068
+ const hasNFT = burnRequest.capability || burnRequest.commitment;
1069
+ let utxoIds = [];
1070
+ let changeSendRequests;
1071
+ if (hasNFT) {
1072
+ // does not have FT tokens, let us destroy the token completely
1073
+ if (totalFungibleAmount === 0n) {
1074
+ changeSendRequests = [];
1075
+ utxoIds.push(tokenUtxos[0]);
1076
+ }
1077
+ else {
1078
+ // add utxos to spend from
1079
+ let available = 0n;
1080
+ for (const token of tokenUtxos.filter((val) => val.token?.amount)) {
1081
+ utxoIds.push(token);
1082
+ available += token.token.amount;
1083
+ if (available >= fungibleBurnAmount) {
1084
+ break;
1085
+ }
1086
+ }
1087
+ // if there are FT, reduce their amount
1088
+ const newAmount = totalFungibleAmount - fungibleBurnAmount;
1089
+ const safeNewAmount = newAmount < 0n ? 0n : newAmount;
1090
+ changeSendRequests = [
1091
+ new TokenSendRequest({
1092
+ cashaddr: burnRequest.cashaddr || this.tokenaddr,
1093
+ tokenId: burnRequest.tokenId,
1094
+ capability: burnRequest.capability,
1095
+ commitment: burnRequest.commitment,
1096
+ amount: safeNewAmount,
1097
+ value: tokenUtxos[0].satoshis,
1098
+ }),
1099
+ ];
1100
+ }
1101
+ }
1102
+ else {
1103
+ // if we are burning last fungible tokens, let us destroy the token completely
1104
+ if (totalFungibleAmount === fungibleBurnAmount) {
1105
+ changeSendRequests = [];
1106
+ utxoIds.push(...tokenUtxos);
1107
+ }
1108
+ else {
1109
+ // add utxos to spend from
1110
+ let available = 0n;
1111
+ for (const token of tokenUtxos.filter((val) => val.token?.amount)) {
1112
+ utxoIds.push(token);
1113
+ available += token.token.amount;
1114
+ if (available >= fungibleBurnAmount) {
1115
+ break;
1116
+ }
1117
+ }
1118
+ // reduce the FT amount
1119
+ const newAmount = available - fungibleBurnAmount;
1120
+ const safeNewAmount = newAmount < 0n ? 0n : newAmount;
1121
+ changeSendRequests = [
1122
+ new TokenSendRequest({
1123
+ cashaddr: burnRequest.cashaddr || this.tokenaddr,
1124
+ tokenId: burnRequest.tokenId,
1125
+ amount: safeNewAmount,
1126
+ value: tokenUtxos.reduce((a, c) => a + c.satoshis, 0),
1127
+ }),
1128
+ ];
1129
+ }
1130
+ }
1131
+ const opReturn = OpReturnData.fromString(message || "");
1132
+ return this.send([opReturn, ...changeSendRequests], {
1133
+ ...options,
1134
+ checkTokenQuantities: false,
1135
+ queryBalance: false,
1136
+ ensureUtxos: utxoIds.length > 0 ? utxoIds : undefined,
1137
+ tokenOperation: "burn",
1138
+ });
1139
+ }
1140
+ /**
1141
+ * getTokenUtxos Get unspent token outputs for the wallet
1142
+ * will return utxos only for the specified token if `tokenId` provided
1143
+ * @param {string?} tokenId tokenId (category) to filter utxos by, if not set will return utxos from all tokens
1144
+ * @returns {UtxoI[]} token utxos
1145
+ */
1146
+ async getTokenUtxos(tokenId) {
1147
+ const utxos = await this.getAddressUtxos(this.cashaddr);
1148
+ return utxos.filter((val) => tokenId ? val.token?.tokenId === tokenId : val.token);
1149
+ }
1150
+ /**
1151
+ * getTokenBalance Gets fungible token balance
1152
+ * for NFT token balance see @ref getNftTokenBalance
1153
+ * @param {string} tokenId tokenId to get balance for
1154
+ * @returns {bigint} fungible token balance
1155
+ */
1156
+ async getTokenBalance(tokenId) {
1157
+ const utxos = (await this.getTokenUtxos(tokenId)).filter((val) => val.token?.amount);
1158
+ return sumTokenAmounts(utxos, tokenId);
1159
+ }
1160
+ /**
1161
+ * getNftTokenBalance Gets non-fungible token (NFT) balance for a particular tokenId
1162
+ * disregards fungible token balances
1163
+ * for fungible token balance see @ref getTokenBalance
1164
+ * @param {string} tokenId tokenId to get balance for
1165
+ * @returns {number} non-fungible token balance
1166
+ */
1167
+ async getNftTokenBalance(tokenId) {
1168
+ const utxos = (await this.getTokenUtxos(tokenId)).filter((val) => val.token?.commitment !== undefined);
1169
+ return utxos.length;
1170
+ }
1171
+ /**
1172
+ * getAllTokenBalances Gets all fungible token balances in this wallet
1173
+ * @returns {Object} a map [tokenId => balance] for all tokens in this wallet
1174
+ */
1175
+ async getAllTokenBalances() {
1176
+ const result = {};
1177
+ const utxos = (await this.getTokenUtxos()).filter((val) => val.token?.amount);
1178
+ for (const utxo of utxos) {
1179
+ if (!result[utxo.token.tokenId]) {
1180
+ result[utxo.token.tokenId] = 0n;
1181
+ }
1182
+ result[utxo.token.tokenId] += utxo.token.amount;
1183
+ }
1184
+ return result;
1185
+ }
1186
+ /**
1187
+ * getAllNftTokenBalances Gets all non-fungible token (NFT) balances in this wallet
1188
+ * @returns {Object} a map [tokenId => balance] for all NFTs in this wallet
1189
+ */
1190
+ async getAllNftTokenBalances() {
1191
+ const result = {};
1192
+ const utxos = (await this.getTokenUtxos()).filter((val) => val.token?.commitment !== undefined);
1193
+ for (const utxo of utxos) {
1194
+ if (!result[utxo.token.tokenId]) {
1195
+ result[utxo.token.tokenId] = 0;
1196
+ }
1197
+ result[utxo.token.tokenId] += 1;
1198
+ }
1199
+ return result;
1200
+ }
1201
+ }
1202
+ /**
1203
+ * Class to manage a mainnet watch wallet.
1204
+ */
1205
+ export class WatchWallet extends BaseWallet {
1206
+ static { this.networkPrefix = CashAddressNetworkPrefix.mainnet; }
1207
+ static { this.walletType = WalletTypeEnum.Watch; }
1208
+ constructor() {
1209
+ super(NetworkType.Mainnet);
1210
+ }
1211
+ }
1212
+ /**
1213
+ * Class to manage a testnet watch wallet.
1214
+ */
1215
+ export class TestNetWatchWallet extends BaseWallet {
1216
+ static { this.networkPrefix = CashAddressNetworkPrefix.testnet; }
1217
+ static { this.walletType = WalletTypeEnum.Watch; }
1218
+ constructor() {
1219
+ super(NetworkType.Testnet);
1220
+ }
1221
+ }
1222
+ /**
1223
+ * Class to manage a regtest watch wallet.
1224
+ */
1225
+ export class RegTestWatchWallet extends BaseWallet {
1226
+ static { this.networkPrefix = CashAddressNetworkPrefix.regtest; }
1227
+ static { this.walletType = WalletTypeEnum.Watch; }
1228
+ constructor() {
1229
+ super(NetworkType.Regtest);
387
1230
  }
388
1231
  }
389
1232
  /**