cashscript 0.13.0-next.6 → 0.13.0-next.8
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/Argument.d.ts +16 -0
- package/dist/Argument.js +16 -0
- package/dist/Contract.d.ts +44 -6
- package/dist/Contract.js +38 -11
- package/dist/Errors.d.ts +15 -0
- package/dist/Errors.js +25 -0
- package/dist/SignatureTemplate.d.ts +50 -0
- package/dist/SignatureTemplate.js +49 -0
- package/dist/TransactionBuilder.d.ts +176 -1
- package/dist/TransactionBuilder.js +170 -5
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/interfaces.d.ts +4 -0
- package/dist/network/ElectrumNetworkProvider.d.ts +33 -0
- package/dist/network/ElectrumNetworkProvider.js +78 -1
- package/dist/network/MockNetworkProvider.d.ts +35 -0
- package/dist/network/MockNetworkProvider.js +26 -0
- package/dist/network/errors.d.ts +19 -0
- package/dist/network/errors.js +38 -0
- package/dist/network/index.d.ts +0 -2
- package/dist/network/index.js +0 -2
- package/dist/transaction-utils.d.ts +23 -0
- package/dist/transaction-utils.js +19 -0
- package/dist/utils.d.ts +25 -3
- package/dist/utils.js +61 -15
- package/dist/walletconnect-utils.d.ts +23 -0
- package/dist/walletconnect-utils.js +23 -0
- package/package.json +5 -5
- package/dist/network/BitcoinRpcNetworkProvider.d.ts +0 -17
- package/dist/network/BitcoinRpcNetworkProvider.js +0 -32
- package/dist/network/FullStackNetworkProvider.d.ts +0 -41
- package/dist/network/FullStackNetworkProvider.js +0 -36
|
@@ -6,22 +6,45 @@ import { debugLibauthTemplate, getLibauthTemplate, getBitauthUri } from './libau
|
|
|
6
6
|
import { getWcContractInfo } from './walletconnect-utils.js';
|
|
7
7
|
import semver from 'semver';
|
|
8
8
|
const DEFAULT_SEQUENCE = 0xfffffffe;
|
|
9
|
+
/**
|
|
10
|
+
* Fluent builder for constructing, debugging, and broadcasting CashScript transactions.
|
|
11
|
+
*
|
|
12
|
+
* Inputs are added via `addInput` / `addInputs` with an `Unlocker` produced by a `Contract` or
|
|
13
|
+
* `SignatureTemplate`, outputs are added via `addOutput` / `addOutputs`, and the resulting
|
|
14
|
+
* transaction can be built (`build`), debugged (`debug`), or broadcast (`send`).
|
|
15
|
+
*/
|
|
9
16
|
export class TransactionBuilder {
|
|
17
|
+
/**
|
|
18
|
+
* Create a new TransactionBuilder.
|
|
19
|
+
*
|
|
20
|
+
* @param options - Builder options, including the network provider and optional fee/burn limits.
|
|
21
|
+
*/
|
|
10
22
|
constructor(options) {
|
|
11
23
|
this.inputs = [];
|
|
12
24
|
this.outputs = [];
|
|
13
25
|
this.locktime = 0;
|
|
26
|
+
this.changeLocks = {};
|
|
14
27
|
this.provider = options.provider;
|
|
15
28
|
this.options = {
|
|
16
29
|
allowImplicitFungibleTokenBurn: options.allowImplicitFungibleTokenBurn ?? false,
|
|
17
30
|
...options,
|
|
18
31
|
};
|
|
19
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Add a single UTXO as an input to the transaction.
|
|
35
|
+
*
|
|
36
|
+
* @param utxo - The UTXO to spend.
|
|
37
|
+
* @param unlocker - The unlocker to generate the unlocking bytecode for this input. Typically
|
|
38
|
+
* obtained from `contract.unlock.<functionName>(...args)` or `signatureTemplate.unlockP2PKH()`.
|
|
39
|
+
* @param options - Optional per-input options such as a custom sequence number.
|
|
40
|
+
* @returns This builder for chaining.
|
|
41
|
+
* @throws If the UTXO is invalid.
|
|
42
|
+
*/
|
|
20
43
|
addInput(utxo, unlocker, options) {
|
|
21
44
|
return this.addInputs([utxo], unlocker, options);
|
|
22
45
|
}
|
|
23
46
|
addInputs(utxos, unlocker, options) {
|
|
24
|
-
utxos.forEach(validateInput);
|
|
47
|
+
utxos.forEach((utxo) => validateInput(utxo, this.changeLocks));
|
|
25
48
|
if ((!unlocker && utxos.some((utxo) => !isUnlockableUtxo(utxo)))
|
|
26
49
|
|| (unlocker && utxos.some((utxo) => isUnlockableUtxo(utxo)))) {
|
|
27
50
|
throw new Error('Either all UTXOs must have an individual unlocker specified, or no UTXOs must have an individual unlocker specified and a shared unlocker must be provided');
|
|
@@ -33,19 +56,53 @@ export class TransactionBuilder {
|
|
|
33
56
|
this.inputs = this.inputs.concat(utxos.map(((utxo) => ({ ...utxo, unlocker, options }))));
|
|
34
57
|
return this;
|
|
35
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Add a single output to the transaction.
|
|
61
|
+
*
|
|
62
|
+
* @param output - The output to add. Its address, amount and optional token are validated
|
|
63
|
+
* against the provider's network.
|
|
64
|
+
* @returns This builder for chaining.
|
|
65
|
+
* @throws If the output is invalid.
|
|
66
|
+
*/
|
|
36
67
|
addOutput(output) {
|
|
37
68
|
return this.addOutputs([output]);
|
|
38
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Add multiple outputs to the transaction.
|
|
72
|
+
*
|
|
73
|
+
* @param outputs - The outputs to add. Each output is validated against the provider's network.
|
|
74
|
+
* @returns This builder for chaining.
|
|
75
|
+
* @throws If any output is invalid.
|
|
76
|
+
*/
|
|
39
77
|
addOutputs(outputs) {
|
|
40
|
-
outputs.forEach(validateOutput);
|
|
78
|
+
outputs.forEach((output) => validateOutput(output, this.provider.network, this.changeLocks));
|
|
41
79
|
this.outputs = this.outputs.concat(outputs);
|
|
42
80
|
return this;
|
|
43
81
|
}
|
|
44
82
|
// TODO: allow uint8array for chunks
|
|
83
|
+
/**
|
|
84
|
+
* Append an `OP_RETURN` output containing the provided data chunks. Hex strings prefixed with
|
|
85
|
+
* `0x` are decoded as bytes; other strings are encoded as UTF-8.
|
|
86
|
+
*
|
|
87
|
+
* @param chunks - The data chunks to include after the `OP_RETURN` opcode.
|
|
88
|
+
* @returns This builder for chaining.
|
|
89
|
+
*/
|
|
45
90
|
addOpReturnOutput(chunks) {
|
|
46
|
-
this.
|
|
91
|
+
this.addOutput(createOpReturnOutput(chunks));
|
|
47
92
|
return this;
|
|
48
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Add a BCH change output for the remaining value after fees, if it exceeds the dust limit. The
|
|
96
|
+
* fee is computed from the transaction size at the configured fee rate; dust-sized change is
|
|
97
|
+
* simply absorbed into the fee.
|
|
98
|
+
*
|
|
99
|
+
* Should be called *after* all explicit inputs and outputs are added.
|
|
100
|
+
*
|
|
101
|
+
* @param changeOutputOptions - The destination address and the fee rate (in sats/byte) to use.
|
|
102
|
+
* @returns This builder for chaining.
|
|
103
|
+
* @throws If the available surplus is insufficient to cover the fee for the configured rate or
|
|
104
|
+
* if a BCH change output was already added.
|
|
105
|
+
*/
|
|
49
106
|
addBchChangeOutputIfNeeded(changeOutputOptions) {
|
|
50
107
|
const totalBchInputAmount = this.inputs.reduce((total, input) => total + input.satoshis, 0n);
|
|
51
108
|
const totalBchOutputAmount = this.outputs.reduce((total, output) => total + output.amount, 0n);
|
|
@@ -66,15 +123,64 @@ export class TransactionBuilder {
|
|
|
66
123
|
const changeOutput = { to: changeOutputOptions.to, amount: changeAmount };
|
|
67
124
|
const changeOutputDust = calculateDust(changeOutput);
|
|
68
125
|
if (changeAmount < changeOutputDust) {
|
|
126
|
+
this.changeLocks.BCH = true;
|
|
69
127
|
return this;
|
|
70
128
|
}
|
|
71
|
-
this.
|
|
129
|
+
this.addOutput(changeOutput);
|
|
130
|
+
this.changeLocks.BCH = true;
|
|
72
131
|
return this;
|
|
73
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Add a fungible token change output for the configured category if the transaction's inputs
|
|
135
|
+
* contain more tokens of that category than its outputs. The change output is sent to the
|
|
136
|
+
* provided token address and carries the dust-minimum BCH amount.
|
|
137
|
+
*
|
|
138
|
+
* Should be called *after* all explicit token outputs for the category are added and *before*
|
|
139
|
+
* `addBchChangeOutputIfNeeded`.
|
|
140
|
+
*
|
|
141
|
+
* @param changeOutputOptions - The token category to handle and the destination token address.
|
|
142
|
+
* @returns This builder for chaining.
|
|
143
|
+
* @throws If the destination is not a token-supporting address, or if a corresponding change output
|
|
144
|
+
* or BCH change output was already added.
|
|
145
|
+
*/
|
|
146
|
+
addTokenChangeOutputIfNeeded(changeOutputOptions) {
|
|
147
|
+
const { category, to } = changeOutputOptions;
|
|
148
|
+
const inputAmount = this.inputs
|
|
149
|
+
.filter((input) => input.token?.category === category)
|
|
150
|
+
.reduce((total, input) => total + input.token.amount, 0n);
|
|
151
|
+
const outputAmount = this.outputs
|
|
152
|
+
.filter((output) => output.token?.category === category)
|
|
153
|
+
.reduce((total, output) => total + output.token.amount, 0n);
|
|
154
|
+
const changeAmount = inputAmount - outputAmount;
|
|
155
|
+
if (changeAmount <= 0n) {
|
|
156
|
+
this.changeLocks[category] = true;
|
|
157
|
+
return this;
|
|
158
|
+
}
|
|
159
|
+
const changeOutput = {
|
|
160
|
+
to,
|
|
161
|
+
amount: 0n,
|
|
162
|
+
token: { amount: changeAmount, category },
|
|
163
|
+
};
|
|
164
|
+
changeOutput.amount = BigInt(calculateDust(changeOutput));
|
|
165
|
+
this.addOutput(changeOutput);
|
|
166
|
+
this.changeLocks[category] = true;
|
|
167
|
+
return this;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Build the transaction (skipping fee and burn checks) and return its encoded byte length.
|
|
171
|
+
*
|
|
172
|
+
* @returns The size of the transaction in bytes.
|
|
173
|
+
*/
|
|
74
174
|
getTransactionSize() {
|
|
75
175
|
const transaction = this.buildLibauthTransaction(true);
|
|
76
176
|
return BigInt(encodeTransaction(transaction).byteLength);
|
|
77
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Set the `nLockTime` of the transaction.
|
|
180
|
+
*
|
|
181
|
+
* @param locktime - The absolute locktime to use (block height or UNIX timestamp).
|
|
182
|
+
* @returns This builder for chaining.
|
|
183
|
+
*/
|
|
78
184
|
setLocktime(locktime) {
|
|
79
185
|
this.locktime = locktime;
|
|
80
186
|
return this;
|
|
@@ -144,10 +250,26 @@ export class TransactionBuilder {
|
|
|
144
250
|
}
|
|
145
251
|
return transaction;
|
|
146
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* Build the transaction, applying fee and implicit-burn checks, and return the hex-encoded
|
|
255
|
+
* transaction bytes.
|
|
256
|
+
*
|
|
257
|
+
* @returns The signed transaction as a hex string.
|
|
258
|
+
* @throws If the transaction fee exceeds the configured maximum, or if fungible tokens are
|
|
259
|
+
* implicitly burned without `allowImplicitFungibleTokenBurn` enabled.
|
|
260
|
+
*/
|
|
147
261
|
build() {
|
|
148
262
|
const transaction = this.buildLibauthTransaction();
|
|
149
263
|
return binToHex(encodeTransaction(transaction));
|
|
150
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* Locally evaluate the transaction against the Bitcoin Cash VM using debug information from the
|
|
267
|
+
* contract artifacts. Throws a descriptive error if any input fails evaluation.
|
|
268
|
+
*
|
|
269
|
+
* @returns The full debug execution trace for every scenario in the generated libauth template.
|
|
270
|
+
* @throws If the transaction contains inputs with custom (non-standard) unlockers, or if the
|
|
271
|
+
* evaluation fails (e.g. a failing `require` statement).
|
|
272
|
+
*/
|
|
151
273
|
debug() {
|
|
152
274
|
if (this.inputs.some((input) => !isStandardUnlockableUtxo(input))) {
|
|
153
275
|
throw new Error('Cannot debug a transaction with custom unlocker');
|
|
@@ -161,6 +283,15 @@ export class TransactionBuilder {
|
|
|
161
283
|
}
|
|
162
284
|
return debugLibauthTemplate(this.getLibauthTemplate(), this);
|
|
163
285
|
}
|
|
286
|
+
/**
|
|
287
|
+
* Compute VM resource usage (ops, op cost budget, sigchecks, hash iterations) for each input
|
|
288
|
+
* by running the transaction through `debug`.
|
|
289
|
+
*
|
|
290
|
+
* @param verbose - When `true`, also prints a formatted table of the results via `console.table`.
|
|
291
|
+
* @returns One entry per input with its VM resource usage metrics.
|
|
292
|
+
* @throws If the transaction contains inputs with custom (non-standard) unlockers, or if the
|
|
293
|
+
* evaluation fails.
|
|
294
|
+
*/
|
|
164
295
|
getVmResourceUsage(verbose = false) {
|
|
165
296
|
// Note that only StandardUnlockableUtxo inputs are supported for debugging, so any transaction with custom unlockers
|
|
166
297
|
// cannot be debugged (and therefore cannot return VM resource usage)
|
|
@@ -191,10 +322,27 @@ export class TransactionBuilder {
|
|
|
191
322
|
}
|
|
192
323
|
return vmResourceUsage;
|
|
193
324
|
}
|
|
325
|
+
/**
|
|
326
|
+
* Build a Bitauth IDE URI that loads the transaction (and all private keys required to sign it)
|
|
327
|
+
* in the online Bitauth IDE debugger.
|
|
328
|
+
*
|
|
329
|
+
* WARNING: The URI embeds every private key used in the transaction. Do not share this URI if
|
|
330
|
+
* the transaction is signed with real private keys.
|
|
331
|
+
*
|
|
332
|
+
* @returns A Bitauth IDE URL for debugging this transaction.
|
|
333
|
+
* @throws If the transaction cannot be built (fee exceeds limit or fungible tokens burned).
|
|
334
|
+
*/
|
|
194
335
|
getBitauthUri() {
|
|
195
336
|
console.warn('WARNING: it is unsafe to use this Bitauth URI when using real private keys as they are included in the transaction template');
|
|
196
337
|
return getBitauthUri(this.getLibauthTemplate());
|
|
197
338
|
}
|
|
339
|
+
/**
|
|
340
|
+
* Build the transaction and return the corresponding libauth `WalletTemplate`. Useful for
|
|
341
|
+
* exporting the transaction to external libauth-compatible tooling.
|
|
342
|
+
*
|
|
343
|
+
* @returns A libauth `WalletTemplate` describing this transaction.
|
|
344
|
+
* @throws If the transaction cannot be built (fee exceeds limit or fungible tokens burned).
|
|
345
|
+
*/
|
|
198
346
|
getLibauthTemplate() {
|
|
199
347
|
const libauthTransaction = this.buildLibauthTransaction();
|
|
200
348
|
return getLibauthTemplate(this, libauthTransaction);
|
|
@@ -212,7 +360,14 @@ export class TransactionBuilder {
|
|
|
212
360
|
}
|
|
213
361
|
catch (e) {
|
|
214
362
|
const reason = e.error ?? e.message;
|
|
215
|
-
|
|
363
|
+
try {
|
|
364
|
+
const bitauthUri = getBitauthUri(this.getLibauthTemplate());
|
|
365
|
+
throw new FailedTransactionError(reason, bitauthUri);
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
// Preserve the original broadcast failure reason if URI generation fails
|
|
369
|
+
throw new FailedTransactionError(reason, 'Bitauth URI generation failed');
|
|
370
|
+
}
|
|
216
371
|
}
|
|
217
372
|
}
|
|
218
373
|
async getTxDetails(txid, raw) {
|
|
@@ -232,6 +387,16 @@ export class TransactionBuilder {
|
|
|
232
387
|
// Should not happen
|
|
233
388
|
throw new Error('Could not retrieve transaction details for over 10 minutes');
|
|
234
389
|
}
|
|
390
|
+
/**
|
|
391
|
+
* Build the transaction and format it as a BCH WalletConnect transaction object suitable for
|
|
392
|
+
* signing and broadcasting via a BCH WalletConnect-compatible Bitcoin Cash wallet.
|
|
393
|
+
*
|
|
394
|
+
* See the [BCH WalletConnect spec](https://github.com/mainnet-pat/wc2-bch-bcr) for the object format.
|
|
395
|
+
*
|
|
396
|
+
* @param options - Optional WalletConnect options such as `broadcast` and `userPrompt`.
|
|
397
|
+
* @returns A WalletConnect transaction object ready to be sent to a WalletConnect wallet.
|
|
398
|
+
* @throws If the transaction cannot be built (fee exceeds limit or fungible tokens burned).
|
|
399
|
+
*/
|
|
235
400
|
generateWcTransactionObject(options) {
|
|
236
401
|
const encodedTransaction = this.build();
|
|
237
402
|
const transaction = decodeTransactionUnsafe(hexToBin(encodedTransaction));
|
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,8 @@ export type { Artifact, AbiFunction, AbiInput } from '@cashscript/utils';
|
|
|
6
6
|
export * as utils from '@cashscript/utils';
|
|
7
7
|
export * from './interfaces.js';
|
|
8
8
|
export * from './Errors.js';
|
|
9
|
-
export
|
|
9
|
+
export * from './network/errors.js';
|
|
10
|
+
export { type NetworkProvider, ElectrumNetworkProvider, MockNetworkProvider, } from './network/index.js';
|
|
10
11
|
export { randomUtxo, randomToken, randomNFT } from './utils.js';
|
|
11
12
|
export * from './walletconnect-utils.js';
|
|
12
13
|
export { gatherBchUtxos, gatherFungibleTokenUtxos, type GatherUtxosResult } from './transaction-utils.js';
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,8 @@ export { encodeFunctionArgument, } from './Argument.js';
|
|
|
5
5
|
export * as utils from '@cashscript/utils';
|
|
6
6
|
export * from './interfaces.js';
|
|
7
7
|
export * from './Errors.js';
|
|
8
|
-
export
|
|
8
|
+
export * from './network/errors.js';
|
|
9
|
+
export { ElectrumNetworkProvider, MockNetworkProvider, } from './network/index.js';
|
|
9
10
|
export { randomUtxo, randomToken, randomNFT } from './utils.js';
|
|
10
11
|
export * from './walletconnect-utils.js';
|
|
11
12
|
export { gatherBchUtxos, gatherFungibleTokenUtxos } from './transaction-utils.js';
|
package/dist/interfaces.d.ts
CHANGED
|
@@ -62,6 +62,10 @@ export interface BchChangeOutputOptions {
|
|
|
62
62
|
to: string | Uint8Array;
|
|
63
63
|
feeRate: number;
|
|
64
64
|
}
|
|
65
|
+
export interface TokenChangeOutputOptions {
|
|
66
|
+
category: string;
|
|
67
|
+
to: string | Uint8Array;
|
|
68
|
+
}
|
|
65
69
|
export interface TokenDetails {
|
|
66
70
|
amount: bigint;
|
|
67
71
|
category: string;
|
|
@@ -11,11 +11,23 @@ interface CustomElectrumOptions extends OptionsBase {
|
|
|
11
11
|
electrum: ElectrumClient<ElectrumClientEvents>;
|
|
12
12
|
}
|
|
13
13
|
type Options = OptionsBase | CustomHostNameOptions | CustomElectrumOptions;
|
|
14
|
+
/**
|
|
15
|
+
* A `NetworkProvider` implementation backed by an Electrum Cash server. By default it manages
|
|
16
|
+
* its own connection lifecycle, connecting on demand and disconnecting when idle; for long-lived
|
|
17
|
+
* clients, pass `manualConnectionManagement: true` and call `connect` / `disconnect` explicitly.
|
|
18
|
+
*/
|
|
14
19
|
export default class ElectrumNetworkProvider implements NetworkProvider {
|
|
15
20
|
network: Network;
|
|
16
21
|
private electrum;
|
|
17
22
|
private concurrentRequests;
|
|
18
23
|
private manualConnectionManagement;
|
|
24
|
+
/**
|
|
25
|
+
* Create a new ElectrumNetworkProvider.
|
|
26
|
+
*
|
|
27
|
+
* @param network - The BCH network to connect to. Defaults to `Network.MAINNET`.
|
|
28
|
+
* @param options - Optional hostname, pre-configured `ElectrumClient`, and/or
|
|
29
|
+
* `manualConnectionManagement` flag.
|
|
30
|
+
*/
|
|
19
31
|
constructor(network?: Network, options?: Options);
|
|
20
32
|
private instantiateElectrumClient;
|
|
21
33
|
private getServerForNetwork;
|
|
@@ -24,8 +36,29 @@ export default class ElectrumNetworkProvider implements NetworkProvider {
|
|
|
24
36
|
getBlockHeight(): Promise<number>;
|
|
25
37
|
getRawTransaction(txid: string): Promise<string>;
|
|
26
38
|
sendRawTransaction(txHex: string): Promise<string>;
|
|
39
|
+
/**
|
|
40
|
+
* Manually open the underlying Electrum connection. Only allowed when the provider was created
|
|
41
|
+
* with `manualConnectionManagement: true`.
|
|
42
|
+
*
|
|
43
|
+
* @throws If manual connection management is disabled.
|
|
44
|
+
*/
|
|
27
45
|
connect(): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Manually close the underlying Electrum connection. Only allowed when the provider was created
|
|
48
|
+
* with `manualConnectionManagement: true`.
|
|
49
|
+
*
|
|
50
|
+
* @throws If manual connection management is disabled.
|
|
51
|
+
* @returns Whether the connection was disconnected successfully.
|
|
52
|
+
*/
|
|
28
53
|
disconnect(): Promise<boolean>;
|
|
54
|
+
/**
|
|
55
|
+
* Perform an arbitrary Electrum JSON-RPC request against the underlying server. Automatically
|
|
56
|
+
* manages the connection lifecycle unless `manualConnectionManagement` was set.
|
|
57
|
+
*
|
|
58
|
+
* @param name - The Electrum method name (e.g. `blockchain.transaction.get`).
|
|
59
|
+
* @param parameters - Parameters passed to the method.
|
|
60
|
+
* @returns The raw Electrum server response.
|
|
61
|
+
*/
|
|
29
62
|
performRequest(name: string, ...parameters: (string | number | boolean)[]): Promise<RequestResponse>;
|
|
30
63
|
private shouldConnect;
|
|
31
64
|
private shouldDisconnect;
|
|
@@ -3,7 +3,20 @@ import { sha256 } from '@cashscript/utils';
|
|
|
3
3
|
import { ElectrumClient, } from '@electrum-cash/network';
|
|
4
4
|
import { Network } from '../interfaces.js';
|
|
5
5
|
import { addressToLockScript } from '../utils.js';
|
|
6
|
+
import { NetworkProviderMissingInputsError, NetworkProviderMempoolConflictError, NetworkProviderTransactionAlreadySubmittedError, NetworkProviderAbsoluteTimelockError, NetworkProviderRelativeTimelockError, } from './errors.js';
|
|
7
|
+
/**
|
|
8
|
+
* A `NetworkProvider` implementation backed by an Electrum Cash server. By default it manages
|
|
9
|
+
* its own connection lifecycle, connecting on demand and disconnecting when idle; for long-lived
|
|
10
|
+
* clients, pass `manualConnectionManagement: true` and call `connect` / `disconnect` explicitly.
|
|
11
|
+
*/
|
|
6
12
|
export default class ElectrumNetworkProvider {
|
|
13
|
+
/**
|
|
14
|
+
* Create a new ElectrumNetworkProvider.
|
|
15
|
+
*
|
|
16
|
+
* @param network - The BCH network to connect to. Defaults to `Network.MAINNET`.
|
|
17
|
+
* @param options - Optional hostname, pre-configured `ElectrumClient`, and/or
|
|
18
|
+
* `manualConnectionManagement` flag.
|
|
19
|
+
*/
|
|
7
20
|
constructor(network = Network.MAINNET, options = {}) {
|
|
8
21
|
this.network = network;
|
|
9
22
|
this.concurrentRequests = 0;
|
|
@@ -62,20 +75,47 @@ export default class ElectrumNetworkProvider {
|
|
|
62
75
|
return await this.performRequest('blockchain.transaction.get', txid);
|
|
63
76
|
}
|
|
64
77
|
async sendRawTransaction(txHex) {
|
|
65
|
-
|
|
78
|
+
try {
|
|
79
|
+
return await this.performRequest('blockchain.transaction.broadcast', txHex);
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
const errorMessage = e.message ?? String(e);
|
|
83
|
+
throw classifyNetworkProviderError(errorMessage);
|
|
84
|
+
}
|
|
66
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Manually open the underlying Electrum connection. Only allowed when the provider was created
|
|
88
|
+
* with `manualConnectionManagement: true`.
|
|
89
|
+
*
|
|
90
|
+
* @throws If manual connection management is disabled.
|
|
91
|
+
*/
|
|
67
92
|
async connect() {
|
|
68
93
|
if (!this.manualConnectionManagement) {
|
|
69
94
|
throw new Error('Manual connection management is disabled');
|
|
70
95
|
}
|
|
71
96
|
return this.electrum.connect();
|
|
72
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Manually close the underlying Electrum connection. Only allowed when the provider was created
|
|
100
|
+
* with `manualConnectionManagement: true`.
|
|
101
|
+
*
|
|
102
|
+
* @throws If manual connection management is disabled.
|
|
103
|
+
* @returns Whether the connection was disconnected successfully.
|
|
104
|
+
*/
|
|
73
105
|
async disconnect() {
|
|
74
106
|
if (!this.manualConnectionManagement) {
|
|
75
107
|
throw new Error('Manual connection management is disabled');
|
|
76
108
|
}
|
|
77
109
|
return this.electrum.disconnect();
|
|
78
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Perform an arbitrary Electrum JSON-RPC request against the underlying server. Automatically
|
|
113
|
+
* manages the connection lifecycle unless `manualConnectionManagement` was set.
|
|
114
|
+
*
|
|
115
|
+
* @param name - The Electrum method name (e.g. `blockchain.transaction.get`).
|
|
116
|
+
* @param parameters - Parameters passed to the method.
|
|
117
|
+
* @returns The raw Electrum server response.
|
|
118
|
+
*/
|
|
79
119
|
async performRequest(name, ...parameters) {
|
|
80
120
|
// Only connect the electrum client when no concurrent requests are running
|
|
81
121
|
if (this.shouldConnect()) {
|
|
@@ -113,6 +153,43 @@ export default class ElectrumNetworkProvider {
|
|
|
113
153
|
return true;
|
|
114
154
|
}
|
|
115
155
|
}
|
|
156
|
+
const MISSING_INPUTS_PATTERNS = [
|
|
157
|
+
'Missing inputs',
|
|
158
|
+
'bad-txns-inputs-missingorspent',
|
|
159
|
+
'bad-txns-inputs-spent',
|
|
160
|
+
];
|
|
161
|
+
const MEMPOOL_CONFLICT_PATTERNS = [
|
|
162
|
+
'txn-mempool-conflict',
|
|
163
|
+
];
|
|
164
|
+
const ALREADY_SUBMITTED_PATTERNS = [
|
|
165
|
+
'transaction already in block chain',
|
|
166
|
+
'txn-already-known',
|
|
167
|
+
'txn-already-in-mempool',
|
|
168
|
+
];
|
|
169
|
+
const ABSOLUTE_TIMELOCK_PATTERNS = [
|
|
170
|
+
'bad-txns-nonfinal',
|
|
171
|
+
];
|
|
172
|
+
const RELATIVE_TIMELOCK_PATTERNS = [
|
|
173
|
+
'non-BIP68-final',
|
|
174
|
+
];
|
|
175
|
+
function classifyNetworkProviderError(errorMessage) {
|
|
176
|
+
if (MISSING_INPUTS_PATTERNS.some((pattern) => errorMessage.includes(pattern))) {
|
|
177
|
+
return new NetworkProviderMissingInputsError(errorMessage);
|
|
178
|
+
}
|
|
179
|
+
if (MEMPOOL_CONFLICT_PATTERNS.some((pattern) => errorMessage.includes(pattern))) {
|
|
180
|
+
return new NetworkProviderMempoolConflictError(errorMessage);
|
|
181
|
+
}
|
|
182
|
+
if (ALREADY_SUBMITTED_PATTERNS.some((pattern) => errorMessage.includes(pattern))) {
|
|
183
|
+
return new NetworkProviderTransactionAlreadySubmittedError(errorMessage);
|
|
184
|
+
}
|
|
185
|
+
if (ABSOLUTE_TIMELOCK_PATTERNS.some((pattern) => errorMessage.includes(pattern))) {
|
|
186
|
+
return new NetworkProviderAbsoluteTimelockError(errorMessage);
|
|
187
|
+
}
|
|
188
|
+
if (RELATIVE_TIMELOCK_PATTERNS.some((pattern) => errorMessage.includes(pattern))) {
|
|
189
|
+
return new NetworkProviderRelativeTimelockError(errorMessage);
|
|
190
|
+
}
|
|
191
|
+
return new Error(errorMessage);
|
|
192
|
+
}
|
|
116
193
|
function lockingBytecodeToElectrumScriptHash(lockingBytecode) {
|
|
117
194
|
const scriptHash = sha256(lockingBytecode);
|
|
118
195
|
scriptHash.reverse();
|
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import { Utxo, Network, VmTarget } from '../interfaces.js';
|
|
2
2
|
import NetworkProvider from './NetworkProvider.js';
|
|
3
|
+
/**
|
|
4
|
+
* Options accepted by the `MockNetworkProvider` constructor.
|
|
5
|
+
*/
|
|
3
6
|
export interface MockNetworkProviderOptions {
|
|
7
|
+
/**
|
|
8
|
+
* When `true` (default), broadcasting a transaction via `sendRawTransaction` updates the
|
|
9
|
+
* in-memory UTXO set: input UTXOs are removed and output UTXOs are added. Set to `false` to
|
|
10
|
+
* keep the UTXO set static.
|
|
11
|
+
*/
|
|
4
12
|
updateUtxoSet?: boolean;
|
|
13
|
+
/** The BCH VM target used for local debugging. Defaults to the current stable VM. */
|
|
5
14
|
vmTarget?: VmTarget;
|
|
6
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* An in-memory `NetworkProvider` useful for tests and examples. It does not connect to any
|
|
18
|
+
* external server; instead UTXOs are manually added via `addUtxo` and transactions are tracked
|
|
19
|
+
* in memory.
|
|
20
|
+
*/
|
|
7
21
|
export default class MockNetworkProvider implements NetworkProvider {
|
|
8
22
|
private utxoSet;
|
|
9
23
|
private transactionMap;
|
|
@@ -11,13 +25,34 @@ export default class MockNetworkProvider implements NetworkProvider {
|
|
|
11
25
|
network: Network;
|
|
12
26
|
options: MockNetworkProviderOptions;
|
|
13
27
|
vmTarget: VmTarget;
|
|
28
|
+
/**
|
|
29
|
+
* Create a new MockNetworkProvider.
|
|
30
|
+
*
|
|
31
|
+
* @param options - Optional settings controlling UTXO-set updating and the VM target used by
|
|
32
|
+
* `TransactionBuilder.debug`.
|
|
33
|
+
*/
|
|
14
34
|
constructor(options?: Partial<MockNetworkProviderOptions>);
|
|
15
35
|
getUtxos(address: string): Promise<Utxo[]>;
|
|
16
36
|
getUtxosForLockingBytecode(lockingBytecode: Uint8Array | string): Promise<Utxo[]>;
|
|
37
|
+
/**
|
|
38
|
+
* Override the current block height returned by `getBlockHeight`.
|
|
39
|
+
*
|
|
40
|
+
* @param newBlockHeight - The block height to report for subsequent queries.
|
|
41
|
+
*/
|
|
17
42
|
setBlockHeight(newBlockHeight: number): void;
|
|
18
43
|
getBlockHeight(): Promise<number>;
|
|
19
44
|
getRawTransaction(txid: string): Promise<string>;
|
|
20
45
|
sendRawTransaction(txHex: string): Promise<string>;
|
|
46
|
+
/**
|
|
47
|
+
* Add a UTXO to the in-memory set so that it becomes spendable by the specified address or
|
|
48
|
+
* locking bytecode.
|
|
49
|
+
*
|
|
50
|
+
* @param addressOrLockingBytecode - Either a CashAddress or a hex-encoded locking bytecode.
|
|
51
|
+
* @param utxo - The UTXO to make spendable.
|
|
52
|
+
*/
|
|
21
53
|
addUtxo(addressOrLockingBytecode: string, utxo: Utxo): void;
|
|
54
|
+
/**
|
|
55
|
+
* Clear the in-memory UTXO set and transaction history. Block height is preserved.
|
|
56
|
+
*/
|
|
22
57
|
reset(): void;
|
|
23
58
|
}
|
|
@@ -3,7 +3,18 @@ import { sha256 } from '@cashscript/utils';
|
|
|
3
3
|
import { Network } from '../interfaces.js';
|
|
4
4
|
import { addressToLockScript, libauthTokenDetailsToCashScriptTokenDetails } from '../utils.js';
|
|
5
5
|
import { DEFAULT_VM_TARGET } from '../libauth-template/utils.js';
|
|
6
|
+
/**
|
|
7
|
+
* An in-memory `NetworkProvider` useful for tests and examples. It does not connect to any
|
|
8
|
+
* external server; instead UTXOs are manually added via `addUtxo` and transactions are tracked
|
|
9
|
+
* in memory.
|
|
10
|
+
*/
|
|
6
11
|
export default class MockNetworkProvider {
|
|
12
|
+
/**
|
|
13
|
+
* Create a new MockNetworkProvider.
|
|
14
|
+
*
|
|
15
|
+
* @param options - Optional settings controlling UTXO-set updating and the VM target used by
|
|
16
|
+
* `TransactionBuilder.debug`.
|
|
17
|
+
*/
|
|
7
18
|
constructor(options) {
|
|
8
19
|
// we use lockingBytecode hex as the key for utxoMap to make cash addresses and token addresses interchangeable
|
|
9
20
|
this.utxoSet = [];
|
|
@@ -21,6 +32,11 @@ export default class MockNetworkProvider {
|
|
|
21
32
|
const lockingBytecodeHex = typeof lockingBytecode === 'string' ? lockingBytecode : binToHex(lockingBytecode);
|
|
22
33
|
return this.utxoSet.filter(([key]) => key === lockingBytecodeHex).map(([, utxo]) => utxo);
|
|
23
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Override the current block height returned by `getBlockHeight`.
|
|
37
|
+
*
|
|
38
|
+
* @param newBlockHeight - The block height to report for subsequent queries.
|
|
39
|
+
*/
|
|
24
40
|
setBlockHeight(newBlockHeight) {
|
|
25
41
|
this.blockHeight = newBlockHeight;
|
|
26
42
|
}
|
|
@@ -62,11 +78,21 @@ export default class MockNetworkProvider {
|
|
|
62
78
|
// Note: the user can technically add the same UTXO multiple times (txid + vout), to the same or different addresses
|
|
63
79
|
// but we don't check for this in the sendRawTransaction method. We might want to prevent duplicates from being added
|
|
64
80
|
// in the first place.
|
|
81
|
+
/**
|
|
82
|
+
* Add a UTXO to the in-memory set so that it becomes spendable by the specified address or
|
|
83
|
+
* locking bytecode.
|
|
84
|
+
*
|
|
85
|
+
* @param addressOrLockingBytecode - Either a CashAddress or a hex-encoded locking bytecode.
|
|
86
|
+
* @param utxo - The UTXO to make spendable.
|
|
87
|
+
*/
|
|
65
88
|
addUtxo(addressOrLockingBytecode, utxo) {
|
|
66
89
|
const lockingBytecode = isHex(addressOrLockingBytecode) ?
|
|
67
90
|
addressOrLockingBytecode : binToHex(addressToLockScript(addressOrLockingBytecode));
|
|
68
91
|
this.utxoSet.push([lockingBytecode, utxo]);
|
|
69
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Clear the in-memory UTXO set and transaction history. Block height is preserved.
|
|
95
|
+
*/
|
|
70
96
|
reset() {
|
|
71
97
|
this.utxoSet = [];
|
|
72
98
|
this.transactionMap = {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare class NetworkProviderError extends Error {
|
|
2
|
+
originalError: string;
|
|
3
|
+
constructor(message: string, originalError: string);
|
|
4
|
+
}
|
|
5
|
+
export declare class NetworkProviderMissingInputsError extends NetworkProviderError {
|
|
6
|
+
constructor(originalError: string);
|
|
7
|
+
}
|
|
8
|
+
export declare class NetworkProviderMempoolConflictError extends NetworkProviderError {
|
|
9
|
+
constructor(originalError: string);
|
|
10
|
+
}
|
|
11
|
+
export declare class NetworkProviderTransactionAlreadySubmittedError extends NetworkProviderError {
|
|
12
|
+
constructor(originalError: string);
|
|
13
|
+
}
|
|
14
|
+
export declare class NetworkProviderAbsoluteTimelockError extends NetworkProviderError {
|
|
15
|
+
constructor(originalError: string);
|
|
16
|
+
}
|
|
17
|
+
export declare class NetworkProviderRelativeTimelockError extends NetworkProviderError {
|
|
18
|
+
constructor(originalError: string);
|
|
19
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Base class for errors returned by a network provider when broadcasting a transaction.
|
|
2
|
+
export class NetworkProviderError extends Error {
|
|
3
|
+
constructor(message, originalError) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.originalError = originalError;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
// Thrown when one or more inputs reference UTXOs that are missing or already spent.
|
|
9
|
+
export class NetworkProviderMissingInputsError extends NetworkProviderError {
|
|
10
|
+
constructor(originalError) {
|
|
11
|
+
super(`Transaction inputs are missing or already spent: ${originalError}`, originalError);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
// Thrown when an input is already being spent by another transaction in the mempool (double-spend).
|
|
15
|
+
export class NetworkProviderMempoolConflictError extends NetworkProviderError {
|
|
16
|
+
constructor(originalError) {
|
|
17
|
+
super(`Transaction conflicts with an unconfirmed transaction in the mempool: ${originalError}`, originalError);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
// Thrown when the same transaction has already been submitted (already in mempool or confirmed).
|
|
21
|
+
export class NetworkProviderTransactionAlreadySubmittedError extends NetworkProviderError {
|
|
22
|
+
constructor(originalError) {
|
|
23
|
+
super(`Transaction has already been submitted: ${originalError}`, originalError);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// Thrown when the transaction's nLockTime has not been satisfied (transaction is not yet final).
|
|
27
|
+
export class NetworkProviderAbsoluteTimelockError extends NetworkProviderError {
|
|
28
|
+
constructor(originalError) {
|
|
29
|
+
super(`Transaction is not yet final (nLockTime not satisfied): ${originalError}`, originalError);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// Thrown when a BIP68 relative timelock (sequence lock) on an input has not been satisfied.
|
|
33
|
+
export class NetworkProviderRelativeTimelockError extends NetworkProviderError {
|
|
34
|
+
constructor(originalError) {
|
|
35
|
+
super(`BIP68 sequence lock not satisfied: ${originalError}`, originalError);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=errors.js.map
|
package/dist/network/index.d.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
1
|
export type { default as NetworkProvider } from './NetworkProvider.js';
|
|
2
|
-
export { default as BitcoinRpcNetworkProvider } from './BitcoinRpcNetworkProvider.js';
|
|
3
2
|
export { default as ElectrumNetworkProvider } from './ElectrumNetworkProvider.js';
|
|
4
|
-
export { default as FullStackNetworkProvider } from './FullStackNetworkProvider.js';
|
|
5
3
|
export { default as MockNetworkProvider } from './MockNetworkProvider.js';
|
package/dist/network/index.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
export { default as BitcoinRpcNetworkProvider } from './BitcoinRpcNetworkProvider.js';
|
|
2
1
|
export { default as ElectrumNetworkProvider } from './ElectrumNetworkProvider.js';
|
|
3
|
-
export { default as FullStackNetworkProvider } from './FullStackNetworkProvider.js';
|
|
4
2
|
export { default as MockNetworkProvider } from './MockNetworkProvider.js';
|
|
5
3
|
//# sourceMappingURL=index.js.map
|