Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/cashscript/src/TransactionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
isUnlockableUtxo,
isStandardUnlockableUtxo,
StandardUnlockableUtxo,
isP2PKHUnlocker,
} from './interfaces.js';
import { NetworkProvider } from './network/index.js';
import {
Expand Down Expand Up @@ -157,6 +158,11 @@ 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');
}
Expand Down
53 changes: 53 additions & 0 deletions packages/cashscript/test/TransactionBuilder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
carolAddress,
carolPriv,
bobTokenAddress,
aliceAddress,
alicePriv,
} from './fixture/vars.js';
import { Network } from '../src/interfaces.js';
import { utxoComparator, calculateDust, randomUtxo, randomToken, isNonTokenUtxo, isFungibleTokenUtxo } from '../src/utils.js';
Expand Down Expand Up @@ -293,4 +295,55 @@ describe('Transaction Builder', () => {
expect(JSON.parse(stringify(wcTransactionObj))).toEqual(expectedResult);
});
});

it('should not fail when validly spending from only P2PKH inputs', async () => {
const aliceUtxos = (await provider.getUtxos(aliceAddress)).filter(isNonTokenUtxo);
const sigTemplate = new SignatureTemplate(alicePriv);

expect(aliceUtxos.length).toBeGreaterThan(2);

const change = aliceUtxos[0].satoshis + aliceUtxos[1].satoshis - 1000n;

const transaction = new TransactionBuilder({ provider })
.addInput(aliceUtxos[0], sigTemplate.unlockP2PKH())
.addInput(aliceUtxos[1], sigTemplate.unlockP2PKH())
.addOutput({ to: aliceAddress, amount: change });

await expect(transaction.send()).resolves.not.toThrow();
});

// TODO: Currently, P2PKH inputs are not evaluated at all
it.skip('should fail when invalidly spending from only P2PKH inputs', async () => {
const aliceUtxos = (await provider.getUtxos(aliceAddress)).filter(isNonTokenUtxo);
const incorrectSigTemplate = new SignatureTemplate(bobPriv);

expect(aliceUtxos.length).toBeGreaterThan(2);

const change = aliceUtxos[0].satoshis + aliceUtxos[1].satoshis - 1000n;

const transaction = new TransactionBuilder({ provider })
.addInput(aliceUtxos[0], incorrectSigTemplate.unlockP2PKH())
.addInput(aliceUtxos[1], incorrectSigTemplate.unlockP2PKH())
.addOutput({ to: aliceAddress, amount: change });

await expect(transaction.send()).rejects.toThrow();
});

// TODO: Currently, P2PKH inputs are not evaluated at all
it.skip('should fail when invalidly spending from P2PKH and correctly from contract inputs', async () => {
const aliceUtxos = (await provider.getUtxos(aliceAddress)).filter(isNonTokenUtxo);
const p2pkhUtxos = (await p2pkhInstance.getUtxos()).filter(isNonTokenUtxo).sort(utxoComparator).reverse();
const incorrectSigTemplate = new SignatureTemplate(bobPriv);

expect(aliceUtxos.length).toBeGreaterThan(2);

const change = aliceUtxos[0].satoshis + aliceUtxos[1].satoshis - 1000n;

const transaction = new TransactionBuilder({ provider })
.addInput(aliceUtxos[0], incorrectSigTemplate.unlockP2PKH())
.addInput(p2pkhUtxos[0], p2pkhInstance.unlock.spend(carolPub, new SignatureTemplate(carolPriv)))
.addOutput({ to: aliceAddress, amount: change });

await expect(transaction.send()).rejects.toThrow();
});
});
57 changes: 57 additions & 0 deletions packages/cashscript/test/debugging-old-artifacts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Contract, MockNetworkProvider, randomUtxo, SignatureTemplate, TransactionBuilder } from '../src/index.js';
import { alicePkh, alicePriv, alicePub, bobPriv } from './fixture/vars.js';

const artifact = {
contractName: 'P2PKH',
constructorInputs: [
{ name: 'pkh', type: 'bytes20' },
],
abi: [
{
name: 'spend',
inputs: [
{ name: 'pk', type: 'pubkey' },
{ name: 's', type: 'sig' },
],
},
],
bytecode: 'OP_OVER OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG',
source: 'pragma cashscript ^0.7.0;\n\ncontract P2PKH(bytes20 pkh) {\n // Require pk to match stored pkh and signature to match\n function spend(pubkey pk, sig s) {\n require(hash160(pk) == pkh);\n require(checkSig(s, pk));\n }\n}\n',
compiler: {
name: 'cashc',
version: '0.7.0',
},
updatedAt: '2025-08-05T09:04:50.388Z',
};

describe('Debugging tests - old artifacts', () => {
it('should succeed when passing the correct parameters', () => {
const provider = new MockNetworkProvider();
const contractTestLogs = new Contract(artifact, [alicePkh], { provider });
const contractUtxo = randomUtxo();
provider.addUtxo(contractTestLogs.address, contractUtxo);

const transaction = new TransactionBuilder({ provider })
.addInput(contractUtxo, contractTestLogs.unlock.spend(alicePub, new SignatureTemplate(alicePriv)))
.addOutput({ to: contractTestLogs.address, amount: 10000n });

console.warn(transaction.bitauthUri());

expect(() => transaction.debug()).not.toThrow();
});

it('should fail when passing the wrong parameters', () => {
const provider = new MockNetworkProvider();
const contractTestLogs = new Contract(artifact, [alicePkh], { provider });
const contractUtxo = randomUtxo();
provider.addUtxo(contractTestLogs.address, contractUtxo);

const transaction = new TransactionBuilder({ provider })
.addInput(contractUtxo, contractTestLogs.unlock.spend(alicePub, new SignatureTemplate(bobPriv)))
.addOutput({ to: contractTestLogs.address, amount: 10000n });

console.warn(transaction.bitauthUri());

expect(() => transaction.debug()).toThrow();
});
});