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
2 changes: 0 additions & 2 deletions packages/cashscript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,6 @@
"@cashscript/utils": "^0.11.4",
"@electrum-cash/network": "^4.1.3",
"@mr-zwets/bchn-api-wrapper": "^1.0.1",
"@types/node": "^22.17.0",
"delay": "^6.0.0",
"fast-deep-equal": "^3.1.3",
"pako": "^2.1.0",
"semver": "^7.7.2"
Expand Down
2 changes: 1 addition & 1 deletion packages/cashscript/src/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
Transaction as LibauthTransaction,
WalletTemplate,
} from '@bitauth/libauth';
import delay from 'delay';
import {
AbiFunction,
encodeBip68,
Expand Down Expand Up @@ -32,6 +31,7 @@ import {
calculateDust,
getOutputSize,
utxoTokenComparator,
delay,
} from './utils.js';
import SignatureTemplate from './SignatureTemplate.js';
import { P2PKH_INPUT_SIZE } from './constants.js';
Expand Down
2 changes: 1 addition & 1 deletion packages/cashscript/src/TransactionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
Transaction as LibauthTransaction,
WalletTemplate,
} from '@bitauth/libauth';
import delay from 'delay';
import {
Unlocker,
Output,
Expand All @@ -24,6 +23,7 @@ import { NetworkProvider } from './network/index.js';
import {
cashScriptOutputToLibauthOutput,
createOpReturnOutput,
delay,
generateLibauthSourceOutputs,
validateInput,
validateOutput,
Expand Down
13 changes: 11 additions & 2 deletions packages/cashscript/src/debugging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ const debugSingleScenario = (
.filter((log) => executedDebugSteps.some((debugStep) => log.ip === debugStep.ip));

for (const log of executedLogs) {
logConsoleLogStatement(log, executedDebugSteps, artifact);
const inputIndex = extractInputIndexFromScenario(scenarioId);
logConsoleLogStatement(log, executedDebugSteps, artifact, inputIndex);
}

const lastExecutedDebugStep = executedDebugSteps[executedDebugSteps.length - 1];
Expand Down Expand Up @@ -139,6 +140,13 @@ const debugSingleScenario = (
return fullDebugSteps;
};

// Note: this relies on the naming convention that the scenario ID is of the form <name>_input<index>_evaluate
const extractInputIndexFromScenario = (scenarioId: string): number => {
const match = scenarioId.match(/_input(\d+)_/);
if (!match) throw new Error(`Invalid scenario ID: ${scenarioId}`);
return parseInt(match[1]);
};

/* eslint-disable @typescript-eslint/indent */
type VM = AuthenticationVirtualMachine<
ResolvedTransactionCommon,
Expand Down Expand Up @@ -185,6 +193,7 @@ const logConsoleLogStatement = (
log: LogEntry,
debugSteps: AuthenticationProgramStateCommon[],
artifact: Artifact,
inputIndex: number,
): void => {
let line = `${artifact.contractName}.cash:${log.line}`;
const decodedData = log.data.map((element) => {
Expand All @@ -194,7 +203,7 @@ const logConsoleLogStatement = (
const transformedDebugStep = applyStackItemTransformations(element, debugStep);
return decodeStackItem(element, transformedDebugStep.stack);
});
console.log(`${line} ${decodedData.join(' ')}`);
console.log(`[Input #${inputIndex}] ${line} ${decodedData.join(' ')}`);
};

const applyStackItemTransformations = (
Expand Down
15 changes: 9 additions & 6 deletions packages/cashscript/src/types/type-inference.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import type SignatureTemplate from '../SignatureTemplate.js';

type BytesType = Uint8Array | string;
type SignatureType = SignatureTemplate | BytesType;

type TypeMap = {
[k: `bytes${number}`]: Uint8Array | string; // Matches any "bytes<number>" pattern
[k: `bytes${number}`]: BytesType; // Matches any "bytes<number>" pattern
} & {
byte: Uint8Array | string;
bytes: Uint8Array | string;
byte: BytesType;
bytes: BytesType;
bool: boolean;
int: bigint;
string: string;
pubkey: Uint8Array | string;
sig: SignatureTemplate | Uint8Array | string;
datasig: Uint8Array | string;
pubkey: BytesType;
sig: SignatureType;
datasig: BytesType;
};

// Helper type to process a single parameter by mapping its `type` to a value in `TypeMap`.
Expand Down
28 changes: 3 additions & 25 deletions packages/cashscript/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
generateSigningSerializationBch,
utf8ToBin,
hexToBin,
flattenBinArray,
LockingBytecodeType,
encodeTransactionOutput,
isHex,
Expand All @@ -24,6 +23,7 @@ import {
Op,
Script,
scriptToBytecode,
encodeNullDataScript,
} from '@cashscript/utils';
import {
Utxo,
Expand Down Expand Up @@ -302,30 +302,6 @@ export function getNetworkPrefix(network: string): 'bitcoincash' | 'bchtest' | '
}
}

// ////////////////////////////////////////////////////////////////////////////
// For encoding OP_RETURN data (doesn't require BIP62.3 / MINIMALDATA)
function encodeNullDataScript(chunks: (number | Uint8Array)[]): Uint8Array {
return flattenBinArray(
chunks.map((chunk) => {
if (typeof chunk === 'number') {
return new Uint8Array([chunk]);
}

const pushdataOpcode = getPushDataOpcode(chunk);
return new Uint8Array([...pushdataOpcode, ...chunk]);
}),
);
}

function getPushDataOpcode(data: Uint8Array): Uint8Array {
const { byteLength } = data;

if (byteLength === 0) return Uint8Array.from([0x4c, 0x00]);
if (byteLength < 76) return Uint8Array.from([byteLength]);
if (byteLength < 256) return Uint8Array.from([0x4c, byteLength]);
throw new Error('Pushdata too large');
}

const randomInt = (): bigint => BigInt(Math.floor(Math.random() * 10000));

export const randomUtxo = (defaults?: Partial<Utxo>): Utxo => ({
Expand Down Expand Up @@ -390,3 +366,5 @@ export const isFungibleTokenUtxo = (utxo: Utxo): boolean => (
);

export const isNonTokenUtxo = (utxo: Utxo): boolean => utxo.token === undefined;

export const delay = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
46 changes: 24 additions & 22 deletions packages/cashscript/test/debugging.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe('Debugging tests', () => {
.addOutput({ to: contractTestLogs.address, amount: 10000n });

// console.log(ownerSig, owner, num, beef, 1, "test", true);
const expectedLog = new RegExp(`^Test.cash:10 0x[0-9a-f]{130} 0x${binToHex(alicePub)} 1000 0xbeef 1 test true$`);
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);
});

Expand All @@ -39,7 +39,7 @@ describe('Debugging tests', () => {
.addOutput({ to: contractTestLogs.address, amount: 10000n });

// console.log(ownerSig, owner, num, beef, 1, "test", true);
const expectedLog = new RegExp(`^Test.cash:10 0x[0-9a-f]{130} 0x${binToHex(alicePub)} 100 0xbeef 1 test true$`);
const expectedLog = new RegExp(`^\\[Input #0] Test.cash:10 0x[0-9a-f]{130} 0x${binToHex(alicePub)} 100 0xbeef 1 test true$`);
expect(transaction).toLog(expectedLog);
});

Expand All @@ -49,7 +49,7 @@ describe('Debugging tests', () => {
.addInput(contractUtxo, contractTestLogs.unlock.transfer(new SignatureTemplate(incorrectPriv), 1000n))
.addOutput({ to: contractTestLogs.address, amount: 10000n });

const expectedLog = new RegExp(`^Test.cash:10 0x[0-9a-f]{130} 0x${binToHex(alicePub)} 1000 0xbeef 1 test true$`);
const expectedLog = new RegExp(`^\\[Input #0] Test.cash:10 0x[0-9a-f]{130} 0x${binToHex(alicePub)} 1000 0xbeef 1 test true$`);
expect(transaction).not.toLog(expectedLog);
});

Expand All @@ -58,7 +58,7 @@ describe('Debugging tests', () => {
.addInput(contractUtxo, contractTestLogs.unlock.secondFunction())
.addOutput({ to: contractTestLogs.address, amount: 10000n });

expect(transaction).toLog(new RegExp('^Test.cash:16 Hello Second Function$'));
expect(transaction).toLog(new RegExp('^\\[Input #0] Test.cash:16 Hello Second Function$'));
expect(transaction).not.toLog(/Hello First Function/);
});

Expand All @@ -76,7 +76,7 @@ describe('Debugging tests', () => {
.addInput(utxo, contractTestMultipleConstructorParameters.unlock.secondFunction())
.addOutput({ to: contractTestMultipleConstructorParameters.address, amount: 10000n });

expect(transaction).toLog(new RegExp('^Test.cash:20 Hello Second Function$'));
expect(transaction).toLog(new RegExp('^\\[Input #0] Test.cash:20 Hello Second Function$'));
expect(transaction).not.toLog(/Hello First Function/);
});

Expand All @@ -85,18 +85,18 @@ describe('Debugging tests', () => {
.addInput(contractUtxo, contractTestLogs.unlock.functionWithIfStatement(1n))
.addOutput({ to: contractTestLogs.address, amount: 10000n });

expect(transaction1).toLog(new RegExp('^Test.cash:24 a is 1$'));
expect(transaction1).toLog(new RegExp('^Test.cash:31 a equals 1$'));
expect(transaction1).toLog(new RegExp('^Test.cash:32 b equals 1$'));
expect(transaction1).toLog(new RegExp('^\\[Input #0] Test.cash:24 a is 1$'));
expect(transaction1).toLog(new RegExp('^\\[Input #0] Test.cash:31 a equals 1$'));
expect(transaction1).toLog(new RegExp('^\\[Input #0] Test.cash:32 b equals 1$'));
expect(transaction1).not.toLog(/a is not 1/);

const transaction2 = new TransactionBuilder({ provider })
.addInput(contractUtxo, contractTestLogs.unlock.functionWithIfStatement(2n))
.addOutput({ to: contractTestLogs.address, amount: 10000n });

expect(transaction2).toLog(new RegExp('^Test.cash:27 a is not 1$'));
expect(transaction2).toLog(new RegExp('^Test.cash:31 a equals 2$'));
expect(transaction2).toLog(new RegExp('^Test.cash:32 b equals 2$'));
expect(transaction2).toLog(new RegExp('^\\[Input #0] Test.cash:27 a is not 1$'));
expect(transaction2).toLog(new RegExp('^\\[Input #0] Test.cash:31 a equals 2$'));
expect(transaction2).toLog(new RegExp('^\\[Input #0] Test.cash:32 b equals 2$'));
expect(transaction2).not.toLog(/a is 1/);
});

Expand All @@ -111,9 +111,9 @@ describe('Debugging tests', () => {
.addOutput({ to: contractTestConsecutiveLogs.address, amount: 10000n });

// console.log(ownerSig, owner, num, beef);
expect(transaction).toLog(new RegExp(`^Test.cash:9 0x[0-9a-f]{130} 0x${binToHex(alicePub)} 100$`));
expect(transaction).toLog(new RegExp(`^\\[Input #0] Test.cash:9 0x[0-9a-f]{130} 0x${binToHex(alicePub)} 100$`));
// console.log(1, "test", true)
expect(transaction).toLog(new RegExp('^Test.cash:10 0xbeef 1 test true$'));
expect(transaction).toLog(new RegExp('^\\[Input #0] Test.cash:10 0xbeef 1 test true$'));
});

it('should log multiple console.log statements with other statements in between', async () => {
Expand All @@ -127,10 +127,10 @@ describe('Debugging tests', () => {
.addOutput({ to: contractTestMultipleLogs.address, amount: 10000n });

// console.log(ownerSig, owner, num);
const expectedFirstLog = new RegExp(`^Test.cash:6 0x[0-9a-f]{130} 0x${binToHex(alicePub)} 100$`);
const expectedFirstLog = new RegExp(`^\\[Input #0] Test.cash:6 0x[0-9a-f]{130} 0x${binToHex(alicePub)} 100$`);
expect(transaction).toLog(expectedFirstLog);

const expectedSecondLog = new RegExp('^Test.cash:11 0xbeef 1 test true$');
const expectedSecondLog = new RegExp('^\\[Input #0] Test.cash:11 0xbeef 1 test true$');
expect(transaction).toLog(expectedSecondLog);
});

Expand All @@ -140,8 +140,8 @@ describe('Debugging tests', () => {
.addInput(contractUtxo, contractTestLogs.unlock.test_log_inside_notif_statement(false))
.addOutput({ to: contractTestLogs.address, amount: contractUtxo.satoshis - 1000n });

expect(transaction).toLog(new RegExp(`^Test.cash:52 before: ${contractUtxo.satoshis}$`));
expect(transaction).toLog(new RegExp(`^Test.cash:54 after: ${contractUtxo.satoshis}$`));
expect(transaction).toLog(new RegExp(`^\\[Input #0] Test.cash:52 before: ${contractUtxo.satoshis}$`));
expect(transaction).toLog(new RegExp(`^\\[Input #0] Test.cash:54 after: ${contractUtxo.satoshis}$`));
});

it('should log intermediate results that get optimised out', async () => {
Expand All @@ -150,7 +150,7 @@ describe('Debugging tests', () => {
.addOutput({ to: contractTestLogs.address, amount: 10000n });

const expectedHash = binToHex(sha256(alicePub));
expect(transaction).toLog(new RegExp(`^Test.cash:43 0x${expectedHash}$`));
expect(transaction).toLog(new RegExp(`^\\[Input #0] Test.cash:43 0x${expectedHash}$`));
});

it.todo('intermediate results that is more complex than the test above');
Expand Down Expand Up @@ -542,7 +542,7 @@ describe('Debugging tests', () => {

expect(
() => expect(transaction).toLog('^This is definitely not the log$'),
).toThrow(/Expected: .*This is definitely not the log.*\nReceived: (.|\n)*?Test.cash:4 Hello World/);
).toThrow(/Expected: .*This is definitely not the log.*\nReceived: (.|\n)*?\[Input #0] Test.cash:4 Hello World/);
});

it('should fail the JestExtensions test if a log is logged that is NOT expected', async () => {
Expand All @@ -554,12 +554,14 @@ describe('Debugging tests', () => {
.addOutput({ to: contractTestRequires.address, amount: 10000n });

expect(
() => expect(transaction).not.toLog('^Test.cash:4 Hello World$'),
).toThrow(/Expected: not .*Test.cash:4 Hello World.*\nReceived: (.|\n)*?Test.cash:4 Hello World/);
() => expect(transaction).not.toLog('^\\[Input #0] Test.cash:4 Hello World$'),
).toThrow(
/Expected: not .*\\\\\[Input #0] Test.cash:4 Hello World.*\nReceived: (.|\n)*?\[Input #0] Test.cash:4 Hello World/,
);

expect(
() => expect(transaction).not.toLog(),
).toThrow(/Expected: not .*undefined.*\nReceived: (.|\n)*?Test.cash:4 Hello World/);
).toThrow(/Expected: not .*undefined.*\nReceived: (.|\n)*?\[Input #0] Test.cash:4 Hello World/);
});

it('should fail the JestExtensions test if a log is expected where no log is logged', async () => {
Expand Down
10 changes: 5 additions & 5 deletions packages/cashscript/test/multi-contract-debugging.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ describe('Multi-Contract-Debugging tests', () => {
amount: sameNameDifferentPathContractUtxo.satoshis - 2000n,
});

expect(tx).toLog('SiblingIntrospection.cash:6 outputBytecode: 0xaa2092e16594dd458916b3aa6cae4bf41352d1b3b39658698e8cddbeedd687efec7587\nSiblingIntrospection.cash:10 inputBytecode: 0xaa2092e16594dd458916b3aa6cae4bf41352d1b3b39658698e8cddbeedd687efec7587\nSameNameDifferentPath.cash:5 a is 0');
expect(tx).toLog('[Input #0] SiblingIntrospection.cash:6 outputBytecode: 0xaa2092e16594dd458916b3aa6cae4bf41352d1b3b39658698e8cddbeedd687efec7587\n[Input #0] SiblingIntrospection.cash:10 inputBytecode: 0xaa2092e16594dd458916b3aa6cae4bf41352d1b3b39658698e8cddbeedd687efec7587\n[Input #1] SameNameDifferentPath.cash:5 a is 0');
});

it('should log correct data for the reached console.log statements if a require statement fails, and not log unreached console.log statements', () => {
Expand All @@ -66,9 +66,9 @@ describe('Multi-Contract-Debugging tests', () => {
amount: sameNameDifferentPathContractUtxo.satoshis - 2000n,
});

expect(tx).toLog('SiblingIntrospection.cash:6 outputBytecode: 0xaa20d510df1721debb0d678d8e424b5f64f04f820005f017cb4731f7be94cd63755787');
expect(tx).not.toLog('SiblingIntrospection.cash:10 inputBytecode: 0xaa2092e16594dd458916b3aa6cae4bf41352d1b3b39658698e8cddbeedd687efec7587');
expect(tx).not.toLog('SameNameDifferentPath.cash:5 a is 0');
expect(tx).toLog('[Input #0] SiblingIntrospection.cash:6 outputBytecode: 0xaa20d510df1721debb0d678d8e424b5f64f04f820005f017cb4731f7be94cd63755787');
expect(tx).not.toLog('[Input #0] SiblingIntrospection.cash:10 inputBytecode: 0xaa2092e16594dd458916b3aa6cae4bf41352d1b3b39658698e8cddbeedd687efec7587');
expect(tx).not.toLog('[Input #1] SameNameDifferentPath.cash:5 a is 0');
});
});

Expand All @@ -88,7 +88,7 @@ describe('Multi-Contract-Debugging tests', () => {
.addOutput({ to: sameNameDifferentPathContract1.address, amount: sameNameDifferentPathContract1Utxo.satoshis })
.addOutput({ to: sameNameDifferentPathContract2.address, amount: sameNameDifferentPathContract2Utxo.satoshis });

expect(tx).toLog('SameNameDifferentPath.cash:5 a is 0\nSameNameDifferentPath.cash:8 a is not 0');
expect(tx).toLog('[Input #0] SameNameDifferentPath.cash:5 a is 0\n[Input #1] SameNameDifferentPath.cash:8 a is not 0');
});
});

Expand Down
10 changes: 9 additions & 1 deletion website/docs/releases/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,17 @@
title: Release Notes
---

## v0.11.5

#### CashScript SDK
- :sparkles: Include input index in console.log statements for debugging.
- :sparkles: Improve type inference for function and constructor arguments in the `Contract` class.
- :hammer_and_wrench: Replace redundant dependencies.
- :bug: Remove accidental dependency inclusion of `@types/node`.

## v0.11.4

### CashScript SDK
#### CashScript SDK
- :sparkles: Add `updateUtxoSet` option to `MockNetworkProvider` to allow for updating the UTXO set after a transaction is sent.
- :bug: Fix bug where sending P2PKH-only transactions would throw `No placeholder scenario ID or script ID found`.

Expand Down
5 changes: 0 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4719,11 +4719,6 @@ defined@^1.0.0:
resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=

delay@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/delay/-/delay-6.0.0.tgz#43749aefdf6cabd9e17b0d00bd3904525137e607"
integrity sha512-2NJozoOHQ4NuZuVIr5CWd0iiLVIRSDepakaovIN+9eIDHEhdCAEvSy2cuf1DCrPPQLvHmbqTHODlhHg8UCy4zw==

delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
Expand Down