diff --git a/examples/PriceOracle.ts b/examples/PriceOracle.ts index eb174099..e0eb5664 100644 --- a/examples/PriceOracle.ts +++ b/examples/PriceOracle.ts @@ -1,8 +1,9 @@ -import { padMinimallyEncodedVmNumber, flattenBinArray, secp256k1 } from '@bitauth/libauth'; +import { padMinimallyEncodedVmNumber, flattenBinArray } from '@bitauth/libauth'; import { encodeInt, sha256 } from '@cashscript/utils'; +import { SignatureAlgorithm, SignatureTemplate } from 'cashscript'; export class PriceOracle { - constructor(public privateKey: Uint8Array) {} + constructor(public privateKey: Uint8Array) { } // Encode a blockHeight and bchUsdPrice into a byte sequence of 8 bytes (4 bytes per value) createMessage(blockHeight: bigint, bchUsdPrice: bigint): Uint8Array { @@ -12,9 +13,8 @@ export class PriceOracle { return flattenBinArray([encodedBlockHeight, encodedBchUsdPrice]); } - signMessage(message: Uint8Array): Uint8Array { - const signature = secp256k1.signMessageHashSchnorr(this.privateKey, sha256(message)); - if (typeof signature === 'string') throw new Error(); - return signature; + signMessage(message: Uint8Array, signatureAlgorithm: SignatureAlgorithm = SignatureAlgorithm.SCHNORR): Uint8Array { + const signatureTemplate = new SignatureTemplate(this.privateKey, undefined, signatureAlgorithm); + return signatureTemplate.signMessageHash(sha256(message)); } } diff --git a/examples/announcement.cash b/examples/announcement.cash index a678e80a..d8dbce8a 100644 --- a/examples/announcement.cash +++ b/examples/announcement.cash @@ -1,4 +1,4 @@ -pragma cashscript ^0.11.0; +pragma cashscript ^0.12.0; /* This is a contract showcasing covenants outside of regular transactional use. * It enforces the contract to make an "announcement" on Memo.cash, and send the diff --git a/examples/hodl_vault.cash b/examples/hodl_vault.cash index 1af8c96b..95adbfa8 100644 --- a/examples/hodl_vault.cash +++ b/examples/hodl_vault.cash @@ -1,4 +1,4 @@ -pragma cashscript ^0.11.0; +pragma cashscript ^0.12.0; // This contract forces HODLing until a certain price target has been reached // A minimum block is provided to ensure that oracle price entries from before this block are disregarded diff --git a/examples/mecenas.cash b/examples/mecenas.cash index 341a523d..aef868ab 100644 --- a/examples/mecenas.cash +++ b/examples/mecenas.cash @@ -1,4 +1,4 @@ -pragma cashscript ^0.11.0; +pragma cashscript ^0.12.0; /* This is an unofficial CashScript port of Licho's Mecenas contract. It is * not compatible with Licho's EC plugin, but rather meant as a demonstration diff --git a/examples/mecenas_locktime.cash b/examples/mecenas_locktime.cash index cb0e56ff..f4d1ccdc 100644 --- a/examples/mecenas_locktime.cash +++ b/examples/mecenas_locktime.cash @@ -1,4 +1,4 @@ -pragma cashscript ^0.11.0; +pragma cashscript ^0.12.0; // This is an experimental contract for a more "streaming" Mecenas experience // Completely untested, just a concept diff --git a/examples/p2pkh.cash b/examples/p2pkh.cash index 6cc64059..0cd49ca0 100644 --- a/examples/p2pkh.cash +++ b/examples/p2pkh.cash @@ -1,4 +1,4 @@ -pragma cashscript ^0.11.0; +pragma cashscript ^0.12.0; contract P2PKH(bytes20 pkh) { // Require pk to match stored pkh and signature to match diff --git a/examples/package.json b/examples/package.json index 9ac02049..ff3f9988 100644 --- a/examples/package.json +++ b/examples/package.json @@ -1,7 +1,7 @@ { "name": "cashscript-examples", "private": true, - "version": "0.11.5", + "version": "0.12.0", "description": "Usage examples of the CashScript SDK", "main": "p2pkh.js", "type": "module", @@ -11,10 +11,10 @@ "lint": "eslint . --ext .ts --ignore-path ../.eslintignore" }, "dependencies": { - "@bitauth/libauth": "^3.1.0-next.2", + "@bitauth/libauth": "^3.1.0-next.8", "@types/node": "^22.17.0", - "cashc": "^0.11.5", - "cashscript": "^0.11.5", + "cashc": "^0.12.0", + "cashscript": "^0.12.0", "eslint": "^8.56.0", "typescript": "^5.9.2" } diff --git a/examples/testing-suite/artifacts/example.json b/examples/testing-suite/artifacts/example.json index 79c72269..b28c0de2 100644 --- a/examples/testing-suite/artifacts/example.json +++ b/examples/testing-suite/artifacts/example.json @@ -41,7 +41,7 @@ }, "compiler": { "name": "cashc", - "version": "0.11.3" + "version": "0.12.0" }, - "updatedAt": "2025-08-05T08:32:16.100Z" + "updatedAt": "2025-10-02T09:56:11.510Z" } \ No newline at end of file diff --git a/examples/testing-suite/package.json b/examples/testing-suite/package.json index 4502082b..53b4f682 100644 --- a/examples/testing-suite/package.json +++ b/examples/testing-suite/package.json @@ -1,6 +1,6 @@ { "name": "testing-suite", - "version": "0.11.5", + "version": "0.12.0", "description": "Example project to develop and test CashScript contracts", "main": "index.js", "type": "module", @@ -25,9 +25,9 @@ "test": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest" }, "dependencies": { - "@bitauth/libauth": "^3.1.0-next.2", - "cashc": "^0.11.5", - "cashscript": "^0.11.5", + "@bitauth/libauth": "^3.1.0-next.8", + "cashc": "^0.12.0", + "cashscript": "^0.12.0", "url-join": "^5.0.0" }, "devDependencies": { diff --git a/examples/transfer_with_timeout.cash b/examples/transfer_with_timeout.cash index f49b0e9d..98723e44 100644 --- a/examples/transfer_with_timeout.cash +++ b/examples/transfer_with_timeout.cash @@ -1,4 +1,4 @@ -pragma cashscript ^0.11.0; +pragma cashscript ^0.12.0; contract TransferWithTimeout( pubkey sender, diff --git a/packages/cashc/package.json b/packages/cashc/package.json index 0c592984..43e145a6 100644 --- a/packages/cashc/package.json +++ b/packages/cashc/package.json @@ -1,6 +1,6 @@ { "name": "cashc", - "version": "0.11.5", + "version": "0.12.0", "description": "Compile Bitcoin Cash contracts to Bitcoin Cash Script or artifacts", "keywords": [ "bitcoin", @@ -51,8 +51,8 @@ "test": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest" }, "dependencies": { - "@bitauth/libauth": "^3.1.0-next.2", - "@cashscript/utils": "^0.11.5", + "@bitauth/libauth": "^3.1.0-next.8", + "@cashscript/utils": "^0.12.0", "antlr4": "^4.13.2", "commander": "^14.0.0", "semver": "^7.7.2" diff --git a/packages/cashc/src/index.ts b/packages/cashc/src/index.ts index 15cdeb27..a4efe3fc 100644 --- a/packages/cashc/src/index.ts +++ b/packages/cashc/src/index.ts @@ -2,4 +2,4 @@ export * from './Errors.js'; export * as utils from '@cashscript/utils'; export { compileFile, compileString } from './compiler.js'; -export const version = '0.11.5'; +export const version = '0.12.0'; diff --git a/packages/cashscript/README.md b/packages/cashscript/README.md index 7fd1f807..f2db0b22 100644 --- a/packages/cashscript/README.md +++ b/packages/cashscript/README.md @@ -37,17 +37,29 @@ Using the CashScript SDK, you can import contract artifact files, create new ins const provider = new ElectrumNetworkProvider('mainnet'); // Create a new P2PKH contract with constructor arguments: { pkh: pkh } - const contract = new Contract(P2PKH, [pkh], provider); + const contract = new Contract(P2PKH, [pkh], { provider }); // Get contract balance & output address + balance console.log('contract address:', contract.address); console.log('contract balance:', await contract.getBalance()); - // Call the spend function with the owner's signature - // And use it to send 0. 000 100 00 BCH back to the contract's address - const txDetails = await contract.functions - .spend(pk, new SignatureTemplate(keypair)) - .to(contract.address, 10000) + const transactionBuilder = new TransactionBuilder({ provider }); + const contractUtxos = await contract.getUtxos(); + + const sendAmount = 10_000n; + const destinationAddress = '... some address ...'; + + // Calculate the change amount, accounting for a miner fee of 1000 satoshis + const changeAmount = contractUtxos[0].satoshis - sendAmount - 1000n; + + // Construct a transaction with the transaction builder + const txDetails = await transactionBuilder + // Add a contract input that spends from the contract using the 'spend' function + .addInput(contractUtxos[0], contract.unlock.spend(pk, new SignatureTemplate(keypair))) + // Add an output that sends 0. 000 100 00 BCH back to the destination address + .addOutput({ to: destinationAddress, amount: sendAmount }) + // Add a change output that sends the change back to the contract's address + .addOutput({ to: contract.address, amount: changeAmount }) .send(); console.log(txDetails); diff --git a/packages/cashscript/package.json b/packages/cashscript/package.json index d200fc41..f830fbc2 100644 --- a/packages/cashscript/package.json +++ b/packages/cashscript/package.json @@ -1,6 +1,6 @@ { "name": "cashscript", - "version": "0.11.5", + "version": "0.12.0", "description": "Easily write and interact with Bitcoin Cash contracts", "keywords": [ "bitcoin cash", @@ -45,11 +45,10 @@ "test": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest" }, "dependencies": { - "@bitauth/libauth": "^3.1.0-next.2", - "@cashscript/utils": "^0.11.5", + "@bitauth/libauth": "^3.1.0-next.8", + "@cashscript/utils": "^0.12.0", "@electrum-cash/network": "^4.1.3", "@mr-zwets/bchn-api-wrapper": "^1.0.1", - "fast-deep-equal": "^3.1.3", "pako": "^2.1.0", "semver": "^7.7.2" }, diff --git a/packages/cashscript/src/Argument.ts b/packages/cashscript/src/Argument.ts index 915fe84d..9871ac99 100644 --- a/packages/cashscript/src/Argument.ts +++ b/packages/cashscript/src/Argument.ts @@ -59,16 +59,20 @@ export function encodeFunctionArgument(argument: FunctionArgument, typeStr: stri throw Error(`Value for type ${type} should be a Uint8Array or hex string`); } - // Redefine SIG as a bytes65 so it is included in the size checks below - // Note that ONLY Schnorr signatures are accepted - if (type === PrimitiveType.SIG && argument.byteLength !== 0) { - type = new BytesType(65); + // Redefine SIG as a bytes65 (Schnorr) or bytes71, bytes72, bytes73 (ECDSA) or bytes0 (for NULLFAIL) + if (type === PrimitiveType.SIG) { + if (![0, 65, 71, 72, 73].includes(argument.byteLength)) { + throw new TypeError(`bytes${argument.byteLength}`, type); + } + type = new BytesType(argument.byteLength); } - // Redefine DATASIG as a bytes64 so it is included in the size checks below - // Note that ONLY Schnorr signatures are accepted - if (type === PrimitiveType.DATASIG && argument.byteLength !== 0) { - type = new BytesType(64); + // Redefine DATASIG as a bytes64 (Schnorr) or bytes70, bytes71, bytes72 (ECDSA) or bytes0 (for NULLFAIL) + if (type === PrimitiveType.DATASIG) { + if (![0, 64, 70, 71, 72].includes(argument.byteLength)) { + throw new TypeError(`bytes${argument.byteLength}`, type); + } + type = new BytesType(argument.byteLength); } // Bounded bytes types require a correctly sized argument diff --git a/packages/cashscript/src/Contract.ts b/packages/cashscript/src/Contract.ts index e2baf3d2..e145e6d5 100644 --- a/packages/cashscript/src/Contract.ts +++ b/packages/cashscript/src/Contract.ts @@ -10,9 +10,8 @@ import { Script, scriptToBytecode, } from '@cashscript/utils'; -import { Transaction } from './Transaction.js'; import { - ConstructorArgument, encodeFunctionArgument, encodeConstructorArguments, encodeFunctionArguments, FunctionArgument, + ConstructorArgument, encodeFunctionArgument, encodeConstructorArguments, FunctionArgument, } from './Argument.js'; import { Unlocker, ContractOptions, GenerateUnlockingBytecodeOptions, Utxo, AddressType, ContractUnlocker, @@ -22,7 +21,6 @@ import { addressToLockScript, createInputScript, createSighashPreimage, scriptToAddress, } from './utils.js'; import SignatureTemplate from './SignatureTemplate.js'; -import { ElectrumNetworkProvider } from './network/index.js'; import { ParamsToTuple, AbiToFunctionMap } from './types/type-inference.js'; import semver from 'semver'; @@ -30,12 +28,10 @@ export class Contract< TArtifact extends Artifact = Artifact, TResolved extends { constructorInputs: ConstructorArgument[]; - functions: Record; unlock: Record; } = { constructorInputs: ParamsToTuple; - functions: AbiToFunctionMap; unlock: AbiToFunctionMap; }, > { @@ -45,10 +41,7 @@ export class Contract< bytecode: string; bytesize: number; opcount: number; - - functions: TResolved['functions']; unlock: TResolved['unlock']; - redeemScript: Script; public provider: NetworkProvider; public addressType: AddressType; @@ -57,10 +50,10 @@ export class Contract< constructor( public artifact: TArtifact, constructorArgs: TResolved['constructorInputs'], - private options?: ContractOptions, + private options: ContractOptions, ) { - this.provider = this.options?.provider ?? new ElectrumNetworkProvider(); - this.addressType = this.options?.addressType ?? 'p2sh32'; + this.provider = this.options.provider; + this.addressType = this.options.addressType ?? 'p2sh32'; const expectedProperties = ['abi', 'bytecode', 'constructorInputs', 'contractName', 'compiler']; if (!expectedProperties.every((property) => property in artifact)) { @@ -80,21 +73,7 @@ export class Contract< this.redeemScript = generateRedeemScript(asmToScript(this.artifact.bytecode), this.encodedConstructorArgs); - // Populate the functions object with the contract's functions - // (with a special case for single function, which has no "function selector") - this.functions = {}; - if (artifact.abi.length === 1) { - const f = artifact.abi[0]; - // @ts-ignore TODO: see if we can use generics to make TypeScript happy - this.functions[f.name] = this.createFunction(f); - } else { - artifact.abi.forEach((f, i) => { - // @ts-ignore TODO: see if we can use generics to make TypeScript happy - this.functions[f.name] = this.createFunction(f, i); - }); - } - - // Populate the functions object with the contract's functions + // Populate the 'unlock' object with the contract's functions // (with a special case for single function, which has no "function selector") this.unlock = {}; if (artifact.abi.length === 1) { @@ -125,27 +104,6 @@ export class Contract< return this.provider.getUtxos(this.address); } - private createFunction(abiFunction: AbiFunction, selector?: number): ContractFunction { - return (...args: FunctionArgument[]) => { - if (abiFunction.inputs.length !== args.length) { - throw new Error(`Incorrect number of arguments passed to function ${abiFunction.name}. Expected ${abiFunction.inputs.length} arguments (${abiFunction.inputs.map((input) => input.type)}) but got ${args.length}`); - } - - // Encode passed args (this also performs type checking) - const encodedArgs = encodeFunctionArguments(abiFunction, args); - - const unlocker = this.createUnlocker(abiFunction, selector)(...args); - - return new Transaction( - this, - unlocker, - abiFunction, - encodedArgs, - selector, - ); - }; - } - private createUnlocker(abiFunction: AbiFunction, selector?: number): ContractFunctionUnlocker { return (...args: FunctionArgument[]) => { if (abiFunction.inputs.length !== args.length) { @@ -180,5 +138,4 @@ export class Contract< } } -export type ContractFunction = (...args: FunctionArgument[]) => Transaction; type ContractFunctionUnlocker = (...args: FunctionArgument[]) => ContractUnlocker; diff --git a/packages/cashscript/src/Errors.ts b/packages/cashscript/src/Errors.ts index ca03fec7..d717253c 100644 --- a/packages/cashscript/src/Errors.ts +++ b/packages/cashscript/src/Errors.ts @@ -37,7 +37,7 @@ export class NoDebugInformationInArtifactError extends Error { } export class FailedTransactionError extends Error { - constructor(public reason: string, public bitauthUri?: string) { + constructor(public reason: string, public bitauthUri: string) { const warning = 'WARNING: it is unsafe to use this Bitauth URI when using real private keys as they are included in the transaction template'; super(`${reason}${bitauthUri ? `\n\n${warning}\n\nBitauth URI: ${bitauthUri}` : ''}`); } diff --git a/packages/cashscript/src/LibauthTemplate.ts b/packages/cashscript/src/LibauthTemplate.ts deleted file mode 100644 index dc4ae1e0..00000000 --- a/packages/cashscript/src/LibauthTemplate.ts +++ /dev/null @@ -1,502 +0,0 @@ -import { - AbiFunction, - AbiInput, - Artifact, - bytecodeToScript, - formatBitAuthScript, -} from '@cashscript/utils'; -import { - hexToBin, - WalletTemplateScenarioTransactionOutput, - WalletTemplateScenario, - decodeTransaction, - binToHex, - WalletTemplate, - WalletTemplateScenarioInput, - TransactionBCH, - binToBase64, - utf8ToBin, - isHex, - WalletTemplateScenarioOutput, - WalletTemplateVariable, - WalletTemplateScriptLocking, - WalletTemplateScriptUnlocking, - WalletTemplateScenarioBytecode, -} from '@bitauth/libauth'; -import { deflate } from 'pako'; -import { - Utxo, - isUtxoP2PKH, - TokenDetails, - LibauthTokenDetails, - Output, - AddressType, - SignatureAlgorithm, - HashType, - isUnlockableUtxo, - isStandardUnlockableUtxo, -} from './interfaces.js'; -import SignatureTemplate from './SignatureTemplate.js'; -import { Transaction } from './Transaction.js'; -import { EncodedConstructorArgument, EncodedFunctionArgument } from './Argument.js'; -import { addressToLockScript, extendedStringify, zip } from './utils.js'; -import { Contract } from './Contract.js'; -import { generateUnlockingScriptParams } from './advanced/LibauthTemplate.js'; - -interface BuildTemplateOptions { - transaction: Transaction; - transactionHex?: string; -} - -export const buildTemplate = async ({ - transaction, - transactionHex = undefined, // set this argument to prevent unnecessary call `transaction.build()` -}: BuildTemplateOptions): Promise => { - const contract = transaction.contract; - const txHex = transactionHex ?? await transaction.build(); - - const template = { - $schema: 'https://ide.bitauth.com/authentication-template-v0.schema.json', - description: 'Imported from cashscript', - name: 'CashScript Generated Debugging Template', - supported: ['BCH_2025_05'], - version: 0, - entities: generateTemplateEntities(contract.artifact, transaction.abiFunction, transaction.encodedFunctionArgs), - scripts: generateTemplateScripts( - contract.artifact, - contract.addressType, - transaction.abiFunction, - transaction.encodedFunctionArgs, - contract.encodedConstructorArgs, - ), - scenarios: generateTemplateScenarios( - contract, - transaction, - txHex, - contract.artifact, - transaction.abiFunction, - transaction.encodedFunctionArgs, - contract.encodedConstructorArgs, - ), - } as WalletTemplate; - - transaction.inputs - .forEach((input, index) => { - if (!isUtxoP2PKH(input)) return; - - const lockScriptName = `p2pkh_placeholder_lock_${index}`; - const unlockScriptName = `p2pkh_placeholder_unlock_${index}`; - const placeholderKeyName = `placeholder_key_${index}`; - - const signatureAlgorithmName = getSignatureAlgorithmName(input.template.getSignatureAlgorithm()); - const hashtypeName = getHashTypeName(input.template.getHashType(false)); - const signatureString = `${placeholderKeyName}.${signatureAlgorithmName}.${hashtypeName}`; - - template.entities[contract.name + '_parameters'].scripts!.push(lockScriptName, unlockScriptName); - template.entities[contract.name + '_parameters'].variables = { - ...template.entities[contract.name + '_parameters'].variables, - [placeholderKeyName]: { - description: placeholderKeyName, - name: placeholderKeyName, - type: 'Key', - }, - }; - - // add extra unlocking and locking script for P2PKH inputs spent alongside our contract - // this is needed for correct cross-references in the template - template.scripts[unlockScriptName] = { - name: unlockScriptName, - script: - `<${signatureString}>\n<${placeholderKeyName}.public_key>`, - unlocks: lockScriptName, - }; - template.scripts[lockScriptName] = { - lockingType: 'standard', - name: lockScriptName, - script: - `OP_DUP\nOP_HASH160 <$(<${placeholderKeyName}.public_key> OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG`, - }; - }); - - return template; -}; - -export const getBitauthUri = (template: WalletTemplate): string => { - const base64toBase64Url = (base64: string): string => base64.replace(/\+/g, '-').replace(/\//g, '_'); - const payload = base64toBase64Url(binToBase64(deflate(utf8ToBin(extendedStringify(template))))); - return `https://ide.bitauth.com/import-template/${payload}`; -}; - - -const generateTemplateEntities = ( - artifact: Artifact, - abiFunction: AbiFunction, - encodedFunctionArgs: EncodedFunctionArgument[], -): WalletTemplate['entities'] => { - const functionParameters = Object.fromEntries( - abiFunction.inputs.map((input, index) => ([ - input.name, - { - description: `"${input.name}" parameter of function "${abiFunction.name}"`, - name: input.name, - type: encodedFunctionArgs[index] instanceof SignatureTemplate ? 'Key' : 'WalletData', - }, - ])), - ); - - const constructorParameters = Object.fromEntries( - artifact.constructorInputs.map((input) => ([ - input.name, - { - description: `"${input.name}" parameter of this contract`, - name: input.name, - type: 'WalletData', - }, - ])), - ); - - const entities = { - [artifact.contractName + '_parameters']: { - description: 'Contract creation and function parameters', - name: artifact.contractName + '_parameters', - scripts: [ - artifact.contractName + '_lock', - artifact.contractName + '_unlock', - ], - variables: { - ...functionParameters, - ...constructorParameters, - }, - }, - }; - - // function_index is a special variable that indicates the function to execute - if (artifact.abi.length > 1) { - entities[artifact.contractName + '_parameters'].variables.function_index = { - description: 'Script function index to execute', - name: 'function_index', - type: 'WalletData', - }; - } - - return entities; -}; - -const generateTemplateScripts = ( - artifact: Artifact, - addressType: AddressType, - abiFunction: AbiFunction, - encodedFunctionArgs: EncodedFunctionArgument[], - encodedConstructorArgs: EncodedConstructorArgument[], -): WalletTemplate['scripts'] => { - // definition of locking scripts and unlocking scripts with their respective bytecode - return { - [artifact.contractName + '_unlock']: generateTemplateUnlockScript(artifact, abiFunction, encodedFunctionArgs), - [artifact.contractName + '_lock']: generateTemplateLockScript(artifact, addressType, encodedConstructorArgs), - }; -}; - -const generateTemplateLockScript = ( - artifact: Artifact, - addressType: AddressType, - constructorArguments: EncodedFunctionArgument[], -): WalletTemplateScriptLocking => { - return { - lockingType: addressType, - name: artifact.contractName + '_lock', - script: [ - `// "${artifact.contractName}" contract constructor parameters`, - formatParametersForDebugging(artifact.constructorInputs, constructorArguments), - '', - '// bytecode', - formatBytecodeForDebugging(artifact), - ].join('\n'), - }; -}; - -const generateTemplateUnlockScript = ( - artifact: Artifact, - abiFunction: AbiFunction, - encodedFunctionArgs: EncodedFunctionArgument[], -): WalletTemplateScriptUnlocking => { - const functionIndex = artifact.abi.findIndex((func) => func.name === abiFunction.name); - - const functionIndexString = artifact.abi.length > 1 - ? ['// function index in contract', ` // int = <${functionIndex}>`, ''] - : []; - - return { - // this unlocking script must pass our only scenario - passes: [artifact.contractName + '_evaluate'], - name: artifact.contractName + '_unlock', - script: [ - `// "${abiFunction.name}" function parameters`, - formatParametersForDebugging(abiFunction.inputs, encodedFunctionArgs), - '', - ...functionIndexString, - ].join('\n'), - unlocks: artifact.contractName + '_lock', - }; -}; - -const generateTemplateScenarios = ( - contract: Contract, - transaction: Transaction, - transactionHex: string, - artifact: Artifact, - abiFunction: AbiFunction, - encodedFunctionArgs: EncodedFunctionArgument[], - encodedConstructorArgs: EncodedConstructorArgument[], -): WalletTemplate['scenarios'] => { - const libauthTransaction = decodeTransaction(hexToBin(transactionHex)); - if (typeof libauthTransaction === 'string') throw Error(libauthTransaction); - - const scenarios = { - // single scenario to spend out transaction under test given the CashScript parameters provided - [artifact.contractName + '_evaluate']: { - name: artifact.contractName + '_evaluate', - description: 'An example evaluation where this script execution passes.', - data: { - // encode values for the variables defined above in `entities` property - bytecode: { - ...generateTemplateScenarioParametersFunctionIndex(abiFunction, artifact.abi), - ...generateTemplateScenarioParametersValues(abiFunction.inputs, encodedFunctionArgs), - ...generateTemplateScenarioParametersValues(artifact.constructorInputs, encodedConstructorArgs), - }, - currentBlockHeight: 2, - currentBlockTime: Math.round(+new Date() / 1000), - keys: { - privateKeys: generateTemplateScenarioKeys(abiFunction.inputs, encodedFunctionArgs), - }, - }, - transaction: generateTemplateScenarioTransaction(contract, libauthTransaction, transaction), - sourceOutputs: generateTemplateScenarioSourceOutputs(transaction), - }, - }; - - return scenarios; -}; - -const generateTemplateScenarioTransaction = ( - contract: Contract, - libauthTransaction: TransactionBCH, - csTransaction: Transaction, -): WalletTemplateScenario['transaction'] => { - const slotIndex = csTransaction.inputs.findIndex((input) => !isUtxoP2PKH(input)); - - const inputs = libauthTransaction.inputs.map((input, inputIndex) => { - const csInput = csTransaction.inputs[inputIndex] as Utxo; - - return { - outpointIndex: input.outpointIndex, - outpointTransactionHash: binToHex(input.outpointTransactionHash), - sequenceNumber: input.sequenceNumber, - unlockingBytecode: generateTemplateScenarioBytecode(csInput, inputIndex, 'p2pkh_placeholder_unlock', inputIndex === slotIndex), - } as WalletTemplateScenarioInput; - }); - - const locktime = libauthTransaction.locktime; - - const outputs = libauthTransaction.outputs.map((output, index) => { - const csOutput = csTransaction.outputs[index]; - - return { - lockingBytecode: generateTemplateScenarioTransactionOutputLockingBytecode(csOutput, contract), - token: serialiseTokenDetails(output.token), - valueSatoshis: Number(output.valueSatoshis), - } as WalletTemplateScenarioTransactionOutput; - }); - - const version = libauthTransaction.version; - - return { inputs, locktime, outputs, version }; -}; - -export const generateTemplateScenarioTransactionOutputLockingBytecode = ( - csOutput: Output, - contract: Contract, -): string | {} => { - if (csOutput.to instanceof Uint8Array) return binToHex(csOutput.to); - if ([contract.address, contract.tokenAddress].includes(csOutput.to)) return {}; - return binToHex(addressToLockScript(csOutput.to)); -}; - -const generateTemplateScenarioSourceOutputs = ( - csTransaction: Transaction, -): Array> => { - const slotIndex = csTransaction.inputs.findIndex((input) => !isUtxoP2PKH(input)); - - return csTransaction.inputs.map((input, inputIndex) => { - return { - lockingBytecode: generateTemplateScenarioBytecode(input, inputIndex, 'p2pkh_placeholder_lock', inputIndex === slotIndex), - valueSatoshis: Number(input.satoshis), - token: serialiseTokenDetails(input.token), - }; - }); -}; - -// Used for generating the locking / unlocking bytecode for source outputs and inputs -export const generateTemplateScenarioBytecode = ( - input: Utxo, inputIndex: number, p2pkhScriptNameTemplate: string, insertSlot?: boolean, -): WalletTemplateScenarioBytecode | ['slot'] => { - if (insertSlot) return ['slot']; - - const p2pkhScriptName = `${p2pkhScriptNameTemplate}_${inputIndex}`; - const placeholderKeyName = `placeholder_key_${inputIndex}`; - - // This is for P2PKH inputs in the old transaction builder (TODO: remove when we remove old transaction builder) - if (isUtxoP2PKH(input)) { - return { - script: p2pkhScriptName, - overrides: { - keys: { - privateKeys: { - [placeholderKeyName]: binToHex(input.template.privateKey), - }, - }, - }, - }; - } - - if (isUnlockableUtxo(input) && isStandardUnlockableUtxo(input)) { - return generateUnlockingScriptParams(input, p2pkhScriptNameTemplate, inputIndex); - } - - // 'slot' means that we are currently evaluating this specific input, - // {} means that it is the same script type, but not being evaluated - return {}; -}; - -export const generateTemplateScenarioParametersValues = ( - types: readonly AbiInput[], - encodedArgs: EncodedFunctionArgument[], -): Record => { - const typesAndArguments = zip(types, encodedArgs); - - const entries = typesAndArguments - // SignatureTemplates are handled by the 'keys' object in the scenario - .filter(([, arg]) => !(arg instanceof SignatureTemplate)) - .map(([input, arg]) => { - const encodedArgumentHex = binToHex(arg as Uint8Array); - const prefixedEncodedArgument = addHexPrefixExceptEmpty(encodedArgumentHex); - return [input.name, prefixedEncodedArgument] as const; - }); - - return Object.fromEntries(entries); -}; - -export const generateTemplateScenarioParametersFunctionIndex = ( - abiFunction: AbiFunction, - abi: readonly AbiFunction[], -): Record => { - const functionIndex = abi.length > 1 - ? abi.findIndex((func) => func.name === abiFunction.name) - : undefined; - - return functionIndex !== undefined ? { function_index: functionIndex.toString() } : {}; -}; - -export const addHexPrefixExceptEmpty = (value: string): string => { - return value.length > 0 ? `0x${value}` : ''; -}; - -export const generateTemplateScenarioKeys = ( - types: readonly AbiInput[], - encodedArgs: EncodedFunctionArgument[], -): Record => { - const typesAndArguments = zip(types, encodedArgs); - - const entries = typesAndArguments - .filter(([, arg]) => arg instanceof SignatureTemplate) - .map(([input, arg]) => ([input.name, binToHex((arg as SignatureTemplate).privateKey)] as const)); - - return Object.fromEntries(entries); -}; - -export const formatParametersForDebugging = (types: readonly AbiInput[], args: EncodedFunctionArgument[]): string => { - if (types.length === 0) return '// none'; - - // We reverse the arguments because the order of the arguments in the bytecode is reversed - const typesAndArguments = zip(types, args).reverse(); - - return typesAndArguments.map(([input, arg]) => { - if (arg instanceof SignatureTemplate) { - const signatureAlgorithmName = getSignatureAlgorithmName(arg.getSignatureAlgorithm()); - const hashtypeName = getHashTypeName(arg.getHashType(false)); - return `<${input.name}.${signatureAlgorithmName}.${hashtypeName}> // ${input.type}`; - } - - const typeStr = input.type === 'bytes' ? `bytes${arg.length}` : input.type; - - // we output these values as pushdata, comment will contain the type and the value of the variable - // e.g. // int = <0xa08601> - return `<${input.name}> // ${typeStr} = <${`0x${binToHex(arg)}`}>`; - }).join('\n'); -}; - -export const getSignatureAlgorithmName = (signatureAlgorithm: SignatureAlgorithm): string => { - const signatureAlgorithmNames = { - [SignatureAlgorithm.SCHNORR]: 'schnorr_signature', - [SignatureAlgorithm.ECDSA]: 'ecdsa_signature', - }; - - return signatureAlgorithmNames[signatureAlgorithm]; -}; - -export const getHashTypeName = (hashType: HashType): string => { - const hashtypeNames = { - [HashType.SIGHASH_ALL]: 'all_outputs', - [HashType.SIGHASH_ALL | HashType.SIGHASH_ANYONECANPAY]: 'all_outputs_single_input', - [HashType.SIGHASH_ALL | HashType.SIGHASH_UTXOS]: 'all_outputs_all_utxos', - [HashType.SIGHASH_ALL | HashType.SIGHASH_ANYONECANPAY | HashType.SIGHASH_UTXOS]: 'all_outputs_single_input_INVALID_all_utxos', - [HashType.SIGHASH_SINGLE]: 'corresponding_output', - [HashType.SIGHASH_SINGLE | HashType.SIGHASH_ANYONECANPAY]: 'corresponding_output_single_input', - [HashType.SIGHASH_SINGLE | HashType.SIGHASH_UTXOS]: 'corresponding_output_all_utxos', - [HashType.SIGHASH_SINGLE | HashType.SIGHASH_ANYONECANPAY | HashType.SIGHASH_UTXOS]: 'corresponding_output_single_input_INVALID_all_utxos', - [HashType.SIGHASH_NONE]: 'no_outputs', - [HashType.SIGHASH_NONE | HashType.SIGHASH_ANYONECANPAY]: 'no_outputs_single_input', - [HashType.SIGHASH_NONE | HashType.SIGHASH_UTXOS]: 'no_outputs_all_utxos', - [HashType.SIGHASH_NONE | HashType.SIGHASH_ANYONECANPAY | HashType.SIGHASH_UTXOS]: 'no_outputs_single_input_INVALID_all_utxos', - }; - - return hashtypeNames[hashType]; -}; - -export const formatBytecodeForDebugging = (artifact: Artifact): string => { - if (!artifact.debug) { - return artifact.bytecode - .split(' ') - .map((asmElement) => (isHex(asmElement) ? `<0x${asmElement}>` : asmElement)) - .join('\n'); - } - - return formatBitAuthScript( - bytecodeToScript(hexToBin(artifact.debug.bytecode)), - artifact.debug.sourceMap, - artifact.source, - ); -}; - -export const serialiseTokenDetails = ( - token?: TokenDetails | LibauthTokenDetails, -): LibauthTemplateTokenDetails | undefined => { - if (!token) return undefined; - - return { - amount: token.amount.toString(), - category: token.category instanceof Uint8Array ? binToHex(token.category) : token.category, - nft: token.nft ? { - capability: token.nft.capability, - commitment: token.nft.commitment instanceof Uint8Array ? binToHex(token.nft.commitment) : token.nft.commitment, - } : undefined, - }; -}; - -export interface LibauthTemplateTokenDetails { - amount: string; - category: string; - nft?: { - capability: 'none' | 'mutable' | 'minting'; - commitment: string; - }; -} diff --git a/packages/cashscript/src/SignatureTemplate.ts b/packages/cashscript/src/SignatureTemplate.ts index e834a32b..804c37d7 100644 --- a/packages/cashscript/src/SignatureTemplate.ts +++ b/packages/cashscript/src/SignatureTemplate.ts @@ -1,4 +1,4 @@ -import { decodePrivateKeyWif, secp256k1, SigningSerializationFlag } from '@bitauth/libauth'; +import { decodePrivateKeyWif, hexToBin, isHex, secp256k1, SigningSerializationFlag } from '@bitauth/libauth'; import { hash256, scriptToBytecode } from '@cashscript/utils'; import { GenerateUnlockingBytecodeOptions, @@ -20,19 +20,28 @@ export default class SignatureTemplate { const wif = signer.toWIF(); this.privateKey = decodeWif(wif); } else if (typeof signer === 'string') { - this.privateKey = decodeWif(signer); + const maybeHexString = signer.startsWith('0x') ? signer.slice(2) : signer; + if (isHex(maybeHexString)) { + this.privateKey = hexToBin(maybeHexString); + } else { + this.privateKey = decodeWif(maybeHexString); + } } else { this.privateKey = signer; } } - // TODO: Allow signing of non-transaction messages (i.e. don't add the hashtype) generateSignature(payload: Uint8Array, bchForkId?: boolean): Uint8Array { + const signature = this.signMessageHash(payload); + return Uint8Array.from([...signature, this.getHashType(bchForkId)]); + } + + signMessageHash(payload: Uint8Array): Uint8Array { const signature = this.signatureAlgorithm === SignatureAlgorithm.SCHNORR ? secp256k1.signMessageHashSchnorr(this.privateKey, payload) as Uint8Array : secp256k1.signMessageHashDER(this.privateKey, payload) as Uint8Array; - return Uint8Array.from([...signature, this.getHashType(bchForkId)]); + return signature; } getHashType(bchForkId: boolean = true): number { @@ -66,7 +75,7 @@ export default class SignatureTemplate { } } -// Works for both BITBOX/bitcoincash.js ECPair and bitcore-lib-cash PrivateKey +// Works for both bitcoincash.js/bchjs ECPair and bitcore-lib-cash PrivateKey interface Keypair { toWIF(): string; } diff --git a/packages/cashscript/src/Transaction.ts b/packages/cashscript/src/Transaction.ts deleted file mode 100644 index 92d1c3dd..00000000 --- a/packages/cashscript/src/Transaction.ts +++ /dev/null @@ -1,515 +0,0 @@ -import { - hexToBin, - decodeTransaction, - Transaction as LibauthTransaction, - WalletTemplate, -} from '@bitauth/libauth'; -import { - AbiFunction, - encodeBip68, - placeholder, -} from '@cashscript/utils'; -import deepEqual from 'fast-deep-equal'; -import { - Utxo, - Output, - Recipient, - TokenDetails, - NftObject, - isUtxoP2PKH, - TransactionDetails, - Unlocker, - SignatureAlgorithm, -} from './interfaces.js'; -import { - createInputScript, - getInputSize, - createOpReturnOutput, - getTxSizeWithoutInputs, - validateOutput, - utxoComparator, - calculateDust, - getOutputSize, - utxoTokenComparator, - delay, -} from './utils.js'; -import SignatureTemplate from './SignatureTemplate.js'; -import { P2PKH_INPUT_SIZE } from './constants.js'; -import { TransactionBuilder } from './TransactionBuilder.js'; -import { Contract } from './Contract.js'; -import { buildTemplate, getBitauthUri } from './LibauthTemplate.js'; -import { debugTemplate, DebugResults } from './debugging.js'; -import { EncodedFunctionArgument } from './Argument.js'; -import { FailedTransactionError } from './Errors.js'; -import semver from 'semver'; - -export class Transaction { - public inputs: Utxo[] = []; - public outputs: Output[] = []; - - private sequence = 0xfffffffe; - private locktime: number; - private feePerByte: number = 1.0; - private hardcodedFee: bigint; - private minChange: bigint = 0n; - private tokenChange: boolean = true; - - constructor( - public contract: Contract, - private unlocker: Unlocker, - public abiFunction: AbiFunction, - public encodedFunctionArgs: EncodedFunctionArgument[], - private selector?: number, - ) { } - - from(input: Utxo): this; - from(inputs: Utxo[]): this; - - from(inputOrInputs: Utxo | Utxo[]): this { - if (!Array.isArray(inputOrInputs)) { - inputOrInputs = [inputOrInputs]; - } - - this.inputs = this.inputs.concat(inputOrInputs); - - return this; - } - - fromP2PKH(input: Utxo, template: SignatureTemplate): this; - fromP2PKH(inputs: Utxo[], template: SignatureTemplate): this; - - fromP2PKH(inputOrInputs: Utxo | Utxo[], template: SignatureTemplate): this { - if (!Array.isArray(inputOrInputs)) { - inputOrInputs = [inputOrInputs]; - } - - inputOrInputs = inputOrInputs.map((input) => ({ ...input, template })); - - this.inputs = this.inputs.concat(inputOrInputs); - - return this; - } - - to(to: string, amount: bigint, token?: TokenDetails): this; - to(outputs: Recipient[]): this; - - to(toOrOutputs: string | Recipient[], amount?: bigint, token?: TokenDetails): this { - if (typeof toOrOutputs === 'string' && typeof amount === 'bigint') { - const recipient = { to: toOrOutputs, amount, token }; - return this.to([recipient]); - } - - if (Array.isArray(toOrOutputs) && amount === undefined) { - toOrOutputs.forEach(validateOutput); - this.outputs = this.outputs.concat(toOrOutputs); - return this; - } - - throw new Error('Incorrect arguments passed to function \'to\''); - } - - withOpReturn(chunks: string[]): this { - this.outputs.push(createOpReturnOutput(chunks)); - return this; - } - - withAge(age: number): this { - this.sequence = encodeBip68({ blocks: age }); - return this; - } - - withTime(time: number): this { - this.locktime = time; - return this; - } - - withHardcodedFee(hardcodedFee: bigint): this { - this.hardcodedFee = hardcodedFee; - return this; - } - - withFeePerByte(feePerByte: number): this { - this.feePerByte = feePerByte; - return this; - } - - withMinChange(minChange: bigint): this { - this.minChange = minChange; - return this; - } - - withoutChange(): this { - return this.withMinChange(BigInt(Number.MAX_VALUE)); - } - - withoutTokenChange(): this { - this.tokenChange = false; - return this; - } - - async build(): Promise { - this.locktime = this.locktime ?? await this.contract.provider.getBlockHeight(); - await this.setInputsAndOutputs(); - - const builder = new TransactionBuilder({ provider: this.contract.provider }); - - this.inputs.forEach((utxo) => { - if (isUtxoP2PKH(utxo)) { - builder.addInput(utxo, utxo.template.unlockP2PKH(), { sequence: this.sequence }); - } else { - builder.addInput(utxo, this.unlocker, { sequence: this.sequence }); - } - }); - - builder.addOutputs(this.outputs); - builder.setLocktime(this.locktime); - - return builder.build(); - } - - async send(): Promise; - async send(raw: true): Promise; - - async send(raw?: true): Promise { - const tx = await this.build(); - - // Debug the transaction locally before sending so any errors are caught early - await this.debug(); - - try { - const txid = await this.contract.provider.sendRawTransaction(tx); - return raw ? await this.getTxDetails(txid, raw) : await this.getTxDetails(txid); - } catch (error: any) { - const reason = error.error ?? error.message ?? error; - throw new FailedTransactionError(reason, await this.bitauthUri()); - } - } - - // method to debug the transaction with libauth VM, throws upon evaluation error - async debug(): Promise { - if (!semver.satisfies(this.contract.artifact.compiler.version, '>=0.11.0')) { - console.warn('For the best debugging experience, please recompile your contract with cashc version 0.11.0 or newer.'); - } - - const template = await this.getLibauthTemplate(); - return debugTemplate(template, [this.contract.artifact]); - } - - async bitauthUri(): Promise { - console.warn('WARNING: it is unsafe to use this Bitauth URI when using real private keys as they are included in the transaction template'); - const template = await this.getLibauthTemplate(); - return getBitauthUri(template); - } - - async getLibauthTemplate(): Promise { - return buildTemplate({ transaction: this }); - } - - private async getTxDetails(txid: string): Promise; - private async getTxDetails(txid: string, raw: true): Promise; - - private async getTxDetails(txid: string, raw?: true): Promise { - for (let retries = 0; retries < 1200; retries += 1) { - await delay(500); - try { - const hex = await this.contract.provider.getRawTransaction(txid); - - if (raw) return hex; - - const libauthTransaction = decodeTransaction(hexToBin(hex)) as LibauthTransaction; - return { ...libauthTransaction, txid, hex }; - } catch (ignored) { - // ignored - } - } - - // Should not happen - throw new Error('Could not retrieve transaction details for over 10 minutes'); - } - - private async setInputsAndOutputs(): Promise { - if (this.outputs.length === 0) { - throw new Error('Attempted to build a transaction without outputs'); - } - - // Fetched utxos are only used when no inputs are available, so only fetch in that case. - const allUtxos: Utxo[] = this.inputs.length === 0 - ? await this.contract.provider.getUtxos(this.contract.address) - : []; - - const tokenInputs = this.inputs.length > 0 - ? this.inputs.filter((input) => input.token) - : selectAllTokenUtxos(allUtxos, this.outputs); - - // This throws if the manually selected inputs are not enough to cover the outputs - if (this.inputs.length > 0) { - selectAllTokenUtxos(this.inputs, this.outputs); - } - - if (this.tokenChange) { - const tokenChangeOutputs = createFungibleTokenChangeOutputs( - tokenInputs, this.outputs, this.contract.tokenAddress, - ); - this.outputs.push(...tokenChangeOutputs); - } - - // Construct list with all nfts in inputs - const listNftsInputs: NftObject[] = []; - // If inputs are manually selected, add their tokens to balance - this.inputs.forEach((input) => { - if (!input.token) return; - if (input.token.nft) { - listNftsInputs.push({ ...input.token.nft, category: input.token.category }); - } - }); - // Construct list with all nfts in outputs - let listNftsOutputs: NftObject[] = []; - // Subtract all token outputs from the token balances - this.outputs.forEach((output) => { - if (!output.token) 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) { - // 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 an immutable 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 += 1) { - // Deep equality check token objects - if (deepEqual(listNftsOutputs[i], nftInput)) { - listNftsOutputs.splice(i, 1); - unusedNfts = unusedNfts.filter((nft) => !deepEqual(nft, nftInput)); - break; - } - } - } - } - for (const nftInput of listNftsInputs) { - if (nftInput.capability === 'minting') { - // eslint-disable-next-line max-len - const newListNftsOutputs: NftObject[] = listNftsOutputs.filter((nftOutput) => nftOutput.category !== nftInput.category); - if (newListNftsOutputs !== listNftsOutputs) { - unusedNfts = unusedNfts.filter((nft) => !deepEqual(nft, nftInput)); - listNftsOutputs = newListNftsOutputs; - } - } - if (nftInput.capability === 'mutable') { - for (let i = 0; i < listNftsOutputs.length; i += 1) { - if (listNftsOutputs[i].category === nftInput.category) { - listNftsOutputs.splice(i, 1); - unusedNfts = unusedNfts.filter((nft) => !deepEqual(nft, nftInput)); - break; - } - } - } - } - for (const nftOutput of listNftsOutputs) { - const genesisUtxo = getTokenGenesisUtxo(this.inputs, nftOutput.category); - if (genesisUtxo) { - listNftsOutputs = listNftsOutputs.filter((nft) => !deepEqual(nft, nftOutput)); - } - } - if (listNftsOutputs.length !== 0) { - throw new Error(`NFT output with token category ${listNftsOutputs[0].category} does not have corresponding input`); - } - if (this.tokenChange) { - for (const unusedNft of unusedNfts) { - const tokenDetails: TokenDetails = { - category: unusedNft.category, - amount: BigInt(0), - nft: { - capability: unusedNft.capability, - commitment: unusedNft.commitment, - }, - }; - const nftChangeOutput = { to: this.contract.tokenAddress, amount: BigInt(1000), token: tokenDetails }; - this.outputs.push(nftChangeOutput); - } - } - } - - // Replace all SignatureTemplate with placeholder Uint8Arrays - const placeholderArgs = this.encodedFunctionArgs.map((arg) => { - if (!(arg instanceof SignatureTemplate)) return arg; - - // Schnorr signatures are *always* 65 bytes: 64 for signature + 1 byte for hashtype. - if (arg.getSignatureAlgorithm() === SignatureAlgorithm.SCHNORR) return placeholder(65); - - // ECDSA signatures are at least 71 bytes: 64 bytes for signature + 1 byte for hashtype + 6 bytes for encoding - // overhead. But it may have up to 2 extra bytes for padding, so we overestimate by 2 bytes. - // (see https://transactionfee.info/charts/bitcoin-script-ecdsa-length/) - return placeholder(73); - }); - - // Create a placeholder input script for size calculation using the placeholder arguments - const placeholderScript = createInputScript( - this.contract.redeemScript, - placeholderArgs, - this.selector, - ); - - // Add one extra byte per input to over-estimate tx-in count - const contractInputSize = getInputSize(placeholderScript) + 1; - - // Note that we use the addPrecision function to add "decimal points" to BigInt numbers - - // Calculate amount to send and base fee (excluding additional fees per UTXO) - let amount = addPrecision(this.outputs.reduce((acc, output) => acc + output.amount, 0n)); - let fee = addPrecision(this.hardcodedFee ?? getTxSizeWithoutInputs(this.outputs) * this.feePerByte); - - // Select and gather UTXOs and calculate fees and available funds - let satsAvailable = 0n; - if (this.inputs.length > 0) { - // If inputs are already defined, the user provided the UTXOs and we perform no further UTXO selection - if (!this.hardcodedFee) { - const totalInputSize = this.inputs.reduce( - (acc, input) => acc + (isUtxoP2PKH(input) ? P2PKH_INPUT_SIZE : contractInputSize), - 0, - ); - fee += addPrecision(totalInputSize * this.feePerByte); - } - - satsAvailable = addPrecision(this.inputs.reduce((acc, input) => acc + input.satoshis, 0n)); - } else { - // If inputs are not defined yet, we retrieve the contract's UTXOs and perform selection - 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 - bchUtxos.sort(utxoComparator).reverse(); - - // Add all automatically added token inputs to the transaction - for (const utxo of tokenInputs) { - this.inputs.push(utxo); - satsAvailable += addPrecision(utxo.satoshis); - if (!this.hardcodedFee) fee += addPrecision(contractInputSize * 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(contractInputSize * this.feePerByte); - } - } - - // Remove "decimal points" from BigInt numbers (rounding up for fee, down for others) - satsAvailable = removePrecisionFloor(satsAvailable); - amount = removePrecisionFloor(amount); - fee = removePrecisionCeil(fee); - - // Calculate change and check available funds - let change = satsAvailable - amount - fee; - - if (change < 0) { - throw new Error(`Insufficient funds: available (${satsAvailable}) < needed (${amount + fee}).`); - } - - // Account for the fee of adding a change output - if (!this.hardcodedFee) { - const changeOutputSize = getOutputSize({ to: this.contract.address, amount: 0n }); - change -= BigInt(changeOutputSize * this.feePerByte); - } - - // Add a change output if applicable - const changeOutput = { to: this.contract.address, amount: change }; - if (change >= this.minChange && change >= calculateDust(changeOutput)) { - this.outputs.push(changeOutput); - } - } -} - -const getTokenGenesisUtxo = (utxos: Utxo[], tokenCategory: string): Utxo | undefined => { - const creationUtxo = utxos.find((utxo) => utxo.vout === 0 && utxo.txid === tokenCategory); - return creationUtxo; -}; - -const getTokenCategories = (outputs: Array): string[] => ( - outputs - .filter((output) => output.token) - .map((output) => output.token!.category) -); - -const calculateTotalTokenAmount = (outputs: Array, tokenCategory: string): bigint => ( - outputs - .filter((output) => output.token?.category === tokenCategory) - .reduce((acc, output) => acc + output.token!.amount, 0n) -); - -const selectTokenUtxos = (utxos: Utxo[], amountNeeded: bigint, tokenCategory: string): Utxo[] => { - const genesisUtxo = getTokenGenesisUtxo(utxos, tokenCategory); - if (genesisUtxo) return [genesisUtxo]; - - const tokenUtxos = utxos.filter((utxo) => utxo.token?.category === tokenCategory && utxo.token?.amount > 0n); - - // 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: Utxo[] = []; - - // 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) { - if (amountAvailable >= amountNeeded) break; - selectedUtxos.push(utxo); - amountAvailable += utxo.token!.amount; - } - - if (amountAvailable < amountNeeded) { - throw new Error(`Insufficient funds for token ${tokenCategory}: available (${amountAvailable}) < needed (${amountNeeded}).`); - } - - return selectedUtxos; -}; - -const selectAllTokenUtxos = (utxos: Utxo[], outputs: Output[]): Utxo[] => { - const tokenCategories = getTokenCategories(outputs); - return tokenCategories.flatMap( - (tokenCategory) => selectTokenUtxos(utxos, calculateTotalTokenAmount(outputs, tokenCategory), tokenCategory), - ); -}; - -const createFungibleTokenChangeOutputs = (utxos: Utxo[], outputs: Output[], address: string): Output[] => { - const tokenCategories = getTokenCategories(utxos); - - const changeOutputs = tokenCategories.map((tokenCategory) => { - const required = calculateTotalTokenAmount(outputs, tokenCategory); - const available = calculateTotalTokenAmount(utxos, tokenCategory); - const change = available - required; - - if (change === 0n) return undefined; - - return { to: address, amount: BigInt(1000), token: { category: tokenCategory, amount: change } }; - }); - - return changeOutputs.filter((output) => output !== undefined) as Output[]; -}; - -// Note: the below is a very simple implementation of a "decimal point" system for BigInt numbers -// It is safe to use for UTXO fee calculations due to its low numbers, but should not be used for other purposes -// Also note that multiplication and division between two "decimal" bigints is not supported - -// High precision may not work with some 'number' inputs, so we set the default to 6 "decimal places" -const addPrecision = (amount: number | bigint, precision: number = 6): bigint => { - if (typeof amount === 'number') { - return BigInt(Math.ceil(amount * 10 ** precision)); - } - - return amount * BigInt(10 ** precision); -}; - -const removePrecisionFloor = (amount: bigint, precision: number = 6): bigint => ( - amount / (10n ** BigInt(precision)) -); - -const removePrecisionCeil = (amount: bigint, precision: number = 6): bigint => { - const multiplier = 10n ** BigInt(precision); - return (amount + multiplier - 1n) / multiplier; -}; diff --git a/packages/cashscript/src/TransactionBuilder.ts b/packages/cashscript/src/TransactionBuilder.ts index 2be2413b..dffeae7a 100644 --- a/packages/cashscript/src/TransactionBuilder.ts +++ b/packages/cashscript/src/TransactionBuilder.ts @@ -17,7 +17,8 @@ import { isUnlockableUtxo, isStandardUnlockableUtxo, StandardUnlockableUtxo, - isP2PKHUnlocker, + VmResourceUsage, + isContractUnlocker, } from './interfaces.js'; import { NetworkProvider } from './network/index.js'; import { @@ -30,14 +31,16 @@ import { } from './utils.js'; import { FailedTransactionError } from './Errors.js'; import { DebugResults } from './debugging.js'; -import { getBitauthUri } from './LibauthTemplate.js'; -import { debugLibauthTemplate, getLibauthTemplates } from './advanced/LibauthTemplate.js'; +import { debugLibauthTemplate, getLibauthTemplate, getBitauthUri } from './libauth-template/LibauthTemplate.js'; import { getWcContractInfo, WcSourceOutput, WcTransactionOptions } from './walletconnect-utils.js'; import semver from 'semver'; import { WcTransactionObject } from './walletconnect-utils.js'; export interface TransactionBuilderOptions { provider: NetworkProvider; + maximumFeeSatoshis?: bigint; + maximumFeeSatsPerByte?: number; + allowImplicitFungibleTokenBurn?: boolean; } const DEFAULT_SEQUENCE = 0xfffffffe; @@ -48,12 +51,16 @@ export class TransactionBuilder { public outputs: Output[] = []; public locktime: number = 0; - public maxFee?: bigint; + public options: TransactionBuilderOptions; constructor( options: TransactionBuilderOptions, ) { this.provider = options.provider; + this.options = { + allowImplicitFungibleTokenBurn: options.allowImplicitFungibleTokenBurn ?? false, + ...options, + }; } addInput(utxo: Utxo, unlocker: Unlocker, options?: InputOptions): this { @@ -102,25 +109,52 @@ export class TransactionBuilder { return this; } - setMaxFee(maxFee: bigint): this { - this.maxFee = maxFee; - return this; - } - - private checkMaxFee(): void { - if (!this.maxFee) return; - + private checkMaxFee(transaction: LibauthTransaction): void { const totalInputAmount = this.inputs.reduce((total, input) => total + input.satoshis, 0n); const totalOutputAmount = this.outputs.reduce((total, output) => total + output.amount, 0n); const fee = totalInputAmount - totalOutputAmount; - if (fee > this.maxFee) { - throw new Error(`Transaction fee of ${fee} is higher than max fee of ${this.maxFee}`); + if (this.options.maximumFeeSatoshis && fee > this.options.maximumFeeSatoshis) { + throw new Error(`Transaction fee of ${fee} is higher than max fee of ${this.options.maximumFeeSatoshis}`); + } + + if (this.options.maximumFeeSatsPerByte) { + const transactionSize = encodeTransaction(transaction).byteLength; + const feePerByte = Number((Number(fee) / transactionSize).toFixed(2)); + + if (feePerByte > this.options.maximumFeeSatsPerByte) { + throw new Error(`Transaction fee per byte of ${feePerByte} is higher than max fee per byte of ${this.options.maximumFeeSatsPerByte}`); + } + } + } + + private checkFungibleTokenBurn(): void { + if (this.options.allowImplicitFungibleTokenBurn) return; + + const tokenInputAmounts: Record = {}; + const tokenOutputAmounts: Record = {}; + + for (const input of this.inputs) { + if (input.token?.amount) { + tokenInputAmounts[input.token.category] = (tokenInputAmounts[input.token.category] || 0n) + input.token.amount; + } + } + for (const output of this.outputs) { + if (output.token?.amount) { + tokenOutputAmounts[output.token.category] = (tokenOutputAmounts[output.token.category] || 0n) + output.token.amount; + } + } + + for (const [category, inputAmount] of Object.entries(tokenInputAmounts)) { + const outputAmount = tokenOutputAmounts[category] || 0n; + if (outputAmount < inputAmount) { + throw new Error(`Implicit burning of fungible tokens for category ${category} is not allowed (input amount: ${inputAmount}, output amount: ${outputAmount}). If this is intended, set allowImplicitFungibleTokenBurn to true.`); + } } } buildLibauthTransaction(): LibauthTransaction { - this.checkMaxFee(); + this.checkFungibleTokenBurn(); const inputs: LibauthTransaction['inputs'] = this.inputs.map((utxo) => ({ outpointIndex: utxo.vout, @@ -149,6 +183,8 @@ export class TransactionBuilder { transaction.inputs[i].unlockingBytecode = script; }); + this.checkMaxFee(transaction); + return transaction; } @@ -158,11 +194,6 @@ export class TransactionBuilder { } debug(): DebugResults { - // do not debug a pure P2PKH-spend transaction - if (this.inputs.every((input) => isP2PKHUnlocker(input.unlocker))) { - return {}; - } - if (this.inputs.some((input) => !isStandardUnlockableUtxo(input))) { throw new Error('Cannot debug a transaction with custom unlocker'); } @@ -179,13 +210,50 @@ export class TransactionBuilder { return debugLibauthTemplate(this.getLibauthTemplate(), this); } - bitauthUri(): string { + getVmResourceUsage(verbose: boolean = false): Array { + // Note that only StandardUnlockableUtxo inputs are supported for debugging, so any transaction with custom unlockers + // cannot be debugged (and therefore cannot return VM resource usage) + const results = this.debug(); + const vmResourceUsage: Array = []; + const tableData: Array> = []; + + const formatMetric = (value: number, total: number, withPercentage: boolean = false): string => + `${formatNumber(value)} / ${formatNumber(total)}${withPercentage ? ` (${(value / total * 100).toFixed(0)}%)` : ''}`; + const formatNumber = (value: number): string => value.toLocaleString('en'); + + const resultEntries = Object.entries(results); + for (const [index, input] of this.inputs.entries()) { + const [, result] = resultEntries.find(([entryKey]) => entryKey.includes(`input${index}`)) ?? []; + const metrics = result?.at(-1)?.metrics; + + // Should not happen + if (!metrics) throw new Error('VM resource could not be calculated'); + + vmResourceUsage.push(metrics); + tableData.push({ + 'Contract - Function': isContractUnlocker(input.unlocker) ? `${input.unlocker.contract.name} - ${input.unlocker.abiFunction.name}` : 'P2PKH Input', + Ops: metrics.evaluatedInstructionCount, + 'Op Cost Budget Usage': formatMetric(metrics.operationCost, metrics.maximumOperationCost, true), + SigChecks: formatMetric(metrics.signatureCheckCount, metrics.maximumSignatureCheckCount), + Hashes: formatMetric(metrics.hashDigestIterations, metrics.maximumHashDigestIterations), + }); + } + + if (verbose) { + console.log('VM Resource usage by inputs:'); + console.table(tableData); + } + + return vmResourceUsage; + } + + getBitauthUri(): string { console.warn('WARNING: it is unsafe to use this Bitauth URI when using real private keys as they are included in the transaction template'); return getBitauthUri(this.getLibauthTemplate()); } getLibauthTemplate(): WalletTemplate { - return getLibauthTemplates(this); + return getLibauthTemplate(this); } async send(): Promise; @@ -204,7 +272,7 @@ export class TransactionBuilder { return raw ? await this.getTxDetails(txid, raw) : await this.getTxDetails(txid); } catch (e: any) { const reason = e.error ?? e.message; - throw new FailedTransactionError(reason); + throw new FailedTransactionError(reason, this.getBitauthUri()); } } diff --git a/packages/cashscript/src/advanced/LibauthTemplate.ts b/packages/cashscript/src/advanced/LibauthTemplate.ts deleted file mode 100644 index 5cbeb711..00000000 --- a/packages/cashscript/src/advanced/LibauthTemplate.ts +++ /dev/null @@ -1,619 +0,0 @@ -import { - binToHex, - decodeCashAddress, - TransactionBch, - WalletTemplate, - WalletTemplateEntity, - WalletTemplateScenario, - WalletTemplateScenarioBytecode, - WalletTemplateScenarioInput, - WalletTemplateScenarioOutput, - WalletTemplateScenarioTransactionOutput, - WalletTemplateScript, - WalletTemplateScriptLocking, - WalletTemplateScriptUnlocking, - WalletTemplateVariable, -} from '@bitauth/libauth'; -import { - AbiFunction, - Artifact, -} from '@cashscript/utils'; -import { EncodedConstructorArgument, EncodedFunctionArgument, encodeFunctionArguments } from '../Argument.js'; -import { Contract } from '../Contract.js'; -import { DebugResults, debugTemplate } from '../debugging.js'; -import { - isP2PKHUnlocker, - isStandardUnlockableUtxo, - StandardUnlockableUtxo, - Utxo, -} from '../interfaces.js'; -import { - addHexPrefixExceptEmpty, - formatBytecodeForDebugging, - formatParametersForDebugging, - generateTemplateScenarioBytecode, - generateTemplateScenarioKeys, - generateTemplateScenarioParametersFunctionIndex, - generateTemplateScenarioParametersValues, - generateTemplateScenarioTransactionOutputLockingBytecode, - getHashTypeName, - getSignatureAlgorithmName, - serialiseTokenDetails, -} from '../LibauthTemplate.js'; -import SignatureTemplate from '../SignatureTemplate.js'; -import { Transaction } from '../Transaction.js'; -import { addressToLockScript } from '../utils.js'; -import { TransactionBuilder } from '../TransactionBuilder.js'; - - -/** - * Generates template entities for P2PKH (Pay to Public Key Hash) placeholder scripts. - * - * Follows the WalletTemplateEntity specification from: - * https://ide.bitauth.com/authentication-template-v0.schema.json - * - */ -export const generateTemplateEntitiesP2PKH = ( - inputIndex: number, -): WalletTemplate['entities'] => { - const lockScriptName = `p2pkh_placeholder_lock_${inputIndex}`; - const unlockScriptName = `p2pkh_placeholder_unlock_${inputIndex}`; - - return { - [`signer_${inputIndex}`]: { - scripts: [lockScriptName, unlockScriptName], - description: `placeholder_key_${inputIndex}`, - name: `P2PKH Signer (input #${inputIndex})`, - variables: { - [`placeholder_key_${inputIndex}`]: { - description: '', - name: `P2PKH Placeholder Key (input #${inputIndex})`, - type: 'Key', - }, - }, - }, - }; -}; - -/** - * Generates template entities for P2SH (Pay to Script Hash) placeholder scripts. - * - * Follows the WalletTemplateEntity specification from: - * https://ide.bitauth.com/authentication-template-v0.schema.json - * - */ -export const generateTemplateEntitiesP2SH = ( - contract: Contract, - abiFunction: AbiFunction, - encodedFunctionArgs: EncodedFunctionArgument[], - inputIndex: number, -): WalletTemplate['entities'] => { - const entities = { - [contract.artifact.contractName + '_input' + inputIndex + '_parameters']: { - description: 'Contract creation and function parameters', - name: `${contract.artifact.contractName} (input #${inputIndex})`, - scripts: [ - getLockScriptName(contract), - getUnlockScriptName(contract, abiFunction, inputIndex), - ], - variables: createWalletTemplateVariables(contract.artifact, abiFunction, encodedFunctionArgs), - }, - }; - - // function_index is a special variable that indicates the function to execute - if (contract.artifact.abi.length > 1) { - entities[contract.artifact.contractName + '_input' + inputIndex + '_parameters'].variables.function_index = { - description: 'Script function index to execute', - name: 'function_index', - type: 'WalletData', - }; - } - - return entities; -}; - -const createWalletTemplateVariables = ( - artifact: Artifact, - abiFunction: AbiFunction, - encodedFunctionArgs: EncodedFunctionArgument[], -): Record => { - const functionParameters = Object.fromEntries( - abiFunction.inputs.map((input, index) => ([ - input.name, - { - description: `"${input.name}" parameter of function "${abiFunction.name}"`, - name: input.name, - type: encodedFunctionArgs[index] instanceof SignatureTemplate ? 'Key' : 'WalletData', - }, - ])), - ); - - const constructorParameters = Object.fromEntries( - artifact.constructorInputs.map((input) => ([ - input.name, - { - description: `"${input.name}" parameter of this contract`, - name: input.name, - type: 'WalletData', - }, - ])), - ); - - return { ...functionParameters, ...constructorParameters }; -}; - -/** - * Generates template scripts for P2PKH (Pay to Public Key Hash) placeholder scripts. - * - * Follows the WalletTemplateScript specification from: - * https://ide.bitauth.com/authentication-template-v0.schema.json - * - */ -export const generateTemplateScriptsP2PKH = ( - template: SignatureTemplate, - inputIndex: number, -): WalletTemplate['scripts'] => { - const scripts: WalletTemplate['scripts'] = {}; - const lockScriptName = `p2pkh_placeholder_lock_${inputIndex}`; - const unlockScriptName = `p2pkh_placeholder_unlock_${inputIndex}`; - const placeholderKeyName = `placeholder_key_${inputIndex}`; - - const signatureAlgorithmName = getSignatureAlgorithmName(template.getSignatureAlgorithm()); - const hashtypeName = getHashTypeName(template.getHashType(false)); - const signatureString = `${placeholderKeyName}.${signatureAlgorithmName}.${hashtypeName}`; - // add extra unlocking and locking script for P2PKH inputs spent alongside our contract - // this is needed for correct cross-references in the template - scripts[unlockScriptName] = { - name: `P2PKH Unlock (input #${inputIndex})`, - script: - `<${signatureString}>\n<${placeholderKeyName}.public_key>`, - unlocks: lockScriptName, - }; - scripts[lockScriptName] = { - lockingType: 'standard', - name: `P2PKH Lock (input #${inputIndex})`, - script: - `OP_DUP\nOP_HASH160 <$(<${placeholderKeyName}.public_key> OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG`, - }; - - return scripts; -}; - -/** - * Generates template scripts for P2SH (Pay to Script Hash) placeholder scripts. - * - * Follows the WalletTemplateScript specification from: - * https://ide.bitauth.com/authentication-template-v0.schema.json - * - */ -export const generateTemplateScriptsP2SH = ( - contract: Contract, - abiFunction: AbiFunction, - encodedFunctionArgs: EncodedFunctionArgument[], - encodedConstructorArgs: EncodedConstructorArgument[], - inputIndex: number, -): WalletTemplate['scripts'] => { - // definition of locking scripts and unlocking scripts with their respective bytecode - const unlockingScriptName = getUnlockScriptName(contract, abiFunction, inputIndex); - const lockingScriptName = getLockScriptName(contract); - - return { - [unlockingScriptName]: generateTemplateUnlockScript(contract, abiFunction, encodedFunctionArgs, inputIndex), - [lockingScriptName]: generateTemplateLockScript(contract, encodedConstructorArgs), - }; -}; - -/** - * Generates a template lock script for a P2SH (Pay to Script Hash) placeholder script. - * - * Follows the WalletTemplateScriptLocking specification from: - * https://ide.bitauth.com/authentication-template-v0.schema.json - * - */ -const generateTemplateLockScript = ( - contract: Contract, - constructorArguments: EncodedFunctionArgument[], -): WalletTemplateScriptLocking => { - return { - lockingType: contract.addressType, - name: contract.artifact.contractName, - script: [ - `// "${contract.artifact.contractName}" contract constructor parameters`, - formatParametersForDebugging(contract.artifact.constructorInputs, constructorArguments), - '', - '// bytecode', - formatBytecodeForDebugging(contract.artifact), - ].join('\n'), - }; -}; - -/** - * Generates a template unlock script for a P2SH (Pay to Script Hash) placeholder script. - * - * Follows the WalletTemplateScriptUnlocking specification from: - * https://ide.bitauth.com/authentication-template-v0.schema.json - * - */ -const generateTemplateUnlockScript = ( - contract: Contract, - abiFunction: AbiFunction, - encodedFunctionArgs: EncodedFunctionArgument[], - inputIndex: number, -): WalletTemplateScriptUnlocking => { - const scenarioIdentifier = `${contract.artifact.contractName}_${abiFunction.name}_input${inputIndex}_evaluate`; - const functionIndex = contract.artifact.abi.findIndex((func) => func.name === abiFunction.name); - - const functionIndexString = contract.artifact.abi.length > 1 - ? ['// function index in contract', ` // int = <${functionIndex}>`, ''] - : []; - - return { - // this unlocking script must pass our only scenario - passes: [scenarioIdentifier], - name: `${abiFunction.name} (input #${inputIndex})`, - script: [ - `// "${abiFunction.name}" function parameters`, - formatParametersForDebugging(abiFunction.inputs, encodedFunctionArgs), - '', - ...functionIndexString, - ].join('\n'), - unlocks: getLockScriptName(contract), - }; -}; - -export const generateTemplateScenarios = ( - contract: Contract, - libauthTransaction: TransactionBch, - csTransaction: Transaction, - abiFunction: AbiFunction, - encodedFunctionArgs: EncodedFunctionArgument[], - inputIndex: number, -): WalletTemplate['scenarios'] => { - const artifact = contract.artifact; - const encodedConstructorArgs = contract.encodedConstructorArgs; - const scenarioIdentifier = `${artifact.contractName}_${abiFunction.name}_input${inputIndex}_evaluate`; - - const scenarios = { - // single scenario to spend out transaction under test given the CashScript parameters provided - [scenarioIdentifier]: { - name: `Evaluate ${artifact.contractName} ${abiFunction.name} (input #${inputIndex})`, - description: 'An example evaluation where this script execution passes.', - data: { - // encode values for the variables defined above in `entities` property - bytecode: { - ...generateTemplateScenarioParametersValues(abiFunction.inputs, encodedFunctionArgs), - ...generateTemplateScenarioParametersValues(artifact.constructorInputs, encodedConstructorArgs), - }, - currentBlockHeight: 2, - currentBlockTime: Math.round(+new Date() / 1000), - keys: { - privateKeys: generateTemplateScenarioKeys(abiFunction.inputs, encodedFunctionArgs), - }, - }, - transaction: generateTemplateScenarioTransaction(contract, libauthTransaction, csTransaction, inputIndex), - sourceOutputs: generateTemplateScenarioSourceOutputs(csTransaction, inputIndex), - }, - }; - - if (artifact.abi.length > 1) { - const functionIndex = artifact.abi.findIndex((func) => func.name === abiFunction.name); - scenarios![scenarioIdentifier].data!.bytecode!.function_index = functionIndex.toString(); - } - - return scenarios; -}; - -const generateTemplateScenarioTransaction = ( - contract: Contract, - libauthTransaction: TransactionBch, - csTransaction: Transaction, - slotIndex: number, -): WalletTemplateScenario['transaction'] => { - const inputs = libauthTransaction.inputs.map((input, inputIndex) => { - const csInput = csTransaction.inputs[inputIndex] as Utxo; - - return { - outpointIndex: input.outpointIndex, - outpointTransactionHash: binToHex(input.outpointTransactionHash), - sequenceNumber: input.sequenceNumber, - unlockingBytecode: generateTemplateScenarioBytecode(csInput, inputIndex, 'p2pkh_placeholder_unlock', slotIndex === inputIndex), - } as WalletTemplateScenarioInput; - }); - - const locktime = libauthTransaction.locktime; - - const outputs = libauthTransaction.outputs.map((output, index) => { - const csOutput = csTransaction.outputs[index]; - - return { - lockingBytecode: generateTemplateScenarioTransactionOutputLockingBytecode(csOutput, contract), - token: serialiseTokenDetails(output.token), - valueSatoshis: Number(output.valueSatoshis), - } as WalletTemplateScenarioTransactionOutput; - }); - - const version = libauthTransaction.version; - - return { inputs, locktime, outputs, version }; -}; - -const generateTemplateScenarioSourceOutputs = ( - csTransaction: Transaction, - slotIndex: number, -): Array> => { - return csTransaction.inputs.map((input, inputIndex) => { - return { - lockingBytecode: generateTemplateScenarioBytecode(input, inputIndex, 'p2pkh_placeholder_lock', inputIndex === slotIndex), - valueSatoshis: Number(input.satoshis), - token: serialiseTokenDetails(input.token), - }; - }); -}; - - -/** - * Creates a transaction object from a TransactionBuilder instance - * - * @param txn - The TransactionBuilder instance to convert - * @returns A transaction object containing inputs, outputs, locktime and version - */ -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -const createCSTransaction = (txn: TransactionBuilder) => { - const csTransaction = { - inputs: txn.inputs, - locktime: txn.locktime, - outputs: txn.outputs, - version: 2, - }; - - return csTransaction; -}; - -export const getLibauthTemplates = ( - txn: TransactionBuilder, -): WalletTemplate => { - if (txn.inputs.some((input) => !isStandardUnlockableUtxo(input))) { - throw new Error('Cannot use debugging functionality with a transaction that contains custom unlockers'); - } - - const libauthTransaction = txn.buildLibauthTransaction(); - const csTransaction = createCSTransaction(txn); - - const baseTemplate: WalletTemplate = { - $schema: 'https://ide.bitauth.com/authentication-template-v0.schema.json', - description: 'Imported from cashscript', - name: 'CashScript Generated Debugging Template', - supported: ['BCH_2025_05'], - version: 0, - entities: {}, - scripts: {}, - scenarios: {}, - }; - - // Initialize collections for entities, scripts, and scenarios - const entities: Record = {}; - const scripts: Record = {}; - const scenarios: Record = {}; - - // Initialize collections for P2PKH entities and scripts - const p2pkhEntities: Record = {}; - const p2pkhScripts: Record = {}; - - // Initialize bytecode mappings, these will be used to map the locking and unlocking scripts and naming the scripts - const unlockingBytecodeToLockingBytecodeParams: Record = {}; - const lockingBytecodeToLockingBytecodeParams: Record = {}; - - // We can typecast this because we check that all inputs are standard unlockable at the top of this function - for (const [inputIndex, input] of (txn.inputs as StandardUnlockableUtxo[]).entries()) { - // If template exists on the input, it indicates this is a P2PKH (Pay to Public Key Hash) input - if ('template' in input.unlocker) { - // @ts-ignore TODO: Remove UtxoP2PKH type and only use UnlockableUtxo in Libauth Template generation - input.template = input.unlocker?.template; // Added to support P2PKH inputs in buildTemplate - Object.assign(p2pkhEntities, generateTemplateEntitiesP2PKH(inputIndex)); - Object.assign(p2pkhScripts, generateTemplateScriptsP2PKH(input.unlocker.template, inputIndex)); - - continue; - } - - // If contract exists on the input, it indicates this is a contract input - if ('contract' in input.unlocker) { - const contract = input.unlocker?.contract; - const abiFunction = input.unlocker?.abiFunction; - - if (!abiFunction) { - throw new Error('No ABI function found in unlocker'); - } - - // Encode the function arguments for this contract input - const encodedArgs = encodeFunctionArguments( - abiFunction, - input.unlocker.params ?? [], - ); - - // Generate a scenario object for this contract input - Object.assign(scenarios, - generateTemplateScenarios( - contract, - libauthTransaction, - csTransaction as any, - abiFunction, - encodedArgs, - inputIndex, - ), - ); - - // Generate entities for this contract input - const entity = generateTemplateEntitiesP2SH( - contract, - abiFunction, - encodedArgs, - inputIndex, - ); - - // Generate scripts for this contract input - const script = generateTemplateScriptsP2SH( - contract, - abiFunction, - encodedArgs, - contract.encodedConstructorArgs, - inputIndex, - ); - - // Find the lock script name for this contract input - const lockScriptName = Object.keys(script).find(scriptName => scriptName.includes('_lock')); - if (lockScriptName) { - // Generate bytecodes for this contract input - const unlockingBytecode = binToHex(libauthTransaction.inputs[inputIndex].unlockingBytecode); - const lockingScriptParams = generateLockingScriptParams(input.unlocker.contract, input, lockScriptName); - - // Assign a name to the unlocking bytecode so later it can be used to replace the bytecode/slot in scenarios - unlockingBytecodeToLockingBytecodeParams[unlockingBytecode] = lockingScriptParams; - // Assign a name to the locking bytecode so later it can be used to replace with bytecode/slot in scenarios - lockingBytecodeToLockingBytecodeParams[binToHex(addressToLockScript(contract.address))] = lockingScriptParams; - } - - // Add entities and scripts to the base template and repeat the process for the next input - Object.assign(entities, entity); - Object.assign(scripts, script); - } - } - - Object.assign(entities, p2pkhEntities); - Object.assign(scripts, p2pkhScripts); - - const finalTemplate = { ...baseTemplate, entities, scripts, scenarios }; - - // Loop through all scenarios and map the locking and unlocking scripts to the scenarios - // Replace the script tag with the identifiers we created earlier - - // For Inputs - for (const scenario of Object.values(scenarios)) { - for (const [idx, input] of libauthTransaction.inputs.entries()) { - const unlockingBytecode = binToHex(input.unlockingBytecode); - - // If false then it stays lockingBytecode: {} - if (unlockingBytecodeToLockingBytecodeParams[unlockingBytecode]) { - // ['slot'] this identifies the source output in which the locking script under test will be placed - if (Array.isArray(scenario?.sourceOutputs?.[idx]?.lockingBytecode)) continue; - - // If true then assign a name to the locking bytecode script. - if (scenario.sourceOutputs && scenario.sourceOutputs[idx]) { - scenario.sourceOutputs[idx] = { - ...scenario.sourceOutputs[idx], - lockingBytecode: unlockingBytecodeToLockingBytecodeParams[unlockingBytecode], - }; - } - } - } - - // For Outputs - for (const [idx, output] of libauthTransaction.outputs.entries()) { - const lockingBytecode = binToHex(output.lockingBytecode); - - // If false then it stays lockingBytecode: {} - if (lockingBytecodeToLockingBytecodeParams[lockingBytecode]) { - - // ['slot'] this identifies the source output in which the locking script under test will be placed - if (Array.isArray(scenario?.transaction?.outputs?.[idx]?.lockingBytecode)) continue; - - // If true then assign a name to the locking bytecode script. - if (scenario?.transaction && scenario?.transaction?.outputs && scenario?.transaction?.outputs[idx]) { - scenario.transaction.outputs[idx] = { - ...scenario.transaction.outputs[idx], - lockingBytecode: lockingBytecodeToLockingBytecodeParams[lockingBytecode], - }; - } - } - } - - } - - return finalTemplate; -}; - -export const debugLibauthTemplate = (template: WalletTemplate, transaction: TransactionBuilder): DebugResults => { - const allArtifacts = transaction.inputs - .map(input => 'contract' in input.unlocker ? input.unlocker.contract : undefined) - .filter((contract): contract is Contract => Boolean(contract)) - .map(contract => contract.artifact); - - return debugTemplate(template, allArtifacts); -}; - -const generateLockingScriptParams = ( - contract: Contract, - { unlocker }: StandardUnlockableUtxo, - lockScriptName: string, -): WalletTemplateScenarioBytecode => { - if (isP2PKHUnlocker(unlocker)) { - return { - script: lockScriptName, - }; - } - - const constructorParamsEntries = contract.artifact.constructorInputs - .map(({ name }, index) => [ - name, - addHexPrefixExceptEmpty( - binToHex(unlocker.contract.encodedConstructorArgs[index]), - ), - ]); - - const constructorParams = Object.fromEntries(constructorParamsEntries); - - return { - script: lockScriptName, - overrides: { - bytecode: { ...constructorParams }, - }, - }; -}; - -export const generateUnlockingScriptParams = ( - csInput: StandardUnlockableUtxo, - p2pkhScriptNameTemplate: string, - inputIndex: number, -): WalletTemplateScenarioBytecode => { - if (isP2PKHUnlocker(csInput.unlocker)) { - return { - script: `${p2pkhScriptNameTemplate}_${inputIndex}`, - overrides: { - keys: { - privateKeys: { - [`placeholder_key_${inputIndex}`]: binToHex(csInput.unlocker.template.privateKey), - }, - }, - }, - }; - } - - const abiFunction = csInput.unlocker.abiFunction; - const contract = csInput.unlocker.contract; - const encodedFunctionArgs = encodeFunctionArguments(abiFunction, csInput.unlocker.params); - - return { - script: getUnlockScriptName(contract, abiFunction, inputIndex), - overrides: { - // encode values for the variables defined above in `entities` property - bytecode: { - ...generateTemplateScenarioParametersFunctionIndex(abiFunction, contract.artifact.abi), - ...generateTemplateScenarioParametersValues(abiFunction.inputs, encodedFunctionArgs), - ...generateTemplateScenarioParametersValues(contract.artifact.constructorInputs, contract.encodedConstructorArgs), - }, - keys: { - privateKeys: generateTemplateScenarioKeys(abiFunction.inputs, encodedFunctionArgs), - }, - }, - }; -}; - -const getLockScriptName = (contract: Contract): string => { - const result = decodeCashAddress(contract.address); - if (typeof result === 'string') throw new Error(result); - - return `${contract.artifact.contractName}_${binToHex(result.payload)}_lock`; -}; - -const getUnlockScriptName = (contract: Contract, abiFunction: AbiFunction, inputIndex: number): string => { - return `${contract.artifact.contractName}_${abiFunction.name}_input${inputIndex}_unlock`; -}; diff --git a/packages/cashscript/src/debugging.ts b/packages/cashscript/src/debugging.ts index 15a6daba..e12a0442 100644 --- a/packages/cashscript/src/debugging.ts +++ b/packages/cashscript/src/debugging.ts @@ -1,12 +1,37 @@ -import { AuthenticationErrorCommon, AuthenticationInstruction, AuthenticationProgramCommon, AuthenticationProgramStateCommon, AuthenticationVirtualMachine, ResolvedTransactionCommon, WalletTemplate, WalletTemplateScriptUnlocking, binToHex, createCompiler, createVirtualMachineBch2025, decodeAuthenticationInstructions, encodeAuthenticationInstruction, walletTemplateToCompilerConfiguration } from '@bitauth/libauth'; +import { AuthenticationErrorCommon, AuthenticationInstruction, AuthenticationProgramCommon, AuthenticationProgramStateCommon, AuthenticationVirtualMachine, ResolvedTransactionCommon, WalletTemplate, WalletTemplateScriptUnlocking, binToHex, createCompiler, createVirtualMachineBch2023, createVirtualMachineBch2025, createVirtualMachineBch2026, createVirtualMachineBchSpec, decodeAuthenticationInstructions, encodeAuthenticationInstruction, walletTemplateToCompilerConfiguration } from '@bitauth/libauth'; import { Artifact, LogEntry, Op, PrimitiveType, StackItem, asmToBytecode, bytecodeToAsm, decodeBool, decodeInt, decodeString } from '@cashscript/utils'; import { findLastIndex, toRegExp } from './utils.js'; import { FailedRequireError, FailedTransactionError, FailedTransactionEvaluationError } from './Errors.js'; -import { getBitauthUri } from './LibauthTemplate.js'; +import { getBitauthUri } from './libauth-template/LibauthTemplate.js'; +import { VmTarget } from './interfaces.js'; export type DebugResult = AuthenticationProgramStateCommon[]; export type DebugResults = Record; +/* eslint-disable @typescript-eslint/indent */ +type VM = AuthenticationVirtualMachine< + ResolvedTransactionCommon, + AuthenticationProgramCommon, + AuthenticationProgramStateCommon +>; +/* eslint-enable @typescript-eslint/indent */ + +const createVirtualMachine = (vmTarget: VmTarget): VM => { + switch (vmTarget) { + case 'BCH_2023_05': + return createVirtualMachineBch2023(); + case 'BCH_2025_05': + return createVirtualMachineBch2025(); + case 'BCH_2026_05': + return createVirtualMachineBch2026(); + case 'BCH_SPEC': + // TODO: This typecast is shitty, but it's hard to fix + return createVirtualMachineBchSpec() as unknown as VM; + default: + throw new Error(`Debugging is not supported for the ${vmTarget} virtual machine.`); + } +}; + // debugs the template, optionally logging the execution data export const debugTemplate = (template: WalletTemplate, artifacts: Artifact[]): DebugResults => { // If a contract has the same name, but a different bytecode, then it is considered a name collision @@ -25,15 +50,9 @@ export const debugTemplate = (template: WalletTemplate, artifacts: Artifact[]): for (const unlockingScriptId of unlockingScriptIds) { const scenarioIds = (template.scripts[unlockingScriptId] as WalletTemplateScriptUnlocking).passes ?? []; - // There are no scenarios defined for P2PKH placeholder scripts, so we skip them - if (scenarioIds.length === 0) continue; const matchingArtifact = artifacts.find((artifact) => unlockingScriptId.startsWith(artifact.contractName)); - if (!matchingArtifact) { - throw new Error(`No artifact found for unlocking script ${unlockingScriptId}`); - } - for (const scenarioId of scenarioIds) { results[`${unlockingScriptId}.${scenarioId}`] = debugSingleScenario(template, matchingArtifact, unlockingScriptId, scenarioId); } @@ -45,7 +64,7 @@ export const debugTemplate = (template: WalletTemplate, artifacts: Artifact[]): }; const debugSingleScenario = ( - template: WalletTemplate, artifact: Artifact, unlockingScriptId: string, scenarioId: string, + template: WalletTemplate, artifact: Artifact | undefined, unlockingScriptId: string, scenarioId: string, ): DebugResult => { const { vm, program } = createProgram(template, unlockingScriptId, scenarioId); @@ -60,12 +79,15 @@ const debugSingleScenario = ( const executedDebugSteps = lockingScriptDebugResult .filter((debugStep) => debugStep.controlStack.every(item => item === true)); - const executedLogs = (artifact.debug?.logs ?? []) - .filter((log) => executedDebugSteps.some((debugStep) => log.ip === debugStep.ip)); + // P2PKH inputs do not have an artifact, so we skip the console.log handling + if (artifact) { + const executedLogs = (artifact.debug?.logs ?? []) + .filter((log) => executedDebugSteps.some((debugStep) => log.ip === debugStep.ip)); - for (const log of executedLogs) { - const inputIndex = extractInputIndexFromScenario(scenarioId); - logConsoleLogStatement(log, executedDebugSteps, artifact, inputIndex); + for (const log of executedLogs) { + const inputIndex = extractInputIndexFromScenario(scenarioId); + logConsoleLogStatement(log, executedDebugSteps, artifact, inputIndex, vm); + } } const lastExecutedDebugStep = executedDebugSteps[executedDebugSteps.length - 1]; @@ -87,11 +109,18 @@ const debugSingleScenario = ( const isNullFail = lastExecutedDebugStep.error.includes(AuthenticationErrorCommon.nonNullSignatureFailure); const requireStatementIp = failingIp + (isNullFail && isSignatureCheckWithoutVerify(failingInstruction) ? 1 : 0); + const { program: { inputIndex }, error } = lastExecutedDebugStep; + + // If there is no artifact, this is a P2PKH debug error, error can occur when final CHECKSIG fails with NULLFAIL or when + // public key does not match pkh in EQUALVERIFY + // Note: due to P2PKHUnlocker implementation, the CHECKSIG cannot fail in practice, only the EQUALVERIFY can fail + if (!artifact) { + throw new FailedTransactionError(error, getBitauthUri(template)); + } + const requireStatement = (artifact.debug?.requires ?? []) .find((statement) => statement.ip === requireStatementIp); - const { program: { inputIndex }, error } = lastExecutedDebugStep; - if (requireStatement) { // Note that we use failingIp here rather than requireStatementIp, see comment above throw new FailedRequireError( @@ -121,11 +150,17 @@ const debugSingleScenario = ( // console.warn('message', finalExecutedVerifyIp); // console.warn(artifact.debug?.requires); + const { program: { inputIndex } } = lastExecutedDebugStep; + + // If there is no artifact, this is a P2PKH debug error, final verify can only occur when final CHECKSIG failed + // Note: due to P2PKHUnlocker implementation, this cannot happen in practice + if (!artifact) { + throw new FailedTransactionError(evaluationResult, getBitauthUri(template)); + } + const requireStatement = (artifact.debug?.requires ?? []) .find((message) => message.ip === finalExecutedVerifyIp); - const { program: { inputIndex } } = lastExecutedDebugStep; - if (requireStatement) { throw new FailedRequireError( artifact, sourcemapInstructionPointer, requireStatement, inputIndex, getBitauthUri(template), @@ -147,21 +182,13 @@ const extractInputIndexFromScenario = (scenarioId: string): number => { return parseInt(match[1]); }; -/* eslint-disable @typescript-eslint/indent */ -type VM = AuthenticationVirtualMachine< - ResolvedTransactionCommon, - AuthenticationProgramCommon, - AuthenticationProgramStateCommon ->; -/* eslint-enable @typescript-eslint/indent */ - type Program = AuthenticationProgramCommon; type CreateProgramResult = { vm: VM, program: Program }; // internal util. instantiates the virtual machine and compiles the template into a program const createProgram = (template: WalletTemplate, unlockingScriptId: string, scenarioId: string): CreateProgramResult => { const configuration = walletTemplateToCompilerConfiguration(template); - const vm = createVirtualMachineBch2025(); + const vm = createVirtualMachine(template.supported[0] as VmTarget); const compiler = createCompiler(configuration); if (!template.scripts[unlockingScriptId]) { @@ -194,13 +221,14 @@ const logConsoleLogStatement = ( debugSteps: AuthenticationProgramStateCommon[], artifact: Artifact, inputIndex: number, + vm: VM, ): void => { let line = `${artifact.contractName}.cash:${log.line}`; const decodedData = log.data.map((element) => { if (typeof element === 'string') return element; const debugStep = debugSteps.find((step) => step.ip === element.ip)!; - const transformedDebugStep = applyStackItemTransformations(element, debugStep); + const transformedDebugStep = applyStackItemTransformations(element, debugStep, vm); return decodeStackItem(element, transformedDebugStep.stack); }); console.log(`[Input #${inputIndex}] ${line} ${decodedData.join(' ')}`); @@ -209,6 +237,7 @@ const logConsoleLogStatement = ( const applyStackItemTransformations = ( element: StackItem, debugStep: AuthenticationProgramStateCommon, + vm: VM, ): AuthenticationProgramStateCommon => { if (!element.transformations) return debugStep; @@ -226,9 +255,10 @@ const applyStackItemTransformations = ( instructions: transformationsAuthenticationInstructions, signedMessages: [], program: { ...debugStep.program }, + functionTable: debugStep.functionTable ?? {}, + functionCount: debugStep.functionCount ?? 0, }; - const vm = createVirtualMachineBch2025(); const transformationsEndState = vm.stateEvaluate(transformationsStartState); return transformationsEndState; diff --git a/packages/cashscript/src/index.ts b/packages/cashscript/src/index.ts index a1e87529..4a5633f2 100644 --- a/packages/cashscript/src/index.ts +++ b/packages/cashscript/src/index.ts @@ -1,6 +1,5 @@ export { default as SignatureTemplate } from './SignatureTemplate.js'; -export { Contract, type ContractFunction } from './Contract.js'; -export { Transaction } from './Transaction.js'; +export { Contract } from './Contract.js'; export { TransactionBuilder } from './TransactionBuilder.js'; export { type ConstructorArgument, diff --git a/packages/cashscript/src/interfaces.ts b/packages/cashscript/src/interfaces.ts index d3876fd3..badfc116 100644 --- a/packages/cashscript/src/interfaces.ts +++ b/packages/cashscript/src/interfaces.ts @@ -1,4 +1,4 @@ -import { type Transaction } from '@bitauth/libauth'; +import { AuthenticationProgramStateResourceLimits, type Transaction } from '@bitauth/libauth'; import type { NetworkProvider } from './network/index.js'; import type SignatureTemplate from './SignatureTemplate.js'; import { Contract } from './Contract.js'; @@ -75,14 +75,6 @@ export function isPlaceholderUnlocker(unlocker: Unlocker): unlocker is Placehold return 'placeholder' in unlocker; } -export interface UtxoP2PKH extends Utxo { - template: SignatureTemplate; -} - -export function isUtxoP2PKH(utxo: Utxo): utxo is UtxoP2PKH { - return 'template' in utxo; -} - export interface Recipient { to: string; amount: bigint; @@ -152,14 +144,25 @@ export const Network = { export type Network = (typeof Network)[keyof typeof Network]; +export const VmTarget = { + BCH_2023_05: literal('BCH_2023_05'), + BCH_2025_05: literal('BCH_2025_05'), + BCH_2026_05: literal('BCH_2026_05'), + BCH_SPEC: literal('BCH_SPEC'), +}; + +export type VmTarget = (typeof VmTarget)[keyof typeof VmTarget]; + export interface TransactionDetails extends Transaction { txid: string; hex: string; } export interface ContractOptions { - provider?: NetworkProvider, + provider: NetworkProvider, addressType?: AddressType, } export type AddressType = 'p2sh20' | 'p2sh32'; + +export type VmResourceUsage = AuthenticationProgramStateResourceLimits['metrics']; diff --git a/packages/cashscript/src/libauth-template/LibauthTemplate.ts b/packages/cashscript/src/libauth-template/LibauthTemplate.ts new file mode 100644 index 00000000..27d15a50 --- /dev/null +++ b/packages/cashscript/src/libauth-template/LibauthTemplate.ts @@ -0,0 +1,657 @@ +import { + binToBase64, + binToHex, + Input, + TransactionBch, + utf8ToBin, + WalletTemplate, + WalletTemplateScenario, + WalletTemplateScenarioBytecode, + WalletTemplateScenarioOutput, + WalletTemplateScriptLocking, + WalletTemplateScriptUnlocking, + WalletTemplateVariable, +} from '@bitauth/libauth'; +import { + AbiFunction, + AbiInput, + Artifact, +} from '@cashscript/utils'; +import { EncodedConstructorArgument, EncodedFunctionArgument, encodeFunctionArguments } from '../Argument.js'; +import { Contract } from '../Contract.js'; +import { DebugResults, debugTemplate } from '../debugging.js'; +import { + isContractUnlocker, + isP2PKHUnlocker, + isStandardUnlockableUtxo, + isUnlockableUtxo, + Output, + StandardUnlockableUtxo, + Utxo, + VmTarget, +} from '../interfaces.js'; +import SignatureTemplate from '../SignatureTemplate.js'; +import { addressToLockScript, extendedStringify, zip } from '../utils.js'; +import { TransactionBuilder } from '../TransactionBuilder.js'; +import { deflate } from 'pako'; +import MockNetworkProvider from '../network/MockNetworkProvider.js'; +import { addHexPrefixExceptEmpty, formatBytecodeForDebugging, formatParametersForDebugging, getLockScriptName, getSignatureAndPubkeyFromP2PKHInput, getUnlockScriptName, lockingBytecodeIsSetToSlot, serialiseTokenDetails } from './utils.js'; + +// TODO: Add / improve descriptions throughout the template generation + +export const getLibauthTemplate = ( + transactionBuilder: TransactionBuilder, +): WalletTemplate => { + if (transactionBuilder.inputs.some((input) => !isStandardUnlockableUtxo(input))) { + throw new Error('Cannot use debugging functionality with a transaction that contains custom unlockers'); + } + + const libauthTransaction = transactionBuilder.buildLibauthTransaction(); + + const vmTarget = transactionBuilder.provider instanceof MockNetworkProvider + ? transactionBuilder.provider.vmTarget + : VmTarget.BCH_2025_05; + + const template: WalletTemplate = { + $schema: 'https://ide.bitauth.com/authentication-template-v0.schema.json', + description: 'Imported from cashscript', + name: 'CashScript Generated Debugging Template', + supported: [vmTarget], + version: 0, + entities: generateAllTemplateEntities(transactionBuilder), + scripts: generateAllTemplateScripts(transactionBuilder), + scenarios: generateAllTemplateScenarios(libauthTransaction, transactionBuilder), + }; + + // TODO: Refactor the below code to not have deep reassignment of scenario.sourceOutputs and scenario.transaction.outputs + + // Initialize bytecode mappings, these will be used to map the locking and unlocking scripts and naming the scripts + const unlockingBytecodeToLockingBytecodeParams: Record = {}; + const lockingBytecodeToLockingBytecodeParams: Record = {}; + + // We can typecast this because we check that all inputs are standard unlockable at the top of this function + for (const [inputIndex, input] of (transactionBuilder.inputs as StandardUnlockableUtxo[]).entries()) { + if (isContractUnlocker(input.unlocker)) { + const lockScriptName = getLockScriptName(input.unlocker.contract); + if (!lockScriptName) continue; + + const lockingScriptParams = generateLockingScriptParams(input.unlocker.contract, input, lockScriptName); + + const unlockingBytecode = binToHex(libauthTransaction.inputs[inputIndex].unlockingBytecode); + unlockingBytecodeToLockingBytecodeParams[unlockingBytecode] = lockingScriptParams; + + const lockingBytecode = binToHex(addressToLockScript(input.unlocker.contract.address)); + lockingBytecodeToLockingBytecodeParams[lockingBytecode] = lockingScriptParams; + } + } + + for (const scenario of Object.values(template.scenarios!)) { + // For Inputs + for (const [idx, input] of libauthTransaction.inputs.entries()) { + const unlockingBytecode = binToHex(input.unlockingBytecode); + const lockingBytecodeParams = unlockingBytecodeToLockingBytecodeParams[unlockingBytecode]; + + // If lockingBytecodeParams is unknown, then it stays at default: {} + if (!lockingBytecodeParams) continue; + + // If locking bytecode is set to ['slot'] then this is being evaluated by the scenario, so we don't replace bytecode + if (lockingBytecodeIsSetToSlot(scenario?.sourceOutputs?.[idx]?.lockingBytecode)) continue; + + // If lockingBytecodeParams is known, and this input is not ['slot'] then assign a locking bytecode as source output + if (scenario.sourceOutputs?.[idx]) { + scenario.sourceOutputs[idx] = { + ...scenario.sourceOutputs[idx], + lockingBytecode: lockingBytecodeParams, + }; + } + } + + // For Outputs + for (const [idx, output] of libauthTransaction.outputs.entries()) { + const lockingBytecode = binToHex(output.lockingBytecode); + const lockingBytecodeParams = lockingBytecodeToLockingBytecodeParams[lockingBytecode]; + + // If lockingBytecodeParams is unknown, then it stays at default: {} + if (!lockingBytecodeParams) continue; + + // If locking bytecode is set to ['slot'] then this is being evaluated by the scenario, so we don't replace bytecode + if (lockingBytecodeIsSetToSlot(scenario?.transaction?.outputs?.[idx]?.lockingBytecode)) continue; + + // If lockingBytecodeParams is known, and this input is not ['slot'] then assign a locking bytecode as source output + if (scenario?.transaction?.outputs?.[idx]) { + scenario.transaction.outputs[idx] = { + ...scenario.transaction.outputs[idx], + lockingBytecode: lockingBytecodeParams, + }; + } + } + } + + return template; +}; + +export const debugLibauthTemplate = (template: WalletTemplate, transaction: TransactionBuilder): DebugResults => { + const allArtifacts = transaction.inputs + .map(input => isContractUnlocker(input.unlocker) ? input.unlocker.contract : undefined) + .filter((contract): contract is Contract => Boolean(contract)) + .map(contract => contract.artifact); + + return debugTemplate(template, allArtifacts); +}; + +export const getBitauthUri = (template: WalletTemplate): string => { + const base64toBase64Url = (base64: string): string => base64.replace(/\+/g, '-').replace(/\//g, '_'); + const payload = base64toBase64Url(binToBase64(deflate(utf8ToBin(extendedStringify(template))))); + return `https://ide.bitauth.com/import-template/${payload}`; +}; + +const generateAllTemplateEntities = ( + transactionBuilder: TransactionBuilder, +): WalletTemplate['entities'] => { + const entities = transactionBuilder.inputs.map((input, inputIndex) => { + if (isP2PKHUnlocker(input.unlocker)) { + return generateTemplateEntitiesP2PKH(inputIndex); + } + + if (isContractUnlocker(input.unlocker)) { + const encodedArgs = encodeFunctionArguments(input.unlocker.abiFunction, input.unlocker.params ?? []); + return generateTemplateEntitiesP2SH(input.unlocker.contract, input.unlocker.abiFunction, encodedArgs, inputIndex); + } + + throw new Error('Unknown unlocker type'); + }); + + return entities.reduce((acc, entity) => ({ ...acc, ...entity }), {}); +}; + +const generateAllTemplateScripts = ( + transactionBuilder: TransactionBuilder, +): WalletTemplate['scripts'] => { + const scripts = transactionBuilder.inputs.map((input, inputIndex) => { + if (isP2PKHUnlocker(input.unlocker)) { + return generateTemplateScriptsP2PKH(inputIndex); + } + + if (isContractUnlocker(input.unlocker)) { + const encodedArgs = encodeFunctionArguments(input.unlocker.abiFunction, input.unlocker.params ?? []); + return generateTemplateScriptsP2SH( + input.unlocker.contract, + input.unlocker.abiFunction, + encodedArgs, + input.unlocker.contract.encodedConstructorArgs, + inputIndex, + ); + } + + throw new Error('Unknown unlocker type'); + }); + + return scripts.reduce((acc, script) => ({ ...acc, ...script }), {}); +}; + +const generateAllTemplateScenarios = ( + libauthTransaction: TransactionBch, + transactionBuilder: TransactionBuilder, +): WalletTemplate['scenarios'] => { + const scenarios = transactionBuilder.inputs.map((input, inputIndex) => { + if (isP2PKHUnlocker(input.unlocker)) { + return generateTemplateScenariosP2PKH(libauthTransaction, transactionBuilder, inputIndex); + } + + if (isContractUnlocker(input.unlocker)) { + const encodedArgs = encodeFunctionArguments(input.unlocker.abiFunction, input.unlocker.params ?? []); + return generateTemplateScenarios( + input.unlocker.contract, + libauthTransaction, + transactionBuilder, + input.unlocker.abiFunction, + encodedArgs, + inputIndex, + ); + } + + throw new Error('Unknown unlocker type'); + }); + + return scenarios.reduce((acc, scenario) => ({ ...acc, ...scenario }), {}); +}; + +const generateTemplateEntitiesP2PKH = ( + inputIndex: number, +): WalletTemplate['entities'] => { + const lockScriptName = `p2pkh_placeholder_lock_${inputIndex}`; + const unlockScriptName = `p2pkh_placeholder_unlock_${inputIndex}`; + + return { + [`signer_${inputIndex}`]: { + scripts: [lockScriptName, unlockScriptName], + description: `P2PKH data for input ${inputIndex}`, + name: `P2PKH Signer (input #${inputIndex})`, + variables: { + [`signature_${inputIndex}`]: { + description: '', + name: `P2PKH Signature (input #${inputIndex})`, + type: 'WalletData', + }, + [`public_key_${inputIndex}`]: { + description: '', + name: `P2PKH public key (input #${inputIndex})`, + type: 'WalletData', + }, + }, + }, + }; +}; + +const generateTemplateEntitiesP2SH = ( + contract: Contract, + abiFunction: AbiFunction, + encodedFunctionArgs: EncodedFunctionArgument[], + inputIndex: number, +): WalletTemplate['entities'] => { + const entities = { + [contract.artifact.contractName + '_input' + inputIndex + '_parameters']: { + description: 'Contract creation and function parameters', + name: `${contract.artifact.contractName} (input #${inputIndex})`, + scripts: [ + getLockScriptName(contract), + getUnlockScriptName(contract, abiFunction, inputIndex), + ], + variables: { + ...createWalletTemplateVariables(contract.artifact, abiFunction, encodedFunctionArgs), + ...generateFunctionIndexTemplateVariable(contract.artifact.abi), + }, + }, + }; + + return entities; +}; + +const createWalletTemplateVariables = ( + artifact: Artifact, + abiFunction: AbiFunction, + encodedFunctionArgs: EncodedFunctionArgument[], +): Record => { + const functionParameters = Object.fromEntries( + abiFunction.inputs.map((input, index) => ([ + input.name, + { + description: `"${input.name}" parameter of function "${abiFunction.name}"`, + name: input.name, + type: encodedFunctionArgs[index] instanceof SignatureTemplate ? 'Key' : 'WalletData', + }, + ])), + ); + + const constructorParameters = Object.fromEntries( + artifact.constructorInputs.map((input) => ([ + input.name, + { + description: `"${input.name}" parameter of this contract`, + name: input.name, + type: 'WalletData', + }, + ])), + ); + + return { ...functionParameters, ...constructorParameters }; +}; + +const generateTemplateScriptsP2PKH = ( + inputIndex: number, +): WalletTemplate['scripts'] => { + const lockScriptName = `p2pkh_placeholder_lock_${inputIndex}`; + const unlockScriptName = `p2pkh_placeholder_unlock_${inputIndex}`; + + const scripts = { + [unlockScriptName]: { + passes: [`P2PKH_spend_input${inputIndex}_evaluate`], + name: `P2PKH Unlock (input #${inputIndex})`, + script: + `\n`, + unlocks: lockScriptName, + }, + [lockScriptName]: { + lockingType: 'standard', + name: `P2PKH Lock (input #${inputIndex})`, + script: + `OP_DUP\nOP_HASH160 <$( OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG`, + }, + }; + + return scripts; +}; + +const generateTemplateScriptsP2SH = ( + contract: Contract, + abiFunction: AbiFunction, + encodedFunctionArgs: EncodedFunctionArgument[], + encodedConstructorArgs: EncodedConstructorArgument[], + inputIndex: number, +): WalletTemplate['scripts'] => { + const unlockingScriptName = getUnlockScriptName(contract, abiFunction, inputIndex); + const lockingScriptName = getLockScriptName(contract); + + return { + [unlockingScriptName]: generateTemplateUnlockScript(contract, abiFunction, encodedFunctionArgs, inputIndex), + [lockingScriptName]: generateTemplateLockScript(contract, encodedConstructorArgs), + }; +}; + +const generateTemplateLockScript = ( + contract: Contract, + constructorArguments: EncodedFunctionArgument[], +): WalletTemplateScriptLocking => { + return { + lockingType: contract.addressType, + name: contract.artifact.contractName, + script: [ + `// "${contract.artifact.contractName}" contract constructor parameters`, + formatParametersForDebugging(contract.artifact.constructorInputs, constructorArguments), + '', + '// bytecode', + formatBytecodeForDebugging(contract.artifact), + ].join('\n'), + }; +}; + +const generateTemplateUnlockScript = ( + contract: Contract, + abiFunction: AbiFunction, + encodedFunctionArgs: EncodedFunctionArgument[], + inputIndex: number, +): WalletTemplateScriptUnlocking => { + const scenarioIdentifier = `${contract.artifact.contractName}_${abiFunction.name}_input${inputIndex}_evaluate`; + const functionIndex = contract.artifact.abi.findIndex((func) => func.name === abiFunction.name); + + const functionIndexString = contract.artifact.abi.length > 1 + ? ['// function index in contract', ` // int = <${functionIndex}>`, ''] + : []; + + return { + // this unlocking script must pass our only scenario + passes: [scenarioIdentifier], + name: `${abiFunction.name} (input #${inputIndex})`, + script: [ + `// "${abiFunction.name}" function parameters`, + formatParametersForDebugging(abiFunction.inputs, encodedFunctionArgs), + '', + ...functionIndexString, + ].join('\n'), + unlocks: getLockScriptName(contract), + }; +}; + +const generateTemplateScenarios = ( + contract: Contract, + libauthTransaction: TransactionBch, + transactionBuilder: TransactionBuilder, + abiFunction: AbiFunction, + encodedFunctionArgs: EncodedFunctionArgument[], + inputIndex: number, +): WalletTemplate['scenarios'] => { + const artifact = contract.artifact; + const encodedConstructorArgs = contract.encodedConstructorArgs; + const scenarioIdentifier = `${artifact.contractName}_${abiFunction.name}_input${inputIndex}_evaluate`; + + const scenarios = { + // single scenario to spend out transaction under test given the CashScript parameters provided + [scenarioIdentifier]: { + name: `Evaluate ${artifact.contractName} ${abiFunction.name} (input #${inputIndex})`, + description: 'An example evaluation where this script execution passes.', + data: { + // encode values for the variables defined above in `entities` property + bytecode: { + ...generateTemplateScenarioParametersFunctionIndex(abiFunction, contract.artifact.abi), + ...generateTemplateScenarioParametersValues(abiFunction.inputs, encodedFunctionArgs), + ...generateTemplateScenarioParametersValues(artifact.constructorInputs, encodedConstructorArgs), + }, + keys: { + privateKeys: generateTemplateScenarioKeys(abiFunction.inputs, encodedFunctionArgs), + }, + }, + transaction: generateTemplateScenarioTransaction(contract, libauthTransaction, transactionBuilder, inputIndex), + sourceOutputs: generateTemplateScenarioSourceOutputs(transactionBuilder, libauthTransaction, inputIndex), + }, + }; + + return scenarios; +}; + +const generateTemplateScenariosP2PKH = ( + libauthTransaction: TransactionBch, + transactionBuilder: TransactionBuilder, + inputIndex: number, +): WalletTemplate['scenarios'] => { + const scenarioIdentifier = `P2PKH_spend_input${inputIndex}_evaluate`; + const { signature, publicKey } = getSignatureAndPubkeyFromP2PKHInput(libauthTransaction.inputs[inputIndex]); + + const scenarios = { + // single scenario to spend out transaction under test given the CashScript parameters provided + [scenarioIdentifier]: { + name: `Evaluate P2PKH spend (input #${inputIndex})`, + description: 'An example evaluation where this script execution passes.', + data: { + // encode values for the variables defined above in `entities` property + bytecode: { + [`signature_${inputIndex}`]: `0x${binToHex(signature)}`, + [`public_key_${inputIndex}`]: `0x${binToHex(publicKey)}`, + }, + }, + transaction: generateTemplateScenarioTransaction(undefined, libauthTransaction, transactionBuilder, inputIndex), + sourceOutputs: generateTemplateScenarioSourceOutputs(transactionBuilder, libauthTransaction, inputIndex), + }, + }; + + return scenarios; +}; + +const generateTemplateScenarioTransaction = ( + contract: Contract | undefined, + libauthTransaction: TransactionBch, + transactionBuilder: TransactionBuilder, + slotIndex: number, +): WalletTemplateScenario['transaction'] => { + const zippedInputs = zip(transactionBuilder.inputs, libauthTransaction.inputs); + const inputs = zippedInputs.map(([csInput, libauthInput], inputIndex) => { + return { + outpointIndex: libauthInput.outpointIndex, + outpointTransactionHash: binToHex(libauthInput.outpointTransactionHash), + sequenceNumber: libauthInput.sequenceNumber, + unlockingBytecode: generateTemplateScenarioBytecode(csInput, libauthInput, inputIndex, 'p2pkh_placeholder_unlock', slotIndex === inputIndex), + }; + }); + + const locktime = libauthTransaction.locktime; + + const zippedOutputs = zip(transactionBuilder.outputs, libauthTransaction.outputs); + const outputs = zippedOutputs.map(([csOutput, libauthOutput]) => { + if (csOutput && contract) { + return { + lockingBytecode: generateTemplateScenarioTransactionOutputLockingBytecode(csOutput, contract), + token: serialiseTokenDetails(libauthOutput.token), + valueSatoshis: Number(libauthOutput.valueSatoshis), + }; + } + + return { + lockingBytecode: `${binToHex(libauthOutput.lockingBytecode)}`, + token: serialiseTokenDetails(libauthOutput.token), + valueSatoshis: Number(libauthOutput.valueSatoshis), + }; + }); + + const version = libauthTransaction.version; + + return { inputs, locktime, outputs, version }; +}; + +const generateTemplateScenarioSourceOutputs = ( + transactionBuilder: TransactionBuilder, + libauthTransaction: TransactionBch, + slotIndex: number, +): Array> => { + const zippedInputs = zip(transactionBuilder.inputs, libauthTransaction.inputs); + return zippedInputs.map(([csInput, libauthInput], inputIndex) => { + return { + lockingBytecode: generateTemplateScenarioBytecode(csInput, libauthInput, inputIndex, 'p2pkh_placeholder_lock', inputIndex === slotIndex), + valueSatoshis: Number(csInput.satoshis), + token: serialiseTokenDetails(csInput.token), + }; + }); +}; + +const generateLockingScriptParams = ( + contract: Contract, + { unlocker }: StandardUnlockableUtxo, + lockScriptName: string, +): WalletTemplateScenarioBytecode => { + if (isP2PKHUnlocker(unlocker)) { + return { + script: lockScriptName, + }; + } + + const constructorParamsEntries = contract.artifact.constructorInputs + .map(({ name }, index) => [ + name, + addHexPrefixExceptEmpty( + binToHex(unlocker.contract.encodedConstructorArgs[index]), + ), + ]); + + const constructorParams = Object.fromEntries(constructorParamsEntries); + + return { + script: lockScriptName, + overrides: { + bytecode: { ...constructorParams }, + }, + }; +}; + +const generateUnlockingScriptParams = ( + csInput: StandardUnlockableUtxo, + libauthInput: Input, + p2pkhScriptNameTemplate: string, + inputIndex: number, +): WalletTemplateScenarioBytecode => { + if (isP2PKHUnlocker(csInput.unlocker)) { + const { signature, publicKey } = getSignatureAndPubkeyFromP2PKHInput(libauthInput); + + return { + script: `${p2pkhScriptNameTemplate}_${inputIndex}`, + overrides: { + bytecode: { + [`signature_${inputIndex}`]: `0x${binToHex(signature)}`, + [`public_key_${inputIndex}`]: `0x${binToHex(publicKey)}`, + }, + }, + }; + } + + const abiFunction = csInput.unlocker.abiFunction; + const contract = csInput.unlocker.contract; + const encodedFunctionArgs = encodeFunctionArguments(abiFunction, csInput.unlocker.params); + + return { + script: getUnlockScriptName(contract, abiFunction, inputIndex), + overrides: { + // encode values for the variables defined above in `entities` property + bytecode: { + ...generateTemplateScenarioParametersFunctionIndex(abiFunction, contract.artifact.abi), + ...generateTemplateScenarioParametersValues(abiFunction.inputs, encodedFunctionArgs), + ...generateTemplateScenarioParametersValues(contract.artifact.constructorInputs, contract.encodedConstructorArgs), + }, + keys: { + privateKeys: generateTemplateScenarioKeys(abiFunction.inputs, encodedFunctionArgs), + }, + }, + }; +}; + +const generateTemplateScenarioParametersValues = ( + types: readonly AbiInput[], + encodedArgs: EncodedFunctionArgument[], +): Record => { + const typesAndArguments = zip(types, encodedArgs); + + const entries = typesAndArguments + // SignatureTemplates are handled by the 'keys' object in the scenario + .filter(([, arg]) => !(arg instanceof SignatureTemplate)) + .map(([input, arg]) => { + const encodedArgumentHex = binToHex(arg as Uint8Array); + const prefixedEncodedArgument = addHexPrefixExceptEmpty(encodedArgumentHex); + return [input.name, prefixedEncodedArgument] as const; + }); + + return Object.fromEntries(entries); +}; + +const generateTemplateScenarioKeys = ( + types: readonly AbiInput[], + encodedArgs: EncodedFunctionArgument[], +): Record => { + const typesAndArguments = zip(types, encodedArgs); + + const entries = typesAndArguments + .filter(([, arg]) => arg instanceof SignatureTemplate) + .map(([input, arg]) => ([input.name, binToHex((arg as SignatureTemplate).privateKey)] as const)); + + return Object.fromEntries(entries); +}; + +// Used for generating the locking / unlocking bytecode for source outputs and inputs +const generateTemplateScenarioBytecode = ( + input: Utxo, + libauthInput: Input, + inputIndex: number, + p2pkhScriptNameTemplate: string, + insertSlot?: boolean, +): WalletTemplateScenarioBytecode | ['slot'] => { + if (insertSlot) return ['slot']; + + if (isUnlockableUtxo(input) && isStandardUnlockableUtxo(input)) { + return generateUnlockingScriptParams(input, libauthInput, p2pkhScriptNameTemplate, inputIndex); + } + + // 'slot' means that we are currently evaluating this specific input, + // {} means that it is the same script type, but not being evaluated + return {}; +}; + +const generateTemplateScenarioTransactionOutputLockingBytecode = ( + csOutput: Output, + contract: Contract, +): string | {} => { + if (csOutput.to instanceof Uint8Array) return binToHex(csOutput.to); + if ([contract.address, contract.tokenAddress].includes(csOutput.to)) return {}; + return binToHex(addressToLockScript(csOutput.to)); +}; + +const generateTemplateScenarioParametersFunctionIndex = ( + abiFunction: AbiFunction, + abi: readonly AbiFunction[], +): Record => { + const functionIndex = abi.length > 1 + ? abi.findIndex((func) => func.name === abiFunction.name) + : undefined; + + return functionIndex !== undefined ? { function_index: functionIndex.toString() } : {}; +}; + +const generateFunctionIndexTemplateVariable = ( + abi: readonly AbiFunction[], +): Record => { + if (abi.length > 1) { + return { + function_index: { + description: 'Script function index to execute', + name: 'function_index', + type: 'WalletData', + }, + }; + } + + return {}; +}; diff --git a/packages/cashscript/src/libauth-template/utils.ts b/packages/cashscript/src/libauth-template/utils.ts new file mode 100644 index 00000000..f4e3b02c --- /dev/null +++ b/packages/cashscript/src/libauth-template/utils.ts @@ -0,0 +1,126 @@ +import { AbiFunction, AbiInput, Artifact, bytecodeToScript, formatBitAuthScript } from '@cashscript/utils'; +import { HashType, LibauthTokenDetails, SignatureAlgorithm, TokenDetails } from '../interfaces.js'; +import { hexToBin, binToHex, isHex, decodeCashAddress, type WalletTemplateScenarioBytecode, Input, assertSuccess, decodeAuthenticationInstructions, AuthenticationInstructionPush } from '@bitauth/libauth'; +import { EncodedFunctionArgument } from '../Argument.js'; +import { zip } from '../utils.js'; +import SignatureTemplate from '../SignatureTemplate.js'; +import { Contract } from '../Contract.js'; + +export const getLockScriptName = (contract: Contract): string => { + const result = decodeCashAddress(contract.address); + if (typeof result === 'string') throw new Error(result); + + return `${contract.artifact.contractName}_${binToHex(result.payload)}_lock`; +}; + +export const getUnlockScriptName = (contract: Contract, abiFunction: AbiFunction, inputIndex: number): string => { + return `${contract.artifact.contractName}_${abiFunction.name}_input${inputIndex}_unlock`; +}; + +export const getSignatureAlgorithmName = (signatureAlgorithm: SignatureAlgorithm): string => { + const signatureAlgorithmNames = { + [SignatureAlgorithm.SCHNORR]: 'schnorr_signature', + [SignatureAlgorithm.ECDSA]: 'ecdsa_signature', + }; + + return signatureAlgorithmNames[signatureAlgorithm]; +}; + +export const getHashTypeName = (hashType: HashType): string => { + const hashtypeNames = { + [HashType.SIGHASH_ALL]: 'all_outputs', + [HashType.SIGHASH_ALL | HashType.SIGHASH_ANYONECANPAY]: 'all_outputs_single_input', + [HashType.SIGHASH_ALL | HashType.SIGHASH_UTXOS]: 'all_outputs_all_utxos', + [HashType.SIGHASH_ALL | HashType.SIGHASH_ANYONECANPAY | HashType.SIGHASH_UTXOS]: 'all_outputs_single_input_INVALID_all_utxos', + [HashType.SIGHASH_SINGLE]: 'corresponding_output', + [HashType.SIGHASH_SINGLE | HashType.SIGHASH_ANYONECANPAY]: 'corresponding_output_single_input', + [HashType.SIGHASH_SINGLE | HashType.SIGHASH_UTXOS]: 'corresponding_output_all_utxos', + [HashType.SIGHASH_SINGLE | HashType.SIGHASH_ANYONECANPAY | HashType.SIGHASH_UTXOS]: 'corresponding_output_single_input_INVALID_all_utxos', + [HashType.SIGHASH_NONE]: 'no_outputs', + [HashType.SIGHASH_NONE | HashType.SIGHASH_ANYONECANPAY]: 'no_outputs_single_input', + [HashType.SIGHASH_NONE | HashType.SIGHASH_UTXOS]: 'no_outputs_all_utxos', + [HashType.SIGHASH_NONE | HashType.SIGHASH_ANYONECANPAY | HashType.SIGHASH_UTXOS]: 'no_outputs_single_input_INVALID_all_utxos', + }; + + return hashtypeNames[hashType]; +}; + +export const addHexPrefixExceptEmpty = (value: string): string => { + return value.length > 0 ? `0x${value}` : ''; +}; + +export const formatParametersForDebugging = (types: readonly AbiInput[], args: EncodedFunctionArgument[]): string => { + if (types.length === 0) return '// none'; + + // We reverse the arguments because the order of the arguments in the bytecode is reversed + const typesAndArguments = zip(types, args).reverse(); + + return typesAndArguments.map(([input, arg]) => { + if (arg instanceof SignatureTemplate) { + const signatureAlgorithmName = getSignatureAlgorithmName(arg.getSignatureAlgorithm()); + const hashtypeName = getHashTypeName(arg.getHashType(false)); + return `<${input.name}.${signatureAlgorithmName}.${hashtypeName}> // ${input.type}`; + } + + const typeStr = input.type === 'bytes' ? `bytes${arg.length}` : input.type; + + // we output these values as pushdata, comment will contain the type and the value of the variable + // e.g. // int = <0xa08601> + return `<${input.name}> // ${typeStr} = <${`0x${binToHex(arg)}`}>`; + }).join('\n'); +}; + +export const formatBytecodeForDebugging = (artifact: Artifact): string => { + if (!artifact.debug) { + return artifact.bytecode + .split(' ') + .map((asmElement) => (isHex(asmElement) ? `<0x${asmElement}>` : asmElement)) + .join('\n'); + } + + return formatBitAuthScript( + bytecodeToScript(hexToBin(artifact.debug.bytecode)), + artifact.debug.sourceMap, + artifact.source, + ); +}; + +export const serialiseTokenDetails = ( + token?: TokenDetails | LibauthTokenDetails, +): LibauthTemplateTokenDetails | undefined => { + if (!token) return undefined; + + return { + amount: token.amount.toString(), + category: token.category instanceof Uint8Array ? binToHex(token.category) : token.category, + nft: token.nft ? { + capability: token.nft.capability, + commitment: token.nft.commitment instanceof Uint8Array ? binToHex(token.nft.commitment) : token.nft.commitment, + } : undefined, + }; +}; + +interface LibauthTemplateTokenDetails { + amount: string; + category: string; + nft?: { + capability: 'none' | 'mutable' | 'minting'; + commitment: string; + }; +} + +export const lockingBytecodeIsSetToSlot = (lockingBytecode?: WalletTemplateScenarioBytecode | ['slot']): boolean => { + return Array.isArray(lockingBytecode) && lockingBytecode.length === 1 && lockingBytecode[0] === 'slot'; +}; + +export const getSignatureAndPubkeyFromP2PKHInput = ( + libauthInput: Input, +): { signature: Uint8Array; publicKey: Uint8Array } => { + const inputData = (assertSuccess( + decodeAuthenticationInstructions(libauthInput.unlockingBytecode)) + ) as AuthenticationInstructionPush[]; + const signature = inputData[0].data; + const publicKey = inputData[1].data; + + return { signature, publicKey }; +}; diff --git a/packages/cashscript/src/network/MockNetworkProvider.ts b/packages/cashscript/src/network/MockNetworkProvider.ts index 667b2ca7..2af9b763 100644 --- a/packages/cashscript/src/network/MockNetworkProvider.ts +++ b/packages/cashscript/src/network/MockNetworkProvider.ts @@ -1,20 +1,14 @@ import { binToHex, decodeTransactionUnsafe, hexToBin, isHex } from '@bitauth/libauth'; import { sha256 } from '@cashscript/utils'; -import { Utxo, Network } from '../interfaces.js'; +import { Utxo, Network, VmTarget } from '../interfaces.js'; import NetworkProvider from './NetworkProvider.js'; -import { addressToLockScript, libauthTokenDetailsToCashScriptTokenDetails, randomUtxo } from '../utils.js'; +import { addressToLockScript, libauthTokenDetailsToCashScriptTokenDetails } from '../utils.js'; -// redeclare the addresses from vars.ts instead of importing them -const aliceAddress = 'bchtest:qpgjmwev3spwlwkgmyjrr2s2cvlkkzlewq62mzgjnp'; -const bobAddress = 'bchtest:qz6q5gqnxdldkr07xpls5474mmzmlesd6qnux4skuc'; -const carolAddress = 'bchtest:qqsr7nqwe6rq5crj63gy5gdqchpnwmguusmr7tfmsj'; - -interface MockNetworkProviderOptions { +export interface MockNetworkProviderOptions { updateUtxoSet: boolean; + vmTarget?: VmTarget; } -// We are setting the default updateUtxoSet to 'false' so that it doesn't break the current behaviour -// TODO: in a future breaking release we want to set this to 'true' by default export default class MockNetworkProvider implements NetworkProvider { // we use lockingBytecode hex as the key for utxoMap to make cash addresses and token addresses interchangeable private utxoSet: Array<[string, Utxo]> = []; @@ -22,15 +16,11 @@ export default class MockNetworkProvider implements NetworkProvider { public network: Network = Network.MOCKNET; public blockHeight: number = 133700; public options: MockNetworkProviderOptions; + public vmTarget: VmTarget; constructor(options?: Partial) { - this.options = { updateUtxoSet: false, ...options }; - - for (let i = 0; i < 3; i += 1) { - this.addUtxo(aliceAddress, randomUtxo()); - this.addUtxo(bobAddress, randomUtxo()); - this.addUtxo(carolAddress, randomUtxo()); - } + this.options = { updateUtxoSet: true, ...options }; + this.vmTarget = this.options.vmTarget ?? VmTarget.BCH_2025_05; } async getUtxos(address: string): Promise { diff --git a/packages/cashscript/src/test/JestExtensions.ts b/packages/cashscript/src/test/JestExtensions.ts index 929bda94..5f30b703 100644 --- a/packages/cashscript/src/test/JestExtensions.ts +++ b/packages/cashscript/src/test/JestExtensions.ts @@ -33,11 +33,10 @@ expect.extend({ // silence actual stdout output loggerSpy.mockImplementation(() => { }); + // Run debug, ignoring any errors because we only care about the logs, even if the transaction fails try { - executeDebug(transaction); - } catch (error) { - if (error instanceof OldTransactionBuilderError) throw error; - } + transaction.debug(); + } catch (error) { } // We concatenate all the logs into a single string - if no logs are present, we set received to undefined const receivedBase = loggerSpy.mock.calls.reduce((acc, [log]) => `${acc}\n${log}`, '').trim(); @@ -75,14 +74,11 @@ expect.extend({ match: RegExp | string, ): SyncExpectationResult { try { - executeDebug(transaction); - + transaction.debug(); const matcherHint = this.utils.matcherHint('.toFailRequireWith', undefined, match.toString(), { isNot: this.isNot }); const message = (): string => `${matcherHint}\n\nContract function did not fail a require statement.`; return { message, pass: false }; } catch (transactionError: any) { - if (transactionError instanceof OldTransactionBuilderError) throw transactionError; - const matcherHint = this.utils.matcherHint('toFailRequireWith', 'received', 'expected', { isNot: this.isNot }); const expectedText = `Expected pattern: ${this.isNot ? 'not ' : ''}${this.utils.printExpected(match)}`; const receivedText = `Received string: ${this.utils.printReceived(transactionError?.message ?? '')}`; @@ -101,34 +97,13 @@ expect.extend({ transaction: Debuggable, ): SyncExpectationResult { try { - executeDebug(transaction); + transaction.debug(); const message = (): string => 'Contract function did not fail a require statement.'; return { message, pass: false }; } catch (transactionError: any) { - if (transactionError instanceof OldTransactionBuilderError) throw transactionError; - const receivedText = `Received string: ${this.utils.printReceived(transactionError?.message ?? '')}`; const message = (): string => `Contract function failed a require statement.\n${receivedText}`; return { message, pass: true }; } }, }); - - -// Wrapper function with custom error in case people use it with the old transaction builder -// This is a temporary solution until we fully remove the old transaction builder from the SDK -const executeDebug = (transaction: Debuggable): void => { - const debugResults = transaction.debug(); - - if (debugResults instanceof Promise) { - debugResults.catch(() => { }); - throw new OldTransactionBuilderError(); - } -}; - -class OldTransactionBuilderError extends Error { - constructor() { - super('The CashScript JestExtensions do not support the old transaction builder since v0.11.0. Please use the new TransactionBuilder class.'); - this.name = 'OldTransactionBuilderError'; - } -} diff --git a/packages/cashscript/test/Contract.test.ts b/packages/cashscript/test/Contract.test.ts index a31a16d0..159d0d2d 100644 --- a/packages/cashscript/test/Contract.test.ts +++ b/packages/cashscript/test/Contract.test.ts @@ -7,10 +7,13 @@ import { Network, randomUtxo, SignatureTemplate, + TransactionBuilder, } from '../src/index.js'; import { + aliceAddress, alicePkh, alicePriv, alicePub, bobPriv, } from './fixture/vars.js'; +import { generateLibauthSourceOutputs } from '../src/utils.js'; import p2pkhArtifact from './fixture/p2pkh.artifact.js'; import twtArtifact from './fixture/transfer_with_timeout.artifact.js'; import hodlVaultArtifact from './fixture/hodl_vault.artifact.js'; @@ -59,7 +62,7 @@ describe('Contract', () => { const instance = new Contract(p2pkhArtifact, [placeholder(20)], { provider }); expect(typeof instance.address).toBe('string'); - expect(typeof instance.functions.spend).toBe('function'); + expect(typeof instance.unlock.spend).toBe('function'); expect(instance.name).toEqual(p2pkhArtifact.contractName); }); @@ -68,8 +71,8 @@ describe('Contract', () => { const instance = new Contract(twtArtifact, [placeholder(65), placeholder(65), 1000000n], { provider }); expect(typeof instance.address).toBe('string'); - expect(typeof instance.functions.transfer).toBe('function'); - expect(typeof instance.functions.timeout).toBe('function'); + expect(typeof instance.unlock.transfer).toBe('function'); + expect(typeof instance.unlock.timeout).toBe('function'); expect(instance.name).toEqual(twtArtifact.contractName); }); @@ -78,7 +81,7 @@ describe('Contract', () => { const instance = new Contract(hodlVaultArtifact, [placeholder(65), placeholder(65), 1000000n, 10000n], { provider }); expect(typeof instance.address).toBe('string'); - expect(typeof instance.functions.spend).toBe('function'); + expect(typeof instance.unlock.spend).toBe('function'); expect(instance.name).toEqual(hodlVaultArtifact.contractName); }); @@ -87,8 +90,8 @@ describe('Contract', () => { const instance = new Contract(mecenasArtifact, [placeholder(20), placeholder(20), 1000000n], { provider }); expect(typeof instance.address).toBe('string'); - expect(typeof instance.functions.receive).toBe('function'); - expect(typeof instance.functions.reclaim).toBe('function'); + expect(typeof instance.unlock.receive).toBe('function'); + expect(typeof instance.unlock.reclaim).toBe('function'); expect(instance.name).toEqual(mecenasArtifact.contractName); }); @@ -136,32 +139,58 @@ describe('Contract', () => { }); }); - describe('Contract functions', () => { + describe('Contract unlockers', () => { + let provider: MockNetworkProvider; let instance: Contract; let bbInstance: Contract; + beforeEach(() => { - const provider = new ElectrumNetworkProvider(Network.CHIPNET); + provider = new MockNetworkProvider(); instance = new Contract(p2pkhArtifact, [alicePkh], { provider }); bbInstance = new Contract(boundedBytesArtifact, [], { provider }); }); it('can\'t call spend with incorrect signature', () => { - expect(() => instance.functions.spend()).toThrow(); - expect(() => instance.functions.spend(0n, 1n)).toThrow(); - expect(() => instance.functions.spend(alicePub, new SignatureTemplate(alicePriv), 0n)).toThrow(); - expect(() => bbInstance.functions.spend(hexToBin('e803'), 1000n)).toThrow(); - expect(() => bbInstance.functions.spend(hexToBin('e803000000'), 1000n)).toThrow(); + expect(() => instance.unlock.spend()).toThrow(); + expect(() => instance.unlock.spend(0n, 1n)).toThrow(); + expect(() => instance.unlock.spend(alicePub, new SignatureTemplate(alicePriv), 0n)).toThrow(); + expect(() => bbInstance.unlock.spend(hexToBin('e803'), 1000n)).toThrow(); + expect(() => bbInstance.unlock.spend(hexToBin('e803000000'), 1000n)).toThrow(); }); it('can call spend with incorrect arguments', () => { - expect(() => instance.functions.spend(alicePub, new SignatureTemplate(bobPriv))).not.toThrow(); - expect(() => instance.functions.spend(alicePkh, placeholder(65))).not.toThrow(); - expect(() => bbInstance.functions.spend(hexToBin('e8031234'), 1000n)).not.toThrow(); + expect(() => instance.unlock.spend(alicePub, new SignatureTemplate(bobPriv))).not.toThrow(); + expect(() => instance.unlock.spend(alicePkh, placeholder(65))).not.toThrow(); + expect(() => bbInstance.unlock.spend(hexToBin('e8031234'), 1000n)).not.toThrow(); }); it('can call spend with correct arguments', () => { - expect(() => instance.functions.spend(alicePub, new SignatureTemplate(alicePriv))).not.toThrow(); - expect(() => bbInstance.functions.spend(hexToBin('e8030000'), 1000n)).not.toThrow(); + expect(() => instance.unlock.spend(alicePub, new SignatureTemplate(alicePriv))).not.toThrow(); + expect(() => bbInstance.unlock.spend(hexToBin('e8030000'), 1000n)).not.toThrow(); + }); + + it('generates correct locking bytecode', () => { + expect(instance.unlock.spend(alicePub, new SignatureTemplate(alicePriv)).generateLockingBytecode()) + .toEqual(hexToBin('aa2034d9ffce86b4d136ca74e9db6f6433d3548966a6be064052e728a4c1d16aa3a587')); + }); + + it('generates correct unlocking bytecode', () => { + const utxo = { + txid: 'e5ac1aa9730d7514b541895e466c987327a4b0c57fcbbd50fc73788f5c0f65d9', + vout: 4, + satoshis: 102745n, + }; + + const unlocker = instance.unlock.spend(alicePub, new SignatureTemplate(alicePriv)); + const transactionBuilder = new TransactionBuilder({ provider }) + .addInput(utxo, unlocker) + .addOutput({ to: aliceAddress, amount: 1000n }); + + const transaction = transactionBuilder.buildLibauthTransaction(); + const sourceOutputs = generateLibauthSourceOutputs(transactionBuilder.inputs); + + expect(unlocker.generateUnlockingBytecode({ transaction, sourceOutputs, inputIndex: 0 })) + .toEqual(hexToBin('4135fac4118af15e0d66f30548dd0c31e1108f3389af96bb9db4f2305706e18fe52cc7163f6440fae98c48332d09c30380527a90604f14b4b3fc0c3aa0884c9c0a61210373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c0881914512dbb2c8c02efbac8d92431aa0ac33f6b0bf97078a988ac')); }); }); }); diff --git a/packages/cashscript/test/SignatureTemplate.test.ts b/packages/cashscript/test/SignatureTemplate.test.ts new file mode 100644 index 00000000..31d4e2ec --- /dev/null +++ b/packages/cashscript/test/SignatureTemplate.test.ts @@ -0,0 +1,96 @@ +import { generateLibauthSourceOutputs } from 'cashscript/dist/utils.js'; +import { HashType, MockNetworkProvider, SignatureAlgorithm, SignatureTemplate, TransactionBuilder } from '../src/index.js'; +import { aliceAddress, alicePriv, alicePub, aliceWif } from './fixture/vars.js'; +import { binToHex, hexToBin } from '@bitauth/libauth'; + +describe('SignatureTemplate', () => { + describe('constructor', () => { + it('should properly convert different signer formats to raw private key', () => { + const mockKeyPair = { toWIF: () => aliceWif }; + const fromKeyPair = new SignatureTemplate(mockKeyPair); + const from0xHex = new SignatureTemplate(`0x${binToHex(alicePriv)}`); + const fromHex = new SignatureTemplate(binToHex(alicePriv)); + const fromWif = new SignatureTemplate(aliceWif); + const fromPriv = new SignatureTemplate(alicePriv); + expect(fromKeyPair.privateKey).toEqual(alicePriv); + expect(from0xHex.privateKey).toEqual(alicePriv); + expect(fromHex.privateKey).toEqual(alicePriv); + expect(fromWif.privateKey).toEqual(alicePriv); + expect(fromPriv.privateKey).toEqual(alicePriv); + }); + }); + + describe('generateSignature', () => { + it('should generate a correct signature using Schnorr', () => { + const signatureTemplate = new SignatureTemplate(alicePriv); + const signature = signatureTemplate.generateSignature(hexToBin('0000000000000000000000')); + expect(signature).toEqual(hexToBin('bcac180e17de108003cce026708bd2af54b860dad2626cee157f4ed5abd993b9085d615015f905978adc51e8878226280ddd27d899f086519c0978e53332d79961')); + }); + + it('should generate a correct signature using ECDSA', () => { + const signatureTemplate = new SignatureTemplate(alicePriv, undefined, SignatureAlgorithm.ECDSA); + const signature = signatureTemplate.generateSignature(hexToBin('0000000000000000000000')); + expect(signature).toEqual(hexToBin('3045022100fa1d6a159a124e99479f78152422d55ff3c16f7fac5ae47fa291907f8f47613f02200d6c906f667b3712860b6f5a1f296ecb7dcd44da83c6a1eb45869b61c6b8dadb61')); + }); + + it('should append the correct hash type when fork ID is true', () => { + const signatureTemplate = new SignatureTemplate(alicePriv, HashType.SIGHASH_SINGLE); + const signature = signatureTemplate.generateSignature(hexToBin('0000000000000000000000'), true); + expect(signature).toEqual(hexToBin('bcac180e17de108003cce026708bd2af54b860dad2626cee157f4ed5abd993b9085d615015f905978adc51e8878226280ddd27d899f086519c0978e53332d79943')); + }); + + it('should append the correct hash type when fork ID is false', () => { + const signatureTemplate = new SignatureTemplate(alicePriv, HashType.SIGHASH_SINGLE); + const signature = signatureTemplate.generateSignature(hexToBin('0000000000000000000000'), false); + expect(signature).toEqual(hexToBin('bcac180e17de108003cce026708bd2af54b860dad2626cee157f4ed5abd993b9085d615015f905978adc51e8878226280ddd27d899f086519c0978e53332d79903')); + }); + }); + + describe('signMessageHash', () => { + it('should generate a correct signature using Schnorr', () => { + const signatureTemplate = new SignatureTemplate(alicePriv); + const signature = signatureTemplate.signMessageHash(hexToBin('0000000000000000000000')); + expect(signature).toEqual(hexToBin('bcac180e17de108003cce026708bd2af54b860dad2626cee157f4ed5abd993b9085d615015f905978adc51e8878226280ddd27d899f086519c0978e53332d799')); + }); + }); + + describe('signMessageHash', () => { + it('should generate a correct signature using ECDSA', () => { + const signatureTemplate = new SignatureTemplate(alicePriv, undefined, SignatureAlgorithm.ECDSA); + const signature = signatureTemplate.signMessageHash(hexToBin('0000000000000000000000')); + expect(signature).toEqual(hexToBin('3045022100fa1d6a159a124e99479f78152422d55ff3c16f7fac5ae47fa291907f8f47613f02200d6c906f667b3712860b6f5a1f296ecb7dcd44da83c6a1eb45869b61c6b8dadb')); + }); + }); + + describe('getPublicKey', () => { + it('should generate a correct public key', () => { + const signatureTemplate = new SignatureTemplate(alicePriv); + expect(signatureTemplate.getPublicKey()).toEqual(alicePub); + }); + }); + + describe('unlockP2PKH', () => { + it('should generate a correct unlocker', () => { + const utxo = { + txid: '043ec3826702c45460a6dd6b13e343a8f1bc06bc047b63ca484f791dfdfd92c2', + vout: 8, + satoshis: 109759n, + }; + + const signatureTemplate = new SignatureTemplate(alicePriv); + const unlocker = signatureTemplate.unlockP2PKH(); + + expect(unlocker.generateLockingBytecode()).toEqual(hexToBin('76a914512dbb2c8c02efbac8d92431aa0ac33f6b0bf97088ac')); + + const transactionBuilder = new TransactionBuilder({ provider: new MockNetworkProvider() }) + .addInput(utxo, unlocker) + .addOutput({ to: aliceAddress, amount: 1000n }); + + const transaction = transactionBuilder.buildLibauthTransaction(); + const sourceOutputs = generateLibauthSourceOutputs(transactionBuilder.inputs); + + expect(unlocker.generateUnlockingBytecode({ transaction, sourceOutputs, inputIndex: 0 })) + .toEqual(hexToBin('415cbd7f111be33daa9578ed7ace1b6721a5d14206302c628b1bfc27cfef92a334943504089731b7ce10173be7f22dc175f6d10c8c5a3d1b41a64db09555ebb00d61210373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088')); + }); + }); +}); diff --git a/packages/cashscript/test/TransactionBuilder.test.ts b/packages/cashscript/test/TransactionBuilder.test.ts index e2321a5c..4627b8f2 100644 --- a/packages/cashscript/test/TransactionBuilder.test.ts +++ b/packages/cashscript/test/TransactionBuilder.test.ts @@ -17,7 +17,7 @@ import { utxoComparator, calculateDust, randomUtxo, randomToken, isNonTokenUtxo, import p2pkhArtifact from './fixture/p2pkh.artifact.js'; import twtArtifact from './fixture/transfer_with_timeout.artifact.js'; import { TransactionBuilder } from '../src/TransactionBuilder.js'; -import { gatherUtxos, getTxOutputs } from './test-util.js'; +import { getTxOutputs } from './test-util.js'; import { generateWcTransactionObjectFixture } from './fixture/walletconnect/fixtures.js'; describe('Transaction Builder', () => { @@ -39,106 +39,14 @@ describe('Transaction Builder', () => { (provider as any).addUtxo?.(p2pkhInstance.address, randomUtxo({ token: randomToken() })); (provider as any).addUtxo?.(twtInstance.address, randomUtxo()); (provider as any).addUtxo?.(twtInstance.address, randomUtxo()); + (provider as any).addUtxo?.(aliceAddress, randomUtxo()); + (provider as any).addUtxo?.(aliceAddress, randomUtxo()); (provider as any).addUtxo?.(bobAddress, randomUtxo()); (provider as any).addUtxo?.(bobAddress, randomUtxo()); (provider as any).addUtxo?.(carolAddress, randomUtxo()); (provider as any).addUtxo?.(carolAddress, randomUtxo()); }); - describe('should return the same transaction as the simple transaction builder', () => { - it('for a single-output (+ change) transaction from a single type of contract', async () => { - // given - const to = p2pkhInstance.address; - const amount = 1000n; - const fee = 2000n; - - const utxos = (await p2pkhInstance.getUtxos()).filter(isNonTokenUtxo).sort(utxoComparator).reverse(); - const { utxos: gathered, total } = gatherUtxos(utxos, { amount, fee }); - - const change = total - amount - fee; - const dustAmount = calculateDust({ to, amount: change }); - - if (change < 0) { - throw new Error('Not enough funds to send transaction'); - } - - // when - const simpleTransaction = await p2pkhInstance.functions - .spend(bobPub, new SignatureTemplate(bobPriv)) - .from(gathered) - .to(to, amount) - .to(change > dustAmount ? [{ to, amount: change }] : []) - .withoutChange() - .withoutTokenChange() - .withTime(0) - .build(); - - const advancedTransaction = new TransactionBuilder({ provider }) - .addInputs(gathered, p2pkhInstance.unlock.spend(bobPub, new SignatureTemplate(bobPriv))) - .addOutput({ to, amount }) - .addOutputs(change > dustAmount ? [{ to, amount: change }] : []) - .build(); - - const simpleDecoded = stringify(decodeTransactionUnsafe(hexToBin(simpleTransaction))); - const advancedDecoded = stringify(decodeTransactionUnsafe(hexToBin(advancedTransaction))); - - // then - expect(advancedDecoded).toEqual(simpleDecoded); - }); - - it('for a multi-output (+ change) transaction with P2SH and P2PKH inputs', async () => { - // given - const to = bobAddress; - const amount = 10000n; - const fee = 2000n; - - const contractUtxos = (await p2pkhInstance.getUtxos()).filter(isNonTokenUtxo).sort(utxoComparator).reverse(); - const bobUtxos = await provider.getUtxos(bobAddress); - const bobTemplate = new SignatureTemplate(bobPriv); - - const totalInputUtxos = [...contractUtxos.slice(0, 2), ...bobUtxos.slice(0, 2)]; - const totalInputAmount = totalInputUtxos.reduce((acc, utxo) => acc + utxo.satoshis, 0n); - - const change = totalInputAmount - (amount * 2n) - fee; - const dustAmount = calculateDust({ to, amount: change }); - - if (change < 0) { - throw new Error('Not enough funds to send transaction'); - } - - // when - const simpleTransaction = await p2pkhInstance.functions - .spend(bobPub, bobTemplate) - .fromP2PKH(bobUtxos[0], bobTemplate) - .from(contractUtxos[0]) - .fromP2PKH(bobUtxos[1], bobTemplate) - .from(contractUtxos[1]) - .to(to, amount) - .to(to, amount) - .to(change > dustAmount ? [{ to, amount: change }] : []) - .withoutChange() - .withoutTokenChange() - .withTime(0) - .build(); - - const advancedTransaction = new TransactionBuilder({ provider }) - .addInput(bobUtxos[0], bobTemplate.unlockP2PKH()) - .addInput(contractUtxos[0], p2pkhInstance.unlock.spend(bobPub, bobTemplate)) - .addInput(bobUtxos[1], bobTemplate.unlockP2PKH()) - .addInput(contractUtxos[1], p2pkhInstance.unlock.spend(bobPub, bobTemplate)) - .addOutput({ to, amount }) - .addOutput({ to, amount }) - .addOutputs(change > dustAmount ? [{ to, amount: change }] : []) - .build(); - - const simpleDecoded = stringify(decodeTransactionUnsafe(hexToBin(simpleTransaction))); - const advancedDecoded = stringify(decodeTransactionUnsafe(hexToBin(advancedTransaction))); - - // then - expect(advancedDecoded).toEqual(simpleDecoded); - }); - }); - describe('test TransactionBuilder.build', () => { it('should build a transaction that can spend from 2 different contracts and P2PKH + OP_RETURN', async () => { const fee = 1000n; @@ -172,9 +80,9 @@ describe('Transaction Builder', () => { expect(txOutputs).toEqual(expect.arrayContaining(outputs)); }); - it('should fail when fee is higher than maxFee', async () => { + it('should fail when fee is higher than maximumFeeSatoshis', async () => { const fee = 2000n; - const maxFee = 1000n; + const maximumFeeSatoshis = 1000n; const p2pkhUtxos = (await p2pkhInstance.getUtxos()).filter(isNonTokenUtxo).sort(utxoComparator).reverse(); const amount = p2pkhUtxos[0].satoshis - fee; @@ -185,17 +93,16 @@ describe('Transaction Builder', () => { } expect(() => { - new TransactionBuilder({ provider }) + new TransactionBuilder({ provider, maximumFeeSatoshis }) .addInput(p2pkhUtxos[0], p2pkhInstance.unlock.spend(carolPub, new SignatureTemplate(carolPriv))) .addOutput({ to: p2pkhInstance.address, amount }) - .setMaxFee(maxFee) .build(); - }).toThrow(`Transaction fee of ${fee} is higher than max fee of ${maxFee}`); + }).toThrow(`Transaction fee of ${fee} is higher than max fee of ${maximumFeeSatoshis}`); }); - it('should succeed when fee is lower than maxFee', async () => { + it('should succeed when fee is lower than maximumFeeSatoshis', async () => { const fee = 1000n; - const maxFee = 2000n; + const maximumFeeSatoshis = 2000n; const p2pkhUtxos = (await p2pkhInstance.getUtxos()).filter(isNonTokenUtxo).sort(utxoComparator).reverse(); const amount = p2pkhUtxos[0].satoshis - fee; @@ -205,10 +112,49 @@ describe('Transaction Builder', () => { throw new Error('Not enough funds to send transaction'); } - const tx = new TransactionBuilder({ provider }) + const tx = new TransactionBuilder({ provider, maximumFeeSatoshis }) + .addInput(p2pkhUtxos[0], p2pkhInstance.unlock.spend(carolPub, new SignatureTemplate(carolPriv))) + .addOutput({ to: p2pkhInstance.address, amount }) + .build(); + + expect(tx).toBeDefined(); + }); + + it('should fail when fee per byte is higher than maximumFeeSatsPerByte', async () => { + const fee = 2000n; + const maximumFeeSatsPerByte = 1.0; + const p2pkhUtxos = (await p2pkhInstance.getUtxos()).filter(isNonTokenUtxo).sort(utxoComparator).reverse(); + + const amount = p2pkhUtxos[0].satoshis - fee; + const dustAmount = calculateDust({ to: p2pkhInstance.address, amount }); + + if (amount < dustAmount) { + throw new Error('Not enough funds to send transaction'); + } + + expect(() => { + new TransactionBuilder({ provider, maximumFeeSatsPerByte }) + .addInput(p2pkhUtxos[0], p2pkhInstance.unlock.spend(carolPub, new SignatureTemplate(carolPriv))) + .addOutput({ to: p2pkhInstance.address, amount }) + .build(); + }).toThrow(`Transaction fee per byte of 9.05 is higher than max fee per byte of ${maximumFeeSatsPerByte}`); + }); + + it('should succeed when fee per byte is lower than maximumFeeSatsPerByte', async () => { + const fee = 1000n; + const maximumFeeSatsPerByte = 10.0; + const p2pkhUtxos = (await p2pkhInstance.getUtxos()).filter(isNonTokenUtxo).sort(utxoComparator).reverse(); + + const amount = p2pkhUtxos[0].satoshis - fee; + const dustAmount = calculateDust({ to: p2pkhInstance.address, amount }); + + if (amount < dustAmount) { + throw new Error('Not enough funds to send transaction'); + } + + const tx = new TransactionBuilder({ provider, maximumFeeSatsPerByte }) .addInput(p2pkhUtxos[0], p2pkhInstance.unlock.spend(carolPub, new SignatureTemplate(carolPriv))) .addOutput({ to: p2pkhInstance.address, amount }) - .setMaxFee(maxFee) .build(); expect(tx).toBeDefined(); @@ -300,7 +246,7 @@ describe('Transaction Builder', () => { const aliceUtxos = (await provider.getUtxos(aliceAddress)).filter(isNonTokenUtxo); const sigTemplate = new SignatureTemplate(alicePriv); - expect(aliceUtxos.length).toBeGreaterThan(2); + expect(aliceUtxos.length).toBe(2); const change = aliceUtxos[0].satoshis + aliceUtxos[1].satoshis - 1000n; diff --git a/packages/cashscript/test/debugging-old-artifacts.test.ts b/packages/cashscript/test/debugging-old-artifacts.test.ts index 35ced7a8..fc33c7a7 100644 --- a/packages/cashscript/test/debugging-old-artifacts.test.ts +++ b/packages/cashscript/test/debugging-old-artifacts.test.ts @@ -35,7 +35,7 @@ describe('Debugging tests - old artifacts', () => { .addInput(contractUtxo, contractTestLogs.unlock.spend(alicePub, new SignatureTemplate(alicePriv))) .addOutput({ to: contractTestLogs.address, amount: 10000n }); - console.warn(transaction.bitauthUri()); + console.warn(transaction.getBitauthUri()); expect(() => transaction.debug()).not.toThrow(); }); @@ -50,7 +50,7 @@ describe('Debugging tests - old artifacts', () => { .addInput(contractUtxo, contractTestLogs.unlock.spend(alicePub, new SignatureTemplate(bobPriv))) .addOutput({ to: contractTestLogs.address, amount: 10000n }); - console.warn(transaction.bitauthUri()); + console.warn(transaction.getBitauthUri()); expect(() => transaction.debug()).toThrow(); }); diff --git a/packages/cashscript/test/debugging.test.ts b/packages/cashscript/test/debugging.test.ts index c952140b..0cd18b9d 100644 --- a/packages/cashscript/test/debugging.test.ts +++ b/packages/cashscript/test/debugging.test.ts @@ -1,4 +1,4 @@ -import { Contract, MockNetworkProvider, SignatureAlgorithm, SignatureTemplate, TransactionBuilder } from '../src/index.js'; +import { Contract, FailedTransactionError, MockNetworkProvider, SignatureAlgorithm, SignatureTemplate, TransactionBuilder, VmTarget } from '../src/index.js'; import { aliceAddress, alicePriv, alicePub, bobPriv, bobPub } from './fixture/vars.js'; import '../src/test/JestExtensions.js'; import { randomUtxo } from '../src/utils.js'; @@ -619,22 +619,97 @@ describe('Debugging tests', () => { () => expect(transaction).not.toFailRequire(), ).toThrow(/Contract function failed a require statement\.*\nReceived string: (.|\n)*?1 should equal 2/); }); + }); - it('should throw an error if the old transaction builder is used', async () => { - const transaction = contractTestRequires.functions.test_require().to(aliceAddress, 1000n); + describe('P2PKH only transaction', () => { + it('should succeed when spending from P2PKH inputs with the corresponding unlocker', async () => { + const provider = new MockNetworkProvider(); + provider.addUtxo(aliceAddress, randomUtxo()); + provider.addUtxo(aliceAddress, randomUtxo()); - // Note: We're wrapping the expect call in another expect, since we expect the inner expect to throw - expect( - () => expect(transaction).toFailRequire(), - ).toThrow('The CashScript JestExtensions do not support the old transaction builder since v0.11.0. Please use the new TransactionBuilder class.'); + const result = new TransactionBuilder({ provider }) + .addInputs(await provider.getUtxos(aliceAddress), new SignatureTemplate(alicePriv).unlockP2PKH()) + .addOutput({ to: aliceAddress, amount: 5000n }) + .debug(); - expect( - () => expect(transaction).toFailRequireWith('1 should equal 2'), - ).toThrow('The CashScript JestExtensions do not support the old transaction builder since v0.11.0. Please use the new TransactionBuilder class.'); + expect(Object.keys(result).length).toBeGreaterThan(0); + }); - expect( - () => expect(transaction).toLog('Hello World'), - ).toThrow('The CashScript JestExtensions do not support the old transaction builder since v0.11.0. Please use the new TransactionBuilder class.'); + // We currently don't have a way to properly handle non-matching UTXOs and unlockers + // Note: that also goes for Contract UTXOs where a user uses an unlocker of a different contract + it.skip('should fail when spending from P2PKH inputs with an unlocker for a different public key', async () => { + const provider = new MockNetworkProvider(); + provider.addUtxo(aliceAddress, randomUtxo()); + provider.addUtxo(aliceAddress, randomUtxo()); + + const transactionBuilder = new TransactionBuilder({ provider }) + .addInputs(await provider.getUtxos(aliceAddress), new SignatureTemplate(bobPriv).unlockP2PKH()) + .addOutput({ to: aliceAddress, amount: 5000n }); + + expect(() => transactionBuilder.debug()).toThrow(FailedTransactionError); }); }); + + describe('VmTargets', () => { + const vmTargets = [ + undefined, + VmTarget.BCH_2023_05, + VmTarget.BCH_2025_05, + VmTarget.BCH_2026_05, + VmTarget.BCH_SPEC, + ] as const; + + for (const vmTarget of vmTargets) { + it(`should execute and log correctly with vmTarget ${vmTarget}`, async () => { + const provider = new MockNetworkProvider({ vmTarget }); + const contractTestLogs = new Contract(artifactTestLogs, [alicePub], { provider }); + const contractUtxo = randomUtxo(); + provider.addUtxo(contractTestLogs.address, contractUtxo); + + const transaction = new TransactionBuilder({ provider }) + .addInput(contractUtxo, contractTestLogs.unlock.transfer(new SignatureTemplate(alicePriv), 1000n)) + .addOutput({ to: contractTestLogs.address, amount: 10000n }); + + expect(transaction.getLibauthTemplate().supported[0]).toBe(vmTarget ?? 'BCH_2025_05'); + + const expectedLog = new RegExp(`^\\[Input #0] Test.cash:10 0x[0-9a-f]{130} 0x${binToHex(alicePub)} 1000 0xbeef 1 test true$`); + expect(transaction).toLog(expectedLog); + }); + } + }); +}); + +describe('VM Resources', () => { + it('Should output VM resource usage', async () => { + const provider = new MockNetworkProvider(); + + const contractSingleFunction = new Contract({ ...artifactTestSingleFunction, contractName: 'SingleFunction' }, [], { provider }); + const contractZeroHandling = new Contract({ ...artifactTestZeroHandling, contractName: 'ZeroHandling' }, [0n], { provider }); + + provider.addUtxo(contractSingleFunction.address, randomUtxo()); + provider.addUtxo(contractZeroHandling.address, randomUtxo()); + provider.addUtxo(aliceAddress, randomUtxo()); + + const tx = new TransactionBuilder({ provider }) + .addInputs(await contractSingleFunction.getUtxos(), contractSingleFunction.unlock.test_require_single_function()) + .addInputs(await contractZeroHandling.getUtxos(), contractZeroHandling.unlock.test_zero_handling(0n)) + .addInput((await provider.getUtxos(aliceAddress))[0], new SignatureTemplate(alicePriv).unlockP2PKH()) + .addOutput({ to: aliceAddress, amount: 1000n }); + + console.log = jest.fn(); + console.table = jest.fn(); + + const vmUsage = tx.getVmResourceUsage(); + expect(console.log).not.toHaveBeenCalled(); + expect(console.table).not.toHaveBeenCalled(); + + tx.getVmResourceUsage(true); + expect(console.log).toHaveBeenCalledWith('VM Resource usage by inputs:'); + expect(console.table).toHaveBeenCalled(); + + jest.restoreAllMocks(); + + expect(vmUsage[0]?.hashDigestIterations).toBeGreaterThan(0); + expect(vmUsage[2]?.hashDigestIterations).toBeGreaterThan(0); + }); }); diff --git a/packages/cashscript/test/e2e/HodlVault.test.ts b/packages/cashscript/test/e2e/HodlVault.test.ts index cf2316b7..922cc200 100644 --- a/packages/cashscript/test/e2e/HodlVault.test.ts +++ b/packages/cashscript/test/e2e/HodlVault.test.ts @@ -5,6 +5,8 @@ import { ElectrumNetworkProvider, Network, TransactionBuilder, + SignatureAlgorithm, + HashType, } from '../../src/index.js'; import { alicePriv, @@ -12,10 +14,11 @@ import { oracle, oraclePub, } from '../fixture/vars.js'; -import { gatherUtxos, getTxOutputs } from '../test-util.js'; +import { gatherUtxos, getTxOutputs, itOrSkip } from '../test-util.js'; import { FailedRequireError } from '../../src/Errors.js'; import artifact from '../fixture/hodl_vault.artifact.js'; import { randomUtxo } from '../../src/utils.js'; +import { placeholder } from '@cashscript/utils'; describe('HodlVault', () => { const provider = process.env.TESTS_USE_CHIPNET @@ -95,5 +98,110 @@ describe('HodlVault', () => { const txOutputs = getTxOutputs(tx); expect(txOutputs).toEqual(expect.arrayContaining([{ to, amount }])); }); + + it('should succeed when price is high enough, ECDSA sig and datasig', async () => { + // given + const message = oracle.createMessage(100000n, 30000n); + const oracleSig = oracle.signMessage(message, SignatureAlgorithm.ECDSA); + const to = hodlVault.address; + const amount = 10000n; + const { utxos, changeAmount } = gatherUtxos(await hodlVault.getUtxos(), { amount, fee: 2000n }); + + const signatureTemplate = new SignatureTemplate(alicePriv, HashType.SIGHASH_ALL, SignatureAlgorithm.ECDSA); + + // when + const tx = await new TransactionBuilder({ provider }) + .addInputs(utxos, hodlVault.unlock.spend(signatureTemplate, oracleSig, message)) + .addOutput({ to: to, amount: amount }) + .addOutput({ to: to, amount: changeAmount }) + .setLocktime(100_000) + .send(); + + // then + const txOutputs = getTxOutputs(tx); + expect(txOutputs).toEqual(expect.arrayContaining([{ to, amount }])); + }); + + itOrSkip(!Boolean(process.env.TESTS_USE_CHIPNET), 'should succeed with precomputed ECDSA signature', async () => { + // given + const cleanProvider = new MockNetworkProvider(); + const contract = new Contract(artifact, [alicePub, oraclePub, 99000n, 30000n], { provider: cleanProvider }); + cleanProvider.addUtxo(contract.address, { + satoshis: 100000n, + txid: '11'.repeat(32), + vout: 0, + }); + const message = oracle.createMessage(100000n, 30000n); + const oracleSig = oracle.signMessage(message, SignatureAlgorithm.ECDSA); + const to = contract.address; + const amount = 10000n; + const { utxos, changeAmount } = gatherUtxos(await contract.getUtxos(), { amount, fee: 2000n }); + const signature = '3045022100aa004a425c0c911594c0333164f990c760991b7f84272f35d98c9c6617d9c53602207dfe4729224d4e61496dff11963982cf79f05d623a6e4004b5f50b7cefa7175241'; + + // when + const tx = await new TransactionBuilder({ provider: cleanProvider }) + .addInputs(utxos, contract.unlock.spend(signature, oracleSig, message)) + .addOutput({ to: to, amount: amount }) + .addOutput({ to: to, amount: changeAmount }) + .setLocktime(100_000) + .send(); + + // then + const txOutputs = getTxOutputs(tx); + expect(txOutputs).toEqual(expect.arrayContaining([{ to, amount }])); + }); + + it('should fail to accept wrong signature lengths', async () => { + // given + const message = oracle.createMessage(100000n, 30000n); + const oracleSig = oracle.signMessage(message, SignatureAlgorithm.ECDSA); + const to = hodlVault.address; + const amount = 10000n; + const { utxos, changeAmount } = gatherUtxos(await hodlVault.getUtxos(), { amount, fee: 2000n }); + + // sig: unlocker should throw when given an improper length + expect(() => hodlVault.unlock.spend(placeholder(100), oracleSig, message)).toThrow("Found type 'bytes100' where type 'sig' was expected"); + + // sig: unlocker should not throw when given a proper length, but transaction should fail on invalid sig + // Note that this fails with "FailedTransactionEvaluationError" because an invalid signature encoding is NOT a failed + // require statement + await expect(new TransactionBuilder({ provider }) + .addInputs(utxos, hodlVault.unlock.spend(placeholder(71), oracleSig, message)) + .addOutput({ to: to, amount: amount }) + .addOutput({ to: to, amount: changeAmount }) + .setLocktime(100_000) + .send()).rejects.toThrow('HodlVault.cash:27 Error in transaction at input 0 in contract HodlVault.cash at line 27'); + + // sig: unlocker should not throw when given an empty byte array, but transaction should fail on require statement + // Note that this fails with "FailedRequireError" because a zero-length signature IS a failed require statement + await expect(new TransactionBuilder({ provider }) + .addInputs(utxos, hodlVault.unlock.spend(placeholder(0), oracleSig, message)) + .addOutput({ to: to, amount: amount }) + .addOutput({ to: to, amount: changeAmount }) + .setLocktime(100_000) + .send()).rejects.toThrow('HodlVault.cash:27 Require statement failed at input 0 in contract HodlVault.cash at line 27'); + + // datasig: unlocker should throw when given an improper length + const signatureTemplate = new SignatureTemplate(alicePriv, HashType.SIGHASH_ALL, SignatureAlgorithm.ECDSA); + expect(() => hodlVault.unlock.spend(signatureTemplate, placeholder(100), message)).toThrow("Found type 'bytes100' where type 'datasig' was expected"); + + // datasig: unlocker should not throw when given a proper length, but transaction should fail on invalid sig + // TODO: This somehow fails with "FailedRequireError" instead of "FailedTransactionEvaluationError", check why + await expect(new TransactionBuilder({ provider }) + .addInputs(utxos, hodlVault.unlock.spend(signatureTemplate, placeholder(64), message)) + .addOutput({ to: to, amount: amount }) + .addOutput({ to: to, amount: changeAmount }) + .setLocktime(100_000) + .send()).rejects.toThrow('HodlVault.cash:26 Require statement failed at input 0 in contract HodlVault.cash at line 26'); + + // datasig: unlocker should not throw when given an empty byte array, but transaction should fail on require statement + // Note that this fails with "FailedRequireError" because a zero-length signature IS a failed require statement + await expect(new TransactionBuilder({ provider }) + .addInputs(utxos, hodlVault.unlock.spend(signatureTemplate, placeholder(0), message)) + .addOutput({ to: to, amount: amount }) + .addOutput({ to: to, amount: changeAmount }) + .setLocktime(100_000) + .send()).rejects.toThrow('HodlVault.cash:26 Require statement failed at input 0 in contract HodlVault.cash at line 26'); + }); }); }); diff --git a/packages/cashscript/test/e2e/MultiContract.test.ts b/packages/cashscript/test/e2e/MultiContract.test.ts index 3167ec1c..8fc3ca66 100644 --- a/packages/cashscript/test/e2e/MultiContract.test.ts +++ b/packages/cashscript/test/e2e/MultiContract.test.ts @@ -87,7 +87,7 @@ describe('Multi Contract', () => { .addInput(bobAddressUtxos[0], bobSignatureTemplate.unlockP2PKH()) .addOutput({ to, amount }); - console.log(transaction.bitauthUri()); + console.log(transaction.getBitauthUri()); const txPromise = transaction.send(); @@ -173,7 +173,7 @@ describe('Multi Contract', () => { .addInput(bobAddressUtxos[0], bobSignatureTemplate.unlockP2PKH()) .addOutput({ to, amount }); - console.log(transaction.bitauthUri()); + console.log(transaction.getBitauthUri()); const txPromise = transaction.send(); diff --git a/packages/cashscript/test/e2e/P2PKH-tokens.test.ts b/packages/cashscript/test/e2e/P2PKH-tokens.test.ts index e61dc0d7..e6e7e12e 100644 --- a/packages/cashscript/test/e2e/P2PKH-tokens.test.ts +++ b/packages/cashscript/test/e2e/P2PKH-tokens.test.ts @@ -1,6 +1,8 @@ import { randomUtxo, randomToken, randomNFT } from '../../src/utils.js'; import { Contract, SignatureTemplate, ElectrumNetworkProvider, MockNetworkProvider, + TransactionBuilder, + NetworkProvider, } from '../../src/index.js'; import { alicePkh, @@ -11,11 +13,13 @@ import { getTxOutputs } from '../test-util.js'; import { Network, TokenDetails, Utxo } from '../../src/interfaces.js'; import artifact from '../fixture/p2pkh.artifact.js'; +// TODO: Replace this with unlockers describe('P2PKH-tokens', () => { let p2pkhInstance: Contract; + let provider: NetworkProvider; beforeAll(() => { - const provider = process.env.TESTS_USE_CHIPNET + provider = process.env.TESTS_USE_CHIPNET ? new ElectrumNetworkProvider(Network.CHIPNET) : new MockNetworkProvider(); @@ -60,15 +64,19 @@ describe('P2PKH-tokens', () => { throw new Error('No token UTXO found with fungible tokens'); } + const fullBchBalance = nonTokenUtxos.reduce((total, utxo) => total + utxo.satoshis, 0n) + tokenUtxo.satoshis; + const to = p2pkhInstance.tokenAddress; const amount = 1000n; const { token } = tokenUtxo; - - const tx = await p2pkhInstance.functions - .spend(alicePub, new SignatureTemplate(alicePriv)) - .from(nonTokenUtxos) - .from(tokenUtxo) - .to(to, amount, token) + const fee = 1000n; + const changeAmount = fullBchBalance - fee - amount; + + const tx = await new TransactionBuilder({ provider }) + .addInputs(nonTokenUtxos, p2pkhInstance.unlock.spend(alicePub, new SignatureTemplate(alicePriv))) + .addInput(tokenUtxo, p2pkhInstance.unlock.spend(alicePub, new SignatureTemplate(alicePriv))) + .addOutput({ to, amount, token }) + .addOutput({ to, amount: changeAmount }) .send(); const txOutputs = getTxOutputs(tx); @@ -86,24 +94,21 @@ describe('P2PKH-tokens', () => { const to = p2pkhInstance.tokenAddress; const amount = 1000n; + const fee = 1000n; + const fullBchBalance = nftUtxo1.satoshis + nftUtxo2.satoshis + nonTokenUtxos.reduce( + (total, utxo) => total + utxo.satoshis, 0n, + ); + const changeAmount = fullBchBalance - fee - amount; - // We ran into a bug with the order of the properties, so we re-order the properties here to test that it works - const reorderedToken1 = { - nft: { - commitment: nftUtxo1.token!.nft!.commitment, - capability: nftUtxo1.token!.nft!.capability, - }, - category: nftUtxo1.token!.category, - amount: 0n, - }; + const unlocker = p2pkhInstance.unlock.spend(alicePub, new SignatureTemplate(alicePriv)); - const tx = await p2pkhInstance.functions - .spend(alicePub, new SignatureTemplate(alicePriv)) - .from(nonTokenUtxos) - .from(nftUtxo1) - .from(nftUtxo2) - .to(to, amount, reorderedToken1) - .to(to, amount, nftUtxo2.token) + const tx = await new TransactionBuilder({ provider }) + .addInputs(nonTokenUtxos, unlocker) + .addInput(nftUtxo1, unlocker) + .addInput(nftUtxo2, unlocker) + .addOutput({ to, amount, token: nftUtxo1.token }) + .addOutput({ to, amount, token: nftUtxo2.token }) + .addOutput({ to, amount: changeAmount }) .send(); const txOutputs = getTxOutputs(tx); @@ -112,59 +117,23 @@ describe('P2PKH-tokens', () => { ); }); - it('can automatically select UTXOs for fungible tokens', async () => { - const contractUtxos = await p2pkhInstance.getUtxos(); - const tokenUtxo = contractUtxos.find(isFungibleTokenUtxo); - - if (!tokenUtxo) { - throw new Error('No token UTXO found with fungible tokens'); - } - + it('can create new token category (NFT and fungible token)', async () => { + const fee = 1000n; const to = p2pkhInstance.tokenAddress; - const amount = 1000n; - const { token } = tokenUtxo; - const tx = await p2pkhInstance.functions - .spend(alicePub, new SignatureTemplate(alicePriv)) - .to(to, amount, token) - .send(); + // As a prerequisite to creating a new token category, we need a vout0 UTXO, so we create one here + const nonTokenUtxosBeforeGenesis = (await p2pkhInstance.getUtxos()).filter(isNonTokenUtxo); + const preGenesisAmount = 10_000n; + const fullBchBalance = nonTokenUtxosBeforeGenesis.reduce((total, utxo) => total + utxo.satoshis, 0n); + const preGenesisChangeAmount = fullBchBalance - fee - preGenesisAmount; - const txOutputs = getTxOutputs(tx); - expect(txOutputs).toEqual(expect.arrayContaining([{ to, amount, token }])); - }); - - it('adds automatic change output for fungible tokens', async () => { - const contractUtxos = await p2pkhInstance.getUtxos(); - const tokenUtxo = contractUtxos.find(isFungibleTokenUtxo); - const nonTokenUtxos = contractUtxos.filter(isNonTokenUtxo); - - if (!tokenUtxo) { - throw new Error('No token UTXO found with fungible tokens'); - } - - const to = p2pkhInstance.tokenAddress; - const amount = 1000n; - const { token } = tokenUtxo; - - const tx = await p2pkhInstance.functions - .spend(alicePub, new SignatureTemplate(alicePriv)) - .from(nonTokenUtxos) - .from(tokenUtxo) - .to(to, amount) + await new TransactionBuilder({ provider }) + .addInputs(nonTokenUtxosBeforeGenesis, p2pkhInstance.unlock.spend(alicePub, new SignatureTemplate(alicePriv))) + .addOutput({ to, amount: preGenesisAmount }) + .addOutput({ to, amount: preGenesisChangeAmount }) .send(); - const txOutputs = getTxOutputs(tx); - expect(txOutputs).toEqual(expect.arrayContaining([{ to, amount, token }])); - }); - - it('can create new token categories', async () => { - const to = p2pkhInstance.tokenAddress; - - // Send a transaction to be used as the genesis UTXO - await p2pkhInstance.functions - .spend(alicePub, new SignatureTemplate(alicePriv)) - .to(to, 10_000n) - .send(); + ////////////////////////////////////////////////////////////////////////////////////////////////// const contractUtxos = await p2pkhInstance.getUtxos(); const [genesisUtxo] = contractUtxos.filter((utxo) => utxo.vout === 0 && utxo.satoshis > 2000); @@ -174,6 +143,7 @@ describe('P2PKH-tokens', () => { } const amount = 1000n; + const changeAmount = genesisUtxo.satoshis - fee - amount; const token: TokenDetails = { amount: 1000n, category: genesisUtxo.txid, @@ -183,96 +153,99 @@ describe('P2PKH-tokens', () => { }, }; - const tx = await p2pkhInstance.functions - .spend(alicePub, new SignatureTemplate(alicePriv)) - .from(genesisUtxo) - .to(to, amount, token) + const tx = await new TransactionBuilder({ provider }) + .addInput(genesisUtxo, p2pkhInstance.unlock.spend(alicePub, new SignatureTemplate(alicePriv))) + .addOutput({ to, amount, token }) + .addOutput({ to, amount: changeAmount }) .send(); const txOutputs = getTxOutputs(tx); expect(txOutputs).toEqual(expect.arrayContaining([{ to, amount, token }])); }); - it('adds automatic change output for NFTs', async () => { + it('should throw an error when trying to send more tokens than the contract has', async () => { const contractUtxos = await p2pkhInstance.getUtxos(); - const nftUtxo = contractUtxos.find(isNftUtxo); + const tokenUtxo = contractUtxos.find(isFungibleTokenUtxo); const nonTokenUtxos = contractUtxos.filter(isNonTokenUtxo); - if (!nftUtxo) { - throw new Error('No token UTXO found with an NFT'); + if (!tokenUtxo) { + throw new Error('No token UTXO found with fungible tokens'); } const to = p2pkhInstance.tokenAddress; const amount = 1000n; - const { token } = nftUtxo; + const token = { ...tokenUtxo.token!, amount: tokenUtxo.token!.amount + 1n }; + const fee = 1000n; + const fullBchBalance = nonTokenUtxos.reduce((total, utxo) => total + utxo.satoshis, 0n) + tokenUtxo.satoshis; + const changeAmount = fullBchBalance - fee - amount; + + const unlocker = p2pkhInstance.unlock.spend(alicePub, new SignatureTemplate(alicePriv)); - const tx = await p2pkhInstance.functions - .spend(alicePub, new SignatureTemplate(alicePriv)) - .from(nonTokenUtxos) - .from(nftUtxo) - .to(to, amount) + const txPromise = new TransactionBuilder({ provider }) + .addInputs(nonTokenUtxos, unlocker) + .addInput(tokenUtxo, unlocker) + .addOutput({ to, amount, token }) + .addOutput({ to, amount: changeAmount }) .send(); - const txOutputs = getTxOutputs(tx); - expect(txOutputs).toEqual(expect.arrayContaining([{ to, amount, token }])); + await expect(txPromise).rejects.toThrow( + /the sum of fungible tokens in the transaction outputs exceed that of the transaction inputs for a category/, + ); }); - it('can disable automatic change output for fungible tokens', async () => { + it('should throw an error when trying to send a token the contract doesn\'t have', async () => { const contractUtxos = await p2pkhInstance.getUtxos(); - const tokenUtxo = contractUtxos.find(isFungibleTokenUtxo); const nonTokenUtxos = contractUtxos.filter(isNonTokenUtxo); - if (!tokenUtxo) { - throw new Error('No token UTXO found with fungible tokens'); - } - const to = p2pkhInstance.tokenAddress; const amount = 1000n; - const token = { ...tokenUtxo.token!, amount: tokenUtxo.token!.amount - 1n }; - - const tx = await p2pkhInstance.functions - .spend(alicePub, new SignatureTemplate(alicePriv)) - .from(nonTokenUtxos) - .from(tokenUtxo) - .to(to, amount, token) - .withoutTokenChange() + const token = { category: '0000000000000000000000000000000000000000000000000000000000000000', amount: 100n }; + const fee = 1000n; + const fullBchBalance = nonTokenUtxos.reduce((total, utxo) => total + utxo.satoshis, 0n); + const changeAmount = fullBchBalance - fee - amount; + + const unlocker = p2pkhInstance.unlock.spend(alicePub, new SignatureTemplate(alicePriv)); + const txPromise = new TransactionBuilder({ provider }) + .addInputs(nonTokenUtxos, unlocker) + .addOutput({ to, amount, token }) + .addOutput({ to, amount: changeAmount }) .send(); - const txOutputs = getTxOutputs(tx); - expect(txOutputs).toEqual(expect.arrayContaining([{ to, amount, token }])); - - // Check that the change output is not present - txOutputs.forEach((output) => { - expect(output.token?.amount).not.toEqual(1n); - }); + await expect(txPromise).rejects.toThrow( + /the transaction creates new fungible tokens for a category without a matching genesis input/, + ); }); - it.todo('can disable automatic change output for NFTs'); - - it('should throw an error when trying to send more tokens than the contract has', async () => { + it('should throw an error when trying to send an NFT the contract doesn\'t have', async () => { const contractUtxos = await p2pkhInstance.getUtxos(); - const tokenUtxo = contractUtxos.find(isFungibleTokenUtxo); + const nftUtxo = contractUtxos.find(isNftUtxo); const nonTokenUtxos = contractUtxos.filter(isNonTokenUtxo); - if (!tokenUtxo) { - throw new Error('No token UTXO found with fungible tokens'); + if (!nftUtxo) { + throw new Error('No token UTXO found with an NFT'); } const to = p2pkhInstance.tokenAddress; const amount = 1000n; - const token = { ...tokenUtxo.token!, amount: tokenUtxo.token!.amount + 1n }; - - const txPromise = p2pkhInstance.functions - .spend(alicePub, new SignatureTemplate(alicePriv)) - .from(nonTokenUtxos) - .from(tokenUtxo) - .to(to, amount, token) + const token = { ...nftUtxo.token!, category: '0000000000000000000000000000000000000000000000000000000000000000' }; + const fee = 1000n; + const fullBchBalance = nonTokenUtxos.reduce((total, utxo) => total + utxo.satoshis, 0n) + nftUtxo.satoshis; + const changeAmount = fullBchBalance - fee - amount; + + const unlocker = p2pkhInstance.unlock.spend(alicePub, new SignatureTemplate(alicePriv)); + const txPromise = new TransactionBuilder({ provider }) + .addInputs(nonTokenUtxos, unlocker) + .addInput(nftUtxo, unlocker) + .addOutput({ to, amount, token }) + .addOutput({ to, amount: changeAmount }) .send(); - await expect(txPromise).rejects.toThrow(/Insufficient funds for token/); + await expect(txPromise).rejects.toThrow( + /the transaction creates an immutable token for a category without a matching minting token/, + ); }); - it('should throw an error when trying to send a token the contract doesn\'t have', async () => { + it('cannot burn fungible tokens when allowImplicitFungibleTokenBurn is false (default)', async () => { const contractUtxos = await p2pkhInstance.getUtxos(); const tokenUtxo = contractUtxos.find(isFungibleTokenUtxo); const nonTokenUtxos = contractUtxos.filter(isNonTokenUtxo); @@ -283,44 +256,48 @@ describe('P2PKH-tokens', () => { const to = p2pkhInstance.tokenAddress; const amount = 1000n; - const token = { ...tokenUtxo.token!, category: '0000000000000000000000000000000000000000000000000000000000000000' }; - - const txPromise = p2pkhInstance.functions - .spend(alicePub, new SignatureTemplate(alicePriv)) - .from(nonTokenUtxos) - .from(tokenUtxo) - .to(to, amount, token) + const token = { ...tokenUtxo.token!, amount: tokenUtxo.token!.amount - 1n }; + const fee = 1000n; + const fullBchBalance = nonTokenUtxos.reduce((total, utxo) => total + utxo.satoshis, 0n) + tokenUtxo.satoshis; + const changeAmount = fullBchBalance - fee - amount; + + const unlocker = p2pkhInstance.unlock.spend(alicePub, new SignatureTemplate(alicePriv)); + const txPromise = new TransactionBuilder({ provider }) + .addInputs(nonTokenUtxos, unlocker) + .addInput(tokenUtxo, unlocker) + .addOutput({ to, amount, token }) + .addOutput({ to, amount: changeAmount }) .send(); - await expect(txPromise).rejects.toThrow(/Insufficient funds for token/); + await expect(txPromise).rejects.toThrow('Implicit burning of fungible tokens for category'); }); - it('should throw an error when trying to send an NFT the contract doesn\'t have', async () => { + it('can burn fungible tokens when allowImplicitFungibleTokenBurn is true', async () => { const contractUtxos = await p2pkhInstance.getUtxos(); - const nftUtxo = contractUtxos.find(isNftUtxo); + const tokenUtxo = contractUtxos.find(isFungibleTokenUtxo); const nonTokenUtxos = contractUtxos.filter(isNonTokenUtxo); - if (!nftUtxo) { - throw new Error('No token UTXO found with an NFT'); + if (!tokenUtxo) { + throw new Error('No token UTXO found with fungible tokens'); } const to = p2pkhInstance.tokenAddress; const amount = 1000n; - const token = { ...nftUtxo.token!, category: '0000000000000000000000000000000000000000000000000000000000000000' }; - - const txPromise = p2pkhInstance.functions - .spend(alicePub, new SignatureTemplate(alicePriv)) - .from(nonTokenUtxos) - .from(nftUtxo) - .to(to, amount, token) + const token = { ...tokenUtxo.token!, amount: tokenUtxo.token!.amount - 1n }; + const fee = 1000n; + const fullBchBalance = nonTokenUtxos.reduce((total, utxo) => total + utxo.satoshis, 0n) + tokenUtxo.satoshis; + const changeAmount = fullBchBalance - fee - amount; + + const unlocker = p2pkhInstance.unlock.spend(alicePub, new SignatureTemplate(alicePriv)); + const txPromise = new TransactionBuilder({ provider, allowImplicitFungibleTokenBurn: true }) + .addInputs(nonTokenUtxos, unlocker) + .addInput(tokenUtxo, unlocker) + .addOutput({ to, amount, token }) + .addOutput({ to, amount: changeAmount }) .send(); - await expect(txPromise).rejects.toThrow(/NFT output with token category .* does not have corresponding input/); + await expect(txPromise).resolves.toBeDefined(); }); - - it.todo('can mint new NFTs if the NFT has minting capabilities'); - it.todo('can change the NFT commitment if the NFT has mutable capabilities'); - // TODO: Add more edge case tests for NFTs (minting, mutable, change outputs with multiple kinds of NFTs) }); }); diff --git a/packages/cashscript/test/e2e/network/MockNetworkProvider.test.ts b/packages/cashscript/test/e2e/network/MockNetworkProvider.test.ts index e6f8aa19..6f9c2668 100644 --- a/packages/cashscript/test/e2e/network/MockNetworkProvider.test.ts +++ b/packages/cashscript/test/e2e/network/MockNetworkProvider.test.ts @@ -13,8 +13,8 @@ import { import { describeOrSkip } from '../../test-util.js'; describeOrSkip(!process.env.TESTS_USE_CHIPNET, 'MockNetworkProvider', () => { - describe('when updateUtxoSet is true', () => { - const provider = new MockNetworkProvider({ updateUtxoSet: true }); + describe('when updateUtxoSet is default (true)', () => { + const provider = new MockNetworkProvider(); let p2pkhInstance: Contract; @@ -69,8 +69,8 @@ describeOrSkip(!process.env.TESTS_USE_CHIPNET, 'MockNetworkProvider', () => { }); }); - describe('when updateUtxoSet is default (false)', () => { - const provider = new MockNetworkProvider(); + describe('when updateUtxoSet is set to false', () => { + const provider = new MockNetworkProvider({ updateUtxoSet: false }); let p2pkhInstance: Contract; diff --git a/packages/cashscript/test/fixture/PriceOracle.ts b/packages/cashscript/test/fixture/PriceOracle.ts index eb174099..82d354e1 100644 --- a/packages/cashscript/test/fixture/PriceOracle.ts +++ b/packages/cashscript/test/fixture/PriceOracle.ts @@ -1,8 +1,9 @@ -import { padMinimallyEncodedVmNumber, flattenBinArray, secp256k1 } from '@bitauth/libauth'; +import { padMinimallyEncodedVmNumber, flattenBinArray } from '@bitauth/libauth'; import { encodeInt, sha256 } from '@cashscript/utils'; +import { SignatureAlgorithm, SignatureTemplate } from '../../src/index.js'; export class PriceOracle { - constructor(public privateKey: Uint8Array) {} + constructor(public privateKey: Uint8Array) { } // Encode a blockHeight and bchUsdPrice into a byte sequence of 8 bytes (4 bytes per value) createMessage(blockHeight: bigint, bchUsdPrice: bigint): Uint8Array { @@ -12,9 +13,8 @@ export class PriceOracle { return flattenBinArray([encodedBlockHeight, encodedBchUsdPrice]); } - signMessage(message: Uint8Array): Uint8Array { - const signature = secp256k1.signMessageHashSchnorr(this.privateKey, sha256(message)); - if (typeof signature === 'string') throw new Error(); - return signature; + signMessage(message: Uint8Array, signatureAlgorithm: SignatureAlgorithm = SignatureAlgorithm.SCHNORR): Uint8Array { + const signatureTemplate = new SignatureTemplate(this.privateKey, undefined, signatureAlgorithm); + return signatureTemplate.signMessageHash(sha256(message)); } } diff --git a/packages/cashscript/test/fixture/libauth-template/fixtures.ts b/packages/cashscript/test/fixture/libauth-template/fixtures.ts index de27e2b3..d0a7857f 100644 --- a/packages/cashscript/test/fixture/libauth-template/fixtures.ts +++ b/packages/cashscript/test/fixture/libauth-template/fixtures.ts @@ -99,8 +99,6 @@ export const fixtures: Fixture[] = [ 'timeout': '0xa08601', 'function_index': '0', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': { 'recipientSig': '71080d8b52ec7b12adaec909ed54cd989b682ce2c35647eec219a16f5f90c528', @@ -233,8 +231,6 @@ export const fixtures: Fixture[] = [ 'timeout': '0xa08601', 'function_index': '1', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': { 'senderSig': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', @@ -366,8 +362,6 @@ export const fixtures: Fixture[] = [ 'pledge': '0x1027', 'function_index': '0', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': {}, }, @@ -528,8 +522,6 @@ export const fixtures: Fixture[] = [ 'pk': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', 'pkh': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': { 's': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', @@ -618,8 +610,6 @@ export const fixtures: Fixture[] = [ 'pk': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', 'pkh': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': { 's': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', @@ -781,8 +771,6 @@ export const fixtures: Fixture[] = [ 's': '0x65f72c5cce773383b45032a3f9f9255814e3d53ee260056e3232cd89e91a0a84278b35daf8938d47047e7d3bd3407fe90b07dfabf4407947af6fb09730a34c0b61', 'pkh': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': {}, }, @@ -937,8 +925,6 @@ export const fixtures: Fixture[] = [ 'pk': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', 'pkh': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': { 's': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', @@ -1035,8 +1021,6 @@ export const fixtures: Fixture[] = [ 'pk': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', 'pkh': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': { 's': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', @@ -1233,8 +1217,6 @@ export const fixtures: Fixture[] = [ 'minBlock': '0xb88201', 'priceTarget': '0x3075', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': { 'ownerSig': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', @@ -1368,13 +1350,18 @@ export const fixtures: Fixture[] = [ 'p2pkh_placeholder_lock_0', 'p2pkh_placeholder_unlock_0', ], - 'description': 'placeholder_key_0', + 'description': 'P2PKH data for input 0', 'name': 'P2PKH Signer (input #0)', 'variables': { - 'placeholder_key_0': { + 'signature_0': { 'description': '', - 'name': 'P2PKH Placeholder Key (input #0)', - 'type': 'Key', + 'name': 'P2PKH Signature (input #0)', + 'type': 'WalletData', + }, + 'public_key_0': { + 'description': '', + 'name': 'P2PKH public key (input #0)', + 'type': 'WalletData', }, }, }, @@ -1383,13 +1370,18 @@ export const fixtures: Fixture[] = [ 'p2pkh_placeholder_lock_2', 'p2pkh_placeholder_unlock_2', ], - 'description': 'placeholder_key_2', + 'description': 'P2PKH data for input 2', 'name': 'P2PKH Signer (input #2)', 'variables': { - 'placeholder_key_2': { + 'signature_2': { 'description': '', - 'name': 'P2PKH Placeholder Key (input #2)', - 'type': 'Key', + 'name': 'P2PKH Signature (input #2)', + 'type': 'WalletData', + }, + 'public_key_2': { + 'description': '', + 'name': 'P2PKH public key (input #2)', + 'type': 'WalletData', }, }, }, @@ -1409,27 +1401,134 @@ export const fixtures: Fixture[] = [ 'script': '// "P2PKH" contract constructor parameters\n // bytes20 = <0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970>\n\n// bytecode\n /* contract P2PKH(bytes20 pkh) { */\n /* // Require pk to match stored pkh and signature to match */\n /* function spend(pubkey pk, sig s) { */\nOP_OVER OP_HASH160 OP_EQUALVERIFY /* require(hash160(pk) == pkh); */\nOP_CHECKSIG /* require(checkSig(s, pk)); */\n /* } */\n /* } */\n /* */', }, 'p2pkh_placeholder_unlock_0': { + 'passes': [ + 'P2PKH_spend_input0_evaluate', + ], 'name': 'P2PKH Unlock (input #0)', - 'script': '\n', + 'script': '\n', 'unlocks': 'p2pkh_placeholder_lock_0', }, 'p2pkh_placeholder_lock_0': { 'lockingType': 'standard', 'name': 'P2PKH Lock (input #0)', - 'script': 'OP_DUP\nOP_HASH160 <$( OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG', + 'script': 'OP_DUP\nOP_HASH160 <$( OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG', }, 'p2pkh_placeholder_unlock_2': { + 'passes': [ + 'P2PKH_spend_input2_evaluate', + ], 'name': 'P2PKH Unlock (input #2)', - 'script': '\n', + 'script': '\n', 'unlocks': 'p2pkh_placeholder_lock_2', }, 'p2pkh_placeholder_lock_2': { 'lockingType': 'standard', 'name': 'P2PKH Lock (input #2)', - 'script': 'OP_DUP\nOP_HASH160 <$( OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG', + 'script': 'OP_DUP\nOP_HASH160 <$( OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG', }, }, 'scenarios': { + 'P2PKH_spend_input0_evaluate': { + 'name': 'Evaluate P2PKH spend (input #0)', + 'description': 'An example evaluation where this script execution passes.', + 'data': { + 'bytecode': { + 'signature_0': expect.stringMatching(/^0x[0-9a-f]{130}$/), + 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + }, + }, + 'transaction': { + 'inputs': [ + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.stringMatching(/^[0-9a-f]{64}$/), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': [ + 'slot', + ], + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.stringMatching(/^[0-9a-f]{64}$/), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'P2PKH_spend_input1_unlock', + 'overrides': { + 'bytecode': { + 'pk': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + 'pkh': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + 'keys': { + 'privateKeys': { + 's': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', + }, + }, + }, + }, + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.stringMatching(/^[0-9a-f]{64}$/), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'p2pkh_placeholder_unlock_2', + 'overrides': { + 'bytecode': { + 'signature_2': expect.stringMatching(/^0x[0-9a-f]{142,146}$/), + 'public_key_2': '0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38', + }, + }, + }, + }, + ], + 'locktime': expect.any(Number), + 'outputs': [ + { + 'lockingBytecode': { + 'script': 'P2PKH_eae136efb95be487872bfe03984fc1eb80b23361_lock', + 'overrides': { + 'bytecode': { + 'pkh': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + }, + }, + 'valueSatoshis': 1000, + }, + ], + 'version': 2, + }, + 'sourceOutputs': [ + { + 'lockingBytecode': [ + 'slot', + ], + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': { + 'script': 'P2PKH_eae136efb95be487872bfe03984fc1eb80b23361_lock', + 'overrides': { + 'bytecode': { + 'pkh': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': { + 'script': 'p2pkh_placeholder_lock_2', + 'overrides': { + 'bytecode': { + 'signature_2': expect.stringMatching(/^0x[0-9a-f]{142,146}$/), + 'public_key_2': '0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + ], + }, 'P2PKH_spend_input1_evaluate': { 'name': 'Evaluate P2PKH spend (input #1)', 'description': 'An example evaluation where this script execution passes.', @@ -1438,8 +1537,6 @@ export const fixtures: Fixture[] = [ 'pk': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', 'pkh': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': { 's': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', @@ -1455,10 +1552,9 @@ export const fixtures: Fixture[] = [ 'unlockingBytecode': { 'script': 'p2pkh_placeholder_unlock_0', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_0': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_0': expect.stringMatching(/^0x[0-9a-f]{130}$/), + 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -1478,16 +1574,15 @@ export const fixtures: Fixture[] = [ 'unlockingBytecode': { 'script': 'p2pkh_placeholder_unlock_2', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_2': '71080d8b52ec7b12adaec909ed54cd989b682ce2c35647eec219a16f5f90c528', - }, + 'bytecode': { + 'signature_2': expect.stringMatching(/^0x[0-9a-f]{142,146}$/), + 'public_key_2': '0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38', }, }, }, }, ], - 'locktime': 0, + 'locktime': expect.any(Number), 'outputs': [ { 'lockingBytecode': { @@ -1508,10 +1603,9 @@ export const fixtures: Fixture[] = [ 'lockingBytecode': { 'script': 'p2pkh_placeholder_lock_0', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_0': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_0': expect.stringMatching(/^0x[0-9a-f]{130}$/), + 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -1527,13 +1621,113 @@ export const fixtures: Fixture[] = [ 'lockingBytecode': { 'script': 'p2pkh_placeholder_lock_2', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_2': '71080d8b52ec7b12adaec909ed54cd989b682ce2c35647eec219a16f5f90c528', + 'bytecode': { + 'signature_2': expect.stringMatching(/^0x[0-9a-f]{142,146}$/), + 'public_key_2': '0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + ], + }, + 'P2PKH_spend_input2_evaluate': { + 'name': 'Evaluate P2PKH spend (input #2)', + 'description': 'An example evaluation where this script execution passes.', + 'data': { + 'bytecode': { + 'signature_2': expect.stringMatching(/^0x[0-9a-f]{142,146}$/), + 'public_key_2': '0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38', + }, + }, + 'transaction': { + 'inputs': [ + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.stringMatching(/^[0-9a-f]{64}$/), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'p2pkh_placeholder_unlock_0', + 'overrides': { + 'bytecode': { + 'signature_0': expect.stringMatching(/^0x[0-9a-f]{130}$/), + 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + }, + }, + }, + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.stringMatching(/^[0-9a-f]{64}$/), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'P2PKH_spend_input1_unlock', + 'overrides': { + 'bytecode': { + 'pk': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + 'pkh': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + 'keys': { + 'privateKeys': { + 's': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', + }, }, }, }, }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.stringMatching(/^[0-9a-f]{64}$/), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': [ + 'slot', + ], + }, + ], + 'locktime': expect.any(Number), + 'outputs': [ + { + 'lockingBytecode': { + 'script': 'P2PKH_eae136efb95be487872bfe03984fc1eb80b23361_lock', + 'overrides': { + 'bytecode': { + 'pkh': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + }, + }, + 'valueSatoshis': 1000, + }, + ], + 'version': 2, + }, + 'sourceOutputs': [ + { + 'lockingBytecode': { + 'script': 'p2pkh_placeholder_lock_0', + 'overrides': { + 'bytecode': { + 'signature_0': expect.stringMatching(/^0x[0-9a-f]{130}$/), + 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': { + 'script': 'P2PKH_eae136efb95be487872bfe03984fc1eb80b23361_lock', + 'overrides': { + 'bytecode': { + 'pkh': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': [ + 'slot', + ], 'valueSatoshis': expect.any(Number), }, ], diff --git a/packages/cashscript/test/fixture/libauth-template/multi-contract-fixtures.ts b/packages/cashscript/test/fixture/libauth-template/multi-contract-fixtures.ts index 1631b1dc..e22dfaf9 100644 --- a/packages/cashscript/test/fixture/libauth-template/multi-contract-fixtures.ts +++ b/packages/cashscript/test/fixture/libauth-template/multi-contract-fixtures.ts @@ -167,122 +167,896 @@ export const fixtures: Fixture[] = [ 'p2pkh_placeholder_lock_0', 'p2pkh_placeholder_unlock_0', ], - 'description': 'placeholder_key_0', + 'description': 'P2PKH data for input 0', 'name': 'P2PKH Signer (input #0)', 'variables': { - 'placeholder_key_0': { + 'signature_0': { 'description': '', - 'name': 'P2PKH Placeholder Key (input #0)', - 'type': 'Key', + 'name': 'P2PKH Signature (input #0)', + 'type': 'WalletData', + }, + 'public_key_0': { + 'description': '', + 'name': 'P2PKH public key (input #0)', + 'type': 'WalletData', + }, + }, + }, + 'signer_1': { + 'scripts': [ + 'p2pkh_placeholder_lock_1', + 'p2pkh_placeholder_unlock_1', + ], + 'description': 'P2PKH data for input 1', + 'name': 'P2PKH Signer (input #1)', + 'variables': { + 'signature_1': { + 'description': '', + 'name': 'P2PKH Signature (input #1)', + 'type': 'WalletData', + }, + 'public_key_1': { + 'description': '', + 'name': 'P2PKH public key (input #1)', + 'type': 'WalletData', + }, + }, + }, + 'signer_2': { + 'scripts': [ + 'p2pkh_placeholder_lock_2', + 'p2pkh_placeholder_unlock_2', + ], + 'description': 'P2PKH data for input 2', + 'name': 'P2PKH Signer (input #2)', + 'variables': { + 'signature_2': { + 'description': '', + 'name': 'P2PKH Signature (input #2)', + 'type': 'WalletData', + }, + 'public_key_2': { + 'description': '', + 'name': 'P2PKH public key (input #2)', + 'type': 'WalletData', + }, + }, + }, + }, + 'scripts': { + 'Bar_funcA_input3_unlock': { + 'passes': [ + 'Bar_funcA_input3_evaluate', + ], + 'name': 'funcA (input #3)', + 'script': '// "funcA" function parameters\n// none\n\n// function index in contract\n // int = <0>\n', + 'unlocks': 'Bar_dfa9a690eb3692ca0655d91a1bebf908bd27f73faf31ec7fe316bde6c0fbed2e_lock', + }, + 'Bar_dfa9a690eb3692ca0655d91a1bebf908bd27f73faf31ec7fe316bde6c0fbed2e_lock': { + 'lockingType': 'p2sh32', + 'name': 'Bar', + 'script': "// \"Bar\" contract constructor parameters\n // bytes20 = <0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970>\n\n// bytecode\n /* pragma cashscript >=0.10.2; */\n /* */\n /* contract Bar(bytes20 pkh_bar) { */\nOP_OVER OP_0 OP_NUMEQUAL OP_IF /* function funcA() { */\nOP_2 OP_2 OP_NUMEQUAL /* require(2==2); */\nOP_NIP OP_NIP OP_ELSE /* } */\n /* */\nOP_OVER OP_1 OP_NUMEQUAL OP_IF /* function funcB() { */\nOP_2 OP_2 OP_NUMEQUAL /* require(2==2); */\nOP_NIP OP_NIP OP_ELSE /* } */\n /* */\nOP_SWAP OP_2 OP_NUMEQUALVERIFY /* function execute(pubkey pk, sig s) { */\n /* console.log(\"Bar 'execute' function called.\"); */\nOP_OVER OP_HASH160 OP_EQUALVERIFY /* require(hash160(pk) == pkh_bar); */\nOP_CHECKSIG /* require(checkSig(s, pk)); */\n /* } */\nOP_ENDIF OP_ENDIF /* } */\n /* */", + }, + 'Bar_execute_input4_unlock': { + 'passes': [ + 'Bar_execute_input4_evaluate', + ], + 'name': 'execute (input #4)', + 'script': '// "execute" function parameters\n // sig\n // pubkey = <0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088>\n\n// function index in contract\n // int = <2>\n', + 'unlocks': 'Bar_dfa9a690eb3692ca0655d91a1bebf908bd27f73faf31ec7fe316bde6c0fbed2e_lock', + }, + 'Foo_execute_input5_unlock': { + 'passes': [ + 'Foo_execute_input5_evaluate', + ], + 'name': 'execute (input #5)', + 'script': '// "execute" function parameters\n // sig\n // pubkey = <0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38>\n', + 'unlocks': 'Foo_432c93902a8a8e49ef246028e707cddaa67a39af46b2b3c11c196cd09c931746_lock', + }, + 'Foo_432c93902a8a8e49ef246028e707cddaa67a39af46b2b3c11c196cd09c931746_lock': { + 'lockingType': 'p2sh32', + 'name': 'Foo', + 'script': "// \"Foo\" contract constructor parameters\n // bytes20 = <0xb40a2013337edb0dfe307f0a57d5dec5bfe60dd0>\n\n// bytecode\n /* pragma cashscript >=0.10.2; */\n /* */\n /* contract Foo(bytes20 pkh_foo) { */\n /* // Require pk to match stored pkh and signature to match */\n /* function execute(pubkey pk, sig s) { */\n /* console.log(\"Foo 'execute' function called.\"); */\nOP_OVER OP_HASH160 OP_EQUALVERIFY /* require(hash160(pk) == pkh_foo); */\nOP_CHECKSIG /* require(checkSig(s, pk)); */\n /* } */\n /* } */\n /* */", + }, + 'Bar_funcB_input6_unlock': { + 'passes': [ + 'Bar_funcB_input6_evaluate', + ], + 'name': 'funcB (input #6)', + 'script': '// "funcB" function parameters\n// none\n\n// function index in contract\n // int = <1>\n', + 'unlocks': 'Bar_dfa9a690eb3692ca0655d91a1bebf908bd27f73faf31ec7fe316bde6c0fbed2e_lock', + }, + 'p2pkh_placeholder_unlock_0': { + 'passes': [ + 'P2PKH_spend_input0_evaluate', + ], + 'name': 'P2PKH Unlock (input #0)', + 'script': '\n', + 'unlocks': 'p2pkh_placeholder_lock_0', + }, + 'p2pkh_placeholder_lock_0': { + 'lockingType': 'standard', + 'name': 'P2PKH Lock (input #0)', + 'script': 'OP_DUP\nOP_HASH160 <$( OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG', + }, + 'p2pkh_placeholder_unlock_1': { + 'passes': [ + 'P2PKH_spend_input1_evaluate', + ], + 'name': 'P2PKH Unlock (input #1)', + 'script': '\n', + 'unlocks': 'p2pkh_placeholder_lock_1', + }, + 'p2pkh_placeholder_lock_1': { + 'lockingType': 'standard', + 'name': 'P2PKH Lock (input #1)', + 'script': 'OP_DUP\nOP_HASH160 <$( OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG', + }, + 'p2pkh_placeholder_unlock_2': { + 'passes': [ + 'P2PKH_spend_input2_evaluate', + ], + 'name': 'P2PKH Unlock (input #2)', + 'script': '\n', + 'unlocks': 'p2pkh_placeholder_lock_2', + }, + 'p2pkh_placeholder_lock_2': { + 'lockingType': 'standard', + 'name': 'P2PKH Lock (input #2)', + 'script': 'OP_DUP\nOP_HASH160 <$( OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG', + }, + }, + 'scenarios': { + 'P2PKH_spend_input0_evaluate': { + 'name': 'Evaluate P2PKH spend (input #0)', + 'description': 'An example evaluation where this script execution passes.', + 'data': { + 'bytecode': { + 'signature_0': expect.any(String), + 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + }, + }, + 'transaction': { + 'inputs': [ + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': [ + 'slot', + ], + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'p2pkh_placeholder_unlock_1', + 'overrides': { + 'bytecode': { + 'signature_1': expect.any(String), + 'public_key_1': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + }, + }, + }, + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'p2pkh_placeholder_unlock_2', + 'overrides': { + 'bytecode': { + 'signature_2': expect.any(String), + 'public_key_2': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + }, + }, + }, + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'Bar_funcA_input3_unlock', + 'overrides': { + 'bytecode': { + 'function_index': '0', + 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + 'keys': { + 'privateKeys': {}, + }, + }, + }, + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'Bar_execute_input4_unlock', + 'overrides': { + 'bytecode': { + 'function_index': '2', + 'pk': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + 'keys': { + 'privateKeys': { + 's': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', + }, + }, + }, + }, + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'Foo_execute_input5_unlock', + 'overrides': { + 'bytecode': { + 'pk': '0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38', + 'pkh_foo': '0xb40a2013337edb0dfe307f0a57d5dec5bfe60dd0', + }, + 'keys': { + 'privateKeys': { + 's': '71080d8b52ec7b12adaec909ed54cd989b682ce2c35647eec219a16f5f90c528', + }, + }, + }, + }, + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'Bar_funcB_input6_unlock', + 'overrides': { + 'bytecode': { + 'function_index': '1', + 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + 'keys': { + 'privateKeys': {}, + }, + }, + }, + }, + ], + 'locktime': 0, + 'outputs': [ + { + 'lockingBytecode': { + 'script': 'Foo_432c93902a8a8e49ef246028e707cddaa67a39af46b2b3c11c196cd09c931746_lock', + 'overrides': { + 'bytecode': { + 'pkh_foo': '0xb40a2013337edb0dfe307f0a57d5dec5bfe60dd0', + }, + }, + }, + 'valueSatoshis': 8000, + }, + { + 'lockingBytecode': '76a914512dbb2c8c02efbac8d92431aa0ac33f6b0bf97088ac', + 'token': { + 'amount': '100000000', + 'category': expect.any(String), + }, + 'valueSatoshis': 800, + }, + { + 'lockingBytecode': '76a914512dbb2c8c02efbac8d92431aa0ac33f6b0bf97088ac', + 'token': { + 'amount': '0', + 'category': expect.any(String), + 'nft': { + 'capability': 'minting', + 'commitment': '00', + }, + }, + 'valueSatoshis': 1000, + }, + { + 'lockingBytecode': '6a0568656c6c6f05776f726c64', + 'valueSatoshis': 0, + }, + ], + 'version': 2, + }, + 'sourceOutputs': [ + { + 'lockingBytecode': [ + 'slot', + ], + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': { + 'script': 'p2pkh_placeholder_lock_1', + 'overrides': { + 'bytecode': { + 'signature_1': expect.any(String), + 'public_key_1': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + }, + }, + }, + 'token': { + 'amount': '100000000', + 'category': expect.stringMatching(/^[0-9a-f]{64}$/), + }, + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': { + 'script': 'p2pkh_placeholder_lock_2', + 'overrides': { + 'bytecode': { + 'signature_2': expect.any(String), + 'public_key_2': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + }, + }, + }, + 'token': { + 'amount': '0', + 'category': expect.stringMatching(/^[0-9a-f]{64}$/), + 'nft': { + 'capability': 'minting', + 'commitment': '00', + }, + }, + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': { + 'script': 'Bar_dfa9a690eb3692ca0655d91a1bebf908bd27f73faf31ec7fe316bde6c0fbed2e_lock', + 'overrides': { + 'bytecode': { + 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': { + 'script': 'Bar_dfa9a690eb3692ca0655d91a1bebf908bd27f73faf31ec7fe316bde6c0fbed2e_lock', + 'overrides': { + 'bytecode': { + 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': { + 'script': 'Foo_432c93902a8a8e49ef246028e707cddaa67a39af46b2b3c11c196cd09c931746_lock', + 'overrides': { + 'bytecode': { + 'pkh_foo': '0xb40a2013337edb0dfe307f0a57d5dec5bfe60dd0', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': { + 'script': 'Bar_dfa9a690eb3692ca0655d91a1bebf908bd27f73faf31ec7fe316bde6c0fbed2e_lock', + 'overrides': { + 'bytecode': { + 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + ], + }, + 'P2PKH_spend_input1_evaluate': { + 'name': 'Evaluate P2PKH spend (input #1)', + 'description': 'An example evaluation where this script execution passes.', + 'data': { + 'bytecode': { + 'signature_1': expect.any(String), + 'public_key_1': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + }, + }, + 'transaction': { + 'inputs': [ + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'p2pkh_placeholder_unlock_0', + 'overrides': { + 'bytecode': { + 'signature_0': expect.any(String), + 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + }, + }, + }, + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': [ + 'slot', + ], + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'p2pkh_placeholder_unlock_2', + 'overrides': { + 'bytecode': { + 'signature_2': expect.any(String), + 'public_key_2': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + }, + }, + }, + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'Bar_funcA_input3_unlock', + 'overrides': { + 'bytecode': { + 'function_index': '0', + 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + 'keys': { + 'privateKeys': {}, + }, + }, + }, + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'Bar_execute_input4_unlock', + 'overrides': { + 'bytecode': { + 'function_index': '2', + 'pk': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + 'keys': { + 'privateKeys': { + 's': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', + }, + }, + }, + }, + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'Foo_execute_input5_unlock', + 'overrides': { + 'bytecode': { + 'pk': '0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38', + 'pkh_foo': '0xb40a2013337edb0dfe307f0a57d5dec5bfe60dd0', + }, + 'keys': { + 'privateKeys': { + 's': '71080d8b52ec7b12adaec909ed54cd989b682ce2c35647eec219a16f5f90c528', + }, + }, + }, + }, + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'Bar_funcB_input6_unlock', + 'overrides': { + 'bytecode': { + 'function_index': '1', + 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + 'keys': { + 'privateKeys': {}, + }, + }, + }, + }, + ], + 'locktime': 0, + 'outputs': [ + { + 'lockingBytecode': { + 'script': 'Foo_432c93902a8a8e49ef246028e707cddaa67a39af46b2b3c11c196cd09c931746_lock', + 'overrides': { + 'bytecode': { + 'pkh_foo': '0xb40a2013337edb0dfe307f0a57d5dec5bfe60dd0', + }, + }, + }, + 'valueSatoshis': 8000, + }, + { + 'lockingBytecode': '76a914512dbb2c8c02efbac8d92431aa0ac33f6b0bf97088ac', + 'token': { + 'amount': '100000000', + 'category': expect.any(String), + }, + 'valueSatoshis': 800, + }, + { + 'lockingBytecode': '76a914512dbb2c8c02efbac8d92431aa0ac33f6b0bf97088ac', + 'token': { + 'amount': '0', + 'category': expect.any(String), + 'nft': { + 'capability': 'minting', + 'commitment': '00', + }, + }, + 'valueSatoshis': 1000, + }, + { + 'lockingBytecode': '6a0568656c6c6f05776f726c64', + 'valueSatoshis': 0, + }, + ], + 'version': 2, + }, + 'sourceOutputs': [ + { + 'lockingBytecode': { + 'script': 'p2pkh_placeholder_lock_0', + 'overrides': { + 'bytecode': { + 'signature_0': expect.any(String), + 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': [ + 'slot', + ], + 'token': { + 'amount': '100000000', + 'category': expect.stringMatching(/^[0-9a-f]{64}$/), + }, + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': { + 'script': 'p2pkh_placeholder_lock_2', + 'overrides': { + 'bytecode': { + 'signature_2': expect.any(String), + 'public_key_2': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + }, + }, + }, + 'token': { + 'amount': '0', + 'category': expect.stringMatching(/^[0-9a-f]{64}$/), + 'nft': { + 'capability': 'minting', + 'commitment': '00', + }, + }, + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': { + 'script': 'Bar_dfa9a690eb3692ca0655d91a1bebf908bd27f73faf31ec7fe316bde6c0fbed2e_lock', + 'overrides': { + 'bytecode': { + 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': { + 'script': 'Bar_dfa9a690eb3692ca0655d91a1bebf908bd27f73faf31ec7fe316bde6c0fbed2e_lock', + 'overrides': { + 'bytecode': { + 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': { + 'script': 'Foo_432c93902a8a8e49ef246028e707cddaa67a39af46b2b3c11c196cd09c931746_lock', + 'overrides': { + 'bytecode': { + 'pkh_foo': '0xb40a2013337edb0dfe307f0a57d5dec5bfe60dd0', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': { + 'script': 'Bar_dfa9a690eb3692ca0655d91a1bebf908bd27f73faf31ec7fe316bde6c0fbed2e_lock', + 'overrides': { + 'bytecode': { + 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + ], + }, + 'P2PKH_spend_input2_evaluate': { + 'name': 'Evaluate P2PKH spend (input #2)', + 'description': 'An example evaluation where this script execution passes.', + 'data': { + 'bytecode': { + 'signature_2': expect.any(String), + 'public_key_2': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + }, + }, + 'transaction': { + 'inputs': [ + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'p2pkh_placeholder_unlock_0', + 'overrides': { + 'bytecode': { + 'signature_0': expect.any(String), + 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + }, + }, + }, + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'p2pkh_placeholder_unlock_1', + 'overrides': { + 'bytecode': { + 'signature_1': expect.any(String), + 'public_key_1': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + }, + }, + }, + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': [ + 'slot', + ], + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'Bar_funcA_input3_unlock', + 'overrides': { + 'bytecode': { + 'function_index': '0', + 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + 'keys': { + 'privateKeys': {}, + }, + }, + }, + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'Bar_execute_input4_unlock', + 'overrides': { + 'bytecode': { + 'function_index': '2', + 'pk': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + 'keys': { + 'privateKeys': { + 's': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', + }, + }, + }, + }, + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'Foo_execute_input5_unlock', + 'overrides': { + 'bytecode': { + 'pk': '0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38', + 'pkh_foo': '0xb40a2013337edb0dfe307f0a57d5dec5bfe60dd0', + }, + 'keys': { + 'privateKeys': { + 's': '71080d8b52ec7b12adaec909ed54cd989b682ce2c35647eec219a16f5f90c528', + }, + }, + }, + }, + }, + { + 'outpointIndex': expect.any(Number), + 'outpointTransactionHash': expect.any(String), + 'sequenceNumber': 4294967294, + 'unlockingBytecode': { + 'script': 'Bar_funcB_input6_unlock', + 'overrides': { + 'bytecode': { + 'function_index': '1', + 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + 'keys': { + 'privateKeys': {}, + }, + }, + }, + }, + ], + 'locktime': 0, + 'outputs': [ + { + 'lockingBytecode': { + 'script': 'Foo_432c93902a8a8e49ef246028e707cddaa67a39af46b2b3c11c196cd09c931746_lock', + 'overrides': { + 'bytecode': { + 'pkh_foo': '0xb40a2013337edb0dfe307f0a57d5dec5bfe60dd0', + }, + }, + }, + 'valueSatoshis': 8000, + }, + { + 'lockingBytecode': '76a914512dbb2c8c02efbac8d92431aa0ac33f6b0bf97088ac', + 'token': { + 'amount': '100000000', + 'category': expect.any(String), + }, + 'valueSatoshis': 800, + }, + { + 'lockingBytecode': '76a914512dbb2c8c02efbac8d92431aa0ac33f6b0bf97088ac', + 'token': { + 'amount': '0', + 'category': expect.any(String), + 'nft': { + 'capability': 'minting', + 'commitment': '00', + }, + }, + 'valueSatoshis': 1000, + }, + { + 'lockingBytecode': '6a0568656c6c6f05776f726c64', + 'valueSatoshis': 0, + }, + ], + 'version': 2, + }, + 'sourceOutputs': [ + { + 'lockingBytecode': { + 'script': 'p2pkh_placeholder_lock_0', + 'overrides': { + 'bytecode': { + 'signature_0': expect.any(String), + 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': { + 'script': 'p2pkh_placeholder_lock_1', + 'overrides': { + 'bytecode': { + 'signature_1': expect.any(String), + 'public_key_1': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', + }, + }, + }, + 'token': { + 'amount': '100000000', + 'category': expect.stringMatching(/^[0-9a-f]{64}$/), + }, + 'valueSatoshis': expect.any(Number), }, - }, - }, - 'signer_1': { - 'scripts': [ - 'p2pkh_placeholder_lock_1', - 'p2pkh_placeholder_unlock_1', - ], - 'description': 'placeholder_key_1', - 'name': 'P2PKH Signer (input #1)', - 'variables': { - 'placeholder_key_1': { - 'description': '', - 'name': 'P2PKH Placeholder Key (input #1)', - 'type': 'Key', + { + 'lockingBytecode': [ + 'slot', + ], + 'token': { + 'amount': '0', + 'category': expect.stringMatching(/^[0-9a-f]{64}$/), + 'nft': { + 'capability': 'minting', + 'commitment': '00', + }, + }, + 'valueSatoshis': expect.any(Number), }, - }, - }, - 'signer_2': { - 'scripts': [ - 'p2pkh_placeholder_lock_2', - 'p2pkh_placeholder_unlock_2', - ], - 'description': 'placeholder_key_2', - 'name': 'P2PKH Signer (input #2)', - 'variables': { - 'placeholder_key_2': { - 'description': '', - 'name': 'P2PKH Placeholder Key (input #2)', - 'type': 'Key', + { + 'lockingBytecode': { + 'script': 'Bar_dfa9a690eb3692ca0655d91a1bebf908bd27f73faf31ec7fe316bde6c0fbed2e_lock', + 'overrides': { + 'bytecode': { + 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': { + 'script': 'Bar_dfa9a690eb3692ca0655d91a1bebf908bd27f73faf31ec7fe316bde6c0fbed2e_lock', + 'overrides': { + 'bytecode': { + 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': { + 'script': 'Foo_432c93902a8a8e49ef246028e707cddaa67a39af46b2b3c11c196cd09c931746_lock', + 'overrides': { + 'bytecode': { + 'pkh_foo': '0xb40a2013337edb0dfe307f0a57d5dec5bfe60dd0', + }, + }, + }, + 'valueSatoshis': expect.any(Number), + }, + { + 'lockingBytecode': { + 'script': 'Bar_dfa9a690eb3692ca0655d91a1bebf908bd27f73faf31ec7fe316bde6c0fbed2e_lock', + 'overrides': { + 'bytecode': { + 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', + }, + }, + }, + 'valueSatoshis': expect.any(Number), }, - }, - }, - }, - 'scripts': { - 'Bar_funcA_input3_unlock': { - 'passes': [ - 'Bar_funcA_input3_evaluate', - ], - 'name': 'funcA (input #3)', - 'script': '// "funcA" function parameters\n// none\n\n// function index in contract\n // int = <0>\n', - 'unlocks': 'Bar_dfa9a690eb3692ca0655d91a1bebf908bd27f73faf31ec7fe316bde6c0fbed2e_lock', - }, - 'Bar_dfa9a690eb3692ca0655d91a1bebf908bd27f73faf31ec7fe316bde6c0fbed2e_lock': { - 'lockingType': 'p2sh32', - 'name': 'Bar', - 'script': "// \"Bar\" contract constructor parameters\n // bytes20 = <0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970>\n\n// bytecode\n /* pragma cashscript >=0.10.2; */\n /* */\n /* contract Bar(bytes20 pkh_bar) { */\nOP_OVER OP_0 OP_NUMEQUAL OP_IF /* function funcA() { */\nOP_2 OP_2 OP_NUMEQUAL /* require(2==2); */\nOP_NIP OP_NIP OP_ELSE /* } */\n /* */\nOP_OVER OP_1 OP_NUMEQUAL OP_IF /* function funcB() { */\nOP_2 OP_2 OP_NUMEQUAL /* require(2==2); */\nOP_NIP OP_NIP OP_ELSE /* } */\n /* */\nOP_SWAP OP_2 OP_NUMEQUALVERIFY /* function execute(pubkey pk, sig s) { */\n /* console.log(\"Bar 'execute' function called.\"); */\nOP_OVER OP_HASH160 OP_EQUALVERIFY /* require(hash160(pk) == pkh_bar); */\nOP_CHECKSIG /* require(checkSig(s, pk)); */\n /* } */\nOP_ENDIF OP_ENDIF /* } */\n /* */", - }, - 'Bar_execute_input4_unlock': { - 'passes': [ - 'Bar_execute_input4_evaluate', - ], - 'name': 'execute (input #4)', - 'script': '// "execute" function parameters\n // sig\n // pubkey = <0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088>\n\n// function index in contract\n // int = <2>\n', - 'unlocks': 'Bar_dfa9a690eb3692ca0655d91a1bebf908bd27f73faf31ec7fe316bde6c0fbed2e_lock', - }, - 'Foo_execute_input5_unlock': { - 'passes': [ - 'Foo_execute_input5_evaluate', - ], - 'name': 'execute (input #5)', - 'script': '// "execute" function parameters\n // sig\n // pubkey = <0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38>\n', - 'unlocks': 'Foo_432c93902a8a8e49ef246028e707cddaa67a39af46b2b3c11c196cd09c931746_lock', - }, - 'Foo_432c93902a8a8e49ef246028e707cddaa67a39af46b2b3c11c196cd09c931746_lock': { - 'lockingType': 'p2sh32', - 'name': 'Foo', - 'script': "// \"Foo\" contract constructor parameters\n // bytes20 = <0xb40a2013337edb0dfe307f0a57d5dec5bfe60dd0>\n\n// bytecode\n /* pragma cashscript >=0.10.2; */\n /* */\n /* contract Foo(bytes20 pkh_foo) { */\n /* // Require pk to match stored pkh and signature to match */\n /* function execute(pubkey pk, sig s) { */\n /* console.log(\"Foo 'execute' function called.\"); */\nOP_OVER OP_HASH160 OP_EQUALVERIFY /* require(hash160(pk) == pkh_foo); */\nOP_CHECKSIG /* require(checkSig(s, pk)); */\n /* } */\n /* } */\n /* */", - }, - 'Bar_funcB_input6_unlock': { - 'passes': [ - 'Bar_funcB_input6_evaluate', ], - 'name': 'funcB (input #6)', - 'script': '// "funcB" function parameters\n// none\n\n// function index in contract\n // int = <1>\n', - 'unlocks': 'Bar_dfa9a690eb3692ca0655d91a1bebf908bd27f73faf31ec7fe316bde6c0fbed2e_lock', - }, - 'p2pkh_placeholder_unlock_0': { - 'name': 'P2PKH Unlock (input #0)', - 'script': '\n', - 'unlocks': 'p2pkh_placeholder_lock_0', - }, - 'p2pkh_placeholder_lock_0': { - 'lockingType': 'standard', - 'name': 'P2PKH Lock (input #0)', - 'script': 'OP_DUP\nOP_HASH160 <$( OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG', - }, - 'p2pkh_placeholder_unlock_1': { - 'name': 'P2PKH Unlock (input #1)', - 'script': '\n', - 'unlocks': 'p2pkh_placeholder_lock_1', - }, - 'p2pkh_placeholder_lock_1': { - 'lockingType': 'standard', - 'name': 'P2PKH Lock (input #1)', - 'script': 'OP_DUP\nOP_HASH160 <$( OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG', - }, - 'p2pkh_placeholder_unlock_2': { - 'name': 'P2PKH Unlock (input #2)', - 'script': '\n', - 'unlocks': 'p2pkh_placeholder_lock_2', - }, - 'p2pkh_placeholder_lock_2': { - 'lockingType': 'standard', - 'name': 'P2PKH Lock (input #2)', - 'script': 'OP_DUP\nOP_HASH160 <$( OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG', }, - }, - 'scenarios': { 'Bar_funcA_input3_evaluate': { 'name': 'Evaluate Bar funcA (input #3)', 'description': 'An example evaluation where this script execution passes.', @@ -291,8 +1065,6 @@ export const fixtures: Fixture[] = [ 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', 'function_index': '0', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': {}, }, @@ -306,10 +1078,9 @@ export const fixtures: Fixture[] = [ 'unlockingBytecode': { 'script': 'p2pkh_placeholder_unlock_0', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_0': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_0': expect.any(String), + 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -321,10 +1092,9 @@ export const fixtures: Fixture[] = [ 'unlockingBytecode': { 'script': 'p2pkh_placeholder_unlock_1', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_1': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_1': expect.any(String), + 'public_key_1': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -336,10 +1106,9 @@ export const fixtures: Fixture[] = [ 'unlockingBytecode': { 'script': 'p2pkh_placeholder_unlock_2', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_2': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_2': expect.any(String), + 'public_key_2': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -357,6 +1126,7 @@ export const fixtures: Fixture[] = [ 'outpointTransactionHash': expect.any(String), 'sequenceNumber': 4294967294, 'unlockingBytecode': { + 'script': 'Bar_execute_input4_unlock', 'overrides': { 'bytecode': { 'function_index': '2', @@ -369,7 +1139,6 @@ export const fixtures: Fixture[] = [ }, }, }, - 'script': 'Bar_execute_input4_unlock', }, }, { @@ -377,6 +1146,7 @@ export const fixtures: Fixture[] = [ 'outpointTransactionHash': expect.any(String), 'sequenceNumber': 4294967294, 'unlockingBytecode': { + 'script': 'Foo_execute_input5_unlock', 'overrides': { 'bytecode': { 'pk': '0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38', @@ -388,7 +1158,6 @@ export const fixtures: Fixture[] = [ }, }, }, - 'script': 'Foo_execute_input5_unlock', }, }, { @@ -396,6 +1165,7 @@ export const fixtures: Fixture[] = [ 'outpointTransactionHash': expect.any(String), 'sequenceNumber': 4294967294, 'unlockingBytecode': { + 'script': 'Bar_funcB_input6_unlock', 'overrides': { 'bytecode': { 'function_index': '1', @@ -405,11 +1175,10 @@ export const fixtures: Fixture[] = [ 'privateKeys': {}, }, }, - 'script': 'Bar_funcB_input6_unlock', }, }, ], - 'locktime': expect.any(Number), + 'locktime': 0, 'outputs': [ { 'lockingBytecode': { @@ -454,10 +1223,9 @@ export const fixtures: Fixture[] = [ 'lockingBytecode': { 'script': 'p2pkh_placeholder_lock_0', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_0': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_0': expect.any(String), + 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -467,26 +1235,36 @@ export const fixtures: Fixture[] = [ 'lockingBytecode': { 'script': 'p2pkh_placeholder_lock_1', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_1': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_1': expect.any(String), + 'public_key_1': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, + 'token': { + 'amount': '100000000', + 'category': expect.stringMatching(/^[0-9a-f]{64}$/), + }, 'valueSatoshis': expect.any(Number), }, { 'lockingBytecode': { 'script': 'p2pkh_placeholder_lock_2', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_2': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_2': expect.any(String), + 'public_key_2': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, + 'token': { + 'amount': '0', + 'category': expect.stringMatching(/^[0-9a-f]{64}$/), + 'nft': { + 'capability': 'minting', + 'commitment': '00', + }, + }, 'valueSatoshis': expect.any(Number), }, { @@ -539,8 +1317,6 @@ export const fixtures: Fixture[] = [ 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', 'function_index': '2', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': { 's': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', @@ -556,10 +1332,9 @@ export const fixtures: Fixture[] = [ 'unlockingBytecode': { 'script': 'p2pkh_placeholder_unlock_0', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_0': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_0': expect.any(String), + 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -571,10 +1346,9 @@ export const fixtures: Fixture[] = [ 'unlockingBytecode': { 'script': 'p2pkh_placeholder_unlock_1', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_1': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_1': expect.any(String), + 'public_key_1': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -586,10 +1360,9 @@ export const fixtures: Fixture[] = [ 'unlockingBytecode': { 'script': 'p2pkh_placeholder_unlock_2', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_2': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_2': expect.any(String), + 'public_key_2': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -599,6 +1372,7 @@ export const fixtures: Fixture[] = [ 'outpointTransactionHash': expect.any(String), 'sequenceNumber': 4294967294, 'unlockingBytecode': { + 'script': 'Bar_funcA_input3_unlock', 'overrides': { 'bytecode': { 'function_index': '0', @@ -608,7 +1382,6 @@ export const fixtures: Fixture[] = [ 'privateKeys': {}, }, }, - 'script': 'Bar_funcA_input3_unlock', }, }, { @@ -624,6 +1397,7 @@ export const fixtures: Fixture[] = [ 'outpointTransactionHash': expect.any(String), 'sequenceNumber': 4294967294, 'unlockingBytecode': { + 'script': 'Foo_execute_input5_unlock', 'overrides': { 'bytecode': { 'pk': '0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38', @@ -635,7 +1409,6 @@ export const fixtures: Fixture[] = [ }, }, }, - 'script': 'Foo_execute_input5_unlock', }, }, { @@ -643,6 +1416,7 @@ export const fixtures: Fixture[] = [ 'outpointTransactionHash': expect.any(String), 'sequenceNumber': 4294967294, 'unlockingBytecode': { + 'script': 'Bar_funcB_input6_unlock', 'overrides': { 'bytecode': { 'function_index': '1', @@ -652,9 +1426,7 @@ export const fixtures: Fixture[] = [ 'privateKeys': {}, }, }, - 'script': 'Bar_funcB_input6_unlock', }, - }, ], 'locktime': 0, @@ -702,10 +1474,9 @@ export const fixtures: Fixture[] = [ 'lockingBytecode': { 'script': 'p2pkh_placeholder_lock_0', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_0': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_0': expect.any(String), + 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -715,26 +1486,36 @@ export const fixtures: Fixture[] = [ 'lockingBytecode': { 'script': 'p2pkh_placeholder_lock_1', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_1': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_1': expect.any(String), + 'public_key_1': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, + 'token': { + 'amount': '100000000', + 'category': expect.stringMatching(/^[0-9a-f]{64}$/), + }, 'valueSatoshis': expect.any(Number), }, { 'lockingBytecode': { 'script': 'p2pkh_placeholder_lock_2', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_2': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_2': expect.any(String), + 'public_key_2': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, + 'token': { + 'amount': '0', + 'category': expect.stringMatching(/^[0-9a-f]{64}$/), + 'nft': { + 'capability': 'minting', + 'commitment': '00', + }, + }, 'valueSatoshis': expect.any(Number), }, { @@ -786,8 +1567,6 @@ export const fixtures: Fixture[] = [ 'pk': '0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38', 'pkh_foo': '0xb40a2013337edb0dfe307f0a57d5dec5bfe60dd0', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': { 's': '71080d8b52ec7b12adaec909ed54cd989b682ce2c35647eec219a16f5f90c528', @@ -803,10 +1582,9 @@ export const fixtures: Fixture[] = [ 'unlockingBytecode': { 'script': 'p2pkh_placeholder_unlock_0', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_0': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_0': expect.any(String), + 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -818,10 +1596,9 @@ export const fixtures: Fixture[] = [ 'unlockingBytecode': { 'script': 'p2pkh_placeholder_unlock_1', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_1': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_1': expect.any(String), + 'public_key_1': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -833,10 +1610,9 @@ export const fixtures: Fixture[] = [ 'unlockingBytecode': { 'script': 'p2pkh_placeholder_unlock_2', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_2': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_2': expect.any(String), + 'public_key_2': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -846,6 +1622,7 @@ export const fixtures: Fixture[] = [ 'outpointTransactionHash': expect.any(String), 'sequenceNumber': 4294967294, 'unlockingBytecode': { + 'script': 'Bar_funcA_input3_unlock', 'overrides': { 'bytecode': { 'function_index': '0', @@ -855,7 +1632,6 @@ export const fixtures: Fixture[] = [ 'privateKeys': {}, }, }, - 'script': 'Bar_funcA_input3_unlock', }, }, { @@ -863,6 +1639,7 @@ export const fixtures: Fixture[] = [ 'outpointTransactionHash': expect.any(String), 'sequenceNumber': 4294967294, 'unlockingBytecode': { + 'script': 'Bar_execute_input4_unlock', 'overrides': { 'bytecode': { 'function_index': '2', @@ -875,7 +1652,6 @@ export const fixtures: Fixture[] = [ }, }, }, - 'script': 'Bar_execute_input4_unlock', }, }, { @@ -891,17 +1667,16 @@ export const fixtures: Fixture[] = [ 'outpointTransactionHash': expect.any(String), 'sequenceNumber': 4294967294, 'unlockingBytecode': { + 'script': 'Bar_funcB_input6_unlock', 'overrides': { 'bytecode': { 'function_index': '1', 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', - }, 'keys': { 'privateKeys': {}, }, }, - 'script': 'Bar_funcB_input6_unlock', }, }, ], @@ -950,10 +1725,9 @@ export const fixtures: Fixture[] = [ 'lockingBytecode': { 'script': 'p2pkh_placeholder_lock_0', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_0': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_0': expect.any(String), + 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -963,26 +1737,36 @@ export const fixtures: Fixture[] = [ 'lockingBytecode': { 'script': 'p2pkh_placeholder_lock_1', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_1': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_1': expect.any(String), + 'public_key_1': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, + 'token': { + 'amount': '100000000', + 'category': expect.stringMatching(/^[0-9a-f]{64}$/), + }, 'valueSatoshis': expect.any(Number), }, { 'lockingBytecode': { 'script': 'p2pkh_placeholder_lock_2', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_2': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_2': expect.any(String), + 'public_key_2': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, + 'token': { + 'amount': '0', + 'category': expect.stringMatching(/^[0-9a-f]{64}$/), + 'nft': { + 'capability': 'minting', + 'commitment': '00', + }, + }, 'valueSatoshis': expect.any(Number), }, { @@ -1034,8 +1818,6 @@ export const fixtures: Fixture[] = [ 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', 'function_index': '1', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': {}, }, @@ -1049,10 +1831,9 @@ export const fixtures: Fixture[] = [ 'unlockingBytecode': { 'script': 'p2pkh_placeholder_unlock_0', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_0': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_0': expect.any(String), + 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -1064,10 +1845,9 @@ export const fixtures: Fixture[] = [ 'unlockingBytecode': { 'script': 'p2pkh_placeholder_unlock_1', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_1': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_1': expect.any(String), + 'public_key_1': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -1079,10 +1859,9 @@ export const fixtures: Fixture[] = [ 'unlockingBytecode': { 'script': 'p2pkh_placeholder_unlock_2', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_2': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_2': expect.any(String), + 'public_key_2': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -1092,6 +1871,7 @@ export const fixtures: Fixture[] = [ 'outpointTransactionHash': expect.any(String), 'sequenceNumber': 4294967294, 'unlockingBytecode': { + 'script': 'Bar_funcA_input3_unlock', 'overrides': { 'bytecode': { 'function_index': '0', @@ -1101,7 +1881,6 @@ export const fixtures: Fixture[] = [ 'privateKeys': {}, }, }, - 'script': 'Bar_funcA_input3_unlock', }, }, { @@ -1109,6 +1888,7 @@ export const fixtures: Fixture[] = [ 'outpointTransactionHash': expect.any(String), 'sequenceNumber': 4294967294, 'unlockingBytecode': { + 'script': 'Bar_execute_input4_unlock', 'overrides': { 'bytecode': { 'function_index': '2', @@ -1121,7 +1901,6 @@ export const fixtures: Fixture[] = [ }, }, }, - 'script': 'Bar_execute_input4_unlock', }, }, { @@ -1129,6 +1908,7 @@ export const fixtures: Fixture[] = [ 'outpointTransactionHash': expect.any(String), 'sequenceNumber': 4294967294, 'unlockingBytecode': { + 'script': 'Foo_execute_input5_unlock', 'overrides': { 'bytecode': { 'pk': '0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38', @@ -1140,7 +1920,6 @@ export const fixtures: Fixture[] = [ }, }, }, - 'script': 'Foo_execute_input5_unlock', }, }, { @@ -1197,10 +1976,9 @@ export const fixtures: Fixture[] = [ 'lockingBytecode': { 'script': 'p2pkh_placeholder_lock_0', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_0': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_0': expect.any(String), + 'public_key_0': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, @@ -1210,26 +1988,36 @@ export const fixtures: Fixture[] = [ 'lockingBytecode': { 'script': 'p2pkh_placeholder_lock_1', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_1': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_1': expect.any(String), + 'public_key_1': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, + 'token': { + 'amount': '100000000', + 'category': expect.stringMatching(/^[0-9a-f]{64}$/), + }, 'valueSatoshis': expect.any(Number), }, { 'lockingBytecode': { 'script': 'p2pkh_placeholder_lock_2', 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_2': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, + 'bytecode': { + 'signature_2': expect.any(String), + 'public_key_2': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', }, }, }, + 'token': { + 'amount': '0', + 'category': expect.stringMatching(/^[0-9a-f]{64}$/), + 'nft': { + 'capability': 'minting', + 'commitment': '00', + }, + }, 'valueSatoshis': expect.any(Number), }, { @@ -1345,8 +2133,6 @@ export const fixtures: Fixture[] = [ 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', 'function_index': '0', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': {}, }, @@ -1543,8 +2329,6 @@ export const fixtures: Fixture[] = [ 'minBlock': '0xb88201', 'priceTarget': '0x3075', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': { 'ownerSig': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', @@ -1635,8 +2419,6 @@ export const fixtures: Fixture[] = [ 'timeout': '0xa08601', 'function_index': '1', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': { 'senderSig': '71080d8b52ec7b12adaec909ed54cd989b682ce2c35647eec219a16f5f90c528', @@ -1944,8 +2726,6 @@ export const fixtures: Fixture[] = [ 'timeout': '0xa08601', 'function_index': '1', }, - 'currentBlockHeight': 2, - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': { 'senderSig': '71080d8b52ec7b12adaec909ed54cd989b682ce2c35647eec219a16f5f90c528', @@ -2102,8 +2882,6 @@ export const fixtures: Fixture[] = [ 'timeout': '0xa08601', 'function_index': '1', }, - 'currentBlockHeight': 2, - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': { 'senderSig': '81597823a901865622658cbf6d50c0286aa1d70fa1af98f897e34a0623a828ff', @@ -2260,8 +3038,6 @@ export const fixtures: Fixture[] = [ 'timeout': '0xa08601', 'function_index': '0', }, - 'currentBlockHeight': 2, - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': { 'recipientSig': '81597823a901865622658cbf6d50c0286aa1d70fa1af98f897e34a0623a828ff', @@ -2418,8 +3194,6 @@ export const fixtures: Fixture[] = [ 'timeout': '0xa08601', 'function_index': '0', }, - 'currentBlockHeight': 2, - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': { 'recipientSig': '71080d8b52ec7b12adaec909ed54cd989b682ce2c35647eec219a16f5f90c528', @@ -2690,8 +3464,6 @@ export const fixtures: Fixture[] = [ 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', 'function_index': '2', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': { 's': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', @@ -2774,8 +3546,6 @@ export const fixtures: Fixture[] = [ 'pkh_bar': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', 'function_index': '2', }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), 'keys': { 'privateKeys': { 's': '71080d8b52ec7b12adaec909ed54cd989b682ce2c35647eec219a16f5f90c528', diff --git a/packages/cashscript/test/fixture/libauth-template/old-fixtures.ts b/packages/cashscript/test/fixture/libauth-template/old-fixtures.ts deleted file mode 100644 index ed6ef42a..00000000 --- a/packages/cashscript/test/fixture/libauth-template/old-fixtures.ts +++ /dev/null @@ -1,1183 +0,0 @@ -import { Contract, HashType, MockNetworkProvider, SignatureAlgorithm, SignatureTemplate, type Transaction, randomNFT, randomToken, randomUtxo } from '../../../src/index.js'; -import TransferWithTimeout from '../transfer_with_timeout.artifact.js'; -import Mecenas from '../mecenas.artifact.js'; -import P2PKH from '../p2pkh.artifact.js'; -import HoldVault from '../hodl_vault.artifact.js'; -import { aliceAddress, alicePkh, alicePriv, alicePub, bobPkh, bobPriv, bobPub, oracle, oraclePub } from '../vars.js'; -import { WalletTemplate, hexToBin } from '@bitauth/libauth'; - -const provider = new MockNetworkProvider(); - -export interface Fixture { - name: string; - transaction: Transaction; - template: WalletTemplate; -} - -export const fixtures: Fixture[] = [ - { - name: 'TransferWithTimeout (transfer function)', - transaction: (() => { - const contract = new Contract(TransferWithTimeout, [alicePub, bobPub, 100000n], { provider }); - provider.addUtxo(contract.address, randomUtxo()); - - const tx = contract.functions - .transfer(new SignatureTemplate(bobPriv)) - .to(contract.address, 10000n) - .withoutChange(); - - return tx; - })(), - template: { - '$schema': 'https://ide.bitauth.com/authentication-template-v0.schema.json', - 'description': 'Imported from cashscript', - 'name': 'CashScript Generated Debugging Template', - 'supported': [ - 'BCH_2025_05', - ], - 'version': 0, - 'entities': { - 'TransferWithTimeout_parameters': { - 'description': 'Contract creation and function parameters', - 'name': 'TransferWithTimeout_parameters', - 'scripts': [ - 'TransferWithTimeout_lock', - 'TransferWithTimeout_unlock', - ], - 'variables': { - 'recipientSig': { - 'description': '"recipientSig" parameter of function "transfer"', - 'name': 'recipientSig', - 'type': 'Key', - }, - 'sender': { - 'description': '"sender" parameter of this contract', - 'name': 'sender', - 'type': 'WalletData', - }, - 'recipient': { - 'description': '"recipient" parameter of this contract', - 'name': 'recipient', - 'type': 'WalletData', - }, - 'timeout': { - 'description': '"timeout" parameter of this contract', - 'name': 'timeout', - 'type': 'WalletData', - }, - 'function_index': { - 'description': 'Script function index to execute', - 'name': 'function_index', - 'type': 'WalletData', - }, - }, - }, - }, - 'scripts': { - 'TransferWithTimeout_unlock': { - 'passes': [ - 'TransferWithTimeout_evaluate', - ], - 'name': 'TransferWithTimeout_unlock', - 'script': '// "transfer" function parameters\n // sig\n\n// function index in contract\n // int = <0>\n', - 'unlocks': 'TransferWithTimeout_lock', - }, - 'TransferWithTimeout_lock': { - 'lockingType': 'p2sh32', - 'name': 'TransferWithTimeout_lock', - 'script': "// \"TransferWithTimeout\" contract constructor parameters\n // int = <0xa08601>\n // pubkey = <0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38>\n // pubkey = <0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088>\n\n// bytecode\n /* contract TransferWithTimeout( */\n /* pubkey sender, */\n /* pubkey recipient, */\n /* int timeout */\n /* ) { */\n /* // Require recipient's signature to match */\nOP_3 OP_PICK OP_0 OP_NUMEQUAL OP_IF /* function transfer(sig recipientSig) { */\nOP_4 OP_ROLL OP_ROT OP_CHECKSIG /* require(checkSig(recipientSig, recipient)); */\nOP_NIP OP_NIP OP_NIP OP_ELSE /* } */\n /* */\n /* // Require timeout time to be reached and sender's signature to match */\nOP_3 OP_ROLL OP_1 OP_NUMEQUALVERIFY /* function timeout(sig senderSig) { */\nOP_3 OP_ROLL OP_SWAP OP_CHECKSIGVERIFY /* require(checkSig(senderSig, sender)); */\nOP_SWAP OP_CHECKLOCKTIMEVERIFY /* require(tx.time >= timeout); */\nOP_2DROP OP_1 /* } */\nOP_ENDIF /* } */\n /* */", - }, - }, - 'scenarios': { - 'TransferWithTimeout_evaluate': { - 'name': 'TransferWithTimeout_evaluate', - 'description': 'An example evaluation where this script execution passes.', - 'data': { - 'bytecode': { - 'sender': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', - 'recipient': '0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38', - 'timeout': '0xa08601', - 'function_index': '0', - }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), - 'keys': { - 'privateKeys': { - 'recipientSig': '71080d8b52ec7b12adaec909ed54cd989b682ce2c35647eec219a16f5f90c528', - }, - }, - }, - 'transaction': { - 'inputs': [ - { - 'outpointIndex': expect.any(Number), - 'outpointTransactionHash': expect.stringMatching(/^[0-9a-f]{64}$/), - 'sequenceNumber': 4294967294, - 'unlockingBytecode': [ - 'slot', - ], - }, - ], - 'locktime': expect.any(Number), - 'outputs': [ - { - 'lockingBytecode': {}, - 'valueSatoshis': 10000, - }, - ], - 'version': 2, - }, - 'sourceOutputs': [ - { - 'lockingBytecode': [ - 'slot', - ], - 'valueSatoshis': expect.any(Number), - }, - ], - }, - }, - }, - }, - { - name: 'TransferWithTimeout (timeout function)', - transaction: (() => { - const contract = new Contract(TransferWithTimeout, [alicePub, bobPub, 100000n], { provider }); - provider.addUtxo(contract.address, randomUtxo()); - - const tx = contract.functions - .timeout(new SignatureTemplate(alicePriv)) - .to(contract.address, 10000n) - .withoutChange(); - - return tx; - })(), - template: { - '$schema': 'https://ide.bitauth.com/authentication-template-v0.schema.json', - 'description': 'Imported from cashscript', - 'name': 'CashScript Generated Debugging Template', - 'supported': [ - 'BCH_2025_05', - ], - 'version': 0, - 'entities': { - 'TransferWithTimeout_parameters': { - 'description': 'Contract creation and function parameters', - 'name': 'TransferWithTimeout_parameters', - 'scripts': [ - 'TransferWithTimeout_lock', - 'TransferWithTimeout_unlock', - ], - 'variables': { - 'senderSig': { - 'description': '"senderSig" parameter of function "timeout"', - 'name': 'senderSig', - 'type': 'Key', - }, - 'sender': { - 'description': '"sender" parameter of this contract', - 'name': 'sender', - 'type': 'WalletData', - }, - 'recipient': { - 'description': '"recipient" parameter of this contract', - 'name': 'recipient', - 'type': 'WalletData', - }, - 'timeout': { - 'description': '"timeout" parameter of this contract', - 'name': 'timeout', - 'type': 'WalletData', - }, - 'function_index': { - 'description': 'Script function index to execute', - 'name': 'function_index', - 'type': 'WalletData', - }, - }, - }, - }, - 'scripts': { - 'TransferWithTimeout_unlock': { - 'passes': [ - 'TransferWithTimeout_evaluate', - ], - 'name': 'TransferWithTimeout_unlock', - 'script': '// "timeout" function parameters\n // sig\n\n// function index in contract\n // int = <1>\n', - 'unlocks': 'TransferWithTimeout_lock', - }, - 'TransferWithTimeout_lock': { - 'lockingType': 'p2sh32', - 'name': 'TransferWithTimeout_lock', - 'script': "// \"TransferWithTimeout\" contract constructor parameters\n // int = <0xa08601>\n // pubkey = <0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38>\n // pubkey = <0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088>\n\n// bytecode\n /* contract TransferWithTimeout( */\n /* pubkey sender, */\n /* pubkey recipient, */\n /* int timeout */\n /* ) { */\n /* // Require recipient's signature to match */\nOP_3 OP_PICK OP_0 OP_NUMEQUAL OP_IF /* function transfer(sig recipientSig) { */\nOP_4 OP_ROLL OP_ROT OP_CHECKSIG /* require(checkSig(recipientSig, recipient)); */\nOP_NIP OP_NIP OP_NIP OP_ELSE /* } */\n /* */\n /* // Require timeout time to be reached and sender's signature to match */\nOP_3 OP_ROLL OP_1 OP_NUMEQUALVERIFY /* function timeout(sig senderSig) { */\nOP_3 OP_ROLL OP_SWAP OP_CHECKSIGVERIFY /* require(checkSig(senderSig, sender)); */\nOP_SWAP OP_CHECKLOCKTIMEVERIFY /* require(tx.time >= timeout); */\nOP_2DROP OP_1 /* } */\nOP_ENDIF /* } */\n /* */", - }, - }, - 'scenarios': { - 'TransferWithTimeout_evaluate': { - 'name': 'TransferWithTimeout_evaluate', - 'description': 'An example evaluation where this script execution passes.', - 'data': { - 'bytecode': { - 'sender': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', - 'recipient': '0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38', - 'timeout': '0xa08601', - 'function_index': '1', - }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), - 'keys': { - 'privateKeys': { - 'senderSig': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, - }, - }, - 'transaction': { - 'inputs': [ - { - 'outpointIndex': expect.any(Number), - 'outpointTransactionHash': expect.stringMatching(/^[0-9a-f]{64}$/), - 'sequenceNumber': 4294967294, - 'unlockingBytecode': [ - 'slot', - ], - }, - ], - 'locktime': expect.any(Number), - 'outputs': [ - { - 'lockingBytecode': {}, - 'valueSatoshis': 10000, - }, - ], - 'version': 2, - }, - 'sourceOutputs': [ - { - 'lockingBytecode': [ - 'slot', - ], - 'valueSatoshis': expect.any(Number), - }, - ], - }, - }, - }, - }, - { - name: 'Mecenas', - transaction: (() => { - const contract = new Contract(Mecenas, [alicePkh, bobPkh, 10_000n], { provider }); - provider.addUtxo(contract.address, randomUtxo()); - - const tx = contract.functions - .receive() - .to(aliceAddress, 10_000n) - .withHardcodedFee(1000n); - - return tx; - })(), - template: { - '$schema': 'https://ide.bitauth.com/authentication-template-v0.schema.json', - 'description': 'Imported from cashscript', - 'name': 'CashScript Generated Debugging Template', - 'supported': [ - 'BCH_2025_05', - ], - 'version': 0, - 'entities': { - 'Mecenas_parameters': { - 'description': 'Contract creation and function parameters', - 'name': 'Mecenas_parameters', - 'scripts': [ - 'Mecenas_lock', - 'Mecenas_unlock', - ], - 'variables': { - 'recipient': { - 'description': '"recipient" parameter of this contract', - 'name': 'recipient', - 'type': 'WalletData', - }, - 'funder': { - 'description': '"funder" parameter of this contract', - 'name': 'funder', - 'type': 'WalletData', - }, - 'pledge': { - 'description': '"pledge" parameter of this contract', - 'name': 'pledge', - 'type': 'WalletData', - }, - 'function_index': { - 'description': 'Script function index to execute', - 'name': 'function_index', - 'type': 'WalletData', - }, - }, - }, - }, - 'scripts': { - 'Mecenas_unlock': { - 'passes': [ - 'Mecenas_evaluate', - ], - 'name': 'Mecenas_unlock', - 'script': '// "receive" function parameters\n// none\n\n// function index in contract\n // int = <0>\n', - 'unlocks': 'Mecenas_lock', - }, - 'Mecenas_lock': { - 'lockingType': 'p2sh32', - 'name': 'Mecenas_lock', - 'script': "// \"Mecenas\" contract constructor parameters\n // int = <0x1027>\n // bytes20 = <0xb40a2013337edb0dfe307f0a57d5dec5bfe60dd0>\n // bytes20 = <0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970>\n\n// bytecode\n /* pragma cashscript >=0.8.0; */\n /* */\n /* \\/* This is an unofficial CashScript port of Licho's Mecenas contract. It is */\n /* * not compatible with Licho's EC plugin, but rather meant as a demonstration */\n /* * of covenants in CashScript. */\n /* * The time checking has been removed so it can be tested without time requirements. */\n /* *\\/ */\n /* contract Mecenas(bytes20 recipient, bytes20 funder, int pledge\\/*, int period *\\/) { */\nOP_3 OP_PICK OP_0 OP_NUMEQUAL OP_IF /* function receive() { */\n /* // require(this.age >= period); */\n /* */\n /* // Check that the first output sends to the recipient */\nOP_0 OP_OUTPUTBYTECODE <0x76a914> OP_ROT OP_CAT <0x88ac> OP_CAT OP_EQUALVERIFY /* require(tx.outputs[0].lockingBytecode == new LockingBytecodeP2PKH(recipient)); */\n /* */\n<0xe803> /* int minerFee = 1000; */\nOP_INPUTINDEX OP_UTXOVALUE /* int currentValue = tx.inputs[this.activeInputIndex].value; */\nOP_DUP OP_4 OP_PICK OP_SUB OP_2 OP_PICK OP_SUB /* int changeValue = currentValue - pledge - minerFee; */\n /* */\n /* // If there is not enough left for *another* pledge after this one, we send the remainder to the recipient */\n /* // Otherwise we send the remainder to the recipient and the change back to the contract */\nOP_DUP OP_5 OP_PICK OP_4 OP_PICK OP_ADD OP_LESSTHANOREQUAL OP_IF /* if (changeValue <= pledge + minerFee) { */\nOP_0 OP_OUTPUTVALUE OP_2OVER OP_SWAP OP_SUB OP_NUMEQUALVERIFY /* require(tx.outputs[0].value == currentValue - minerFee); */\nOP_ELSE /* } else { */\nOP_0 OP_OUTPUTVALUE OP_5 OP_PICK OP_NUMEQUALVERIFY /* require(tx.outputs[0].value == pledge); */\nOP_1 OP_OUTPUTBYTECODE OP_INPUTINDEX OP_UTXOBYTECODE OP_EQUALVERIFY /* require(tx.outputs[1].lockingBytecode == tx.inputs[this.activeInputIndex].lockingBytecode); */\nOP_1 OP_OUTPUTVALUE OP_OVER OP_NUMEQUALVERIFY /* require(tx.outputs[1].value == changeValue); */\nOP_ENDIF /* } */\nOP_2DROP OP_2DROP OP_2DROP OP_1 OP_ELSE /* } */\n /* */\nOP_3 OP_ROLL OP_1 OP_NUMEQUALVERIFY /* function reclaim(pubkey pk, sig s) { */\nOP_3 OP_PICK OP_HASH160 OP_ROT OP_EQUALVERIFY /* require(hash160(pk) == funder); */\nOP_2SWAP OP_CHECKSIG /* require(checkSig(s, pk)); */\nOP_NIP OP_NIP /* } */\nOP_ENDIF /* } */\n /* */", - }, - }, - 'scenarios': { - 'Mecenas_evaluate': { - 'name': 'Mecenas_evaluate', - 'description': 'An example evaluation where this script execution passes.', - 'data': { - 'bytecode': { - 'recipient': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', - 'funder': '0xb40a2013337edb0dfe307f0a57d5dec5bfe60dd0', - 'pledge': '0x1027', - 'function_index': '0', - }, - 'currentBlockHeight': 2, - 'currentBlockTime': expect.any(Number), - 'keys': { - 'privateKeys': {}, - }, - }, - 'transaction': { - 'inputs': [ - { - 'outpointIndex': expect.any(Number), - 'outpointTransactionHash': expect.stringMatching(/^[0-9a-f]{64}$/), - 'sequenceNumber': 4294967294, - 'unlockingBytecode': [ - 'slot', - ], - }, - ], - 'locktime': 133700, - 'outputs': [ - { - 'lockingBytecode': '76a914512dbb2c8c02efbac8d92431aa0ac33f6b0bf97088ac', - 'valueSatoshis': 10000, - }, - { - 'lockingBytecode': {}, - 'valueSatoshis': expect.any(Number), - }, - ], - 'version': 2, - }, - 'sourceOutputs': [ - { - 'lockingBytecode': [ - 'slot', - ], - 'valueSatoshis': expect.any(Number), - }, - ], - }, - }, - }, - }, - { - name: 'P2PKH (sending fungible tokens)', - transaction: (() => { - const contract = new Contract(P2PKH, [alicePkh], { provider }); - - const regularUtxo = randomUtxo(); - const tokenUtxo = randomUtxo({ satoshis: 1000n, token: randomToken() }); - provider.addUtxo(contract.address, regularUtxo); - provider.addUtxo(contract.address, tokenUtxo); - - const to = contract.tokenAddress; - const amount = 1000n; - - const tx = contract.functions - .spend(alicePub, new SignatureTemplate(alicePriv)) - .from(regularUtxo) - .from(tokenUtxo) - .to(to, amount, tokenUtxo.token); - - return tx; - })(), - template: { - '$schema': 'https://ide.bitauth.com/authentication-template-v0.schema.json', - 'description': 'Imported from cashscript', - 'name': 'CashScript Generated Debugging Template', - 'supported': [ - 'BCH_2025_05', - ], - 'version': 0, - 'entities': { - 'P2PKH_parameters': { - 'description': 'Contract creation and function parameters', - 'name': 'P2PKH_parameters', - 'scripts': [ - 'P2PKH_lock', - 'P2PKH_unlock', - ], - 'variables': { - 'pk': { - 'description': '"pk" parameter of function "spend"', - 'name': 'pk', - 'type': 'WalletData', - }, - 's': { - 'description': '"s" parameter of function "spend"', - 'name': 's', - 'type': 'Key', - }, - 'pkh': { - 'description': '"pkh" parameter of this contract', - 'name': 'pkh', - 'type': 'WalletData', - }, - }, - }, - }, - 'scripts': { - 'P2PKH_unlock': { - 'passes': [ - 'P2PKH_evaluate', - ], - 'name': 'P2PKH_unlock', - 'script': '// "spend" function parameters\n // sig\n // pubkey = <0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088>\n', - 'unlocks': 'P2PKH_lock', - }, - 'P2PKH_lock': { - 'lockingType': 'p2sh32', - 'name': 'P2PKH_lock', - 'script': '// "P2PKH" contract constructor parameters\n // bytes20 = <0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970>\n\n// bytecode\n /* contract P2PKH(bytes20 pkh) { */\n /* // Require pk to match stored pkh and signature to match */\n /* function spend(pubkey pk, sig s) { */\nOP_OVER OP_HASH160 OP_EQUALVERIFY /* require(hash160(pk) == pkh); */\nOP_CHECKSIG /* require(checkSig(s, pk)); */\n /* } */\n /* } */\n /* */', - }, - }, - 'scenarios': { - 'P2PKH_evaluate': { - 'name': 'P2PKH_evaluate', - 'description': 'An example evaluation where this script execution passes.', - 'data': { - 'bytecode': { - 'pk': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', - 'pkh': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', - }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), - 'keys': { - 'privateKeys': { - 's': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, - }, - }, - 'transaction': { - 'inputs': [ - { - 'outpointIndex': expect.any(Number), - 'outpointTransactionHash': expect.stringMatching(/^[0-9a-f]{64}$/), - 'sequenceNumber': 4294967294, - 'unlockingBytecode': [ - 'slot', - ], - }, - { - 'outpointIndex': expect.any(Number), - 'outpointTransactionHash': expect.stringMatching(/^[0-9a-f]{64}$/), - 'sequenceNumber': 4294967294, - 'unlockingBytecode': {}, - }, - ], - 'locktime': 133700, - 'outputs': [ - { - 'lockingBytecode': {}, - 'token': { - 'amount': expect.stringMatching(/^[0-9]+$/), - 'category': expect.stringMatching(/^[0-9a-f]{64}$/), - }, - 'valueSatoshis': 1000, - }, - { - 'lockingBytecode': {}, - 'valueSatoshis': expect.any(Number), - }, - ], - 'version': 2, - }, - 'sourceOutputs': [ - { - 'lockingBytecode': [ - 'slot', - ], - 'valueSatoshis': expect.any(Number), - }, - { - 'lockingBytecode': {}, - 'valueSatoshis': 1000, - 'token': { - 'amount': expect.stringMatching(/^[0-9]+$/), - 'category': expect.stringMatching(/^[0-9a-f]{64}$/), - }, - }, - ], - }, - }, - }, - }, - { - name: 'P2PKH (hardcoded signature)', - transaction: (() => { - const contract = new Contract(P2PKH, [alicePkh], { provider }); - - const regularUtxo = randomUtxo(); - provider.addUtxo(contract.address, regularUtxo); - - const to = contract.tokenAddress; - const amount = 1000n; - - const hardcodedSignature = new SignatureTemplate(alicePriv).generateSignature(hexToBin('c0ffee')); - const tx = contract.functions - .spend(alicePub, hardcodedSignature) - .from(regularUtxo) - .to(to, amount); - - return tx; - })(), - template: { - '$schema': 'https://ide.bitauth.com/authentication-template-v0.schema.json', - 'description': 'Imported from cashscript', - 'name': 'CashScript Generated Debugging Template', - 'supported': [ - 'BCH_2025_05', - ], - 'version': 0, - 'entities': { - 'P2PKH_parameters': { - 'description': 'Contract creation and function parameters', - 'name': 'P2PKH_parameters', - 'scripts': [ - 'P2PKH_lock', - 'P2PKH_unlock', - ], - 'variables': { - 'pk': { - 'description': '"pk" parameter of function "spend"', - 'name': 'pk', - 'type': 'WalletData', - }, - 's': { - 'description': '"s" parameter of function "spend"', - 'name': 's', - 'type': 'WalletData', - }, - 'pkh': { - 'description': '"pkh" parameter of this contract', - 'name': 'pkh', - 'type': 'WalletData', - }, - }, - }, - }, - 'scripts': { - 'P2PKH_unlock': { - 'passes': [ - 'P2PKH_evaluate', - ], - 'name': 'P2PKH_unlock', - 'script': '// "spend" function parameters\n // sig = <0x65f72c5cce773383b45032a3f9f9255814e3d53ee260056e3232cd89e91a0a84278b35daf8938d47047e7d3bd3407fe90b07dfabf4407947af6fb09730a34c0b61>\n // pubkey = <0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088>\n', - 'unlocks': 'P2PKH_lock', - }, - 'P2PKH_lock': { - 'lockingType': 'p2sh32', - 'name': 'P2PKH_lock', - 'script': '// "P2PKH" contract constructor parameters\n // bytes20 = <0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970>\n\n// bytecode\n /* contract P2PKH(bytes20 pkh) { */\n /* // Require pk to match stored pkh and signature to match */\n /* function spend(pubkey pk, sig s) { */\nOP_OVER OP_HASH160 OP_EQUALVERIFY /* require(hash160(pk) == pkh); */\nOP_CHECKSIG /* require(checkSig(s, pk)); */\n /* } */\n /* } */\n /* */', - }, - }, - 'scenarios': { - 'P2PKH_evaluate': { - 'name': 'P2PKH_evaluate', - 'description': 'An example evaluation where this script execution passes.', - 'data': { - 'bytecode': { - 'pk': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', - 's': '0x65f72c5cce773383b45032a3f9f9255814e3d53ee260056e3232cd89e91a0a84278b35daf8938d47047e7d3bd3407fe90b07dfabf4407947af6fb09730a34c0b61', - 'pkh': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', - }, - 'currentBlockHeight': 2, - 'currentBlockTime': expect.any(Number), - 'keys': { - 'privateKeys': {}, - }, - }, - 'transaction': { - 'inputs': [ - { - 'outpointIndex': expect.any(Number), - 'outpointTransactionHash': expect.stringMatching(/^[0-9a-f]{64}$/), - 'sequenceNumber': 4294967294, - 'unlockingBytecode': [ - 'slot', - ], - }, - ], - 'locktime': 133700, - 'outputs': [ - { - 'lockingBytecode': {}, - 'valueSatoshis': 1000, - }, - { - 'lockingBytecode': {}, - 'valueSatoshis': expect.any(Number), - }, - ], - 'version': 2, - }, - 'sourceOutputs': [ - { - 'lockingBytecode': [ - 'slot', - ], - 'valueSatoshis': expect.any(Number), - }, - ], - }, - }, - }, - }, - { - name: 'P2PKH (sending NFTs)', - transaction: (() => { - const contract = new Contract(P2PKH, [alicePkh], { provider }); - - const regularUtxo = randomUtxo(); - const nftUtxo = randomUtxo({ satoshis: 1000n, token: randomNFT() }); - provider.addUtxo(contract.address, regularUtxo); - provider.addUtxo(contract.address, nftUtxo); - - const to = contract.tokenAddress; - const amount = 1000n; - - const tx = contract.functions - .spend(alicePub, new SignatureTemplate(alicePriv)) - .from(regularUtxo) - .from(nftUtxo) - .to(to, amount, nftUtxo.token); - - return tx; - })(), - template: { - '$schema': 'https://ide.bitauth.com/authentication-template-v0.schema.json', - 'description': 'Imported from cashscript', - 'name': 'CashScript Generated Debugging Template', - 'supported': [ - 'BCH_2025_05', - ], - 'version': 0, - 'entities': { - 'P2PKH_parameters': { - 'description': 'Contract creation and function parameters', - 'name': 'P2PKH_parameters', - 'scripts': [ - 'P2PKH_lock', - 'P2PKH_unlock', - ], - 'variables': { - 'pk': { - 'description': '"pk" parameter of function "spend"', - 'name': 'pk', - 'type': 'WalletData', - }, - 's': { - 'description': '"s" parameter of function "spend"', - 'name': 's', - 'type': 'Key', - }, - 'pkh': { - 'description': '"pkh" parameter of this contract', - 'name': 'pkh', - 'type': 'WalletData', - }, - }, - }, - }, - 'scripts': { - 'P2PKH_unlock': { - 'passes': [ - 'P2PKH_evaluate', - ], - 'name': 'P2PKH_unlock', - 'script': '// "spend" function parameters\n // sig\n // pubkey = <0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088>\n', - 'unlocks': 'P2PKH_lock', - }, - 'P2PKH_lock': { - 'lockingType': 'p2sh32', - 'name': 'P2PKH_lock', - 'script': '// "P2PKH" contract constructor parameters\n // bytes20 = <0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970>\n\n// bytecode\n /* contract P2PKH(bytes20 pkh) { */\n /* // Require pk to match stored pkh and signature to match */\n /* function spend(pubkey pk, sig s) { */\nOP_OVER OP_HASH160 OP_EQUALVERIFY /* require(hash160(pk) == pkh); */\nOP_CHECKSIG /* require(checkSig(s, pk)); */\n /* } */\n /* } */\n /* */', - }, - }, - 'scenarios': { - 'P2PKH_evaluate': { - 'name': 'P2PKH_evaluate', - 'description': 'An example evaluation where this script execution passes.', - 'data': { - 'bytecode': { - 'pk': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', - 'pkh': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', - }, - 'currentBlockHeight': 2, - 'currentBlockTime': expect.any(Number), - 'keys': { - 'privateKeys': { - 's': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, - }, - }, - 'transaction': { - 'inputs': [ - { - 'outpointIndex': expect.any(Number), - 'outpointTransactionHash': expect.stringMatching(/^[0-9a-f]{64}$/), - 'sequenceNumber': 4294967294, - 'unlockingBytecode': [ - 'slot', - ], - }, - { - 'outpointIndex': expect.any(Number), - 'outpointTransactionHash': expect.stringMatching(/^[0-9a-f]{64}$/), - 'sequenceNumber': 4294967294, - 'unlockingBytecode': {}, - }, - ], - 'locktime': 133700, - 'outputs': [ - { - 'lockingBytecode': {}, - 'token': { - 'amount': '0', - 'category': expect.stringMatching(/^[0-9a-f]{64}$/), - 'nft': { - 'capability': 'none', - 'commitment': expect.stringMatching(/^[0-9a-f]{8}$/), - }, - }, - 'valueSatoshis': 1000, - }, - { - 'lockingBytecode': {}, - 'valueSatoshis': expect.any(Number), - }, - ], - 'version': 2, - }, - 'sourceOutputs': [ - { - 'lockingBytecode': [ - 'slot', - ], - 'valueSatoshis': expect.any(Number), - }, - { - 'lockingBytecode': {}, - 'valueSatoshis': 1000, - 'token': { - 'amount': '0', - 'category': expect.stringMatching(/^[0-9a-f]{64}$/), - 'nft': { - 'capability': 'none', - 'commitment': expect.stringMatching(/^[0-9a-f]{8}$/), - }, - }, - }, - ], - }, - }, - }, - }, - { - name: 'HodlVault (datasig)', - transaction: (() => { - const contract = new Contract(HoldVault, [alicePub, oraclePub, 99000n, 30000n], { provider }); - provider.addUtxo(contract.address, randomUtxo()); - - // given - const message = oracle.createMessage(100000n, 30000n); - const oracleSig = oracle.signMessage(message); - const to = contract.address; - const amount = 10000n; - - // when - const tx = contract.functions - .spend(new SignatureTemplate(alicePriv), oracleSig, message) - .to(to, amount); - - return tx; - })(), - template: { - '$schema': 'https://ide.bitauth.com/authentication-template-v0.schema.json', - 'description': 'Imported from cashscript', - 'name': 'CashScript Generated Debugging Template', - 'supported': [ - 'BCH_2025_05', - ], - 'version': 0, - 'entities': { - 'HodlVault_parameters': { - 'description': 'Contract creation and function parameters', - 'name': 'HodlVault_parameters', - 'scripts': [ - 'HodlVault_lock', - 'HodlVault_unlock', - ], - 'variables': { - 'ownerSig': { - 'description': '"ownerSig" parameter of function "spend"', - 'name': 'ownerSig', - 'type': 'Key', - }, - 'oracleSig': { - 'description': '"oracleSig" parameter of function "spend"', - 'name': 'oracleSig', - 'type': 'WalletData', - }, - 'oracleMessage': { - 'description': '"oracleMessage" parameter of function "spend"', - 'name': 'oracleMessage', - 'type': 'WalletData', - }, - 'ownerPk': { - 'description': '"ownerPk" parameter of this contract', - 'name': 'ownerPk', - 'type': 'WalletData', - }, - 'oraclePk': { - 'description': '"oraclePk" parameter of this contract', - 'name': 'oraclePk', - 'type': 'WalletData', - }, - 'minBlock': { - 'description': '"minBlock" parameter of this contract', - 'name': 'minBlock', - 'type': 'WalletData', - }, - 'priceTarget': { - 'description': '"priceTarget" parameter of this contract', - 'name': 'priceTarget', - 'type': 'WalletData', - }, - }, - }, - }, - 'scripts': { - 'HodlVault_unlock': { - 'passes': [ - 'HodlVault_evaluate', - ], - 'name': 'HodlVault_unlock', - 'script': '// "spend" function parameters\n // bytes8 = <0xa086010030750000>\n // datasig = <0x569e137142ebdb96127b727787d605e427a858e8b17dc0605092d0019e5fc9d58810ee74c8ba9f9a5605268c9913e50f780f4c3780e06aea7f50766829895b4b>\n // sig\n', - 'unlocks': 'HodlVault_lock', - }, - 'HodlVault_lock': { - 'lockingType': 'p2sh32', - 'name': 'HodlVault_lock', - 'script': '// "HodlVault" contract constructor parameters\n // int = <0x3075>\n // int = <0xb88201>\n // pubkey = <0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38>\n // pubkey = <0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088>\n\n// bytecode\n /* // This contract forces HODLing until a certain price target has been reached */\n /* // A minimum block is provided to ensure that oracle price entries from before this block are disregarded */\n /* // i.e. when the BCH price was $1000 in the past, an oracle entry with the old block number and price can not be used. */\n /* // Instead, a message with a block number and price from after the minBlock needs to be passed. */\n /* // This contract serves as a simple example for checkDataSig-based contracts. */\n /* contract HodlVault( */\n /* pubkey ownerPk, */\n /* pubkey oraclePk, */\n /* int minBlock, */\n /* int priceTarget */\n /* ) { */\n /* function spend(sig ownerSig, datasig oracleSig, bytes8 oracleMessage) { */\n /* // message: { blockHeight, price } */\nOP_6 OP_PICK OP_4 OP_SPLIT /* bytes4 blockHeightBin, bytes4 priceBin = oracleMessage.split(4); */\nOP_SWAP OP_BIN2NUM /* int blockHeight = int(blockHeightBin); */\nOP_SWAP OP_BIN2NUM /* int price = int(priceBin); */\n /* */\n /* // Check that blockHeight is after minBlock and not in the future */\nOP_OVER OP_5 OP_ROLL OP_GREATERTHANOREQUAL OP_VERIFY /* require(blockHeight >= minBlock); */\nOP_SWAP OP_CHECKLOCKTIMEVERIFY OP_DROP /* require(tx.time >= blockHeight); */\n /* */\n /* // Check that current price is at least priceTarget */\nOP_3 OP_ROLL OP_GREATERTHANOREQUAL OP_VERIFY /* require(price >= priceTarget); */\n /* */\n /* // Handle necessary signature checks */\nOP_3 OP_ROLL OP_4 OP_ROLL OP_3 OP_ROLL OP_CHECKDATASIGVERIFY /* require(checkDataSig(oracleSig, oracleMessage, oraclePk)); */\nOP_CHECKSIG /* require(checkSig(ownerSig, ownerPk)); */\n /* } */\n /* } */\n /* */', - }, - }, - 'scenarios': { - 'HodlVault_evaluate': { - 'name': 'HodlVault_evaluate', - 'description': 'An example evaluation where this script execution passes.', - 'data': { - 'bytecode': { - 'oracleSig': '0x569e137142ebdb96127b727787d605e427a858e8b17dc0605092d0019e5fc9d58810ee74c8ba9f9a5605268c9913e50f780f4c3780e06aea7f50766829895b4b', - 'oracleMessage': '0xa086010030750000', - 'ownerPk': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', - 'oraclePk': '0x028f1219c918234d6bb06b4782354ff0759bd73036f3c849b88020c79fe013cd38', - 'minBlock': '0xb88201', - 'priceTarget': '0x3075', - }, - 'currentBlockHeight': 2, - 'currentBlockTime': expect.any(Number), - 'keys': { - 'privateKeys': { - 'ownerSig': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, - }, - }, - 'transaction': { - 'inputs': [ - { - 'outpointIndex': expect.any(Number), - 'outpointTransactionHash': expect.stringMatching(/^[0-9a-f]{64}$/), - 'sequenceNumber': 4294967294, - 'unlockingBytecode': [ - 'slot', - ], - }, - ], - 'locktime': 133700, - 'outputs': [ - { - 'lockingBytecode': {}, - 'valueSatoshis': 10000, - }, - { - 'lockingBytecode': {}, - 'valueSatoshis': expect.any(Number), - }, - ], - 'version': 2, - }, - 'sourceOutputs': [ - { - 'lockingBytecode': [ - 'slot', - ], - 'valueSatoshis': expect.any(Number), - }, - ], - }, - }, - }, - }, - // TODO: Make it work with different hashtypes and signature algorithms - // { - // name: 'P2PKH (sending NFTs)', - // transaction: (() => { - // const contract = new Contract(P2PKH, [alicePkh], { provider }); - // provider.addUtxo(contract.address, randomUtxo()); - - // const to = contract.address; - // const amount = 1000n; - - // const hashtype = HashType.SIGHASH_SINGLE | HashType.SIGHASH_ANYONECANPAY; - // const signatureAlgorithm = SignatureAlgorithm.ECDSA; - - // const tx = contract.functions - // .spend(alicePub, new SignatureTemplate(alicePriv, hashtype, signatureAlgorithm)) - // .to(to, amount); - - // return tx; - // })(), - // template: {} as any, - // }, - { - name: 'P2PKH (with P2PKH inputs & P2SH20 address type & ECDSA signature algorithm)', - transaction: (() => { - const contract = new Contract(P2PKH, [alicePkh], { provider, addressType: 'p2sh20' }); - - const regularUtxo = randomUtxo(); - provider.addUtxo(contract.address, regularUtxo); - - const p2pkhUtxo = randomUtxo(); - provider.addUtxo(aliceAddress, p2pkhUtxo); - - const to = contract.tokenAddress; - const amount = 1000n; - - const tx = contract.functions - .spend(alicePub, new SignatureTemplate(alicePriv, HashType.SIGHASH_NONE, SignatureAlgorithm.ECDSA)) - .fromP2PKH(p2pkhUtxo, new SignatureTemplate(alicePriv)) - .from(regularUtxo) - .fromP2PKH(p2pkhUtxo, new SignatureTemplate(bobPriv, HashType.SIGHASH_ALL, SignatureAlgorithm.ECDSA)) - .to(to, amount); - - return tx; - })(), - template: { - '$schema': 'https://ide.bitauth.com/authentication-template-v0.schema.json', - 'description': 'Imported from cashscript', - 'name': 'CashScript Generated Debugging Template', - 'supported': [ - 'BCH_2025_05', - ], - 'version': 0, - 'entities': { - 'P2PKH_parameters': { - 'description': 'Contract creation and function parameters', - 'name': 'P2PKH_parameters', - 'scripts': [ - 'P2PKH_lock', - 'P2PKH_unlock', - 'p2pkh_placeholder_lock_0', - 'p2pkh_placeholder_unlock_0', - 'p2pkh_placeholder_lock_2', - 'p2pkh_placeholder_unlock_2', - ], - 'variables': { - 'pk': { - 'description': '"pk" parameter of function "spend"', - 'name': 'pk', - 'type': 'WalletData', - }, - 's': { - 'description': '"s" parameter of function "spend"', - 'name': 's', - 'type': 'Key', - }, - 'pkh': { - 'description': '"pkh" parameter of this contract', - 'name': 'pkh', - 'type': 'WalletData', - }, - 'placeholder_key_0': { - 'description': 'placeholder_key_0', - 'name': 'placeholder_key_0', - 'type': 'Key', - }, - 'placeholder_key_2': { - 'description': 'placeholder_key_2', - 'name': 'placeholder_key_2', - 'type': 'Key', - }, - }, - }, - }, - 'scripts': { - 'P2PKH_unlock': { - 'passes': [ - 'P2PKH_evaluate', - ], - 'name': 'P2PKH_unlock', - 'script': '// "spend" function parameters\n // sig\n // pubkey = <0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088>\n', - 'unlocks': 'P2PKH_lock', - }, - 'P2PKH_lock': { - 'lockingType': 'p2sh20', - 'name': 'P2PKH_lock', - 'script': '// "P2PKH" contract constructor parameters\n // bytes20 = <0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970>\n\n// bytecode\n /* contract P2PKH(bytes20 pkh) { */\n /* // Require pk to match stored pkh and signature to match */\n /* function spend(pubkey pk, sig s) { */\nOP_OVER OP_HASH160 OP_EQUALVERIFY /* require(hash160(pk) == pkh); */\nOP_CHECKSIG /* require(checkSig(s, pk)); */\n /* } */\n /* } */\n /* */', - }, - 'p2pkh_placeholder_unlock_0': { - 'name': 'p2pkh_placeholder_unlock_0', - 'script': '\n', - 'unlocks': 'p2pkh_placeholder_lock_0', - }, - 'p2pkh_placeholder_lock_0': { - 'lockingType': 'standard', - 'name': 'p2pkh_placeholder_lock_0', - 'script': 'OP_DUP\nOP_HASH160 <$( OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG', - }, - 'p2pkh_placeholder_unlock_2': { - 'name': 'p2pkh_placeholder_unlock_2', - 'script': '\n', - 'unlocks': 'p2pkh_placeholder_lock_2', - }, - 'p2pkh_placeholder_lock_2': { - 'lockingType': 'standard', - 'name': 'p2pkh_placeholder_lock_2', - 'script': 'OP_DUP\nOP_HASH160 <$( OP_HASH160\n)> OP_EQUALVERIFY\nOP_CHECKSIG', - }, - }, - 'scenarios': { - 'P2PKH_evaluate': { - 'name': 'P2PKH_evaluate', - 'description': 'An example evaluation where this script execution passes.', - 'data': { - 'bytecode': { - 'pk': '0x0373cc07b54c22da627b572a387a20ea190c9382e5e6d48c1d5b89c5cea2c4c088', - 'pkh': '0x512dbb2c8c02efbac8d92431aa0ac33f6b0bf970', - }, - 'currentBlockHeight': expect.any(Number), - 'currentBlockTime': expect.any(Number), - 'keys': { - 'privateKeys': { - 's': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, - }, - }, - 'transaction': { - 'inputs': [ - { - 'outpointIndex': expect.any(Number), - 'outpointTransactionHash': expect.stringMatching(/^[0-9a-f]{64}$/), - 'sequenceNumber': 4294967294, - 'unlockingBytecode': { - 'script': 'p2pkh_placeholder_unlock_0', - 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_0': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, - }, - }, - }, - }, - { - 'outpointIndex': expect.any(Number), - 'outpointTransactionHash': expect.stringMatching(/^[0-9a-f]{64}$/), - 'sequenceNumber': 4294967294, - 'unlockingBytecode': [ - 'slot', - ], - }, - { - 'outpointIndex': expect.any(Number), - 'outpointTransactionHash': expect.stringMatching(/^[0-9a-f]{64}$/), - 'sequenceNumber': 4294967294, - 'unlockingBytecode': { - 'script': 'p2pkh_placeholder_unlock_2', - 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_2': '71080d8b52ec7b12adaec909ed54cd989b682ce2c35647eec219a16f5f90c528', - }, - }, - }, - }, - }, - ], - 'locktime': 133700, - 'outputs': [ - { - 'lockingBytecode': {}, - 'valueSatoshis': 1000, - }, - { - 'lockingBytecode': {}, - 'valueSatoshis': expect.any(Number), - }, - ], - 'version': 2, - }, - 'sourceOutputs': [ - { - 'lockingBytecode': { - 'script': 'p2pkh_placeholder_lock_0', - 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_0': '36f8155c559f3a670586bbbf9fd52beef6f96124f5a3a39c167fc24b052d24d7', - }, - }, - }, - }, - 'valueSatoshis': expect.any(Number), - }, - { - 'lockingBytecode': [ - 'slot', - ], - 'valueSatoshis': expect.any(Number), - }, - { - 'lockingBytecode': { - 'script': 'p2pkh_placeholder_lock_2', - 'overrides': { - 'keys': { - 'privateKeys': { - 'placeholder_key_2': '71080d8b52ec7b12adaec909ed54cd989b682ce2c35647eec219a16f5f90c528', - }, - }, - }, - }, - 'valueSatoshis': expect.any(Number), - }, - ], - }, - }, - }, - }, -]; diff --git a/packages/cashscript/test/fixture/vars.ts b/packages/cashscript/test/fixture/vars.ts index ffd77c38..b562f5bd 100644 --- a/packages/cashscript/test/fixture/vars.ts +++ b/packages/cashscript/test/fixture/vars.ts @@ -3,6 +3,7 @@ import { deriveHdPrivateNodeFromSeed, deriveSeedFromBip39Mnemonic, encodeCashAddress, + encodePrivateKeyWif, secp256k1, } from '@bitauth/libauth'; import { hash160 } from '@cashscript/utils'; @@ -23,18 +24,21 @@ if (typeof bobNode === 'string') throw new Error(); if (typeof carolNode === 'string') throw new Error(); export const alicePriv = aliceNode.privateKey; +export const aliceWif = encodePrivateKeyWif(alicePriv, 'testnet'); export const alicePub = secp256k1.derivePublicKeyCompressed(alicePriv) as Uint8Array; export const alicePkh = hash160(alicePub); export const aliceAddress = encodeCashAddress({ prefix: 'bchtest', type: 'p2pkh', payload: alicePkh, throwErrors: true }).address; export const aliceTokenAddress = encodeCashAddress({ prefix: 'bchtest', type: 'p2pkhWithTokens', payload: alicePkh, throwErrors: true }).address; export const bobPriv = bobNode.privateKey; +export const bobWif = encodePrivateKeyWif(bobPriv, 'testnet'); export const bobPub = secp256k1.derivePublicKeyCompressed(bobPriv) as Uint8Array; export const bobPkh = hash160(bobPub); export const bobAddress = encodeCashAddress({ prefix: 'bchtest', type: 'p2pkh', payload: bobPkh, throwErrors: true }).address; export const bobTokenAddress = encodeCashAddress({ prefix: 'bchtest', type: 'p2pkhWithTokens', payload: bobPkh, throwErrors: true }).address; export const carolPriv = carolNode.privateKey; +export const carolWif = encodePrivateKeyWif(carolPriv, 'testnet'); export const carolPub = secp256k1.derivePublicKeyCompressed(carolPriv) as Uint8Array; export const carolPkh = hash160(carolPub); export const carolAddress = encodeCashAddress({ prefix: 'bchtest', type: 'p2pkh', payload: carolPkh, throwErrors: true }).address; diff --git a/packages/cashscript/test/libauth-template/LibauthTemplate.test.ts b/packages/cashscript/test/libauth-template/LibauthTemplate.test.ts index fce99dc7..40fd0bd3 100644 --- a/packages/cashscript/test/libauth-template/LibauthTemplate.test.ts +++ b/packages/cashscript/test/libauth-template/LibauthTemplate.test.ts @@ -1,21 +1,11 @@ import { fixtures } from '../fixture/libauth-template/fixtures.js'; -import { fixtures as oldFixtures } from '../fixture/libauth-template/old-fixtures.js'; describe('Libauth Template generation tests (single-contract)', () => { fixtures.forEach((fixture) => { it(`should generate a valid libauth template for ${fixture.name}`, () => { const generatedTemplate = fixture.transaction.getLibauthTemplate(); // console.warn(JSON.stringify(generatedTemplate, null, 2)); - // console.warn(fixture.transaction.bitauthUri()); - expect(generatedTemplate).toEqual(fixture.template); - }); - }); - // old-fixtures using the deprecated simple transaction builder - oldFixtures.forEach((fixture) => { - it(`should generate a valid libauth template for old-fixture ${fixture.name}`, async () => { - const generatedTemplate = await fixture.transaction.getLibauthTemplate(); - // console.warn(JSON.stringify(generatedTemplate, null, 2)); - // console.warn(fixture.transaction.bitauthUri()); + // console.warn(fixture.transaction.getBitauthUri()); expect(generatedTemplate).toEqual(fixture.template); }); }); diff --git a/packages/cashscript/test/libauth-template/LibauthTemplateMultiContract.test.ts b/packages/cashscript/test/libauth-template/LibauthTemplateMultiContract.test.ts index 438c1b30..33f4cfb6 100644 --- a/packages/cashscript/test/libauth-template/LibauthTemplateMultiContract.test.ts +++ b/packages/cashscript/test/libauth-template/LibauthTemplateMultiContract.test.ts @@ -6,7 +6,7 @@ describe('Libauth Template generation tests (multi-contract)', () => { const builder = await fixture.transaction; const generatedTemplate = builder.getLibauthTemplate(); // console.warn(JSON.stringify(generatedTemplate, null, 2)); - // console.warn(builder.bitauthUri()); + // console.warn(builder.getBitauthUri()); expect(generatedTemplate).toEqual(fixture.template); }); }); diff --git a/packages/cashscript/test/multi-contract-debugging.test.ts b/packages/cashscript/test/multi-contract-debugging.test.ts index b28c8c59..c9a7032d 100644 --- a/packages/cashscript/test/multi-contract-debugging.test.ts +++ b/packages/cashscript/test/multi-contract-debugging.test.ts @@ -143,7 +143,7 @@ describe('Multi-Contract-Debugging tests', () => { .addInput(bobAddressUtxos[0], bobSignatureTemplate.unlockP2PKH()) .addOutput({ to, amount }); - console.warn(transaction.bitauthUri()); + console.warn(transaction.getBitauthUri()); await expect(transaction).toFailRequireWith('P2PKH.cash:4 Require statement failed at input 0 in contract P2PKH.cash at line 4.'); }); @@ -169,7 +169,7 @@ describe('Multi-Contract-Debugging tests', () => { .addInput(bobAddressUtxos[0], bobSignatureTemplate.unlockP2PKH()) .addOutput({ to, amount }); - console.warn(transaction.bitauthUri()); + console.warn(transaction.getBitauthUri()); await expect(transaction).toFailRequireWith('BigInt.cash:4 Require statement failed at input 1 in contract BigInt.cash at line 4.'); }); @@ -197,7 +197,7 @@ describe('Multi-Contract-Debugging tests', () => { .addInput(bobAddressUtxos[0], bobSignatureTemplate.unlockP2PKH()) .addOutput({ to, amount }); - console.warn(transaction.bitauthUri()); + console.warn(transaction.getBitauthUri()); await expect(transaction).toFailRequireWith('P2PKH.cash:5 Require statement failed at input 0 in contract P2PKH.cash at line 5'); }); @@ -225,7 +225,7 @@ describe('Multi-Contract-Debugging tests', () => { .addInput(bobAddressUtxos[0], bobSignatureTemplate.unlockP2PKH()) .addOutput({ to, amount }); - console.warn(transaction.bitauthUri()); + console.warn(transaction.getBitauthUri()); await expect(transaction).toFailRequireWith('BigInt.cash'); }); diff --git a/packages/cashscript/test/types/Contract.types.test.ts b/packages/cashscript/test/types/Contract.types.test.ts index e7eb0824..587f2be2 100644 --- a/packages/cashscript/test/types/Contract.types.test.ts +++ b/packages/cashscript/test/types/Contract.types.test.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -import { Artifact, Contract, SignatureTemplate, Transaction, Unlocker } from 'cashscript'; +import { Artifact, Contract, MockNetworkProvider, SignatureTemplate } from 'cashscript'; import p2pkhArtifact from '../fixture/p2pkh.artifact'; import p2pkhArtifactJsonNotConst from '../fixture/p2pkh.json' with { type: 'json' }; import announcementArtifact from '../fixture/announcement.artifact'; @@ -32,41 +32,48 @@ interface ManualArtifactType extends Artifact { ] } +// Create a MockNetworkProvider for the tests +const provider = new MockNetworkProvider(); + // describe('P2PKH contract | single constructor input | single function (2 args)') { // describe('Constructor arguments') { // it('should not give type errors when using correct constructor inputs') - new Contract(p2pkhArtifact, [alicePkh]); - new Contract(p2pkhArtifact, [binToHex(alicePkh)]); + new Contract(p2pkhArtifact, [alicePkh], { provider }); + new Contract(p2pkhArtifact, [binToHex(alicePkh)], { provider }); // it('should give type errors when using empty constructor inputs') // @ts-expect-error - new Contract(p2pkhArtifact, []); + new Contract(p2pkhArtifact, [], { provider }); // it('should give type errors when using incorrect constructor input type') // @ts-expect-error - new Contract(p2pkhArtifact, [1000n]); + new Contract(p2pkhArtifact, [1000n], { provider }); // it('should give type errors when using incorrect constructor input length') // @ts-expect-error - new Contract(p2pkhArtifact, [alicePkh, 1000n]); + new Contract(p2pkhArtifact, [alicePkh, 1000n], { provider }); // it('should not perform type checking when cast to any') - new Contract(p2pkhArtifact as any, [alicePkh, 1000n]); + new Contract(p2pkhArtifact as any, [alicePkh, 1000n], { provider }); // it('should not perform type checking when cannot infer type') // Note: would be very nice if it *could* infer the type from static json - new Contract(p2pkhArtifactJsonNotConst, [alicePkh, 1000n]); + new Contract(p2pkhArtifactJsonNotConst, [alicePkh, 1000n], { provider }); // it('should perform type checking when manually specifying a type // @ts-expect-error - new Contract(p2pkhArtifactJsonNotConst as any, [alicePkh, 1000n]); + new Contract(p2pkhArtifactJsonNotConst as any, [alicePkh, 1000n], { provider }); + + // it('requires a provider to be passed') + // @ts-expect-error + new Contract(p2pkhArtifact, [alicePkh]); } // describe('Contract unlockers') { - const contract = new Contract(p2pkhArtifact, [alicePkh]); + const contract = new Contract(p2pkhArtifact, [alicePkh], { provider }); // it('should not give type errors when using correct function inputs') contract.unlock.spend(alicePub, new SignatureTemplate(alicePriv)); @@ -86,14 +93,14 @@ interface ManualArtifactType extends Artifact { contract.unlock.spend(alicePub); // it('should not perform type checking when cast to any') - const contractAsAny = new Contract(p2pkhArtifact as any, [alicePkh, 1000n]); + const contractAsAny = new Contract(p2pkhArtifact as any, [alicePkh, 1000n], { provider }); contractAsAny.unlock.notAFunction(); contractAsAny.unlock.spend(); contractAsAny.unlock.spend(1000n, true); // it('should not perform type checking when cannot infer type') // Note: would be very nice if it *could* infer the type from static json - const contractFromUnknown = new Contract(p2pkhArtifactJsonNotConst, [alicePkh, 1000n]); + const contractFromUnknown = new Contract(p2pkhArtifactJsonNotConst, [alicePkh, 1000n], { provider }); contractFromUnknown.unlock.notAFunction(); contractFromUnknown.unlock.spend(); contractFromUnknown.unlock.spend(1000n, true); @@ -107,52 +114,9 @@ interface ManualArtifactType extends Artifact { contractFromUnknown.unlock.spend().notAFunction(); } - // describe('Contract functions') - { - const contract = new Contract(p2pkhArtifact, [alicePkh]); - - // it('should not give type errors when using correct function inputs') - contract.functions.spend(alicePub, new SignatureTemplate(alicePriv)).build(); - - // it('should give type errors when calling a function that does not exist') - // @ts-expect-error - contract.functions.notAFunction(); - - // it('should give type errors when using incorrect function input types') - // @ts-expect-error - contract.functions.spend(1000n, true); - - // it('should give type errors when using incorrect function input length') - // @ts-expect-error - contract.functions.spend(alicePub, new SignatureTemplate(alicePriv), 100n); - // @ts-expect-error - contract.functions.spend(alicePub); - - // it('should not perform type checking when cast to any') - const contractAsAny = new Contract(p2pkhArtifact as any, [alicePkh, 1000n]); - contractAsAny.functions.notAFunction().build(); - contractAsAny.functions.spend(); - contractAsAny.functions.spend(1000n, true); - - // it('should not perform type checking when cannot infer type') - // Note: would be very nice if it *could* infer the type from static json - const contractFromUnknown = new Contract(p2pkhArtifactJsonNotConst, [alicePkh, 1000n]); - contractFromUnknown.functions.notAFunction().build(); - contractFromUnknown.functions.spend(); - contractFromUnknown.functions.spend(1000n, true); - - // it('should give type errors when calling methods that do not exist on the returned object') - // @ts-expect-error - contract.functions.spend().notAFunction(); - // @ts-expect-error - contractAsAny.functions.spend().notAFunction(); - // @ts-expect-error - contractFromUnknown.functions.spend().notAFunction(); - } - // describe('Contract unlockers') { - const contract = new Contract(p2pkhArtifact, [alicePkh]); + const contract = new Contract(p2pkhArtifact, [alicePkh], { provider }); // it('should not give type errors when using correct function inputs') contract.unlock.spend(alicePub, new SignatureTemplate(alicePriv)).generateLockingBytecode(); @@ -172,14 +136,14 @@ interface ManualArtifactType extends Artifact { contract.unlock.spend(alicePub); // it('should not perform type checking when cast to any') - const contractAsAny = new Contract(p2pkhArtifact as any, [alicePkh, 1000n]); + const contractAsAny = new Contract(p2pkhArtifact as any, [alicePkh, 1000n], { provider }); contractAsAny.unlock.notAFunction().generateLockingBytecode(); contractAsAny.unlock.spend(); contractAsAny.unlock.spend(1000n, true); // it('should not perform type checking when cannot infer type') // Note: would be very nice if it *could* infer the type from static json - const contractFromUnknown = new Contract(p2pkhArtifactJsonNotConst, [alicePkh, 1000n]); + const contractFromUnknown = new Contract(p2pkhArtifactJsonNotConst, [alicePkh, 1000n], { provider }); contractFromUnknown.unlock.notAFunction().generateLockingBytecode(); contractFromUnknown.unlock.spend(); contractFromUnknown.unlock.spend(1000n, true); @@ -199,21 +163,21 @@ interface ManualArtifactType extends Artifact { // describe('Constructor arguments') { // it('should not give type errors when using correct constructor inputs') - new Contract(announcementArtifact, []); + new Contract(announcementArtifact, [], { provider }); // it('should give type errors when using incorrect constructor input length') // @ts-expect-error - new Contract(announcementArtifact, [1000n]); + new Contract(announcementArtifact, [1000n], { provider }); // it('should give type errors when passing in completely incorrect type') // @ts-expect-error - new Contract(announcementArtifact, 'hello'); + new Contract(announcementArtifact, 'hello', { provider }); } // describe('Contract unlockers') { // it('should not give type errors when using correct function inputs') - const contract = new Contract(announcementArtifact, []); + const contract = new Contract(announcementArtifact, [], { provider }); // it('should not give type errors when using correct function inputs') contract.unlock.announce(); @@ -226,23 +190,6 @@ interface ManualArtifactType extends Artifact { // @ts-expect-error contract.unlock.announce('hello world'); } - - // describe('Contract functions') - { - // it('should not give type errors when using correct function inputs') - const contract = new Contract(announcementArtifact, []); - - // it('should not give type errors when using correct function inputs') - contract.functions.announce(); - - // it('should give type errors when calling a function that does not exist') - // @ts-expect-error - contract.functions.notAFunction(); - - // it('should give type errors when using incorrect function input length') - // @ts-expect-error - contract.functions.announce('hello world'); - } } // describe('HodlVault contract | 4 constructor inputs | single function (3 args)') @@ -250,17 +197,17 @@ interface ManualArtifactType extends Artifact { // describe('Constructor arguments') { // it('should not give type errors when using correct constructor inputs') - new Contract(hodlVaultArtifact, [alicePub, binToHex(oraclePub), 1000n, 1000n]); + new Contract(hodlVaultArtifact, [alicePub, binToHex(oraclePub), 1000n, 1000n], { provider }); // it('should give type errors when using too few constructor inputs') // @ts-expect-error - new Contract(hodlVaultArtifact, [alicePub, binToHex(oraclePub)]); + new Contract(hodlVaultArtifact, [alicePub, binToHex(oraclePub)], { provider }); // it('should give type errors when using incorrect constructor input type') // @ts-expect-error - new Contract(hodlVaultArtifact, [alicePub, binToHex(oraclePub), 1000n, 'hello']); + new Contract(hodlVaultArtifact, [alicePub, binToHex(oraclePub), 1000n, 'hello'], { provider }); // @ts-expect-error - new Contract(hodlVaultArtifact, [alicePub, binToHex(oraclePub), true, 1000n]); + new Contract(hodlVaultArtifact, [alicePub, binToHex(oraclePub), true, 1000n], { provider }); } } @@ -270,12 +217,12 @@ interface ManualArtifactType extends Artifact { // describe('Constructor arguments') { // it('should not give type errors when using correct constructor inputs') - new Contract(transferWithTimeoutArtifact, [alicePub, bobPub, 100_000n]); + new Contract(transferWithTimeoutArtifact, [alicePub, bobPub, 100_000n], { provider }); } // describe('Contract unlockers') { - const contract = new Contract(transferWithTimeoutArtifact, [alicePub, bobPub, 100_000n]); + const contract = new Contract(transferWithTimeoutArtifact, [alicePub, bobPub, 100_000n], { provider }); // it('should not give type errors when using correct function inputs') contract.unlock.transfer(new SignatureTemplate(alicePriv)); @@ -297,29 +244,4 @@ interface ManualArtifactType extends Artifact { // @ts-expect-error contract.unlock.timeout(); } - - // describe('Contract functions') - { - const contract = new Contract(transferWithTimeoutArtifact, [alicePub, bobPub, 100_000n]); - - // it('should not give type errors when using correct function inputs') - contract.functions.transfer(new SignatureTemplate(alicePriv)); - contract.functions.timeout(new SignatureTemplate(alicePriv)); - - // it('should give type errors when calling a function that does not exist') - // @ts-expect-error - contract.functions.notAFunction(); - - // it('should give type errors when using incorrect function input types') - // @ts-expect-error - contract.functions.transfer(1000n); - // @ts-expect-error - contract.functions.timeout(true); - - // it('should give type errors when using incorrect function input length') - // @ts-expect-error - contract.functions.transfer(new SignatureTemplate(alicePub), 100n); - // @ts-expect-error - contract.functions.timeout(); - } } diff --git a/packages/utils/package.json b/packages/utils/package.json index 76a99341..d4cdb6bf 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@cashscript/utils", - "version": "0.11.5", + "version": "0.12.0", "description": "CashScript utilities and types", "keywords": [ "bitcoin cash", @@ -44,7 +44,7 @@ "test": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest" }, "dependencies": { - "@bitauth/libauth": "^3.1.0-next.2" + "@bitauth/libauth": "^3.1.0-next.8" }, "devDependencies": { "@jest/globals": "^29.7.0", diff --git a/website/docs/basics/getting-started.md b/website/docs/basics/getting-started.md index 31778f86..2d1dae35 100644 --- a/website/docs/basics/getting-started.md +++ b/website/docs/basics/getting-started.md @@ -52,7 +52,7 @@ We can start from a basic `TransferWithTimeout` smart contract, a simple contrac Open your code editor to start writing your first CashScript smart contract. Then create a new file `TransferWithTimeout.cash` and copy over the smart contracts code from below. ```solidity -pragma cashscript ^0.11.0; +pragma cashscript ^0.12.0; contract TransferWithTimeout(pubkey sender, pubkey recipient, int timeout) { // Allow the recipient to claim their received money diff --git a/website/docs/guides/debugging.md b/website/docs/guides/debugging.md index fe4d7de8..5189623f 100644 --- a/website/docs/guides/debugging.md +++ b/website/docs/guides/debugging.md @@ -42,10 +42,10 @@ To help with debugging you can add `console.log` statements to your CashScript c Whenever a transaction fails, there will be a link in the console to open your smart contract transaction in the BitAuth IDE. This will allow you to inspect the transaction in detail, and see exactly why the transaction failed. In the BitAuth IDE you will see the raw BCH Script mapping to each line in your CashScript contract. Find the failing line and investigate the failing OpCode. You can break up the failing line, one opcode at a time, to see how the stack evolves and ends with your `require` failure. -It's also possible to export the transaction for step-by-step debugging in the BitAuth IDE without failure. To do so, you can call the `bitauthUri()` function on the transaction. This will return a URI that can be opened in the BitAuth IDE. +It's also possible to export the transaction for step-by-step debugging in the BitAuth IDE without failure. To do so, you can call the `getBitauthUri()` function on the transaction. This will return a URI that can be opened in the BitAuth IDE. ```ts -const uri = await transactionBuilder.bitauthUri(); +const uri = await transactionBuilder.getBitauthUri(); ``` :::caution @@ -75,7 +75,7 @@ OP_3 OP_ROLL OP_SWAP OP_CHECKSIGVERIFY /* require(checkSig(senderSig, se OP_SWAP OP_CHECKLOCKTIMEVERIFY /* require(tx.time >= timeout); */ OP_2DROP OP_1 /* } */ OP_ENDIF /* } */ - + ``` [BitauthIDE]: https://ide.bitauth.com/import-template/eJzFWAtv2zYQ_isCN2BJ4drUW8raAK3jNkbSJEvcFUMdGBR1stXakidRWYIg--07SrIk23LgDhlGBKEiHu-7x3fUMY_k55TPYMHIEZkJsUyPer3Qh64XCpaJWZfHi558gEiEnIkwjl4LWCznTMDrO9ot9na_pXFEOsSHlCfhUkqhuuFiGScCfCVI4oXCWTorVlEwYgtAiT6-u8nfKR8hgoRJ6RPwsuk0jKbKqATCDWm2LJSRo6_kff90olHNnFCT3HbIHSRpjkg7RJopQkjJ0SMZJSxKA0i-hGI2ChcQZ2ISRstM0MmSJWiBwI1ScN3sfhyJhHGh8ARyhxUWoQ9ZxPM_GlsrP1qQlIMcSvmJHkrzc_2pNL7NqnnMv6NU25JYNzyLclnpNUtC5s0LV1OIfEhuwum2O-N6cUxq65U4qH0akxJmTGqnap0dIh6W8tUZPJCnTrmyG2oTR8zCVOFlWDcBau1f2HwO4oQJJkES4OEyxHy24VSLe0LVynaglf63YVWh2QtppWgHzirkmE8f7rfhymqoMpOLKSJW4B54lpdCCbShqRUPR4N77RXRTjAUXrI0hZ2U3dgGd2yeyVK93YxEWyHgaq_XZF1beY2jNxUF5TkTxUkyScNpxESWQBfdnOBeVJ1O5HMm7uP0WEG9KDOOxhE-bYQxjKqEofL1AOY7w0gob5U36vFYnmZFKNL2-i5q9qm9aFchlDMeZKMiM0stnenas6fGVoRaZDBavDqi4igVScZFnKxHroxs0yt671KfUh2de1PVQy6wzLzv8FDIUN0JTNM0HN8OdF91dA6e5wDYNtWtAMAzqBMYzKQ6DTzDsWzVtkzX81zDCMDEZX5cJW5buaY6vqFalmq5mouPGlcD29JBM3QwPNPjuh84oOIyZcw1dU-1mQVUc6lmaWbg0uMys96DAB77MI6U_UbvlbJM2HTBGh8i5W_aVdUu_XVPHc3xqvcj0C86fgy64koLlQ7K5BTp6qxyVbGjk3On5NKh8vgvvMZcXcOfWZhArfaXVKnqWJ5tCyb4bB-vL68muoK_rob9MzlT-evi86fBb5_fncvn4YcGdFX9ovT8AFFrK_BYkS7tMUpoQyJcX56fF_NITv3TQf_sZvhxw2s5ksLtA-yQ-HcEO2gid2o7Dg-f4V8JfTG8UramwfnNYDvgT_t4tK_Xe4r-vwzPp5pmq--OnCW7PMk8hlnw80au4PpOCjZptsq12qTZ74Pr4Yc_GtA1zcqikiyrvlx7UmwX9M2Xd1dNnpXoz9Gsgu6UVjxLsDXoNbTzy_7ZaPhpUDvcDHgTWtx382Afv63Oiv2P1BJaO7m-vCqCvXP8FwyXdXRxUpwbzwyEfjHYFfSeoi9fXKRoCyHCK0S8b2NYdXgoXjYwg_JV26dFaW3_1pvddxG2tQzveKCU2mUZ_TUDWZOyvS4_00XvW3SHsiftSk2ywUVTVo1AfQtCxS_RaZC1Kwh5ic6INO4ZZNWOke1LAVFlY8mzJEHs97KJPIVwOsNd2vprGWtypNqGhViaa3QIfr7zhC6T8A4zc1b-2bgfEstwLddjhqepaJvuO6pl2hq1uUZdz0HjTUvXLIeCYXsAGmO2a_HA9j3K8YfbJL9V5N9UxotMPpI8yfKy8EhkUx5j3zAsnEGjVm9G9Z5TbMLQFgBuBa5lqrYWcNc3HGrqAQ-Au6aqqtRmjgm6r_nUU33VBWmyjUmjlu5z3fAC2S_jIQQRh4ts4cnkGxgH17LzaBQdPHbg7yuWfCXpPBbk9gkvKnJR5CHUTIqjsHTlx9ZWYlvMVQ1HM1Xug61xmfvA9wPbMTBArmcyZpgOhs-2ASzHYTLjkthww0ScIqMxWdRFpKfmvy00WY1xlnC4fA5-ZXmbSsNxnm4xL_8AUE0PwQ== diff --git a/website/docs/language/contracts.md b/website/docs/language/contracts.md index 9cb0c75d..f58036a0 100644 --- a/website/docs/language/contracts.md +++ b/website/docs/language/contracts.md @@ -13,7 +13,7 @@ Contract authors should be careful when allowing a range of versions to check th #### Example ```solidity -pragma cashscript ^0.11.0; +pragma cashscript ^0.12.0; pragma cashscript >= 0.7.0 < 0.9.3; ``` @@ -22,7 +22,7 @@ A CashScript constructor works slightly differently than what you might be used #### Example ```solidity -pragma cashscript ^0.11.0; +pragma cashscript ^0.12.0; contract HTLC(pubkey sender, pubkey recipient, int expiration, bytes32 hash) { ... @@ -46,7 +46,7 @@ The main construct in a CashScript contract is the function. A contract can cont #### Example ```solidity -pragma cashscript ^0.11.0; +pragma cashscript ^0.12.0; contract TransferWithTimeout(pubkey sender, pubkey recipient, int timeout) { function transfer(sig recipientSig) { @@ -87,7 +87,7 @@ The error message in a `require` statement is only available in debug evaluation #### Example ```solidity -pragma cashscript ^0.11.0; +pragma cashscript ^0.12.0; contract P2PKH(bytes20 pkh) { function spend(pubkey pk, sig s) { @@ -129,7 +129,7 @@ There is no implicit type conversion from non-boolean to boolean types. So `if ( #### Example ```solidity -pragma cashscript ^0.11.0; +pragma cashscript ^0.12.0; contract OneOfTwo(bytes20 pkh1, bytes32 hash1, bytes20 pkh2, bytes32 hash2) { function spend(pubkey pk, sig s, bytes message) { @@ -156,7 +156,7 @@ Logging is only available in debug evaluation of a transaction, but has no impac #### Example ```solidity -pragma cashscript ^0.11.0; +pragma cashscript ^0.12.0; contract P2PKH(bytes20 pkh) { function spend(pubkey pk, sig s) { diff --git a/website/docs/language/examples.md b/website/docs/language/examples.md index 434f2981..80c12567 100644 --- a/website/docs/language/examples.md +++ b/website/docs/language/examples.md @@ -12,7 +12,7 @@ This smart contract works by connecting with a price oracle. This price oracle i This involves some degree of trust in the price oracle, but since the oracle produces price data for everyone to use, their incentive to attack *your* smart contract is minimised. To improve this situation, you can also choose to connect with multiple oracle providers so you do not have to trust a single party. ```solidity -pragma cashscript ^0.11.0; +pragma cashscript ^0.12.0; // A minimum block is provided to ensure that oracle price entries from before // this block are disregarded. i.e. when the BCH price was $1000 in the past, @@ -53,7 +53,7 @@ The contract works by checking that a UTXO is at least 30 days old, after which Due to the nature of covenants, we have to be very specific about the outputs (amounts and destinations) of the transaction. This also means that we have to account for the special case where the remaining contract balance is lower than the `pledge` amount, meaning no remainder should be sent back. Finally, we have to account for a small fee that has to be taken from the contract's balance to pay the miners. ```solidity -pragma cashscript ^0.11.0; +pragma cashscript ^0.12.0; contract Mecenas(bytes20 recipient, bytes20 funder, int pledge, int period) { function receive() { @@ -95,7 +95,7 @@ AMM DEX contract based on [the Cauldron DEX contract](https://www.cauldron.quest The CashScript contract code has the big advantage of abstracting away any stack management, having variable names, explicit types and a logical order of operations (compared to the 'reverse Polish notation' of raw script). ```solidity -pragma cashscript ^0.11.0; +pragma cashscript ^0.12.0; contract DexContract(bytes20 poolOwnerPkh) { function swap() { diff --git a/website/docs/releases/migration-notes.md b/website/docs/releases/migration-notes.md index f7644f0b..119d7925 100644 --- a/website/docs/releases/migration-notes.md +++ b/website/docs/releases/migration-notes.md @@ -2,6 +2,55 @@ title: Migration Notes --- +## v0.11 to v0.12 + +There are several breaking changes to the SDK in this release. + +### CashScript SDK + +#### Old Transaction Builder Removal + +The most impactful breaking change is the removal of the deprecated 'Simple Transaction Builder'. See [below for steps to migrate to the new transaction builder](/docs/releases/migration-notes#sdk-transaction-builder). + +#### Contract constructor +Before, the `provider` option was optional in the `Contract` constructor. This is no longer the case. + +```ts +// Before: defaults to mainnet ElectrumNetworkProvider +const contract = new Contract(artifact, constructorArgs); + +// After: explicitly specify the provider +const provider = new ElectrumNetworkProvider('mainnet'); +const contract = new Contract(artifact, constructorArgs, { provider }); +``` + +#### Transaction Builder +Before, the `setMaxFee()` method was used to set the maximum fee for the transaction. This was replaced with the `maximumFeeSatoshis` option in the constructor. Additionally, the `maximumFeeSatsPerByte` option was added. + +```ts +// Before: setMaxFee() was used to set the maximum fee +const builder = new TransactionBuilder({ provider }).setMaxFee(1000n); + +// After: maximumFeeSatoshis option was added to the constructor +const builder = new TransactionBuilder({ provider, maximumFeeSatoshis: 1000n }); +``` + +Addtionally, `transactionBuilder.bitauthUri()` was renamed to `transactionBuilder.getBitauthUri()` for consistency. + +#### MockNetworkProvider + +Before, the `updateUtxoSet` option was `false` by default for the `MockNetworkProvider`. This is now `true` by default to better match real-world network behaviour. + +```ts +// Before: updateUtxoSet is false by default +const provider = new MockNetworkProvider(); + +// After: updateUtxoSet is true by default, if you want to keep the old behaviour, set it to false +const provider = new MockNetworkProvider({ updateUtxoSet: false }); +``` + +Earlier, the `MockNetworkProvider` also automatically added some test UTXOs to the provider, which is no longer the case. Make sure to add any UTXOs you need manually. + ## v0.10 to v0.11 There are several breaking changes to the compiler and SDK in this release. They are listed below in their own sections. diff --git a/website/docs/releases/release-notes.md b/website/docs/releases/release-notes.md index c0bc251e..12f05d96 100644 --- a/website/docs/releases/release-notes.md +++ b/website/docs/releases/release-notes.md @@ -2,6 +2,23 @@ title: Release Notes --- +## v0.12.0 + +#### CashScript SDK +- :sparkles: Add `getVmResourceUsage` method to `TransactionBuilder`. +- :sparkles: Add `maximumFeeSatsPerByte` and `allowImplicitFungibleTokenBurn` options to `TransactionBuilder` constructor. +- :sparkles: Add a configurable `vmTarget` option to `MockNetworkProvider`. +- :sparkles: Add support for ECDSA signatures in contract unlockers for `sig` and `datasig` parameters. +- :sparkles: Add `signMessageHash()` method to `SignatureTemplate` to allow for signing of non-transaction messages. +- :boom: **BREAKING**: Remove deprecated "old" transaction builder (`contract.functions`). +- :boom: **BREAKING**: Make `provider` a required option in `Contract` constructor. +- :boom: **BREAKING**: Set `updateUtxoSet` to `true` by default for `MockNetworkProvider`. +- :boom: **BREAKING**: No longer seed the MockNetworkProvider with any test UTXOs. +- :boom: **BREAKING**: Replace `setMaxFee()` method on `TransactionBuilder` with +- :boom: **BREAKING**: Rename `bitauthUri()` method on `TransactionBuilder` to `getBitauthUri()` for consistency. +- :hammer_and_wrench: Improve libauth template generation. +- :bug: Fix bug where `SignatureTemplate` would not accept private key hex strings as a signer. + ## v0.11.5 #### CashScript SDK @@ -48,6 +65,8 @@ This update adds CashScript support for the new BCH 2025 network upgrade. To rea This release also contains several breaking changes, please refer to the [migration notes](/docs/releases/migration-notes) for more information. +Thanks [kiok](https://x.com/cypherpunk_bch) for the significant contributions! + #### cashc compiler - :bug: Fix bug where source code in `--format ts` artifacts used incorrect quotation marks. - :hammer_and_wrench: Remove warning for opcount and update warning for byte size to match new limits. diff --git a/website/docs/sdk/examples.md b/website/docs/sdk/examples.md index 25fd2beb..ffb13050 100644 --- a/website/docs/sdk/examples.md +++ b/website/docs/sdk/examples.md @@ -92,11 +92,12 @@ console.log('contract balance:', await contract.getBalance()); We need the create the functionality for generating and signing the oracle message to use in the HodlVault contract: ```ts title="PriceOracle.ts" -import { padMinimallyEncodedVmNumber, flattenBinArray, secp256k1 } from '@bitauth/libauth'; +import { padMinimallyEncodedVmNumber, flattenBinArray } from '@bitauth/libauth'; import { encodeInt, sha256 } from '@cashscript/utils'; +import { SignatureAlgorithm, SignatureTemplate } from 'cashscript'; export class PriceOracle { - constructor(public privateKey: Uint8Array) {} + constructor(public privateKey: Uint8Array) { } // Encode a blockHeight and bchUsdPrice into a byte sequence of 8 bytes (4 bytes per value) createMessage(blockHeight: bigint, bchUsdPrice: bigint): Uint8Array { @@ -106,12 +107,12 @@ export class PriceOracle { return flattenBinArray([encodedBlockHeight, encodedBchUsdPrice]); } - signMessage(message: Uint8Array): Uint8Array { - const signature = secp256k1.signMessageHashSchnorr(this.privateKey, sha256(message)); - if (typeof signature === 'string') throw new Error(); - return signature; + signMessage(message: Uint8Array, signatureAlgorithm: SignatureAlgorithm = SignatureAlgorithm.SCHNORR): Uint8Array { + const signatureTemplate = new SignatureTemplate(this.privateKey, undefined, signatureAlgorithm); + return signatureTemplate.signMessageHash(sha256(message)); } } + ``` ### Sending a Transaction diff --git a/website/docs/sdk/other-network-providers.md b/website/docs/sdk/other-network-providers.md index 5e3233aa..85429d98 100644 --- a/website/docs/sdk/other-network-providers.md +++ b/website/docs/sdk/other-network-providers.md @@ -14,7 +14,7 @@ The `MockNetworkProvider` is a special network provider that allows you to evalu The `MockNetworkProvider` has extra methods to enable this local emulation such as `.addUtxo()` and `.setBlockHeight()`. You can read more about the `MockNetworkProvider` and automated tests on the [testing setup](/docs/sdk/testing-setup) page. -The `updateUtxoSet` option is used to determine whether the UTXO set should be updated after a transaction is sent. If `updateUtxoSet` is `true`, the UTXO set will be updated to reflect the new state of the mock network. If `updateUtxoSet` is `false` (default), the UTXO set will not be updated. +The `updateUtxoSet` option is used to determine whether the UTXO set should be updated after a transaction is sent. If `updateUtxoSet` is `true` (default), the UTXO set will be updated to reflect the new state of the mock network. If `updateUtxoSet` is `false`, the UTXO set will not be updated. #### Example ```ts diff --git a/website/docs/sdk/signature-templates.md b/website/docs/sdk/signature-templates.md index 88d906d5..9f988966 100644 --- a/website/docs/sdk/signature-templates.md +++ b/website/docs/sdk/signature-templates.md @@ -2,7 +2,7 @@ title: Signature Templates --- -When a contract function has a `sig` parameter, it needs a cryptographic signature from a private key for the spending transaction. +When a contract function has a `sig` parameter, it needs a cryptographic signature from a private key for the spending transaction. In place of a signature, a `SignatureTemplate` can be passed, which will generate the correct signature when the transaction is built. :::tip @@ -59,7 +59,7 @@ transactionBuilder.addInput(aliceUtxos[0], aliceTemplate.unlockP2PKH()); ### getPublicKey() -The `SignatureTemplate` also had a helper method to get the matching PublicKey in the following way: +The `SignatureTemplate` also has a helper method to get the matching PublicKey in the following way: ```ts signatureTemplate.getPublicKey(): Uint8Array @@ -72,6 +72,23 @@ import { aliceTemplate } from './somewhere.js'; const alicePublicKey = aliceTemplate.getPublicKey() ``` +### signMessageHash() + +The `SignatureTemplate` also has a helper method to sign a message hash, which can be used to sign non-transaction messages. This is useful for generating `datasig` signatures for smart contract use cases. + +```ts +signatureTemplate.signMessageHash(message: Uint8Array): Uint8Array +``` + +#### Example +```ts +import { aliceTemplate } from './somewhere.js'; +import { sha256 } from '@cashscript/utils'; +import { hexToBin } from '@bitauth/libauth'; + +const signature = aliceTemplate.signMessageHash(sha256(hexToBin('0000000000000000000000'))); +``` + ## Advanced Usage ### HashType diff --git a/website/docs/sdk/transaction-builder.md b/website/docs/sdk/transaction-builder.md index b3261eb1..d96523a6 100644 --- a/website/docs/sdk/transaction-builder.md +++ b/website/docs/sdk/transaction-builder.md @@ -4,8 +4,6 @@ title: Transaction Builder The CashScript Transaction Builder generalizes transaction building to allow for complex transactions combining multiple different smart contracts within a single transaction or to create basic P2PKH transactions. The Transaction Builder works by adding inputs and outputs to fully specify the transaction shape. -For the documentation for the old and deprecated transaction builder API, refer to [this docs page instead](/docs/sdk/transactions). - :::info Defining the inputs and outputs requires careful consideration because the difference in Bitcoin Cash value between in- and outputs is what's paid in transaction fees to the miners. ::: @@ -15,15 +13,16 @@ Defining the inputs and outputs requires careful consideration because the diffe new TransactionBuilder(options: TransactionBuilderOptions) ``` -To start, you need to instantiate a transaction builder and pass in a `NetworkProvider` instance. +To start, you need to instantiate a transaction builder and pass in a `NetworkProvider` instance and other options. ```ts interface TransactionBuilderOptions { provider: NetworkProvider; + maximumFeeSatoshis?: bigint; + maximumFeeSatsPerByte?: number; } ``` - #### Example ```ts import { ElectrumNetworkProvider, TransactionBuilder, Network } from 'cashscript'; @@ -32,6 +31,24 @@ const provider = new ElectrumNetworkProvider(Network.MAINNET); const transactionBuilder = new TransactionBuilder({ provider }); ``` +### Constructor Options + +#### provider + +The `provider` option is used to specify the network provider to use when sending the transaction. + +#### maximumFeeSatoshis + +The `maximumFeeSatoshis` option is used to specify the maximum fee for the transaction in satoshis. If this fee is exceeded, an error will be thrown when building the transaction. + +#### maximumFeeSatsPerByte + +The `maximumFeeSatsPerByte` option is used to specify the maximum fee per byte for the transaction. If this fee is exceeded, an error will be thrown when building the transaction. + +#### allowImplicitFungibleTokenBurn + +The `allowImplicitFungibleTokenBurn` option is used to specify whether implicit burning of fungible tokens is allowed (default: `false`). If this is set to `true`, the transaction builder will not throw an error when burning fungible tokens. + ## Transaction Building ### addInput() @@ -161,18 +178,6 @@ Sets the locktime for the transaction to set a transaction-level absolute timelo transactionBuilder.setLocktime(((Date.now() / 1000) + 24 * 60 * 60) * 1000); ``` -### setMaxFee() -```ts -transactionBuilder.setMaxFee(maxFee: bigint): this -``` - -Sets a max fee for the transaction. Because the transaction builder does not automatically add a change output, you can set a max fee as a safety measure to make sure you don't accidentally pay too much in fees. If the transaction fee exceeds the max fee, an error will be thrown when building the transaction. - -#### Example -```ts -transactionBuilder.setMaxFee(1000n); -``` - ## Completing the Transaction ### send() ```ts @@ -238,12 +243,12 @@ transactionBuilder.debug(): DebugResult If you want to debug a transaction locally instead of sending it to the network, you can call the `debug()` function on the transaction. This will return intermediate values and the final result of the transaction. It will also show any logged values and `require` error messages. -### bitauthUri() +### getBitauthUri() ```ts -transactionBuilder.bitauthUri(): string +transactionBuilder.getBitauthUri(): string ``` -If you prefer a lower-level debugging experience, you can call the `bitauthUri()` function on the transaction. This will return a URI that can be opened in the BitAuth IDE. This URI is also displayed in the console whenever a transaction fails. +If you prefer a lower-level debugging experience, you can call the `getBitauthUri()` function on the transaction. This will return a URI that can be opened in the BitAuth IDE. This URI is also displayed in the console whenever a transaction fails. You can read more about debugging transactions on the [debugging page](/docs/guides/debugging). :::caution diff --git a/website/docs/sdk/transactions.md b/website/docs/sdk/transactions.md index 96cdee17..5b2ca52d 100644 --- a/website/docs/sdk/transactions.md +++ b/website/docs/sdk/transactions.md @@ -248,11 +248,11 @@ const txHex = await instance.functions .build() ``` -### debug() & bitauthUri() +### debug() & getBitauthUri() If you want to debug a transaction locally instead of sending it to the network, you can call the `debug()` function on the transaction. This will return intermediate values and the final result of the transaction. It will also show any logged values and `require` error messages. -If you prefer a lower-level debugging experience, you can call the `bitauthUri()` function on the transaction. This will return a URI that can be opened in the BitAuth IDE. This URI is also displayed in the console whenever a transaction fails. +If you prefer a lower-level debugging experience, you can call the `getBitauthUri()` function on the transaction. This will return a URI that can be opened in the BitAuth IDE. This URI is also displayed in the console whenever a transaction fails. You can read more about debugging transactions on the [debugging page](/docs/guides/debugging). @@ -260,6 +260,40 @@ You can read more about debugging transactions on the [debugging page](/docs/gui It is unsafe to debug transactions on mainnet using the BitAuth IDE as private keys will be exposed to BitAuth IDE and transmitted over the network. ::: +### getVmResourceUsage() +```ts +transaction.getVmResourceUsage(verbose: boolean = false): Array +``` + +The `getVmResourceUsage()` function allows you to get the VM resource usage for the transaction. This can be useful for debugging and optimization. + +```ts +interface VmResourceUsage { + arithmeticCost: number; + definedFunctions: number; + hashDigestIterations: number; + maximumOperationCost: number; + maximumHashDigestIterations: number; + maximumSignatureCheckCount: number; + densityControlLength: number; + operationCost: number; + signatureCheckCount: number; +} +``` + +The verbose mode logs the VM resource usage for each input to the console. + +``` +VM Resource usage by inputs: +┌─────────┬─────────────────────────────────────────────────┬─────┬──────────────────────────┬───────────┬──────────┐ +│ (index) │ Contract - Function │ Ops │ Op Cost Budget Usage │ SigChecks │ Hashes │ +├─────────┼─────────────────────────────────────────────────┼─────┼──────────────────────────┼───────────┼──────────┤ +│ 0 │ 'SingleFunction - test_require_single_function' │ 7 │ '1,155 / 36,000 (3%)' │ '0 / 1' │ '2 / 22' │ +│ 1 │ 'ZeroHandling - test_zero_handling' │ 13 │ '1,760 / 40,800 (4%)' │ '0 / 1' │ '2 / 25' │ +│ 2 │ 'P2PKH Input' │ 7 │ '28,217 / 112,800 (25%)' │ '1 / 3' │ '7 / 70' │ +└─────────┴─────────────────────────────────────────────────┴─────┴──────────────────────────┴───────────┴──────────┘ +``` + ## Transaction errors When sending a transaction, the CashScript SDK will throw an error if the transaction fails. If you are using an artifact compiled with `cashc@0.10.0` or later, the error will be of the type `FailedRequireError` or `FailedTransactionEvaluationError`. In case of a `FailedRequireError`, the error will refer to the corresponding `require` statement in the contract code so you know where your contract failed. If you want more information about the underlying error, you can check the `libauthErrorMessage` property of the error. diff --git a/yarn.lock b/yarn.lock index 0daa8efd..04d9884d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -625,10 +625,10 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@bitauth/libauth@^3.1.0-next.2": - version "3.1.0-next.2" - resolved "https://registry.yarnpkg.com/@bitauth/libauth/-/libauth-3.1.0-next.2.tgz#121782b38774d9fba8226406db9b9af0c8d8e464" - integrity sha512-XRtk9p8UHvtjSPS38rsfHXzaPHG5j9FpN4qHqqGLoAuZYy675PBiOy9zP6ah8lTnnIVaCFl2ekct8w0Hy1oefw== +"@bitauth/libauth@^3.1.0-next.8": + version "3.1.0-next.8" + resolved "https://registry.yarnpkg.com/@bitauth/libauth/-/libauth-3.1.0-next.8.tgz#d130e5db6c3c8b24731c8d04c4091be07f48b0ee" + integrity sha512-Pm+Ju+YP3JeBLLTiVrBnia2wwE4G17r4XqpvPRMcklElJTe8J6x3JgKRg1by0Xm3ZY6UFxACkEAoSA+x419/zA== "@chris.troutner/bip32-utils@1.0.5": version "1.0.5"