cashscript
Advanced tools
Comparing version 0.8.0-next.2 to 0.8.0-next.3
@@ -32,2 +32,7 @@ import type { Transaction } from '@bitauth/libauth'; | ||
} | ||
export interface NftObject { | ||
category: string; | ||
capability: 'none' | 'mutable' | 'minting'; | ||
commitment: string; | ||
} | ||
export interface LibauthOutput { | ||
@@ -54,2 +59,3 @@ lockingBytecode: Uint8Array; | ||
SIGHASH_SINGLE = 3, | ||
SIGHASH_UTXOS = 32, | ||
SIGHASH_ANYONECANPAY = 128 | ||
@@ -56,0 +62,0 @@ } |
@@ -14,2 +14,3 @@ export function isSignableUtxo(utxo) { | ||
HashType[HashType["SIGHASH_SINGLE"] = 3] = "SIGHASH_SINGLE"; | ||
HashType[HashType["SIGHASH_UTXOS"] = 32] = "SIGHASH_UTXOS"; | ||
HashType[HashType["SIGHASH_ANYONECANPAY"] = 128] = "SIGHASH_ANYONECANPAY"; | ||
@@ -16,0 +17,0 @@ })(HashType || (HashType = {})); |
import { decodePrivateKeyWif, secp256k1, SigningSerializationFlag } from '@bitauth/libauth'; | ||
import { HashType, SignatureAlgorithm } from './interfaces.js'; | ||
export default class SignatureTemplate { | ||
constructor(signer, hashtype = HashType.SIGHASH_ALL, signatureAlgorithm = SignatureAlgorithm.SCHNORR) { | ||
constructor(signer, hashtype = HashType.SIGHASH_ALL | HashType.SIGHASH_UTXOS, signatureAlgorithm = SignatureAlgorithm.SCHNORR) { | ||
this.hashtype = hashtype; | ||
@@ -6,0 +6,0 @@ this.signatureAlgorithm = signatureAlgorithm; |
@@ -23,4 +23,4 @@ import { AbiFunction, Script } from '@cashscript/utils'; | ||
from(inputs: Utxo[]): this; | ||
experimentalFromP2PKH(input: Utxo, template: SignatureTemplate): this; | ||
experimentalFromP2PKH(inputs: Utxo[], template: SignatureTemplate): this; | ||
fromP2PKH(input: Utxo, template: SignatureTemplate): this; | ||
fromP2PKH(inputs: Utxo[], template: SignatureTemplate): this; | ||
to(to: string, amount: bigint, token?: TokenDetails): this; | ||
@@ -27,0 +27,0 @@ to(outputs: Recipient[]): this; |
@@ -1,6 +0,6 @@ | ||
import { hexToBin, binToHex, encodeTransaction, addressContentsToLockingBytecode, decodeTransaction, LockingBytecodeType, } from '@bitauth/libauth'; | ||
import { hexToBin, binToHex, encodeTransaction, decodeTransaction, } from '@bitauth/libauth'; | ||
import delay from 'delay'; | ||
import { hash160, hash256, placeholder, scriptToBytecode, } from '@cashscript/utils'; | ||
import { hash256, placeholder, scriptToBytecode, } from '@cashscript/utils'; | ||
import { isSignableUtxo, } from './interfaces.js'; | ||
import { meep, createInputScript, getInputSize, createOpReturnOutput, getTxSizeWithoutInputs, getPreimageSize, buildError, createSighashPreimage, validateRecipient, utxoComparator, cashScriptOutputToLibauthOutput, calculateDust, getOutputSize, } from './utils.js'; | ||
import { meep, createInputScript, getInputSize, createOpReturnOutput, getTxSizeWithoutInputs, getPreimageSize, buildError, createSighashPreimage, validateRecipient, utxoComparator, cashScriptOutputToLibauthOutput, calculateDust, getOutputSize, addressToLockScript, publicKeyToP2PKHLockingBytecode, utxoTokenComparator, } from './utils.js'; | ||
import SignatureTemplate from './SignatureTemplate.js'; | ||
@@ -30,3 +30,3 @@ const bip68 = await import('bip68'); | ||
} | ||
experimentalFromP2PKH(inputOrInputs, template) { | ||
fromP2PKH(inputOrInputs, template) { | ||
if (!Array.isArray(inputOrInputs)) { | ||
@@ -86,2 +86,3 @@ inputOrInputs = [inputOrInputs]; | ||
const bytecode = scriptToBytecode(this.redeemScript); | ||
const lockingBytecode = addressToLockScript(this.address); | ||
const inputs = this.inputs.map((utxo) => ({ | ||
@@ -93,2 +94,11 @@ outpointIndex: utxo.vout, | ||
})); | ||
// Generate source outputs from inputs (for signing with SIGHASH_UTXOS) | ||
const sourceOutputs = this.inputs.map((input) => { | ||
const sourceOutput = { | ||
amount: input.satoshis, | ||
to: isSignableUtxo(input) ? publicKeyToP2PKHLockingBytecode(input.template.getPublicKey()) : lockingBytecode, | ||
token: input.token, | ||
}; | ||
return cashScriptOutputToLibauthOutput(sourceOutput); | ||
}); | ||
const outputs = this.outputs.map(cashScriptOutputToLibauthOutput); | ||
@@ -106,7 +116,5 @@ const transaction = { | ||
const pubkey = utxo.template.getPublicKey(); | ||
const pubkeyHash = hash160(pubkey); | ||
const addressContents = { payload: pubkeyHash, type: LockingBytecodeType.p2pkh }; | ||
const prevOutScript = addressContentsToLockingBytecode(addressContents); | ||
const prevOutScript = publicKeyToP2PKHLockingBytecode(pubkey); | ||
const hashtype = utxo.template.getHashType(); | ||
const preimage = createSighashPreimage(transaction, this.inputs, i, prevOutScript, hashtype); | ||
const preimage = createSighashPreimage(transaction, sourceOutputs, i, prevOutScript, hashtype); | ||
const sighash = hash256(preimage); | ||
@@ -125,3 +133,3 @@ const signature = utxo.template.generateSignature(sighash); | ||
covenantHashType = arg.getHashType(); | ||
const preimage = createSighashPreimage(transaction, this.inputs, i, bytecode, arg.getHashType()); | ||
const preimage = createSighashPreimage(transaction, sourceOutputs, i, bytecode, arg.getHashType()); | ||
const sighash = hash256(preimage); | ||
@@ -131,3 +139,3 @@ return arg.generateSignature(sighash); | ||
const preimage = this.abiFunction.covenant | ||
? createSighashPreimage(transaction, this.inputs, i, bytecode, covenantHashType) | ||
? createSighashPreimage(transaction, sourceOutputs, i, bytecode, covenantHashType) | ||
: undefined; | ||
@@ -178,63 +186,44 @@ const inputScript = createInputScript(this.redeemScript, completeArgs, this.selector, preimage); | ||
} | ||
// Construct object with total output of fungible tokens by tokenId | ||
const netBalanceTokens = {}; | ||
const allUtxos = await this.provider.getUtxos(this.address); | ||
const manualTokenInputs = this.inputs.filter((input) => input.token); | ||
// This will throw if the amount is not enough | ||
if (manualTokenInputs.length > 0) { | ||
selectAllTokenUtxos(manualTokenInputs, this.outputs); | ||
} | ||
const automaticTokenInputs = selectAllTokenUtxos(allUtxos, this.outputs); | ||
const tokenInputs = manualTokenInputs.length > 0 ? manualTokenInputs : automaticTokenInputs; | ||
if (this.tokenChange) { | ||
const tokenChangeOutputs = createTokenChangeOutputs(tokenInputs, this.outputs, this.address); | ||
this.outputs.push(...tokenChangeOutputs); | ||
} | ||
// Construct list with all nfts in inputs | ||
const listNftsInputs = []; | ||
// If inputs are manually selected, add their tokens to balance | ||
for (const input of this.inputs) { | ||
this.inputs.forEach((input) => { | ||
if (!input.token) | ||
continue; | ||
const tokenCategory = input.token.category; | ||
if (!netBalanceTokens[tokenCategory]) { | ||
netBalanceTokens[tokenCategory] = input.token.amount; | ||
} | ||
else { | ||
netBalanceTokens[tokenCategory] += input.token.amount; | ||
} | ||
return; | ||
if (input.token.nft) { | ||
listNftsInputs.push({ ...input.token.nft, category: input.token.category }); | ||
} | ||
} | ||
}); | ||
// Construct list with all nfts in outputs | ||
let listNftsOutputs = []; | ||
// Subtract all token outputs from the token balances | ||
for (const output of this.outputs) { | ||
this.outputs.forEach((output) => { | ||
if (!output.token) | ||
continue; | ||
const tokenCategory = output.token.category; | ||
if (!netBalanceTokens[tokenCategory]) { | ||
netBalanceTokens[tokenCategory] = -output.token.amount; | ||
} | ||
else { | ||
netBalanceTokens[tokenCategory] -= output.token.amount; | ||
} | ||
return; | ||
if (output.token.nft) { | ||
listNftsOutputs.push({ ...output.token.nft, category: output.token.category }); | ||
} | ||
} | ||
}); | ||
// If inputs are manually provided, check token balances | ||
if (this.inputs.length > 0) { | ||
for (const [category, balance] of Object.entries(netBalanceTokens)) { | ||
// Add token change outputs if applicable | ||
if (this.tokenChange && balance > 0) { | ||
const tokenDetails = { | ||
category, | ||
amount: balance, | ||
}; | ||
const tokenChangeOutput = { to: this.address, amount: BigInt(1000), token: tokenDetails }; | ||
this.outputs.push(tokenChangeOutput); | ||
} | ||
// Throw error when token balance is insufficient | ||
if (balance < 0) { | ||
throw new Error(`Insufficient token balance for token with category ${category}.`); | ||
} | ||
} | ||
// Compare nfts in- and outputs, check if inputs have nfts corresponding to outputs | ||
// Keep list of nfts in inputs without matching output | ||
// First check immutable nfts, then mutable & minting nfts together | ||
// this is so the mutable nft in input does not get match to an output nft corresponding to an immutable nft in the inputs | ||
// This is so an immutible input gets matched first and is removed from the list of unused nfts | ||
let unusedNfts = listNftsInputs; | ||
for (const nftInput of listNftsInputs) { | ||
if (nftInput.capability === 'none') { | ||
for (let i = 0; i < listNftsOutputs.length; i++) { | ||
for (let i = 0; i < listNftsOutputs.length; i += 1) { | ||
// Deep equality check token objects | ||
@@ -251,2 +240,3 @@ if (JSON.stringify(listNftsOutputs[i]) === JSON.stringify(nftInput)) { | ||
if (nftInput.capability === 'minting') { | ||
// eslint-disable-next-line max-len | ||
const newListNftsOutputs = listNftsOutputs.filter((nftOutput) => nftOutput.category !== nftInput.category); | ||
@@ -259,3 +249,3 @@ if (newListNftsOutputs !== listNftsOutputs) { | ||
if (nftInput.capability === 'mutable') { | ||
for (let i = 0; i < listNftsOutputs.length; i++) { | ||
for (let i = 0; i < listNftsOutputs.length; i += 1) { | ||
if (listNftsOutputs[i].category === nftInput.category) { | ||
@@ -269,4 +259,10 @@ listNftsOutputs.splice(i, 1); | ||
} | ||
for (const nftOutput of listNftsOutputs) { | ||
const genesisUtxo = getTokenGenesisUtxo(this.inputs, nftOutput.category); | ||
if (genesisUtxo) { | ||
listNftsOutputs = listNftsOutputs.filter((nft) => nft !== nftOutput); | ||
} | ||
} | ||
if (listNftsOutputs.length !== 0) { | ||
throw new Error('Nfts in outputs don\'t have corresponding nfts in inputs!'); | ||
throw new Error(`NFT output with token category ${listNftsOutputs[0].category} does not have corresponding input`); | ||
} | ||
@@ -313,7 +309,8 @@ if (this.tokenChange) { | ||
// If inputs are not defined yet, we retrieve the contract's UTXOs and perform selection | ||
const utxos = await this.provider.getUtxos(this.address); | ||
const bchUtxos = allUtxos.filter((utxo) => !utxo.token); | ||
// We sort the UTXOs mainly so there is consistent behaviour between network providers | ||
// even if they report UTXOs in a different order | ||
utxos.sort(utxoComparator).reverse(); | ||
for (const utxo of utxos) { | ||
bchUtxos.sort(utxoComparator).reverse(); | ||
// Add all automatically added token inputs to the transaction | ||
for (const utxo of automaticTokenInputs) { | ||
this.inputs.push(utxo); | ||
@@ -323,4 +320,10 @@ satsAvailable += addPrecision(utxo.satoshis); | ||
fee += addPrecision(inputSize * this.feePerByte); | ||
} | ||
for (const utxo of bchUtxos) { | ||
if (satsAvailable > amount + fee) | ||
break; | ||
this.inputs.push(utxo); | ||
satsAvailable += addPrecision(utxo.satoshis); | ||
if (!this.hardcodedFee) | ||
fee += addPrecision(inputSize * this.feePerByte); | ||
} | ||
@@ -349,2 +352,48 @@ } | ||
} | ||
const getTokenGenesisUtxo = (utxos, tokenCategory) => { | ||
const creationUtxo = utxos.find((utxo) => utxo.vout === 0 && utxo.txid === tokenCategory); | ||
return creationUtxo; | ||
}; | ||
const getTokenCategories = (outputs) => (outputs | ||
.filter((output) => output.token) | ||
.map((output) => output.token.category)); | ||
const calculateTotalTokenAmount = (outputs, tokenCategory) => (outputs | ||
.filter((output) => output.token?.category === tokenCategory) | ||
.reduce((acc, output) => acc + output.token.amount, 0n)); | ||
const selectTokenUtxos = (utxos, amountNeeded, tokenCategory) => { | ||
const genesisUtxo = getTokenGenesisUtxo(utxos, tokenCategory); | ||
if (genesisUtxo) { | ||
return [genesisUtxo]; | ||
} | ||
const tokenUtxos = utxos.filter((utxo) => utxo.token?.category === tokenCategory); | ||
// We sort the UTXOs mainly so there is consistent behaviour between network providers | ||
// even if they report UTXOs in a different order | ||
tokenUtxos.sort(utxoTokenComparator).reverse(); | ||
let amountAvailable = 0n; | ||
const selectedUtxos = []; | ||
// Add token UTXOs until we have enough to cover the amount needed (no fee calculation because it's a token) | ||
for (const utxo of tokenUtxos) { | ||
selectedUtxos.push(utxo); | ||
amountAvailable += utxo.token.amount; | ||
if (amountAvailable >= amountNeeded) | ||
break; | ||
} | ||
if (amountAvailable < amountNeeded) { | ||
throw new Error(`Insufficient funds for token ${tokenCategory}: available (${amountAvailable}) < needed (${amountNeeded}).`); | ||
} | ||
return selectedUtxos; | ||
}; | ||
const selectAllTokenUtxos = (utxos, outputs) => { | ||
const tokenCategories = getTokenCategories(outputs); | ||
return tokenCategories.flatMap((tokenCategory) => selectTokenUtxos(utxos, calculateTotalTokenAmount(outputs, tokenCategory), tokenCategory)); | ||
}; | ||
const createTokenChangeOutputs = (utxos, outputs, address) => { | ||
const tokenCategories = getTokenCategories(utxos); | ||
return tokenCategories.map((tokenCategory) => { | ||
const required = calculateTotalTokenAmount(outputs, tokenCategory); | ||
const available = calculateTotalTokenAmount(utxos, tokenCategory); | ||
const change = available - required; | ||
return { to: address, amount: BigInt(1000), token: { category: tokenCategory, amount: change } }; | ||
}); | ||
}; | ||
// Note: the below is a very simple implementation of a "decimal point" system for BigInt numbers | ||
@@ -351,0 +400,0 @@ // It is safe to use for UTXO fee calculations due to its low numbers, but should not be used for other purposes |
@@ -16,3 +16,3 @@ import { Transaction } from '@bitauth/libauth'; | ||
export declare function createOpReturnOutput(opReturnData: string[]): Output; | ||
export declare function createSighashPreimage(transaction: Transaction, inputs: Utxo[], inputIndex: number, coveredBytecode: Uint8Array, hashtype: number): Uint8Array; | ||
export declare function createSighashPreimage(transaction: Transaction, sourceOutputs: LibauthOutput[], inputIndex: number, coveredBytecode: Uint8Array, hashtype: number): Uint8Array; | ||
export declare function buildError(reason: string, meepStr: string): FailedTransactionError; | ||
@@ -22,3 +22,5 @@ export declare function meep(tx: any, utxos: Utxo[], script: Script): string; | ||
export declare function scriptToLockingBytecode(script: Script, addressType: 'p2sh20' | 'p2sh32'): Uint8Array; | ||
export declare function publicKeyToP2PKHLockingBytecode(publicKey: Uint8Array): Uint8Array; | ||
export declare function utxoComparator(a: Utxo, b: Utxo): number; | ||
export declare function utxoTokenComparator(a: Utxo, b: Utxo): number; | ||
/** | ||
@@ -25,0 +27,0 @@ * Helper function to convert an address to a locking script |
@@ -122,11 +122,3 @@ import { cashAddressToLockingBytecode, decodeCashAddress, addressContentsToLockingBytecode, lockingBytecodeToCashAddress, binToHex, generateSigningSerializationBCH, utf8ToBin, hexToBin, flattenBinArray, LockingBytecodeType, encodeTransactionOutput, } from '@bitauth/libauth'; | ||
} | ||
export function createSighashPreimage(transaction, inputs, inputIndex, coveredBytecode, hashtype) { | ||
const sourceOutputs = inputs.map((input) => { | ||
const sourceOutput = { | ||
amount: input.satoshis, | ||
to: Uint8Array.of(), | ||
token: input.token, | ||
}; | ||
return cashScriptOutputToLibauthOutput(sourceOutput); | ||
}); | ||
export function createSighashPreimage(transaction, sourceOutputs, inputIndex, coveredBytecode, hashtype) { | ||
const context = { inputIndex, sourceOutputs, transaction }; | ||
@@ -179,2 +171,8 @@ const signingSerializationType = new Uint8Array([hashtype]); | ||
} | ||
export function publicKeyToP2PKHLockingBytecode(publicKey) { | ||
const pubkeyHash = hash160(publicKey); | ||
const addressContents = { payload: pubkeyHash, type: LockingBytecodeType.p2pkh }; | ||
const lockingBytecode = addressContentsToLockingBytecode(addressContents); | ||
return lockingBytecode; | ||
} | ||
export function utxoComparator(a, b) { | ||
@@ -187,2 +185,13 @@ if (a.satoshis > b.satoshis) | ||
} | ||
export function utxoTokenComparator(a, b) { | ||
if (!a.token || !b.token) | ||
throw new Error('UTXO does not have token data'); | ||
if (!a.token.category !== !b.token.category) | ||
throw new Error('UTXO token categories do not match'); | ||
if (a.token.amount > b.token.amount) | ||
return 1; | ||
if (a.token.amount < b.token.amount) | ||
return -1; | ||
return 0; | ||
} | ||
/** | ||
@@ -189,0 +198,0 @@ * Helper function to convert an address to a locking script |
{ | ||
"name": "cashscript", | ||
"version": "0.8.0-next.2", | ||
"version": "0.8.0-next.3", | ||
"description": "Easily write and interact with Bitcoin Cash contracts", | ||
@@ -41,2 +41,3 @@ "keywords": [ | ||
"prepare": "yarn build", | ||
"prepublishOnly": "yarn test && yarn lint", | ||
"pretest": "yarn build:test", | ||
@@ -47,3 +48,3 @@ "test": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest" | ||
"@bitauth/libauth": "^2.0.0-alpha.8", | ||
"@cashscript/utils": "^0.8.0-next.2", | ||
"@cashscript/utils": "^0.8.0-next.3", | ||
"bip68": "^1.0.4", | ||
@@ -62,3 +63,3 @@ "bitcoin-rpc-promise-retry": "^1.3.0", | ||
}, | ||
"gitHead": "37bcb8e924fc56ef8da2cb2a2640eb5478de39b2" | ||
"gitHead": "398ff2afab6d731f077002e6be021ed2f1996b4a" | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
74465
1527