Skip to content

WIP: Multi Contract and debugging support for Advanced Transaction Builder #257

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 48 commits into from
Feb 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
067921e
debugging using transaction builder, basic flow
kiok46 Nov 4, 2024
0368324
fixes with template entity names for inputs
kiok46 Nov 4, 2024
7c03192
create separate advanced builder
kiok46 Nov 9, 2024
4378198
Enable debugging and remove logs
kiok46 Nov 9, 2024
9818223
change addInput and addInputs parameter values
kiok46 Nov 9, 2024
54aed29
commit latest changes
kiok46 Nov 9, 2024
58cc062
Fix issues introduced by previous changes
kiok46 Nov 9, 2024
72849d6
Introduce scenario identifiers
kiok46 Nov 9, 2024
7c23ef2
Add script ids in the sourceOutputs and outputs
kiok46 Nov 9, 2024
89f8382
Only include new interfaces in the advanced interfaces, remove templa…
kiok46 Jan 9, 2025
7125655
Remove duplicates from libAuthTemplate under advanced
kiok46 Jan 9, 2025
bdfe935
Remove dependency on ContractUnlocker in inputs and remove unnecessar…
kiok46 Jan 11, 2025
92739ec
document getLibauthTemplates
kiok46 Jan 11, 2025
94bc8af
Update develop.js example, add tokenAddress in common-js
kiok46 Jan 12, 2025
59ed3b9
Include mainnet and mocknet example for advanced transaction builder
kiok46 Jan 12, 2025
f1ff276
Remove comments and update logs from Foo.cash and Bar.cash
kiok46 Jan 12, 2025
1fc4c12
Improve documentation, add function titleCase, restoration of code fo…
kiok46 Jan 12, 2025
41a3c4a
Add comment about reference comparison in getLibAuthTemplates function
kiok46 Jan 12, 2025
3c7e56b
Merge branch 'next' into multi-contract-tx-debug
rkalis Jan 14, 2025
95a400c
Merge branch 'next' into multi-contract-tx-debug
rkalis Jan 14, 2025
a74a08e
Refactor develop.js example into automated test case in LibauthTempla…
rkalis Jan 14, 2025
feb203a
Merge branch 'next' into multi-contract-tx-debug
rkalis Jan 27, 2025
5c9a305
Add additional test cases for multi contract template generation
rkalis Jan 27, 2025
c75a42d
Start of refactors
rkalis Jan 27, 2025
4d7a34f
Enable mocknet in advanced TransactionBuilder tests
rkalis Feb 4, 2025
b074fda
Replace default Electrum server for CHIPNET
rkalis Feb 4, 2025
4f17130
Split up TransactionBuilder.test.ts into one that tests the API and o…
rkalis Feb 4, 2025
9ecb5cd
Add additional tests to TransactionBuilder.test.ts and improve error …
rkalis Feb 4, 2025
6ff6db9
move timelocks tests to dedicated file in /misc, remove old /misc test
mr-zwets Feb 10, 2025
8b51ea0
upgrade the bigint.cash test
mr-zwets Feb 10, 2025
d18d9ad
add this.debug to .send, use TransactionBuilder in Announcement.cash
mr-zwets Feb 10, 2025
d5967c8
convert more e2e tests to the TransactionBuilder
mr-zwets Feb 10, 2025
c3b395a
improve structure debugging tests
mr-zwets Feb 10, 2025
e039a3d
Merge branch 'multi-contract-tx-debug' of github.com:kiok46/cashscrip…
rkalis Feb 11, 2025
c304b43
Update old LibauthTemplate fixtures so tests are passing (+ update sn…
rkalis Feb 11, 2025
b9c89a7
convert TransferWithTimeout e2e tests to TransactionBuilder
mr-zwets Feb 11, 2025
d22651b
small improvements exisiting tests
mr-zwets Feb 11, 2025
e3cb5bc
fix issue argument encoding, move BigInt e2e tests to TransactionBuilder
mr-zwets Feb 11, 2025
5552821
Make sure the updated e2e tests work on chipnet
rkalis Feb 14, 2025
cdfc5a7
Update existing debugging tests to use TransactionBuilder
rkalis Feb 14, 2025
4d58d37
start structure MultiContract.test.ts
mr-zwets Feb 18, 2025
f6ca36d
Add some multi-contract tests and fix some bugs
rkalis Feb 18, 2025
8ebb4c3
Update naming of params in Foo and Bar contracts
rkalis Feb 18, 2025
1b0485e
Fix tiny bugs we introduced in the last commits
rkalis Feb 25, 2025
08f6e97
Make TransactionBuilder.debug() run *all* scenarios instead of only 1
rkalis Feb 25, 2025
5e33252
Handle naming collision in unlockingScript.passes (scenarios)
rkalis Feb 25, 2025
433b699
Update fixtures and skip remaining failing test on mocknet
rkalis Feb 25, 2025
945b852
Replace HdKey with Key for P2PKH entities and replace renamed Transac…
rkalis Feb 27, 2025
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
1 change: 1 addition & 0 deletions examples/common-js.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ export const alicePub = secp256k1.derivePublicKeyCompressed(aliceNode.privateKey
export const alicePriv = aliceNode.privateKey;
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;
2 changes: 2 additions & 0 deletions packages/cashscript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"@bitauth/libauth": "^3.1.0-next.2",
"@cashscript/utils": "^0.11.0-next.0",
"@mr-zwets/bchn-api-wrapper": "^1.0.1",
"change-case": "^5.4.4",
"delay": "^6.0.0",
"electrum-cash": "^2.0.10",
"fast-deep-equal": "^3.1.3",
Expand All @@ -55,6 +56,7 @@
"devDependencies": {
"@jest/globals": "^29.7.0",
"@psf/bch-js": "^6.8.0",
"@types/change-case": "^2.3.5",
"@types/pako": "^2.0.3",
"@types/semver": "^7.5.8",
"eslint": "^8.54.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/cashscript/src/Contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ export class Contract<

const generateLockingBytecode = (): Uint8Array => addressToLockScript(this.address);

return { generateUnlockingBytecode, generateLockingBytecode };
return { generateUnlockingBytecode, generateLockingBytecode, contract: this, params: args, abiFunction };
};
}
}
Expand Down
12 changes: 12 additions & 0 deletions packages/cashscript/src/Errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,24 @@ export class TypeError extends Error {
}
}

