cashscript 0.8.0-next.2 → 0.8.0-next.3

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,7 +1,7 @@
1
1
  import { decodePrivateKeyWif, secp256k1, SigningSerializationFlag } from '@bitauth/libauth';
2
2
  import { HashType, SignatureAlgorithm } from './interfaces.js';
3
3
  export default class SignatureTemplate {
4
- constructor(signer, hashtype = HashType.SIGHASH_ALL, signatureAlgorithm = SignatureAlgorithm.SCHNORR) {
4
+ constructor(signer, hashtype = HashType.SIGHASH_ALL | HashType.SIGHASH_UTXOS, signatureAlgorithm = SignatureAlgorithm.SCHNORR) {
5
5
  this.hashtype = hashtype;
6
6
  this.signatureAlgorithm = signatureAlgorithm;
7
7
  if (isKeypair(signer)) {
@@ -20,8 +20,8 @@ export declare class Transaction {
20
20
  constructor(address: string, provider: NetworkProvider, redeemScript: Script, abiFunction: AbiFunction, args: (Uint8Array | SignatureTemplate)[], selector?: number | undefined);
21
21
  from(input: Utxo): this;
22
22
  from(inputs: Utxo[]): this;
23
- experimentalFromP2PKH(input: Utxo, template: SignatureTemplate): this;
24
- experimentalFromP2PKH(inputs: Utxo[], template: SignatureTemplate): this;
23
+ fromP2PKH(input: Utxo, template: SignatureTemplate): this;
24
+ fromP2PKH(inputs: Utxo[], template: SignatureTemplate): this;
25
25
  to(to: string, amount: bigint, token?: TokenDetails): this;
26
26
  to(outputs: Recipient[]): this;
27
27
  withOpReturn(chunks: string[]): this;
@@ -1,8 +1,8 @@
1
- import { hexToBin, binToHex, encodeTransaction, addressContentsToLockingBytecode, decodeTransaction, LockingBytecodeType, } from '@bitauth/libauth';
1
+ import { hexToBin, binToHex, encodeTransaction, decodeTransaction, } from '@bitauth/libauth';
2
2
  import delay from 'delay';
3
- import { hash160, hash256, placeholder, scriptToBytecode, } from '@cashscript/utils';
3
+ import { hash256, placeholder, scriptToBytecode, } from '@cashscript/utils';
4
4
  import { isSignableUtxo, } from './interfaces.js';
5
- import { meep, createInputScript, getInputSize, createOpReturnOutput, getTxSizeWithoutInputs, getPreimageSize, buildError, createSighashPreimage, validateRecipient, utxoComparator, cashScriptOutputToLibauthOutput, calculateDust, getOutputSize, } from './utils.js';
5
+ import { meep, createInputScript, getInputSize, createOpReturnOutput, getTxSizeWithoutInputs, getPreimageSize, buildError, createSighashPreimage, validateRecipient, utxoComparator, cashScriptOutputToLibauthOutput, calculateDust, getOutputSize, addressToLockScript, publicKeyToP2PKHLockingBytecode, utxoTokenComparator, } from './utils.js';
6
6
  import SignatureTemplate from './SignatureTemplate.js';
7
7
  const bip68 = await import('bip68');
8
8
  export class Transaction {
@@ -27,7 +27,7 @@ export class Transaction {
27
27
  this.inputs = this.inputs.concat(inputOrInputs);
28
28
  return this;
29
29
  }
30
- experimentalFromP2PKH(inputOrInputs, template) {
30
+ fromP2PKH(inputOrInputs, template) {
31
31
  if (!Array.isArray(inputOrInputs)) {
32
32
  inputOrInputs = [inputOrInputs];
33
33
  }
@@ -82,12 +82,22 @@ export class Transaction {
82
82
  this.locktime = this.locktime ?? await this.provider.getBlockHeight();
83
83
  await this.setInputsAndOutputs();
84
84
  const bytecode = scriptToBytecode(this.redeemScript);
85
+ const lockingBytecode = addressToLockScript(this.address);
85
86
  const inputs = this.inputs.map((utxo) => ({
86
87
  outpointIndex: utxo.vout,
87
88
  outpointTransactionHash: hexToBin(utxo.txid),
88
89
  sequenceNumber: this.sequence,
89
90
  unlockingBytecode: new Uint8Array(),
90
91
  }));
92
+ // Generate source outputs from inputs (for signing with SIGHASH_UTXOS)
93
+ const sourceOutputs = this.inputs.map((input) => {
94
+ const sourceOutput = {
95
+ amount: input.satoshis,
96
+ to: isSignableUtxo(input) ? publicKeyToP2PKHLockingBytecode(input.template.getPublicKey()) : lockingBytecode,
97
+ token: input.token,
98
+ };
99
+ return cashScriptOutputToLibauthOutput(sourceOutput);
100
+ });
91
101
  const outputs = this.outputs.map(cashScriptOutputToLibauthOutput);
92
102
  const transaction = {
93
103
  inputs,
@@ -100,11 +110,9 @@ export class Transaction {
100
110
  // UTXO's with signature templates are signed using P2PKH
101
111
  if (isSignableUtxo(utxo)) {
102
112
  const pubkey = utxo.template.getPublicKey();
103
- const pubkeyHash = hash160(pubkey);
104
- const addressContents = { payload: pubkeyHash, type: LockingBytecodeType.p2pkh };
105
- const prevOutScript = addressContentsToLockingBytecode(addressContents);
113
+ const prevOutScript = publicKeyToP2PKHLockingBytecode(pubkey);
106
114
  const hashtype = utxo.template.getHashType();
107
- const preimage = createSighashPreimage(transaction, this.inputs, i, prevOutScript, hashtype);
115
+ const preimage = createSighashPreimage(transaction, sourceOutputs, i, prevOutScript, hashtype);
108
116
  const sighash = hash256(preimage);
109
117
  const signature = utxo.template.generateSignature(sighash);
110
118
  const inputScript = scriptToBytecode([signature, pubkey]);
@@ -118,12 +126,12 @@ export class Transaction {
118
126
  // First signature is used for sighash preimage (maybe not the best way)
119
127
  if (covenantHashType < 0)
120
128
  covenantHashType = arg.getHashType();
121
- const preimage = createSighashPreimage(transaction, this.inputs, i, bytecode, arg.getHashType());
129
+ const preimage = createSighashPreimage(transaction, sourceOutputs, i, bytecode, arg.getHashType());
122
130
  const sighash = hash256(preimage);
123
131
  return arg.generateSignature(sighash);
124
132
  });
125
133
  const preimage = this.abiFunction.covenant
126
- ? createSighashPreimage(transaction, this.inputs, i, bytecode, covenantHashType)
134
+ ? createSighashPreimage(transaction, sourceOutputs, i, bytecode, covenantHashType)
127
135
  : undefined;
128
136
  const inputScript = createInputScript(this.redeemScript, completeArgs, this.selector, preimage);
129
137
  inputScripts.push(inputScript);
@@ -169,67 +177,48 @@ export class Transaction {
169
177
  if (this.outputs.length === 0) {
170
178
  throw Error('Attempted to build a transaction without outputs');
171
179
  }
172
- // Construct object with total output of fungible tokens by tokenId
173
- const netBalanceTokens = {};
180
+ const allUtxos = await this.provider.getUtxos(this.address);
181
+ const manualTokenInputs = this.inputs.filter((input) => input.token);
182
+ // This will throw if the amount is not enough
183
+ if (manualTokenInputs.length > 0) {
184
+ selectAllTokenUtxos(manualTokenInputs, this.outputs);
185
+ }
186
+ const automaticTokenInputs = selectAllTokenUtxos(allUtxos, this.outputs);
187
+ const tokenInputs = manualTokenInputs.length > 0 ? manualTokenInputs : automaticTokenInputs;
188
+ if (this.tokenChange) {
189
+ const tokenChangeOutputs = createTokenChangeOutputs(tokenInputs, this.outputs, this.address);
190
+ this.outputs.push(...tokenChangeOutputs);
191
+ }
174
192
  // Construct list with all nfts in inputs
175
193
  const listNftsInputs = [];
176
194
  // If inputs are manually selected, add their tokens to balance
177
- for (const input of this.inputs) {
195
+ this.inputs.forEach((input) => {
178
196
  if (!input.token)
179
- continue;
180
- const tokenCategory = input.token.category;
181
- if (!netBalanceTokens[tokenCategory]) {
182
- netBalanceTokens[tokenCategory] = input.token.amount;
183
- }
184
- else {
185
- netBalanceTokens[tokenCategory] += input.token.amount;
186
- }
197
+ return;
187
198
  if (input.token.nft) {
188
199
  listNftsInputs.push({ ...input.token.nft, category: input.token.category });
189
200
  }
190
- }
201
+ });
191
202
  // Construct list with all nfts in outputs
192
203
  let listNftsOutputs = [];
193
204
  // Subtract all token outputs from the token balances
194
- for (const output of this.outputs) {
205
+ this.outputs.forEach((output) => {
195
206
  if (!output.token)
196
- continue;
197
- const tokenCategory = output.token.category;
198
- if (!netBalanceTokens[tokenCategory]) {
199
- netBalanceTokens[tokenCategory] = -output.token.amount;
200
- }
201
- else {
202
- netBalanceTokens[tokenCategory] -= output.token.amount;
203
- }
207
+ return;
204
208
  if (output.token.nft) {
205
209
  listNftsOutputs.push({ ...output.token.nft, category: output.token.category });
206
210
  }
207
- }
211
+ });
208
212
  // If inputs are manually provided, check token balances
209
213
  if (this.inputs.length > 0) {
210
- for (const [category, balance] of Object.entries(netBalanceTokens)) {
211
- // Add token change outputs if applicable
212
- if (this.tokenChange && balance > 0) {
213
- const tokenDetails = {
214
- category,
215
- amount: balance,
216
- };
217
- const tokenChangeOutput = { to: this.address, amount: BigInt(1000), token: tokenDetails };
218
- this.outputs.push(tokenChangeOutput);
219
- }
220
- // Throw error when token balance is insufficient
221
- if (balance < 0) {
222
- throw new Error(`Insufficient token balance for token with category ${category}.`);
223
- }
224
- }
225
214
  // Compare nfts in- and outputs, check if inputs have nfts corresponding to outputs
226
215
  // Keep list of nfts in inputs without matching output
227
216
  // First check immutable nfts, then mutable & minting nfts together
228
- // this is so the mutable nft in input does not get match to an output nft corresponding to an immutable nft in the inputs
217
+ // This is so an immutible input gets matched first and is removed from the list of unused nfts
229
218
  let unusedNfts = listNftsInputs;
230
219
  for (const nftInput of listNftsInputs) {
231
220
  if (nftInput.capability === 'none') {
232
- for (let i = 0; i < listNftsOutputs.length; i++) {
221
+ for (let i = 0; i < listNftsOutputs.length; i += 1) {
233
222
  // Deep equality check token objects
234
223
  if (JSON.stringify(listNftsOutputs[i]) === JSON.stringify(nftInput)) {
235
224
  listNftsOutputs.splice(i, 1);
@@ -241,6 +230,7 @@ export class Transaction {
241
230
  }
242
231
  for (const nftInput of listNftsInputs) {
243
232
  if (nftInput.capability === 'minting') {
233
+ // eslint-disable-next-line max-len
244
234
  const newListNftsOutputs = listNftsOutputs.filter((nftOutput) => nftOutput.category !== nftInput.category);
245
235
  if (newListNftsOutputs !== listNftsOutputs) {
246
236
  unusedNfts = unusedNfts.filter((nft) => nft !== nftInput);
@@ -248,7 +238,7 @@ export class Transaction {
248
238
  }
249
239
  }
250
240
  if (nftInput.capability === 'mutable') {
251
- for (let i = 0; i < listNftsOutputs.length; i++) {
241
+ for (let i = 0; i < listNftsOutputs.length; i += 1) {
252
242
  if (listNftsOutputs[i].category === nftInput.category) {
253
243
  listNftsOutputs.splice(i, 1);
254
244
  unusedNfts = unusedNfts.filter((nft) => nft !== nftInput);
@@ -257,8 +247,14 @@ export class Transaction {
257
247
  }
258
248
  }
259
249
  }
250
+ for (const nftOutput of listNftsOutputs) {
251
+ const genesisUtxo = getTokenGenesisUtxo(this.inputs, nftOutput.category);
252
+ if (genesisUtxo) {
253
+ listNftsOutputs = listNftsOutputs.filter((nft) => nft !== nftOutput);
254
+ }
255
+ }
260
256
  if (listNftsOutputs.length !== 0) {
261
- throw new Error('Nfts in outputs don\'t have corresponding nfts in inputs!');
257
+ throw new Error(`NFT output with token category ${listNftsOutputs[0].category} does not have corresponding input`);
262
258
  }
263
259
  if (this.tokenChange) {
264
260
  for (const unusedNft of unusedNfts) {
@@ -300,17 +296,24 @@ export class Transaction {
300
296
  }
301
297
  else {
302
298
  // If inputs are not defined yet, we retrieve the contract's UTXOs and perform selection
303
- const utxos = await this.provider.getUtxos(this.address);
299
+ const bchUtxos = allUtxos.filter((utxo) => !utxo.token);
304
300
  // We sort the UTXOs mainly so there is consistent behaviour between network providers
305
301
  // even if they report UTXOs in a different order
306
- utxos.sort(utxoComparator).reverse();
307
- for (const utxo of utxos) {
302
+ bchUtxos.sort(utxoComparator).reverse();
303
+ // Add all automatically added token inputs to the transaction
304
+ for (const utxo of automaticTokenInputs) {
308
305
  this.inputs.push(utxo);
309
306
  satsAvailable += addPrecision(utxo.satoshis);
310
307
  if (!this.hardcodedFee)
311
308
  fee += addPrecision(inputSize * this.feePerByte);
309
+ }
310
+ for (const utxo of bchUtxos) {
312
311
  if (satsAvailable > amount + fee)
313
312
  break;
313
+ this.inputs.push(utxo);
314
+ satsAvailable += addPrecision(utxo.satoshis);
315
+ if (!this.hardcodedFee)
316
+ fee += addPrecision(inputSize * this.feePerByte);
314
317
  }
315
318
  }
316
319
  // Remove "decimal points" from BigInt numbers (rounding up for fee, down for others)
@@ -334,6 +337,52 @@ export class Transaction {
334
337
  }
335
338
  }
336
339
  }
340
+ const getTokenGenesisUtxo = (utxos, tokenCategory) => {
341
+ const creationUtxo = utxos.find((utxo) => utxo.vout === 0 && utxo.txid === tokenCategory);
342
+ return creationUtxo;
343
+ };
344
+ const getTokenCategories = (outputs) => (outputs
345
+ .filter((output) => output.token)
346
+ .map((output) => output.token.category));
347
+ const calculateTotalTokenAmount = (outputs, tokenCategory) => (outputs
348
+ .filter((output) => output.token?.category === tokenCategory)
349
+ .reduce((acc, output) => acc + output.token.amount, 0n));
350
+ const selectTokenUtxos = (utxos, amountNeeded, tokenCategory) => {
351
+ const genesisUtxo = getTokenGenesisUtxo(utxos, tokenCategory);
352
+ if (genesisUtxo) {
353
+ return [genesisUtxo];
354
+ }
355
+ const tokenUtxos = utxos.filter((utxo) => utxo.token?.category === tokenCategory);
356
+ // We sort the UTXOs mainly so there is consistent behaviour between network providers
357
+ // even if they report UTXOs in a different order
358
+ tokenUtxos.sort(utxoTokenComparator).reverse();
359
+ let amountAvailable = 0n;
360
+ const selectedUtxos = [];
361
+ // Add token UTXOs until we have enough to cover the amount needed (no fee calculation because it's a token)
362
+ for (const utxo of tokenUtxos) {
363
+ selectedUtxos.push(utxo);
364
+ amountAvailable += utxo.token.amount;
365
+ if (amountAvailable >= amountNeeded)
366
+ break;
367
+ }
368
+ if (amountAvailable < amountNeeded) {
369
+ throw new Error(`Insufficient funds for token ${tokenCategory}: available (${amountAvailable}) < needed (${amountNeeded}).`);
370
+ }
371
+ return selectedUtxos;
372
+ };
373
+ const selectAllTokenUtxos = (utxos, outputs) => {
374
+ const tokenCategories = getTokenCategories(outputs);
375
+ return tokenCategories.flatMap((tokenCategory) => selectTokenUtxos(utxos, calculateTotalTokenAmount(outputs, tokenCategory), tokenCategory));
376
+ };
377
+ const createTokenChangeOutputs = (utxos, outputs, address) => {
378
+ const tokenCategories = getTokenCategories(utxos);
379
+ return tokenCategories.map((tokenCategory) => {
380
+ const required = calculateTotalTokenAmount(outputs, tokenCategory);
381
+ const available = calculateTotalTokenAmount(utxos, tokenCategory);
382
+ const change = available - required;
383
+ return { to: address, amount: BigInt(1000), token: { category: tokenCategory, amount: change } };
384
+ });
385
+ };
337
386
  // Note: the below is a very simple implementation of a "decimal point" system for BigInt numbers
338
387
  // It is safe to use for UTXO fee calculations due to its low numbers, but should not be used for other purposes
339
388
  // Also note that multiplication and division between two "decimal" bigints is not supported
@@ -29,6 +29,11 @@ export interface TokenDetails {
29
29
  commitment: string;
30
30
  };
31
31
  }
32
+ export interface NftObject {
33
+ category: string;
34
+ capability: 'none' | 'mutable' | 'minting';
35
+ commitment: string;
36
+ }
32
37
  export interface LibauthOutput {
33
38
  lockingBytecode: Uint8Array;
34
39
  valueSatoshis: bigint;
@@ -50,6 +55,7 @@ export declare enum HashType {
50
55
  SIGHASH_ALL = 1,
51
56
  SIGHASH_NONE = 2,
52
57
  SIGHASH_SINGLE = 3,
58
+ SIGHASH_UTXOS = 32,
53
59
  SIGHASH_ANYONECANPAY = 128
54
60
  }
55
61
  export declare const Network: {
@@ -11,6 +11,7 @@ export var HashType;
11
11
  HashType[HashType["SIGHASH_ALL"] = 1] = "SIGHASH_ALL";
12
12
  HashType[HashType["SIGHASH_NONE"] = 2] = "SIGHASH_NONE";
13
13
  HashType[HashType["SIGHASH_SINGLE"] = 3] = "SIGHASH_SINGLE";
14
+ HashType[HashType["SIGHASH_UTXOS"] = 32] = "SIGHASH_UTXOS";
14
15
  HashType[HashType["SIGHASH_ANYONECANPAY"] = 128] = "SIGHASH_ANYONECANPAY";
15
16
  })(HashType || (HashType = {}));
16
17
  // Weird setup to allow both Enum parameters, as well as literal strings
package/dist/utils.d.ts CHANGED
@@ -13,12 +13,14 @@ export declare function getPreimageSize(script: Uint8Array): number;
13
13
  export declare function getTxSizeWithoutInputs(outputs: Output[]): number;
14
14
  export declare function createInputScript(redeemScript: Script, encodedArgs: Uint8Array[], selector?: number, preimage?: Uint8Array): Uint8Array;
15
15
  export declare function createOpReturnOutput(opReturnData: string[]): Output;
16
- export declare function createSighashPreimage(transaction: Transaction, inputs: Utxo[], inputIndex: number, coveredBytecode: Uint8Array, hashtype: number): Uint8Array;
16
+ export declare function createSighashPreimage(transaction: Transaction, sourceOutputs: LibauthOutput[], inputIndex: number, coveredBytecode: Uint8Array, hashtype: number): Uint8Array;
17
17
  export declare function buildError(reason: string, meepStr: string): FailedTransactionError;
18
18
  export declare function meep(tx: any, utxos: Utxo[], script: Script): string;
19
19
  export declare function scriptToAddress(script: Script, network: string, addressType: 'p2sh20' | 'p2sh32', tokenSupport: boolean): string;
20
20
  export declare function scriptToLockingBytecode(script: Script, addressType: 'p2sh20' | 'p2sh32'): Uint8Array;
21
+ export declare function publicKeyToP2PKHLockingBytecode(publicKey: Uint8Array): Uint8Array;
21
22
  export declare function utxoComparator(a: Utxo, b: Utxo): number;
23
+ export declare function utxoTokenComparator(a: Utxo, b: Utxo): number;
22
24
  /**
23
25
  * Helper function to convert an address to a locking script
24
26
  *
package/dist/utils.js CHANGED
@@ -119,15 +119,7 @@ function toBin(output) {
119
119
  const encode = data === output ? utf8ToBin : hexToBin;
120
120
  return encode(data);
121
121
  }
122
- export function createSighashPreimage(transaction, inputs, inputIndex, coveredBytecode, hashtype) {
123
- const sourceOutputs = inputs.map((input) => {
124
- const sourceOutput = {
125
- amount: input.satoshis,
126
- to: Uint8Array.of(),
127
- token: input.token,
128
- };
129
- return cashScriptOutputToLibauthOutput(sourceOutput);
130
- });
122
+ export function createSighashPreimage(transaction, sourceOutputs, inputIndex, coveredBytecode, hashtype) {
131
123
  const context = { inputIndex, sourceOutputs, transaction };
132
124
  const signingSerializationType = new Uint8Array([hashtype]);
133
125
  const sighashPreimage = generateSigningSerializationBCH(context, { coveredBytecode, signingSerializationType });
@@ -175,6 +167,12 @@ export function scriptToLockingBytecode(script, addressType) {
175
167
  const lockingBytecode = addressContentsToLockingBytecode(addressContents);
176
168
  return lockingBytecode;
177
169
  }
170
+ export function publicKeyToP2PKHLockingBytecode(publicKey) {
171
+ const pubkeyHash = hash160(publicKey);
172
+ const addressContents = { payload: pubkeyHash, type: LockingBytecodeType.p2pkh };
173
+ const lockingBytecode = addressContentsToLockingBytecode(addressContents);
174
+ return lockingBytecode;
175
+ }
178
176
  export function utxoComparator(a, b) {
179
177
  if (a.satoshis > b.satoshis)
180
178
  return 1;
@@ -182,6 +180,17 @@ export function utxoComparator(a, b) {
182
180
  return -1;
183
181
  return 0;
184
182
  }
183
+ export function utxoTokenComparator(a, b) {
184
+ if (!a.token || !b.token)
185
+ throw new Error('UTXO does not have token data');
186
+ if (!a.token.category !== !b.token.category)
187
+ throw new Error('UTXO token categories do not match');
188
+ if (a.token.amount > b.token.amount)
189
+ return 1;
190
+ if (a.token.amount < b.token.amount)
191
+ return -1;
192
+ return 0;
193
+ }
185
194
  /**
186
195
  * Helper function to convert an address to a locking script
187
196
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cashscript",
3
- "version": "0.8.0-next.2",
3
+ "version": "0.8.0-next.3",
4
4
  "description": "Easily write and interact with Bitcoin Cash contracts",
5
5
  "keywords": [
6
6
  "bitcoin cash",
@@ -38,12 +38,13 @@
38
38
  "compile:test": "tsc -p tsconfig.test.json",
39
39
  "lint": "eslint . --ext .ts --ignore-path ../../.eslintignore",
40
40
  "prepare": "yarn build",
41
+ "prepublishOnly": "yarn test && yarn lint",
41
42
  "pretest": "yarn build:test",
42
43
  "test": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest"
43
44
  },
44
45
  "dependencies": {
45
46
  "@bitauth/libauth": "^2.0.0-alpha.8",
46
- "@cashscript/utils": "^0.8.0-next.2",
47
+ "@cashscript/utils": "^0.8.0-next.3",
47
48
  "bip68": "^1.0.4",
48
49
  "bitcoin-rpc-promise-retry": "^1.3.0",
49
50
  "delay": "^5.0.0",
@@ -57,5 +58,5 @@
57
58
  "jest": "^29.4.1",
58
59
  "typescript": "^4.1.5"
59
60
  },
60
- "gitHead": "37bcb8e924fc56ef8da2cb2a2640eb5478de39b2"
61
+ "gitHead": "398ff2afab6d731f077002e6be021ed2f1996b4a"
61
62
  }