cashscript 0.11.5 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,384 +0,0 @@
1
- import { hexToBin, decodeTransaction, } from '@bitauth/libauth';
2
- import { encodeBip68, placeholder, } from '@cashscript/utils';
3
- import deepEqual from 'fast-deep-equal';
4
- import { isUtxoP2PKH, SignatureAlgorithm, } from './interfaces.js';
5
- import { createInputScript, getInputSize, createOpReturnOutput, getTxSizeWithoutInputs, validateOutput, utxoComparator, calculateDust, getOutputSize, utxoTokenComparator, delay, } from './utils.js';
6
- import SignatureTemplate from './SignatureTemplate.js';
7
- import { P2PKH_INPUT_SIZE } from './constants.js';
8
- import { TransactionBuilder } from './TransactionBuilder.js';
9
- import { buildTemplate, getBitauthUri } from './LibauthTemplate.js';
10
- import { debugTemplate } from './debugging.js';
11
- import { FailedTransactionError } from './Errors.js';
12
- import semver from 'semver';
13
- export class Transaction {
14
- constructor(contract, unlocker, abiFunction, encodedFunctionArgs, selector) {
15
- this.contract = contract;
16
- this.unlocker = unlocker;
17
- this.abiFunction = abiFunction;
18
- this.encodedFunctionArgs = encodedFunctionArgs;
19
- this.selector = selector;
20
- this.inputs = [];
21
- this.outputs = [];
22
- this.sequence = 0xfffffffe;
23
- this.feePerByte = 1.0;
24
- this.minChange = 0n;
25
- this.tokenChange = true;
26
- }
27
- from(inputOrInputs) {
28
- if (!Array.isArray(inputOrInputs)) {
29
- inputOrInputs = [inputOrInputs];
30
- }
31
- this.inputs = this.inputs.concat(inputOrInputs);
32
- return this;
33
- }
34
- fromP2PKH(inputOrInputs, template) {
35
- if (!Array.isArray(inputOrInputs)) {
36
- inputOrInputs = [inputOrInputs];
37
- }
38
- inputOrInputs = inputOrInputs.map((input) => ({ ...input, template }));
39
- this.inputs = this.inputs.concat(inputOrInputs);
40
- return this;
41
- }
42
- to(toOrOutputs, amount, token) {
43
- if (typeof toOrOutputs === 'string' && typeof amount === 'bigint') {
44
- const recipient = { to: toOrOutputs, amount, token };
45
- return this.to([recipient]);
46
- }
47
- if (Array.isArray(toOrOutputs) && amount === undefined) {
48
- toOrOutputs.forEach(validateOutput);
49
- this.outputs = this.outputs.concat(toOrOutputs);
50
- return this;
51
- }
52
- throw new Error('Incorrect arguments passed to function \'to\'');
53
- }
54
- withOpReturn(chunks) {
55
- this.outputs.push(createOpReturnOutput(chunks));
56
- return this;
57
- }
58
- withAge(age) {
59
- this.sequence = encodeBip68({ blocks: age });
60
- return this;
61
- }
62
- withTime(time) {
63
- this.locktime = time;
64
- return this;
65
- }
66
- withHardcodedFee(hardcodedFee) {
67
- this.hardcodedFee = hardcodedFee;
68
- return this;
69
- }
70
- withFeePerByte(feePerByte) {
71
- this.feePerByte = feePerByte;
72
- return this;
73
- }
74
- withMinChange(minChange) {
75
- this.minChange = minChange;
76
- return this;
77
- }
78
- withoutChange() {
79
- return this.withMinChange(BigInt(Number.MAX_VALUE));
80
- }
81
- withoutTokenChange() {
82
- this.tokenChange = false;
83
- return this;
84
- }
85
- async build() {
86
- this.locktime = this.locktime ?? await this.contract.provider.getBlockHeight();
87
- await this.setInputsAndOutputs();
88
- const builder = new TransactionBuilder({ provider: this.contract.provider });
89
- this.inputs.forEach((utxo) => {
90
- if (isUtxoP2PKH(utxo)) {
91
- builder.addInput(utxo, utxo.template.unlockP2PKH(), { sequence: this.sequence });
92
- }
93
- else {
94
- builder.addInput(utxo, this.unlocker, { sequence: this.sequence });
95
- }
96
- });
97
- builder.addOutputs(this.outputs);
98
- builder.setLocktime(this.locktime);
99
- return builder.build();
100
- }
101
- async send(raw) {
102
- const tx = await this.build();
103
- // Debug the transaction locally before sending so any errors are caught early
104
- await this.debug();
105
- try {
106
- const txid = await this.contract.provider.sendRawTransaction(tx);
107
- return raw ? await this.getTxDetails(txid, raw) : await this.getTxDetails(txid);
108
- }
109
- catch (error) {
110
- const reason = error.error ?? error.message ?? error;
111
- throw new FailedTransactionError(reason, await this.bitauthUri());
112
- }
113
- }
114
- // method to debug the transaction with libauth VM, throws upon evaluation error
115
- async debug() {
116
- if (!semver.satisfies(this.contract.artifact.compiler.version, '>=0.11.0')) {
117
- console.warn('For the best debugging experience, please recompile your contract with cashc version 0.11.0 or newer.');
118
- }
119
- const template = await this.getLibauthTemplate();
120
- return debugTemplate(template, [this.contract.artifact]);
121
- }
122
- async bitauthUri() {
123
- console.warn('WARNING: it is unsafe to use this Bitauth URI when using real private keys as they are included in the transaction template');
124
- const template = await this.getLibauthTemplate();
125
- return getBitauthUri(template);
126
- }
127
- async getLibauthTemplate() {
128
- return buildTemplate({ transaction: this });
129
- }
130
- async getTxDetails(txid, raw) {
131
- for (let retries = 0; retries < 1200; retries += 1) {
132
- await delay(500);
133
- try {
134
- const hex = await this.contract.provider.getRawTransaction(txid);
135
- if (raw)
136
- return hex;
137
- const libauthTransaction = decodeTransaction(hexToBin(hex));
138
- return { ...libauthTransaction, txid, hex };
139
- }
140
- catch (ignored) {
141
- // ignored
142
- }
143
- }
144
- // Should not happen
145
- throw new Error('Could not retrieve transaction details for over 10 minutes');
146
- }
147
- async setInputsAndOutputs() {
148
- if (this.outputs.length === 0) {
149
- throw new Error('Attempted to build a transaction without outputs');
150
- }
151
- // Fetched utxos are only used when no inputs are available, so only fetch in that case.
152
- const allUtxos = this.inputs.length === 0
153
- ? await this.contract.provider.getUtxos(this.contract.address)
154
- : [];
155
- const tokenInputs = this.inputs.length > 0
156
- ? this.inputs.filter((input) => input.token)
157
- : selectAllTokenUtxos(allUtxos, this.outputs);
158
- // This throws if the manually selected inputs are not enough to cover the outputs
159
- if (this.inputs.length > 0) {
160
- selectAllTokenUtxos(this.inputs, this.outputs);
161
- }
162
- if (this.tokenChange) {
163
- const tokenChangeOutputs = createFungibleTokenChangeOutputs(tokenInputs, this.outputs, this.contract.tokenAddress);
164
- this.outputs.push(...tokenChangeOutputs);
165
- }
166
- // Construct list with all nfts in inputs
167
- const listNftsInputs = [];
168
- // If inputs are manually selected, add their tokens to balance
169
- this.inputs.forEach((input) => {
170
- if (!input.token)
171
- return;
172
- if (input.token.nft) {
173
- listNftsInputs.push({ ...input.token.nft, category: input.token.category });
174
- }
175
- });
176
- // Construct list with all nfts in outputs
177
- let listNftsOutputs = [];
178
- // Subtract all token outputs from the token balances
179
- this.outputs.forEach((output) => {
180
- if (!output.token)
181
- return;
182
- if (output.token.nft) {
183
- listNftsOutputs.push({ ...output.token.nft, category: output.token.category });
184
- }
185
- });
186
- // If inputs are manually provided, check token balances
187
- if (this.inputs.length > 0) {
188
- // Compare nfts in- and outputs, check if inputs have nfts corresponding to outputs
189
- // Keep list of nfts in inputs without matching output
190
- // First check immutable nfts, then mutable & minting nfts together
191
- // This is so an immutable input gets matched first and is removed from the list of unused nfts
192
- let unusedNfts = listNftsInputs;
193
- for (const nftInput of listNftsInputs) {
194
- if (nftInput.capability === 'none') {
195
- for (let i = 0; i < listNftsOutputs.length; i += 1) {
196
- // Deep equality check token objects
197
- if (deepEqual(listNftsOutputs[i], nftInput)) {
198
- listNftsOutputs.splice(i, 1);
199
- unusedNfts = unusedNfts.filter((nft) => !deepEqual(nft, nftInput));
200
- break;
201
- }
202
- }
203
- }
204
- }
205
- for (const nftInput of listNftsInputs) {
206
- if (nftInput.capability === 'minting') {
207
- // eslint-disable-next-line max-len
208
- const newListNftsOutputs = listNftsOutputs.filter((nftOutput) => nftOutput.category !== nftInput.category);
209
- if (newListNftsOutputs !== listNftsOutputs) {
210
- unusedNfts = unusedNfts.filter((nft) => !deepEqual(nft, nftInput));
211
- listNftsOutputs = newListNftsOutputs;
212
- }
213
- }
214
- if (nftInput.capability === 'mutable') {
215
- for (let i = 0; i < listNftsOutputs.length; i += 1) {
216
- if (listNftsOutputs[i].category === nftInput.category) {
217
- listNftsOutputs.splice(i, 1);
218
- unusedNfts = unusedNfts.filter((nft) => !deepEqual(nft, nftInput));
219
- break;
220
- }
221
- }
222
- }
223
- }
224
- for (const nftOutput of listNftsOutputs) {
225
- const genesisUtxo = getTokenGenesisUtxo(this.inputs, nftOutput.category);
226
- if (genesisUtxo) {
227
- listNftsOutputs = listNftsOutputs.filter((nft) => !deepEqual(nft, nftOutput));
228
- }
229
- }
230
- if (listNftsOutputs.length !== 0) {
231
- throw new Error(`NFT output with token category ${listNftsOutputs[0].category} does not have corresponding input`);
232
- }
233
- if (this.tokenChange) {
234
- for (const unusedNft of unusedNfts) {
235
- const tokenDetails = {
236
- category: unusedNft.category,
237
- amount: BigInt(0),
238
- nft: {
239
- capability: unusedNft.capability,
240
- commitment: unusedNft.commitment,
241
- },
242
- };
243
- const nftChangeOutput = { to: this.contract.tokenAddress, amount: BigInt(1000), token: tokenDetails };
244
- this.outputs.push(nftChangeOutput);
245
- }
246
- }
247
- }
248
- // Replace all SignatureTemplate with placeholder Uint8Arrays
249
- const placeholderArgs = this.encodedFunctionArgs.map((arg) => {
250
- if (!(arg instanceof SignatureTemplate))
251
- return arg;
252
- // Schnorr signatures are *always* 65 bytes: 64 for signature + 1 byte for hashtype.
253
- if (arg.getSignatureAlgorithm() === SignatureAlgorithm.SCHNORR)
254
- return placeholder(65);
255
- // ECDSA signatures are at least 71 bytes: 64 bytes for signature + 1 byte for hashtype + 6 bytes for encoding
256
- // overhead. But it may have up to 2 extra bytes for padding, so we overestimate by 2 bytes.
257
- // (see https://transactionfee.info/charts/bitcoin-script-ecdsa-length/)
258
- return placeholder(73);
259
- });
260
- // Create a placeholder input script for size calculation using the placeholder arguments
261
- const placeholderScript = createInputScript(this.contract.redeemScript, placeholderArgs, this.selector);
262
- // Add one extra byte per input to over-estimate tx-in count
263
- const contractInputSize = getInputSize(placeholderScript) + 1;
264
- // Note that we use the addPrecision function to add "decimal points" to BigInt numbers
265
- // Calculate amount to send and base fee (excluding additional fees per UTXO)
266
- let amount = addPrecision(this.outputs.reduce((acc, output) => acc + output.amount, 0n));
267
- let fee = addPrecision(this.hardcodedFee ?? getTxSizeWithoutInputs(this.outputs) * this.feePerByte);
268
- // Select and gather UTXOs and calculate fees and available funds
269
- let satsAvailable = 0n;
270
- if (this.inputs.length > 0) {
271
- // If inputs are already defined, the user provided the UTXOs and we perform no further UTXO selection
272
- if (!this.hardcodedFee) {
273
- const totalInputSize = this.inputs.reduce((acc, input) => acc + (isUtxoP2PKH(input) ? P2PKH_INPUT_SIZE : contractInputSize), 0);
274
- fee += addPrecision(totalInputSize * this.feePerByte);
275
- }
276
- satsAvailable = addPrecision(this.inputs.reduce((acc, input) => acc + input.satoshis, 0n));
277
- }
278
- else {
279
- // If inputs are not defined yet, we retrieve the contract's UTXOs and perform selection
280
- const bchUtxos = allUtxos.filter((utxo) => !utxo.token);
281
- // We sort the UTXOs mainly so there is consistent behaviour between network providers
282
- // even if they report UTXOs in a different order
283
- bchUtxos.sort(utxoComparator).reverse();
284
- // Add all automatically added token inputs to the transaction
285
- for (const utxo of tokenInputs) {
286
- this.inputs.push(utxo);
287
- satsAvailable += addPrecision(utxo.satoshis);
288
- if (!this.hardcodedFee)
289
- fee += addPrecision(contractInputSize * this.feePerByte);
290
- }
291
- for (const utxo of bchUtxos) {
292
- if (satsAvailable > amount + fee)
293
- break;
294
- this.inputs.push(utxo);
295
- satsAvailable += addPrecision(utxo.satoshis);
296
- if (!this.hardcodedFee)
297
- fee += addPrecision(contractInputSize * this.feePerByte);
298
- }
299
- }
300
- // Remove "decimal points" from BigInt numbers (rounding up for fee, down for others)
301
- satsAvailable = removePrecisionFloor(satsAvailable);
302
- amount = removePrecisionFloor(amount);
303
- fee = removePrecisionCeil(fee);
304
- // Calculate change and check available funds
305
- let change = satsAvailable - amount - fee;
306
- if (change < 0) {
307
- throw new Error(`Insufficient funds: available (${satsAvailable}) < needed (${amount + fee}).`);
308
- }
309
- // Account for the fee of adding a change output
310
- if (!this.hardcodedFee) {
311
- const changeOutputSize = getOutputSize({ to: this.contract.address, amount: 0n });
312
- change -= BigInt(changeOutputSize * this.feePerByte);
313
- }
314
- // Add a change output if applicable
315
- const changeOutput = { to: this.contract.address, amount: change };
316
- if (change >= this.minChange && change >= calculateDust(changeOutput)) {
317
- this.outputs.push(changeOutput);
318
- }
319
- }
320
- }
321
- const getTokenGenesisUtxo = (utxos, tokenCategory) => {
322
- const creationUtxo = utxos.find((utxo) => utxo.vout === 0 && utxo.txid === tokenCategory);
323
- return creationUtxo;
324
- };
325
- const getTokenCategories = (outputs) => (outputs
326
- .filter((output) => output.token)
327
- .map((output) => output.token.category));
328
- const calculateTotalTokenAmount = (outputs, tokenCategory) => (outputs
329
- .filter((output) => output.token?.category === tokenCategory)
330
- .reduce((acc, output) => acc + output.token.amount, 0n));
331
- const selectTokenUtxos = (utxos, amountNeeded, tokenCategory) => {
332
- const genesisUtxo = getTokenGenesisUtxo(utxos, tokenCategory);
333
- if (genesisUtxo)
334
- return [genesisUtxo];
335
- const tokenUtxos = utxos.filter((utxo) => utxo.token?.category === tokenCategory && utxo.token?.amount > 0n);
336
- // We sort the UTXOs mainly so there is consistent behaviour between network providers
337
- // even if they report UTXOs in a different order
338
- tokenUtxos.sort(utxoTokenComparator).reverse();
339
- let amountAvailable = 0n;
340
- const selectedUtxos = [];
341
- // Add token UTXOs until we have enough to cover the amount needed (no fee calculation because it's a token)
342
- for (const utxo of tokenUtxos) {
343
- if (amountAvailable >= amountNeeded)
344
- break;
345
- selectedUtxos.push(utxo);
346
- amountAvailable += utxo.token.amount;
347
- }
348
- if (amountAvailable < amountNeeded) {
349
- throw new Error(`Insufficient funds for token ${tokenCategory}: available (${amountAvailable}) < needed (${amountNeeded}).`);
350
- }
351
- return selectedUtxos;
352
- };
353
- const selectAllTokenUtxos = (utxos, outputs) => {
354
- const tokenCategories = getTokenCategories(outputs);
355
- return tokenCategories.flatMap((tokenCategory) => selectTokenUtxos(utxos, calculateTotalTokenAmount(outputs, tokenCategory), tokenCategory));
356
- };
357
- const createFungibleTokenChangeOutputs = (utxos, outputs, address) => {
358
- const tokenCategories = getTokenCategories(utxos);
359
- const changeOutputs = tokenCategories.map((tokenCategory) => {
360
- const required = calculateTotalTokenAmount(outputs, tokenCategory);
361
- const available = calculateTotalTokenAmount(utxos, tokenCategory);
362
- const change = available - required;
363
- if (change === 0n)
364
- return undefined;
365
- return { to: address, amount: BigInt(1000), token: { category: tokenCategory, amount: change } };
366
- });
367
- return changeOutputs.filter((output) => output !== undefined);
368
- };
369
- // Note: the below is a very simple implementation of a "decimal point" system for BigInt numbers
370
- // It is safe to use for UTXO fee calculations due to its low numbers, but should not be used for other purposes
371
- // Also note that multiplication and division between two "decimal" bigints is not supported
372
- // High precision may not work with some 'number' inputs, so we set the default to 6 "decimal places"
373
- const addPrecision = (amount, precision = 6) => {
374
- if (typeof amount === 'number') {
375
- return BigInt(Math.ceil(amount * 10 ** precision));
376
- }
377
- return amount * BigInt(10 ** precision);
378
- };
379
- const removePrecisionFloor = (amount, precision = 6) => (amount / (10n ** BigInt(precision)));
380
- const removePrecisionCeil = (amount, precision = 6) => {
381
- const multiplier = 10n ** BigInt(precision);
382
- return (amount + multiplier - 1n) / multiplier;
383
- };
384
- //# sourceMappingURL=Transaction.js.map
@@ -1,45 +0,0 @@
1
- import { TransactionBch, WalletTemplate, WalletTemplateScenarioBytecode } from '@bitauth/libauth';
2
- import { AbiFunction } from '@cashscript/utils';
3
- import { EncodedConstructorArgument, EncodedFunctionArgument } from '../Argument.js';
4
- import { Contract } from '../Contract.js';
5
- import { DebugResults } from '../debugging.js';
6
- import { StandardUnlockableUtxo } from '../interfaces.js';
7
- import SignatureTemplate from '../SignatureTemplate.js';
8
- import { Transaction } from '../Transaction.js';
9
- import { TransactionBuilder } from '../TransactionBuilder.js';
10
- /**
11
- * Generates template entities for P2PKH (Pay to Public Key Hash) placeholder scripts.
12
- *
13
- * Follows the WalletTemplateEntity specification from:
14
- * https://ide.bitauth.com/authentication-template-v0.schema.json
15
- *
16
- */
17
- export declare const generateTemplateEntitiesP2PKH: (inputIndex: number) => WalletTemplate["entities"];
18
- /**
19
- * Generates template entities for P2SH (Pay to Script Hash) placeholder scripts.
20
- *
21
- * Follows the WalletTemplateEntity specification from:
22
- * https://ide.bitauth.com/authentication-template-v0.schema.json
23
- *
24
- */
25
- export declare const generateTemplateEntitiesP2SH: (contract: Contract, abiFunction: AbiFunction, encodedFunctionArgs: EncodedFunctionArgument[], inputIndex: number) => WalletTemplate["entities"];
26
- /**
27
- * Generates template scripts for P2PKH (Pay to Public Key Hash) placeholder scripts.
28
- *
29
- * Follows the WalletTemplateScript specification from:
30
- * https://ide.bitauth.com/authentication-template-v0.schema.json
31
- *
32
- */
33
- export declare const generateTemplateScriptsP2PKH: (template: SignatureTemplate, inputIndex: number) => WalletTemplate["scripts"];
34
- /**
35
- * Generates template scripts for P2SH (Pay to Script Hash) placeholder scripts.
36
- *
37
- * Follows the WalletTemplateScript specification from:
38
- * https://ide.bitauth.com/authentication-template-v0.schema.json
39
- *
40
- */
41
- export declare const generateTemplateScriptsP2SH: (contract: Contract, abiFunction: AbiFunction, encodedFunctionArgs: EncodedFunctionArgument[], encodedConstructorArgs: EncodedConstructorArgument[], inputIndex: number) => WalletTemplate["scripts"];
42
- export declare const generateTemplateScenarios: (contract: Contract, libauthTransaction: TransactionBch, csTransaction: Transaction, abiFunction: AbiFunction, encodedFunctionArgs: EncodedFunctionArgument[], inputIndex: number) => WalletTemplate["scenarios"];
43
- export declare const getLibauthTemplates: (txn: TransactionBuilder) => WalletTemplate;
44
- export declare const debugLibauthTemplate: (template: WalletTemplate, transaction: TransactionBuilder) => DebugResults;
45
- export declare const generateUnlockingScriptParams: (csInput: StandardUnlockableUtxo, p2pkhScriptNameTemplate: string, inputIndex: number) => WalletTemplateScenarioBytecode;