export class UndefinedInputError extends Error {
constructor() {
super('Input is undefined');
}
}

export class OutputSatoshisTooSmallError extends Error {
constructor(satoshis: bigint, minimumAmount: bigint) {
super(`Tried to add an output with ${satoshis} satoshis, which is less than the required minimum for this output-type (${minimumAmount})`);
}
}

export class OutputTokenAmountTooSmallError extends Error {
constructor(amount: bigint) {
super(`Tried to add an output with ${amount} tokens, which is invalid`);
}
}

export class TokensToNonTokenAddressError extends Error {
constructor(address: string) {
super(`Tried to send tokens to an address without token support, ${address}.`);
Expand Down
77 changes: 48 additions & 29 deletions packages/cashscript/src/LibauthTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export const buildTemplate = async ({
const template = {
$schema: 'https://ide.bitauth.com/authentication-template-v0.schema.json',
description: 'Imported from cashscript',
name: contract.artifact.contractName,
name: 'CashScript Generated Debugging Template',
supported: ['BCH_2023_05'],
version: 0,
entities: generateTemplateEntities(contract.artifact, transaction.abiFunction, transaction.encodedFunctionArgs),
Expand All @@ -77,7 +77,6 @@ export const buildTemplate = async ({
),
} as WalletTemplate;


transaction.inputs
.forEach((input, index) => {
if (!isUtxoP2PKH(input)) return;
Expand All @@ -90,9 +89,9 @@ export const buildTemplate = async ({
const hashtypeName = getHashTypeName(input.template.getHashType(false));
const signatureString = `${placeholderKeyName}.${signatureAlgorithmName}.${hashtypeName}`;

template.entities.parameters.scripts!.push(lockScriptName, unlockScriptName);
template.entities.parameters.variables = {
...template.entities.parameters.variables,
template.entities[snakeCase(contract.name + 'Parameters')].scripts!.push(lockScriptName, unlockScriptName);
template.entities[snakeCase(contract.name + 'Parameters')].variables = {
...template.entities[snakeCase(contract.name + 'Parameters')].variables,
[placeholderKeyName]: {
description: placeholderKeyName,
name: placeholderKeyName,
Expand Down Expand Up @@ -154,12 +153,12 @@ const generateTemplateEntities = (
);

const entities = {
parameters: {
[snakeCase(artifact.contractName + 'Parameters')]: {
description: 'Contract creation and function parameters',
name: 'parameters',
name: snakeCase(artifact.contractName + 'Parameters'),
scripts: [
'lock',
'unlock_lock',
snakeCase(artifact.contractName + '_lock'),
snakeCase(artifact.contractName + '_unlock'),
],
variables: {
...functionParameters,
Expand All @@ -170,7 +169,7 @@ const generateTemplateEntities = (

// function_index is a special variable that indicates the function to execute
if (artifact.abi.length > 1) {
entities.parameters.variables.function_index = {
entities[snakeCase(artifact.contractName + 'Parameters')].variables.function_index = {
description: 'Script function index to execute',
name: 'function_index',
type: 'WalletData',
Expand All @@ -189,8 +188,8 @@ const generateTemplateScripts = (
): WalletTemplate['scripts'] => {
// definition of locking scripts and unlocking scripts with their respective bytecode
return {
unlock_lock: generateTemplateUnlockScript(artifact, abiFunction, encodedFunctionArgs),
lock: generateTemplateLockScript(artifact, addressType, encodedConstructorArgs),
[snakeCase(artifact.contractName + '_unlock')]: generateTemplateUnlockScript(artifact, abiFunction, encodedFunctionArgs),
[snakeCase(artifact.contractName + '_lock')]: generateTemplateLockScript(artifact, addressType, encodedConstructorArgs),
};
};

Expand All @@ -201,7 +200,7 @@ const generateTemplateLockScript = (
): WalletTemplateScriptLocking => {
return {
lockingType: addressType,
name: 'lock',
name: snakeCase(artifact.contractName + '_lock'),
script: [
`// "${artifact.contractName}" contract constructor parameters`,
formatParametersForDebugging(artifact.constructorInputs, constructorArguments),
Expand All @@ -225,15 +224,15 @@ const generateTemplateUnlockScript = (

return {
// this unlocking script must pass our only scenario
passes: ['evaluate_function'],
name: 'unlock',
passes: [snakeCase(artifact.contractName + 'Evaluate')],
name: snakeCase(artifact.contractName + '_unlock'),
script: [
`// "${abiFunction.name}" function parameters`,
formatParametersForDebugging(abiFunction.inputs, encodedFunctionArgs),
'',
...functionIndexString,
].join('\n'),
unlocks: 'lock',
unlocks: snakeCase(artifact.contractName + '_lock'),
};
};

Expand All @@ -251,8 +250,8 @@ const generateTemplateScenarios = (

const scenarios = {
// single scenario to spend out transaction under test given the CashScript parameters provided
evaluate_function: {
name: 'Evaluate',
[snakeCase(artifact.contractName + 'Evaluate')]: {
name: snakeCase(artifact.contractName + 'Evaluate'),
description: 'An example evaluation where this script execution passes.',
data: {
// encode values for the variables defined above in `entities` property
Expand All @@ -273,7 +272,7 @@ const generateTemplateScenarios = (

if (artifact.abi.length > 1) {
const functionIndex = artifact.abi.findIndex((func) => func.name === transaction.abiFunction.name);
scenarios!.evaluate_function!.data!.bytecode!.function_index = functionIndex.toString();
scenarios![snakeCase(artifact.contractName + 'Evaluate')].data!.bytecode!.function_index = functionIndex.toString();
}

return scenarios;
Expand Down Expand Up @@ -314,7 +313,7 @@ const generateTemplateScenarioTransaction = (
return { inputs, locktime, outputs, version };
};

const generateTemplateScenarioTransactionOutputLockingBytecode = (
export const generateTemplateScenarioTransactionOutputLockingBytecode = (
csOutput: Output,
contract: Contract,
): string | {} => {
Expand All @@ -323,6 +322,20 @@ const generateTemplateScenarioTransactionOutputLockingBytecode = (
return binToHex(addressToLockScript(csOutput.to));
};

/**
* Generates source outputs for a BitAuth template scenario
*
* @param csTransaction - The CashScript transaction to generate source outputs for
* @returns An array of BitAuth template scenario outputs with locking scripts and values
*
* For each input in the transaction:
* - Generates appropriate locking bytecode (P2PKH or contract)
* - Includes the input value in satoshis
* - Includes any token details if present
*
* The slotIndex tracks which input is the contract input vs P2PKH inputs
* to properly generate the locking scripts.
*/
const generateTemplateScenarioSourceOutputs = (
csTransaction: Transaction,
): Array<WalletTemplateScenarioOutput<true>> => {
Expand All @@ -338,7 +351,7 @@ const generateTemplateScenarioSourceOutputs = (
};

// Used for generating the locking / unlocking bytecode for source outputs and inputs
const generateTemplateScenarioBytecode = (
export const generateTemplateScenarioBytecode = (
input: Utxo, p2pkhScriptName: string, placeholderKeyName: string, insertSlot?: boolean,
): WalletTemplateScenarioBytecode | ['slot'] => {
if (isUtxoP2PKH(input)) {
Expand All @@ -357,7 +370,7 @@ const generateTemplateScenarioBytecode = (
return insertSlot ? ['slot'] : {};
};

const generateTemplateScenarioParametersValues = (
export const generateTemplateScenarioParametersValues = (
types: readonly AbiInput[],
encodedArgs: EncodedFunctionArgument[],
): Record<string, string> => {
Expand All @@ -368,14 +381,18 @@ const generateTemplateScenarioParametersValues = (
.filter(([, arg]) => !(arg instanceof SignatureTemplate))
.map(([input, arg]) => {
const encodedArgumentHex = binToHex(arg as Uint8Array);
const prefixedEncodedArgument = encodedArgumentHex.length > 0 ? `0x${encodedArgumentHex}` : '';
const prefixedEncodedArgument = addHexPrefixExceptEmpty(encodedArgumentHex);
return [snakeCase(input.name), prefixedEncodedArgument] as const;
});

return Object.fromEntries(entries);
};

const generateTemplateScenarioKeys = (
export const addHexPrefixExceptEmpty = (value: string): string => {
return value.length > 0 ? `0x${value}` : '';
};

export const generateTemplateScenarioKeys = (
types: readonly AbiInput[],
encodedArgs: EncodedFunctionArgument[],
): Record<string, string> => {
Expand All @@ -388,7 +405,7 @@ const generateTemplateScenarioKeys = (
return Object.fromEntries(entries);
};

const formatParametersForDebugging = (types: readonly AbiInput[], args: EncodedFunctionArgument[]): string => {
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
Expand All @@ -409,7 +426,7 @@ const formatParametersForDebugging = (types: readonly AbiInput[], args: EncodedF
}).join('\n');
};

const getSignatureAlgorithmName = (signatureAlgorithm: SignatureAlgorithm): string => {
export const getSignatureAlgorithmName = (signatureAlgorithm: SignatureAlgorithm): string => {
const signatureAlgorithmNames = {
[SignatureAlgorithm.SCHNORR]: 'schnorr_signature',
[SignatureAlgorithm.ECDSA]: 'ecdsa_signature',
Expand All @@ -418,7 +435,7 @@ const getSignatureAlgorithmName = (signatureAlgorithm: SignatureAlgorithm): stri
return signatureAlgorithmNames[signatureAlgorithm];
};

const getHashTypeName = (hashType: HashType): string => {
export const getHashTypeName = (hashType: HashType): string => {
const hashtypeNames = {
[HashType.SIGHASH_ALL]: 'all_outputs',
[HashType.SIGHASH_ALL | HashType.SIGHASH_ANYONECANPAY]: 'all_outputs_single_input',
Expand All @@ -437,7 +454,7 @@ const getHashTypeName = (hashType: HashType): string => {
return hashtypeNames[hashType];
};

const formatBytecodeForDebugging = (artifact: Artifact): string => {
export const formatBytecodeForDebugging = (artifact: Artifact): string => {
if (!artifact.debug) {
return artifact.bytecode
.split(' ')
Expand All @@ -452,7 +469,9 @@ const formatBytecodeForDebugging = (artifact: Artifact): string => {
);
};

const serialiseTokenDetails = (token?: TokenDetails | LibauthTokenDetails): LibauthTemplateTokenDetails | undefined => {
export const serialiseTokenDetails = (
token?: TokenDetails | LibauthTokenDetails,
): LibauthTemplateTokenDetails | undefined => {
if (!token) return undefined;

return {
Expand Down
1 change: 1 addition & 0 deletions packages/cashscript/src/SignatureTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export default class SignatureTemplate {
const unlockingBytecode = scriptToBytecode([signature, publicKey]);
return unlockingBytecode;
},
template: this,
};
}
}
Expand Down
6 changes: 3 additions & 3 deletions packages/cashscript/src/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ 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, DebugResult } from './debugging.js';
import { debugTemplate, DebugResults } from './debugging.js';
import { EncodedFunctionArgument } from './Argument.js';
import { FailedTransactionError } from './Errors.js';

Expand Down Expand Up @@ -185,13 +185,13 @@ export class Transaction {
}

// method to debug the transaction with libauth VM, throws upon evaluation error
async debug(): Promise<DebugResult> {
async debug(): Promise<DebugResults> {
if (!this.contract.artifact.debug) {
console.warn('No debug information found in artifact. Recompile with cashc version 0.10.0 or newer to get better debugging information.');
}

const template = await this.getLibauthTemplate();
return debugTemplate(template, this.contract.artifact);
return debugTemplate(template, [this.contract.artifact]);
}

async bitauthUri(): Promise<string> {
Expand Down
Loading