diff --git a/packages/cashscript/src/TransactionBuilder.ts b/packages/cashscript/src/TransactionBuilder.ts index da93ace41..0131e6e95 100644 --- a/packages/cashscript/src/TransactionBuilder.ts +++ b/packages/cashscript/src/TransactionBuilder.ts @@ -407,7 +407,8 @@ export class TransactionBuilder { console.warn('For the best debugging experience, please recompile your contract with cashc version 0.11.0 or newer.'); } - return debugLibauthTemplate(this.getLibauthTemplate(), this); + const template = getLibauthTemplate(this, this.buildLibauthTransaction(), { useTransactionBytecode: true }); + return debugLibauthTemplate(template, this, () => this.getBitauthUriWithFallback()); } /** @@ -518,22 +519,22 @@ export class TransactionBuilder { txid = await this.provider.sendRawTransaction(tx); } catch (e: any) { const reason = e.error ?? e.message; - - const getBitauthUriWithFallback = (): string => { - try { - return getBitauthUri(this.getLibauthTemplate()); - } catch { - return 'Bitauth URI generation failed'; - } - }; - - throw new FailedTransactionError(reason, getBitauthUriWithFallback()); + throw new FailedTransactionError(reason, this.getBitauthUriWithFallback()); } // The transaction was broadcast successfully, so failing to retrieve it afterwards is not a failed transaction return raw ? this.getTxDetails(txid, raw) : this.getTxDetails(txid); } + // Building the link must not replace the error it is attached to + private getBitauthUriWithFallback(): string { + try { + return getBitauthUri(this.getLibauthTemplate()); + } catch { + return 'Bitauth URI generation failed'; + } + } + private async getTxDetails(txid: string): Promise; private async getTxDetails(txid: string, raw: true): Promise; private async getTxDetails(txid: string, raw?: true): Promise { diff --git a/packages/cashscript/src/debugging.ts b/packages/cashscript/src/debugging.ts index 31a90b392..3db15566a 100644 --- a/packages/cashscript/src/debugging.ts +++ b/packages/cashscript/src/debugging.ts @@ -11,7 +11,11 @@ export type DebugResult = AuthenticationProgramStateCommon[]; export type DebugResults = Record; // debugs the template, optionally logging the execution data -export const debugTemplate = (template: WalletTemplate, artifacts: Artifact[]): DebugResults => { +export const debugTemplate = ( + template: WalletTemplate, + artifacts: Artifact[], + getErrorBitauthUri: () => string = () => getBitauthUri(template), +): DebugResults => { // If a contract has the same name, but a different bytecode, then it is considered a name collision const hasArtifactNameCollision = artifacts.some( (artifact) => ( @@ -26,25 +30,33 @@ export const debugTemplate = (template: WalletTemplate, artifacts: Artifact[]): const results: DebugResults = {}; const unlockingScriptIds = Object.keys(template.scripts).filter((key) => 'unlocks' in template.scripts[key]); + const context = createDebugContext(template, getErrorBitauthUri); + for (const unlockingScriptId of unlockingScriptIds) { const scenarioIds = (template.scripts[unlockingScriptId] as WalletTemplateScriptUnlocking).passes ?? []; const matchingArtifact = artifacts.find((artifact) => unlockingScriptId.startsWith(artifact.contractName)); for (const scenarioId of scenarioIds) { - results[`${unlockingScriptId}.${scenarioId}`] = debugSingleScenario(template, matchingArtifact, unlockingScriptId, scenarioId); + results[`${unlockingScriptId}.${scenarioId}`] = debugSingleScenario( + template, context, matchingArtifact, unlockingScriptId, scenarioId, + ); } } - verifyFullTransaction(template); + verifyFullTransaction(template, context); return results; }; const debugSingleScenario = ( - template: WalletTemplate, artifact: Artifact | undefined, unlockingScriptId: string, scenarioId: string, + template: WalletTemplate, + context: DebugContext, + artifact: Artifact | undefined, + unlockingScriptId: string, + scenarioId: string, ): DebugResult => { - const { vm, program } = createProgram(template, unlockingScriptId, scenarioId); + const { vm, program } = createProgram(template, context, unlockingScriptId, scenarioId); const fullDebugSteps = vm.debug(program); @@ -121,7 +133,7 @@ const debugSingleScenario = ( // 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)); + throw new FailedTransactionError(error, context.getErrorBitauthUri()); } const frame = resolveFrame(artifact, lastExecutedDebugStep); @@ -136,13 +148,13 @@ const debugSingleScenario = ( // Note that we use failingIp here rather than requireStatementIp, see comment above throw new FailedRequireError( - artifact, failingIp, requireStatement, inputIndex, getBitauthUri(template), error, frame, callStack, + artifact, failingIp, requireStatement, inputIndex, context.getErrorBitauthUri(), error, frame, callStack, ); } // Note that we use failingIp here rather than requireStatementIp, see comment above throw new FailedTransactionEvaluationError( - artifact, failingIp, inputIndex, getBitauthUri(template), error, frame, + artifact, failingIp, inputIndex, context.getErrorBitauthUri(), error, frame, ); } @@ -167,7 +179,7 @@ const debugSingleScenario = ( // 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)); + throw new FailedTransactionError(evaluationResult, context.getErrorBitauthUri()); } const frame = resolveFrame(artifact, lastExecutedDebugStep); @@ -183,7 +195,7 @@ const debugSingleScenario = ( sourcemapInstructionPointer, requireStatement, inputIndex, - getBitauthUri(template), + context.getErrorBitauthUri(), undefined, frame, callStack, @@ -191,7 +203,7 @@ const debugSingleScenario = ( } throw new FailedTransactionEvaluationError( - artifact, sourcemapInstructionPointer, inputIndex, getBitauthUri(template), evaluationResult, frame, + artifact, sourcemapInstructionPointer, inputIndex, context.getErrorBitauthUri(), evaluationResult, frame, ); } @@ -207,12 +219,19 @@ const extractInputIndexFromScenario = (scenarioId: string): number => { type Program = AuthenticationProgramCommon; type CreateProgramResult = { vm: VM, program: Program }; +type DebugContext = { compiler: ReturnType, vm: VM, getErrorBitauthUri: () => string }; + +const createDebugContext = (template: WalletTemplate, getErrorBitauthUri: () => string): DebugContext => ({ + compiler: createCompiler(walletTemplateToCompilerConfiguration(template)), + vm: createVirtualMachine(template.supported[0] as VmTarget), + getErrorBitauthUri, +}); -// 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 = createVirtualMachine(template.supported[0] as VmTarget); - const compiler = createCompiler(configuration); +// internal util. compiles a scenario of the template into a program +const createProgram = ( + template: WalletTemplate, context: DebugContext, unlockingScriptId: string, scenarioId: string, +): CreateProgramResult => { + const { compiler, vm } = context; if (!template.scripts[unlockingScriptId]) { throw new Error(`No unlock script found in template for ID ${unlockingScriptId}`); @@ -229,11 +248,11 @@ const createProgram = (template: WalletTemplate, unlockingScriptId: string, scen }); if (typeof scenarioGeneration === 'string') { - throw new FailedTransactionError(scenarioGeneration, getBitauthUri(template)); + throw new FailedTransactionError(scenarioGeneration, context.getErrorBitauthUri()); } if (typeof scenarioGeneration.scenario === 'string') { - throw new FailedTransactionError(scenarioGeneration.scenario, getBitauthUri(template)); + throw new FailedTransactionError(scenarioGeneration.scenario, context.getErrorBitauthUri()); } return { vm, program: scenarioGeneration.scenario.program }; @@ -379,7 +398,7 @@ const getFinalExecutedVerifyIp = (executedDebugSteps: AuthenticationProgramState // After debugging, we want to verify the full transaction to ensure it is valid (this catches any errors that are not // necessarily script errors) -const verifyFullTransaction = (template: WalletTemplate): void => { +const verifyFullTransaction = (template: WalletTemplate, context: DebugContext): void => { const placeholderScriptId = Object.keys(template.scripts).find((key) => 'unlocks' in template.scripts[key]); const placeholderScenarioId = (template.scripts[placeholderScriptId ?? ''] as WalletTemplateScriptUnlocking)?.passes?.[0]; @@ -387,7 +406,7 @@ const verifyFullTransaction = (template: WalletTemplate): void => { throw new Error('No placeholder scenario ID or script ID found'); } - const { vm, program } = createProgram(template, placeholderScriptId, placeholderScenarioId); + const { vm, program } = createProgram(template, context, placeholderScriptId, placeholderScenarioId); const verificationResult = vm.verify({ sourceOutputs: program.sourceOutputs, @@ -395,7 +414,7 @@ const verifyFullTransaction = (template: WalletTemplate): void => { }); if (typeof verificationResult === 'string') { - throw new FailedTransactionError(verificationResult, getBitauthUri(template)); + throw new FailedTransactionError(verificationResult, context.getErrorBitauthUri()); } }; diff --git a/packages/cashscript/src/libauth-template/LibauthTemplate.ts b/packages/cashscript/src/libauth-template/LibauthTemplate.ts index f10e2dff8..66c716830 100644 --- a/packages/cashscript/src/libauth-template/LibauthTemplate.ts +++ b/packages/cashscript/src/libauth-template/LibauthTemplate.ts @@ -31,7 +31,7 @@ import { Utxo, } from '../interfaces.js'; import SignatureTemplate from '../SignatureTemplate.js'; -import { addressToLockScript, extendedStringify, zip } from '../utils.js'; +import { addressToLockScript, extendedStringify, generateLibauthSourceOutputs, zip } from '../utils.js'; import { TransactionBuilder } from '../TransactionBuilder.js'; import { zlibSync } from 'fflate'; import MockNetworkProvider from '../network/MockNetworkProvider.js'; @@ -39,9 +39,27 @@ import { addHexPrefixExceptEmpty, DEFAULT_VM_TARGET, formatBytecodeForDebugging, // TODO: Add / improve descriptions throughout the template generation +export interface LibauthTemplateOptions { + // So libauth only compiles each scenario's slot, keeping debugging linear in the number of inputs. + // The other inputs and outputs then show as raw bytecode in BitAuth IDE + useTransactionBytecode?: boolean; +} + +interface TransactionBytecode { + unlocking: string[]; + sourceLocking: string[]; + outputLocking: string[]; +} + +interface ScenarioBytecode { + transaction?: TransactionBytecode; + lockingBytecodeParamsMapping: Record; +} + export const getLibauthTemplate = ( transactionBuilder: TransactionBuilder, libauthTransaction: TransactionBch, + options: LibauthTemplateOptions = {}, ): WalletTemplate => { if (transactionBuilder.inputs.some((input) => !isStandardUnlockableUtxo(input))) { throw new Error('Cannot use debugging functionality with a transaction that contains custom unlockers'); @@ -51,6 +69,10 @@ export const getLibauthTemplate = ( ? transactionBuilder.provider.vmTarget : DEFAULT_VM_TARGET; + const scenarioBytecode: ScenarioBytecode = options.useTransactionBytecode + ? { transaction: getTransactionBytecode(transactionBuilder, libauthTransaction), lockingBytecodeParamsMapping: {} } + : { lockingBytecodeParamsMapping: generateLockingBytecodeParamsMapping(transactionBuilder) }; + const template: WalletTemplate = { $schema: 'https://ide.bitauth.com/authentication-template-v0.schema.json', description: 'Imported from cashscript', @@ -59,19 +81,33 @@ export const getLibauthTemplate = ( version: 0, entities: generateAllTemplateEntities(transactionBuilder), scripts: generateAllTemplateScripts(transactionBuilder), - scenarios: generateAllTemplateScenarios(libauthTransaction, transactionBuilder), + scenarios: generateAllTemplateScenarios(libauthTransaction, transactionBuilder, scenarioBytecode), }; return template; }; -export const debugLibauthTemplate = (template: WalletTemplate, transaction: TransactionBuilder): DebugResults => { +const getTransactionBytecode = ( + transactionBuilder: TransactionBuilder, + libauthTransaction: TransactionBch, +): TransactionBytecode => ({ + unlocking: libauthTransaction.inputs.map((input) => binToHex(input.unlockingBytecode)), + sourceLocking: generateLibauthSourceOutputs(transactionBuilder.inputs) + .map((output) => binToHex(output.lockingBytecode)), + outputLocking: libauthTransaction.outputs.map((output) => binToHex(output.lockingBytecode)), +}); + +export const debugLibauthTemplate = ( + template: WalletTemplate, + transaction: TransactionBuilder, + getErrorBitauthUri?: () => string, +): 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); + return debugTemplate(template, allArtifacts, getErrorBitauthUri); }; export const getBitauthUri = (template: WalletTemplate): string => { @@ -96,7 +132,7 @@ const generateAllTemplateEntities = ( throw new Error('Unknown unlocker type'); }); - return entities.reduce((acc, entity) => ({ ...acc, ...entity }), {}); + return Object.assign({}, ...entities); }; const generateAllTemplateScripts = ( @@ -121,7 +157,7 @@ const generateAllTemplateScripts = ( throw new Error('Unknown unlocker type'); }); - return scripts.reduce((acc, script) => ({ ...acc, ...script }), {}); + return Object.assign({}, ...scripts); }; const generateLockingBytecodeParamsMapping = ( @@ -145,10 +181,11 @@ const generateLockingBytecodeParamsMapping = ( const generateAllTemplateScenarios = ( libauthTransaction: TransactionBch, transactionBuilder: TransactionBuilder, + scenarioBytecode: ScenarioBytecode, ): WalletTemplate['scenarios'] => { const scenarios = transactionBuilder.inputs.map((input, inputIndex) => { if (isP2PKHUnlocker(input.unlocker)) { - return generateTemplateScenariosP2PKH(libauthTransaction, transactionBuilder, inputIndex); + return generateTemplateScenariosP2PKH(libauthTransaction, transactionBuilder, inputIndex, scenarioBytecode); } if (isContractUnlocker(input.unlocker)) { @@ -160,13 +197,14 @@ const generateAllTemplateScenarios = ( input.unlocker.abiFunction, encodedArgs, inputIndex, + scenarioBytecode, ); } throw new Error('Unknown unlocker type'); }); - return scenarios.reduce((acc, scenario) => ({ ...acc, ...scenario }), {}); + return Object.assign({}, ...scenarios); }; const generateTemplateEntitiesP2PKH = ( @@ -342,6 +380,7 @@ const generateTemplateScenarios = ( abiFunction: AbiFunction, encodedFunctionArgs: EncodedFunctionArgument[], inputIndex: number, + scenarioBytecode: ScenarioBytecode, ): WalletTemplate['scenarios'] => { const artifact = contract.artifact; const encodedConstructorArgs = contract.encodedConstructorArgs; @@ -363,8 +402,12 @@ const generateTemplateScenarios = ( privateKeys: generateTemplateScenarioKeys(abiFunction.inputs, encodedFunctionArgs), }, }, - transaction: generateTemplateScenarioTransaction(contract, libauthTransaction, transactionBuilder, inputIndex), - sourceOutputs: generateTemplateScenarioSourceOutputs(transactionBuilder, libauthTransaction, inputIndex), + transaction: generateTemplateScenarioTransaction( + contract, libauthTransaction, transactionBuilder, inputIndex, scenarioBytecode, + ), + sourceOutputs: generateTemplateScenarioSourceOutputs( + transactionBuilder, libauthTransaction, inputIndex, scenarioBytecode, + ), }, }; @@ -375,6 +418,7 @@ const generateTemplateScenariosP2PKH = ( libauthTransaction: TransactionBch, transactionBuilder: TransactionBuilder, inputIndex: number, + scenarioBytecode: ScenarioBytecode, ): WalletTemplate['scenarios'] => { const scenarioIdentifier = `P2PKH_spend_input${inputIndex}_evaluate`; const { signature, publicKey } = getSignatureAndPubkeyFromP2PKHInput(libauthTransaction.inputs[inputIndex]); @@ -391,8 +435,12 @@ const generateTemplateScenariosP2PKH = ( [`public_key_${inputIndex}`]: `0x${binToHex(publicKey)}`, }, }, - transaction: generateTemplateScenarioTransaction(undefined, libauthTransaction, transactionBuilder, inputIndex), - sourceOutputs: generateTemplateScenarioSourceOutputs(transactionBuilder, libauthTransaction, inputIndex), + transaction: generateTemplateScenarioTransaction( + undefined, libauthTransaction, transactionBuilder, inputIndex, scenarioBytecode, + ), + sourceOutputs: generateTemplateScenarioSourceOutputs( + transactionBuilder, libauthTransaction, inputIndex, scenarioBytecode, + ), }, }; @@ -404,8 +452,13 @@ const generateTemplateScenarioTransaction = ( libauthTransaction: TransactionBch, transactionBuilder: TransactionBuilder, slotIndex: number, + scenarioBytecode: ScenarioBytecode, ): WalletTemplateScenario['transaction'] => { - const lockingBytecodeParamsMapping = generateLockingBytecodeParamsMapping(transactionBuilder); + if (scenarioBytecode.transaction) { + return generateTemplateScenarioTransactionFromBytecode(libauthTransaction, slotIndex, scenarioBytecode.transaction); + } + + const { lockingBytecodeParamsMapping } = scenarioBytecode; const zippedInputs = zip(transactionBuilder.inputs, libauthTransaction.inputs); const inputs = zippedInputs.map(([csInput, libauthInput], inputIndex) => { @@ -435,15 +488,43 @@ const generateTemplateScenarioTransaction = ( return { inputs, locktime, outputs, version }; }; +const generateTemplateScenarioTransactionFromBytecode = ( + libauthTransaction: TransactionBch, + slotIndex: number, + transactionBytecode: TransactionBytecode, +): WalletTemplateScenario['transaction'] => { + const inputs = libauthTransaction.inputs.map((libauthInput, inputIndex) => ({ + outpointIndex: libauthInput.outpointIndex, + outpointTransactionHash: binToHex(libauthInput.outpointTransactionHash), + sequenceNumber: libauthInput.sequenceNumber, + unlockingBytecode: inputIndex === slotIndex ? ['slot'] as ['slot'] : transactionBytecode.unlocking[inputIndex], + })); + + const outputs = libauthTransaction.outputs.map((libauthOutput, outputIndex) => ({ + lockingBytecode: transactionBytecode.outputLocking[outputIndex], + token: serialiseTokenDetails(libauthOutput.token), + valueSatoshis: Number(libauthOutput.valueSatoshis), + })); + + return { inputs, locktime: libauthTransaction.locktime, outputs, version: libauthTransaction.version }; +}; + const generateTemplateScenarioSourceOutputs = ( transactionBuilder: TransactionBuilder, libauthTransaction: TransactionBch, slotIndex: number, + scenarioBytecode: ScenarioBytecode, ): Array> => { const zippedInputs = zip(transactionBuilder.inputs, libauthTransaction.inputs); return zippedInputs.map(([csInput, libauthInput], inputIndex) => { + const lockingBytecode = scenarioBytecode.transaction && inputIndex !== slotIndex + ? scenarioBytecode.transaction.sourceLocking[inputIndex] + : generateTemplateScenarioBytecodeForSourceOutputs( + csInput, libauthInput, inputIndex, 'p2pkh_placeholder_lock', inputIndex === slotIndex, + ); + return { - lockingBytecode: generateTemplateScenarioBytecodeForSourceOutputs(csInput, libauthInput, inputIndex, 'p2pkh_placeholder_lock', inputIndex === slotIndex), + lockingBytecode, valueSatoshis: Number(csInput.satoshis), token: serialiseTokenDetails(csInput.token), }; diff --git a/packages/cashscript/test/libauth-template/LibauthTemplate.test.ts b/packages/cashscript/test/libauth-template/LibauthTemplate.test.ts index 13ff6d408..5fc03b964 100644 --- a/packages/cashscript/test/libauth-template/LibauthTemplate.test.ts +++ b/packages/cashscript/test/libauth-template/LibauthTemplate.test.ts @@ -1,4 +1,5 @@ import { fixtures } from '../fixture/libauth-template/fixtures.js'; +import { expectSameDebugOutcomeWithTransactionBytecode } from '../test-util.js'; describe('Libauth Template generation tests (single-contract)', () => { it.each(fixtures)('should generate a valid libauth template for $name', (fixture) => { @@ -7,4 +8,8 @@ describe('Libauth Template generation tests (single-contract)', () => { // console.warn(fixture.transaction.bitauthUri()); expect(generatedTemplate).toEqual(fixture.template); }); + + it.each(fixtures)('should give the same debug outcome when using transaction bytecode for $name', (fixture) => { + expectSameDebugOutcomeWithTransactionBytecode(fixture.transaction); + }); }); diff --git a/packages/cashscript/test/libauth-template/LibauthTemplateMultiContract.test.ts b/packages/cashscript/test/libauth-template/LibauthTemplateMultiContract.test.ts index a1180bae1..20c8099ce 100644 --- a/packages/cashscript/test/libauth-template/LibauthTemplateMultiContract.test.ts +++ b/packages/cashscript/test/libauth-template/LibauthTemplateMultiContract.test.ts @@ -1,4 +1,5 @@ import { fixtures } from '../fixture/libauth-template/multi-contract-fixtures.js'; +import { expectSameDebugOutcomeWithTransactionBytecode } from '../test-util.js'; describe('Libauth Template generation tests (multi-contract)', () => { it.each(fixtures)('should generate a valid libauth template for $name', async (fixture) => { @@ -8,4 +9,8 @@ describe('Libauth Template generation tests (multi-contract)', () => { // console.warn(builder.bitauthUri()); expect(generatedTemplate).toEqual(fixture.template); }); + + it.each(fixtures)('should give the same debug outcome when using transaction bytecode for $name', async (fixture) => { + expectSameDebugOutcomeWithTransactionBytecode(await fixture.transaction); + }); }); diff --git a/packages/cashscript/test/multi-contract-debugging.test.ts b/packages/cashscript/test/multi-contract-debugging.test.ts index 38a29de88..68fb3dfbe 100644 --- a/packages/cashscript/test/multi-contract-debugging.test.ts +++ b/packages/cashscript/test/multi-contract-debugging.test.ts @@ -339,6 +339,30 @@ describe('Multi-Contract-Debugging tests', () => { }); }); + describe('Many inputs', () => { + it('should attribute a failing require statement to the right input among many inputs', async () => { + const contract = new Contract(p2pkhArtifact, [alicePkh], { provider }); + const transaction = new TransactionBuilder({ provider }); + + for (let inputIndex = 0; inputIndex < 40; inputIndex += 1) { + // Unique outpoints, random ones can collide across this many inputs + const utxo = provider.addUtxo( + contract.address, + randomUtxo({ txid: inputIndex.toString(16).padStart(64, '0'), vout: 0, satoshis: 10_000n }), + ); + const unlocker = inputIndex === 33 + ? contract.unlock.spend(bobPub, bobSignatureTemplate) + : contract.unlock.spend(alicePub, new SignatureTemplate(alicePriv)); + transaction.addInput(utxo, unlocker); + } + + transaction.addOutput({ to: contract.address, amount: 300_000n }); + + await expect(transaction) + .toFailRequireWith('P2PKH.cash:4 Require statement failed at input 33 in contract P2PKH.cash at line 4.'); + }); + }); + describe('Non-require error messages', () => { it('should fail with the correct error message when there are name collisions on the contractName', () => { const nameCollision = new Contract(ARTIFACT_NAME_COLLISION, [0n], { provider }); diff --git a/packages/cashscript/test/test-util.ts b/packages/cashscript/test/test-util.ts index e29c158e2..30f81bc35 100644 --- a/packages/cashscript/test/test-util.ts +++ b/packages/cashscript/test/test-util.ts @@ -2,7 +2,9 @@ import { lockingBytecodeToCashAddress, hexToBin, Transaction, + WalletTemplate, binToHex, + decodeTransaction, } from '@bitauth/libauth'; import PQueue from 'p-queue'; import pRetry from 'p-retry'; @@ -14,6 +16,8 @@ import MockNetworkProvider from '../src/network/MockNetworkProvider.js'; import NetworkProvider from '../src/network/NetworkProvider.js'; import SignatureTemplate from '../src/SignatureTemplate.js'; import { TransactionBuilder } from '../src/TransactionBuilder.js'; +import { DebugResults } from '../src/debugging.js'; +import { debugLibauthTemplate, getBitauthUri, getLibauthTemplate } from '../src/libauth-template/LibauthTemplate.js'; export function getTxOutputs(tx: Transaction, network: Network = defaultNetwork): Output[] { return tx.outputs.map((o) => { @@ -141,3 +145,29 @@ export function gatherUtxos( changeAmount, }; } + +// Some fixtures fail evaluation, so the error is compared as well as the results +export function expectSameDebugOutcomeWithTransactionBytecode(transaction: TransactionBuilder): void { + const libauthTransaction = decodeTransaction(hexToBin(transaction.build())); + if (typeof libauthTransaction === 'string') throw new Error(libauthTransaction); + + const scriptTemplate = getLibauthTemplate(transaction, libauthTransaction); + const bytecodeTemplate = getLibauthTemplate(transaction, libauthTransaction, { useTransactionBytecode: true }); + + for (const scenario of Object.values(bytecodeTemplate.scenarios ?? {})) { + const templateInputs = scenario.transaction?.inputs.filter((input) => typeof input.unlockingBytecode !== 'string'); + const templateSourceOutputs = scenario.sourceOutputs?.filter((output) => typeof output.lockingBytecode !== 'string'); + expect(templateInputs).toEqual([expect.objectContaining({ unlockingBytecode: ['slot'] })]); + expect(templateSourceOutputs).toEqual([expect.objectContaining({ lockingBytecode: ['slot'] })]); + } + + const debugOutcome = (template: WalletTemplate): DebugResults | Error => { + try { + return debugLibauthTemplate(template, transaction, () => getBitauthUri(scriptTemplate)); + } catch (error) { + return error as Error; + } + }; + + expect(debugOutcome(bytecodeTemplate)).toEqual(debugOutcome(scriptTemplate)); +} diff --git a/website/docs/releases/release-notes.md b/website/docs/releases/release-notes.md index 83fe5a301..533606a5b 100644 --- a/website/docs/releases/release-notes.md +++ b/website/docs/releases/release-notes.md @@ -44,6 +44,7 @@ This release contains several breaking changes, please refer to the [migration n - :bug: Fix minimum fee check rounding bug. - :bug: Fix bug where `setLocktime()` accepted invalid values. - :bug: Fix edge case bug where a failing final `require(variable)` statement was reported incorrectly in debugging. +- :racehorse: Greatly improve debugging performance for transactions with many inputs, which also speeds up `send()` on the `MockNetworkProvider` and the `toFailRequire` matchers. ## v0.13.3