Debugging & Testing
To speed up contract development and ensure contract quality and security, CashScript takes advantage of deep integration with libauth. This allows for local transaction evaluation without actual interaction with the Bitcoin Cash network. For a quick start, you can check out our testing-suite example that demonstrates a full development and testing environment for CashScript contracts, similar to Hardhat on Ethereum.
The CashScript debugging tools only work with the Simple Transaction Builder. We plan to extend the debugging tools to work with the Advanced Transaction Builder in the future.
MockNetworkProvider
The MockNetworkProvider
is a special network provider that allows you to evaluate transactions locally without interacting with the Bitcoin Cash network. This is useful when writing automated tests for your contracts, or when debugging your contract locally. By default, it generates some random mock UTXOs for the contract address, but you can also add your own UTXOs to the provider.
import { MockNetworkProvider, randomUtxo, randomToken, randomNFT } from 'cashscript';
const provider = new MockNetworkProvider();
provider.addUtxo(contract.address, { vout: 0, txid: "ab...", satoshis: 10000n });
provider.addUtxo(aliceAddress, randomUtxo({
satoshis: 1000n,
token: { ...randomNFT(), ...randomToken() },
}));
The MockNetworkProvider
only evaluates the transactions locally, so any UTXOs added to a transaction still count as "unspent", even after mocking a sendTransaction
using the provider.
Transaction evaluation
If a CashScript transaction is sent using any network provider and is rejected by the network (or the MockNetworkProvider
), the transaction will be evaluated locally using libauth to provide failure reasons and debug information in addition to the failure reason communicated by the network.
It is possible to export the transaction for step-by-step debugging in the BitAuth IDE. This will allow you to inspect the transaction in detail, and see exactly why the transaction failed. 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. This URI is also displayed in the console whenever a transaction fails.
const transaction = contract.functions.exampleFunction(0n).to(contract.address, 10000n);
const uri = await transaction.bitauthUri();
It is unsafe to debug transactions on mainnet as private keys will be exposed to BitAuth IDE and transmitted over the network.
Automated testing
With local transaction evaluation and debugging, it is possible to write efficient automated tests for CashScript contracts and test specific contract behaviour and check console.log
values and require
error messages.
Logging values
You can log values during debug evaluation using the console.log
statement. Any variables or primitive values (such as ints, strings, bytes, etc) can be logged.
Logging is only available in debug evaluation of a transaction, but has no impact on the compiled bytecode or regular (non-debug) execution.
Error messages
The require
statement accepts an optional error message as a second argument. If the condition in the require statement is not met during debug evaluation, the error message is returned. This allows you to write automated tests that check for specific error messages.
Similar to console.log
, the error message in a require
statement is only available in debug evaluation of a transaction, so the error message has no impact on the compiled bytecode or regular (non-debug) execution.
Jest extension
To make writing automated tests for CashScript contracts easier, we provide a Jest extension that enables easy testing of console.log
values and require
error messages. To use the extension, you can import it from cashscript/dist/test/JestExtensions
.
If you're using a different testing framework, you can test for console.log
values by spying on the console output, and test for require
error messages by asserting that an error is thrown with a specific error message.
Example
contract Example() {
function exampleFunction(int value) {
console.log(value, "test");
require(value == 1, "Wrong value passed");
}
}
import artifact from '../artifacts/example.json' assert { type: "json" };
import { Contract, MockNetworkProvider, randomUtxo } from 'cashscript';
import 'cashscript/dist/test/JestExtensions';
const provider = new MockNetworkProvider();
const contract = new Contract(artifact, [], { provider });
provider.addUtxo(contract.address, randomUtxo());
let transaction = contract.functions.exampleFunction(0n).to(contract.address, 10000n);
await (expect(transaction)).toLog(/0 test/);
await (expect(transaction)).toFailRequireWith(/Wrong value passed/);
transaction = contract.functions.exampleFunction(1n).to(contract.address, 10000n);
await expect(transaction.send()).resolves.not.toThrow();