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.
- package/dist/index.html +1 -1
- package/dist/{mainnet-2.6.7.js → mainnet-2.7.0.js} +446 -156
- package/dist/module/cli.js +0 -4
- package/dist/module/cli.js.map +1 -1
- package/dist/module/index.d.ts +1 -3
- package/dist/module/index.d.ts.map +1 -1
- package/dist/module/index.js +1 -3
- package/dist/module/index.js.map +1 -1
- package/dist/module/network/ElectrumNetworkProvider.d.ts +21 -27
- package/dist/module/network/ElectrumNetworkProvider.d.ts.map +1 -1
- package/dist/module/network/ElectrumNetworkProvider.js +92 -102
- package/dist/module/network/ElectrumNetworkProvider.js.map +1 -1
- package/dist/module/network/NetworkProvider.d.ts +9 -22
- package/dist/module/network/NetworkProvider.d.ts.map +1 -1
- package/dist/module/network/constant.d.ts +0 -21
- package/dist/module/network/constant.d.ts.map +1 -1
- package/dist/module/network/constant.js +0 -21
- package/dist/module/network/constant.js.map +1 -1
- package/dist/module/network/default.d.ts +2 -2
- package/dist/module/network/default.d.ts.map +1 -1
- package/dist/module/network/default.js +15 -45
- package/dist/module/network/default.js.map +1 -1
- package/dist/module/network/interface.d.ts +2 -8
- package/dist/module/network/interface.d.ts.map +1 -1
- package/dist/module/network/util.d.ts.map +1 -1
- package/dist/module/network/util.js +4 -5
- package/dist/module/network/util.js.map +1 -1
- package/dist/module/rate/ExchangeRate.js +2 -1
- package/dist/module/rate/ExchangeRate.js.map +1 -1
- package/dist/module/transaction/Wif.d.ts.map +1 -1
- package/dist/module/transaction/Wif.js +1 -1
- package/dist/module/transaction/Wif.js.map +1 -1
- package/dist/module/wallet/Base.d.ts +282 -88
- package/dist/module/wallet/Base.d.ts.map +1 -1
- package/dist/module/wallet/Base.js +1058 -215
- package/dist/module/wallet/Base.js.map +1 -1
- package/dist/module/wallet/Util.d.ts +7 -54
- package/dist/module/wallet/Util.d.ts.map +1 -1
- package/dist/module/wallet/Util.js +12 -79
- package/dist/module/wallet/Util.js.map +1 -1
- package/dist/module/wallet/Wif.d.ts +46 -251
- package/dist/module/wallet/Wif.d.ts.map +1 -1
- package/dist/module/wallet/Wif.js +126 -1026
- package/dist/module/wallet/Wif.js.map +1 -1
- package/dist/module/wallet/createWallet.d.ts +2 -1
- package/dist/module/wallet/createWallet.d.ts.map +1 -1
- package/dist/module/wallet/createWallet.js +2 -3
- package/dist/module/wallet/createWallet.js.map +1 -1
- package/dist/module/wallet/interface.d.ts +2 -4
- package/dist/module/wallet/interface.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -12
- package/src/cli.ts +0 -4
- package/src/index.ts +1 -5
- package/src/network/ElectrumNetworkProvider.ts +133 -188
- package/src/network/NetworkProvider.ts +9 -30
- package/src/network/Rpc.test.ts +14 -5
- package/src/network/constant.ts +0 -23
- package/src/network/default.ts +26 -66
- package/src/network/electrum.test.ts +2 -4
- package/src/network/interface.ts +2 -9
- package/src/network/util.ts +6 -7
- package/src/rate/ExchangeRate.test.ts +1 -1
- package/src/rate/ExchangeRate.ts +2 -1
- package/{polyfill/json.js → src/test/json.test.ts} +7 -1
- package/src/transaction/Wif.ts +2 -1
- package/src/wallet/Base.ts +1520 -273
- package/src/wallet/Cashtokens.test.headless.js +1 -1
- package/src/wallet/Cashtokens.test.ts +7 -8
- package/src/wallet/Util.ts +20 -102
- package/src/wallet/Wif.bip39.test.ts +3 -3
- package/src/wallet/Wif.test.ts +31 -25
- package/src/wallet/Wif.ts +174 -1493
- package/src/wallet/Wif.watchOnly.test.ts +5 -5
- package/src/wallet/createWallet.ts +11 -10
- package/src/wallet/interface.ts +3 -4
- package/webpack.config.cjs +4 -55
- package/dist/module/qr/Qr.d.ts +0 -9
- package/dist/module/qr/Qr.d.ts.map +0 -1
- package/dist/module/qr/Qr.js +0 -22
- package/dist/module/qr/Qr.js.map +0 -1
- package/dist/module/qr/interface.d.ts +0 -6
- package/dist/module/qr/interface.d.ts.map +0 -1
- package/dist/module/qr/interface.js +0 -2
- package/dist/module/qr/interface.js.map +0 -1
- package/dist/module/util/eventsource.d.ts +0 -3
- package/dist/module/util/eventsource.d.ts.map +0 -1
- package/dist/module/util/eventsource.js +0 -11
- package/dist/module/util/eventsource.js.map +0 -1
- package/polyfill/README.md +0 -1
- package/polyfill/eventsource.js +0 -6
- package/polyfill/support/types.js +0 -286
- package/polyfill/util.cjs +0 -249
- package/src/network/default.test.ts +0 -37
- package/src/qr/Qr.test.ts +0 -14
- package/src/qr/Qr.ts +0 -24
- package/src/qr/interface.ts +0 -5
- package/src/util/eventsource.ts +0 -12
package/src/wallet/Base.ts
CHANGED
|
@@ -1,38 +1,142 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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.
|
|
152
|
+
return this.cashaddr;
|
|
49
153
|
}
|
|
50
154
|
|
|
51
155
|
/**
|
|
52
|
-
*
|
|
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
|
|
158
|
+
* @returns The cashtoken aware deposit address as a string
|
|
59
159
|
*/
|
|
60
|
-
public
|
|
61
|
-
return
|
|
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 =
|
|
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 :
|
|
324
|
-
|
|
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
|
-
|
|
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
|
-
|
|
232
|
+
const recoveredWallet = await this.fromId(savedWalletRecord.wallet);
|
|
338
233
|
recoveredWallet.name = savedWalletRecord.name;
|
|
339
234
|
return recoveredWallet;
|
|
340
235
|
} else {
|
|
341
|
-
|
|
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 :
|
|
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 :
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
/**
|