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
8 changes: 3 additions & 5 deletions packages/cashscript/src/Contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
ConstructorArgument, encodeFunctionArgument, encodeConstructorArguments, FunctionArgument,
} from './Argument.js';
import {
Unlocker, ContractOptions, GenerateUnlockingBytecodeOptions, Utxo, ContractType, ContractFunctionUnlocker,
Unlocker, ContractOptions, GenerateUnlockingBytecodeOptions, SpendableUtxo, ContractType, ContractFunctionUnlocker,
} from './interfaces.js';
import NetworkProvider from './network/NetworkProvider.js';
import {
Expand Down Expand Up @@ -84,7 +84,7 @@ class ContractBase {
*
* @returns A list of UTXOs spendable by this contract.
*/
async getUtxos(): Promise<Utxo[]> {
async getUtxos(): Promise<SpendableUtxo[]> {
if (this.contractType === 'p2s') {
return this.provider.getUtxosForLockingBytecode(this.bytecode);
}
Expand Down Expand Up @@ -204,9 +204,7 @@ class ContractInternal<
return unlockingBytecode;
};

const generateLockingBytecode = (): Uint8Array => hexToBin(this.lockingBytecode);

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

export class InputMissingLockingBytecodeError extends Error {
constructor() {
super('Input UTXO is missing its lockingBytecode. UTXOs fetched from a network provider include it automatically; when constructing UTXOs manually, set lockingBytecode to the hex-encoded locking script of the output.');
}
}

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})`);
Expand Down Expand Up @@ -97,6 +103,17 @@ export class UnlockingBytecodeTooLargeError extends Error {
}
}

export class UnlockerLockingBytecodeMismatchError extends Error {
constructor(
public inputIndex: number,
utxoLockScript: string,
unlockerLockScript: string,
unlockerDescription: string,
) {
super(`Input #${inputIndex} is locked by ${utxoLockScript}, which does not match the provided unlocker (${unlockerDescription}, corresponding to ${unlockerLockScript}). This transaction would be rejected by the network. Make sure to use an unlocker that matches the address/contract holding the UTXO.`);
}
}

export class TransactionTooLargeError extends Error {
constructor(size: number, maximumSize: number) {
super(`Transaction size of ${size} is greater than the maximum standard transaction size of ${maximumSize}`);
Expand Down
1 change: 0 additions & 1 deletion packages/cashscript/src/SignatureTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ export default class SignatureTemplate {
const prevOutScript = publicKeyToP2PKHLockingBytecode(this.publicKey);

return {
generateLockingBytecode: () => prevOutScript,
generateUnlockingBytecode: ({ transaction, sourceOutputs, inputIndex }: GenerateUnlockingBytecodeOptions) => {
const preimage = createSighashPreimage(transaction, sourceOutputs, inputIndex, prevOutScript, this.sighashType);
const sighash = hash256(preimage);
Expand Down
13 changes: 9 additions & 4 deletions packages/cashscript/src/TransactionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
Output,
TransactionDetails,
UnlockableUtxo,
Utxo,
SpendableUtxo,
InputOptions,
isUnlockableUtxo,
isStandardUnlockableUtxo,
Expand All @@ -34,6 +34,7 @@ import {
getOutputSize,
validateInput,
validateOutput,
validateUnlocker,
} from './utils.js';
import {
FailedTransactionError,
Expand Down Expand Up @@ -109,7 +110,7 @@ export class TransactionBuilder {
* @returns This builder for chaining.
* @throws If the UTXO is invalid.
*/
addInput(utxo: Utxo, unlocker: Unlocker, options?: InputOptions): this {
addInput(utxo: SpendableUtxo, unlocker: Unlocker, options?: InputOptions): this {
return this.addInputs([utxo], unlocker, options);
}

Expand All @@ -122,7 +123,7 @@ export class TransactionBuilder {
* @returns This builder for chaining.
* @throws If any UTXO is invalid.
*/
addInputs(utxos: Utxo[], unlocker: Unlocker, options?: InputOptions): this;
addInputs(utxos: SpendableUtxo[], unlocker: Unlocker, options?: InputOptions): this;

/**
* Add multiple UTXOs that each carry their own unlocker.
Expand All @@ -133,7 +134,7 @@ export class TransactionBuilder {
*/
addInputs(utxos: UnlockableUtxo[]): this;

addInputs(utxos: Utxo[] | UnlockableUtxo[], unlocker?: Unlocker, options?: InputOptions): this {
addInputs(utxos: SpendableUtxo[] | UnlockableUtxo[], unlocker?: Unlocker, options?: InputOptions): this {
utxos.forEach((utxo) => validateInput(utxo, this.changeLocks));
if (
(!unlocker && utxos.some((utxo) => !isUnlockableUtxo(utxo)))
Expand All @@ -142,6 +143,10 @@ export class TransactionBuilder {
throw new Error('Either all UTXOs must have an individual unlocker specified, or no UTXOs must have an individual unlocker specified and a shared unlocker must be provided');
}

utxos.forEach((utxo, i) => (
validateUnlocker(utxo, unlocker ?? (utxo as UnlockableUtxo).unlocker, this.inputs.length + i, this.provider.network)
));

if (!unlocker) {
this.inputs = this.inputs.concat(utxos as UnlockableUtxo[]);
return this;
Expand Down
27 changes: 2 additions & 25 deletions packages/cashscript/src/debugging.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,15 @@
import { AuthenticationErrorCommon, AuthenticationInstruction, AuthenticationProgramCommon, AuthenticationProgramStateCommon, AuthenticationVirtualMachine, ResolvedTransactionCommon, WalletTemplate, WalletTemplateScriptUnlocking, binToHex, createCompiler, createVirtualMachineBch2023, createVirtualMachineBch2025, createVirtualMachineBch2026, createVirtualMachineBchSpec, decodeAuthenticationInstructions, encodeAuthenticationInstruction, walletTemplateToCompilerConfiguration } from '@bitauth/libauth';
import { AuthenticationErrorCommon, AuthenticationInstruction, AuthenticationProgramCommon, AuthenticationProgramStateCommon, WalletTemplate, WalletTemplateScriptUnlocking, binToHex, createCompiler, decodeAuthenticationInstructions, encodeAuthenticationInstruction, walletTemplateToCompilerConfiguration } from '@bitauth/libauth';
import { Artifact, LogData, LogEntry, Op, PrimitiveType, StackItem, asmToBytecode, bytecodeToAsm, decodeBool, decodeInt, decodeString } from '@cashscript/utils';
import { findLastIndex, toRegExp } from './utils.js';
import { FailedRequireError, FailedTransactionError, FailedTransactionEvaluationError } from './Errors.js';
import { attributeLogEntry, buildCallStack, getActiveBytecode, resolveFrame } from './debug-frame.js';
import { getBitauthUri } from './libauth-template/LibauthTemplate.js';
import { createVirtualMachine, VM } from './libauth-template/utils.js';
import { VmTarget } from './interfaces.js';

export type DebugResult = AuthenticationProgramStateCommon[];
export type DebugResults = Record<string, DebugResult>;

/* eslint-disable @typescript-eslint/indent */
type VM = AuthenticationVirtualMachine<
ResolvedTransactionCommon,
AuthenticationProgramCommon,
AuthenticationProgramStateCommon
>;
/* eslint-enable @typescript-eslint/indent */

const createVirtualMachine = (vmTarget: VmTarget): VM => {
switch (vmTarget) {
case 'BCH_2023_05':
return createVirtualMachineBch2023();
case 'BCH_2025_05':
return createVirtualMachineBch2025();
case 'BCH_2026_05':
return createVirtualMachineBch2026();
case 'BCH_SPEC':
// TODO: This typecast is shitty, but it's hard to fix
return createVirtualMachineBchSpec() as unknown as VM;
default:
throw new Error(`Debugging is not supported for the ${vmTarget} virtual machine.`);
}
};

// debugs the template, optionally logging the execution data
export const debugTemplate = (template: WalletTemplate, artifacts: Artifact[]): DebugResults => {
// If a contract has the same name, but a different bytecode, then it is considered a name collision
Expand Down
10 changes: 7 additions & 3 deletions packages/cashscript/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@ export interface Utxo {
vout: number;
satoshis: bigint;
token?: TokenDetails;
lockingBytecode?: string;
}

export interface UnlockableUtxo extends Utxo {
export interface SpendableUtxo extends Utxo {
lockingBytecode: string;
}

export interface UnlockableUtxo extends SpendableUtxo {
unlocker: Unlocker;
options?: InputOptions;
}
Expand Down Expand Up @@ -40,7 +45,6 @@ export interface GenerateUnlockingBytecodeOptions {
}

export interface Unlocker {
generateLockingBytecode: () => Uint8Array;
generateUnlockingBytecode: (options: GenerateUnlockingBytecodeOptions) => Uint8Array;
}

Expand All @@ -56,7 +60,7 @@ export interface P2PKHUnlocker extends Unlocker {

export type StandardUnlocker = ContractUnlocker | P2PKHUnlocker;

export type PlaceholderP2PKHUnlocker = Unlocker & { placeholder: true };
export type PlaceholderP2PKHUnlocker = Unlocker & { placeholder: true, lockingBytecode: string };

export type ContractFunctionUnlocker = (...args: FunctionArgument[]) => ContractUnlocker;

Expand Down
43 changes: 42 additions & 1 deletion packages/cashscript/src/libauth-template/utils.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,54 @@
import { AbiFunction, AbiInput, Artifact, formatBitAuthScript, sha256 } from '@cashscript/utils';
import { LibauthTokenDetails, SighashType, SignatureAlgorithm, TokenDetails, VmTarget } from '../interfaces.js';
import { hexToBin, binToHex, isHex, decodeCashAddress, Input, assertSuccess, decodeAuthenticationInstructions, AuthenticationInstructionPush } from '@bitauth/libauth';
import {
hexToBin,
binToHex,
isHex,
decodeCashAddress,
Input,
assertSuccess,
decodeAuthenticationInstructions,
AuthenticationInstructionPush,
AuthenticationProgramCommon,
AuthenticationProgramStateCommon,
AuthenticationVirtualMachine,
ResolvedTransactionCommon,
createVirtualMachineBch2023,
createVirtualMachineBch2025,
createVirtualMachineBch2026,
createVirtualMachineBchSpec,
} from '@bitauth/libauth';
import { EncodedFunctionArgument } from '../Argument.js';
import { zip } from '../utils.js';
import SignatureTemplate from '../SignatureTemplate.js';
import { Contract } from '../Contract.js';

export const DEFAULT_VM_TARGET = VmTarget.BCH_2026_05;

/* eslint-disable @typescript-eslint/indent */
export type VM = AuthenticationVirtualMachine<
ResolvedTransactionCommon,
AuthenticationProgramCommon,
AuthenticationProgramStateCommon
>;
/* eslint-enable @typescript-eslint/indent */

export const createVirtualMachine = (vmTarget: VmTarget): VM => {
switch (vmTarget) {
case 'BCH_2023_05':
return createVirtualMachineBch2023();
case 'BCH_2025_05':
return createVirtualMachineBch2025();
case 'BCH_2026_05':
return createVirtualMachineBch2026();
case 'BCH_SPEC':
// TODO: This typecast is shitty, but it's hard to fix
return createVirtualMachineBchSpec() as unknown as VM;
default:
throw new Error(`Evaluation is not supported for the ${vmTarget} virtual machine.`);
}
};

export const getLockScriptName = (contract: Contract): string => {
if (contract.contractType === 'p2s') {
return `${contract.artifact.contractName}_${binToHex(sha256(hexToBin(contract.lockingBytecode)))}_lock`;
Expand Down
7 changes: 4 additions & 3 deletions packages/cashscript/src/network/ElectrumNetworkProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
type RequestResponse,
type ElectrumClientEvents,
} from '@electrum-cash/network';
import { Utxo, Network } from '../interfaces.js';
import { SpendableUtxo, Network } from '../interfaces.js';
import NetworkProvider from './NetworkProvider.js';
import { addressToLockScript } from '../utils.js';
import {
Expand Down Expand Up @@ -75,12 +75,12 @@ export default class ElectrumNetworkProvider implements NetworkProvider {
}
}

async getUtxos(address: string): Promise<Utxo[]> {
async getUtxos(address: string): Promise<SpendableUtxo[]> {
const lockingBytecode = addressToLockScript(address);
return this.getUtxosForLockingBytecode(lockingBytecode);
}

async getUtxosForLockingBytecode(lockingBytecode: Uint8Array | string): Promise<Utxo[]> {
async getUtxosForLockingBytecode(lockingBytecode: Uint8Array | string): Promise<SpendableUtxo[]> {
if (typeof lockingBytecode === 'string' && !isHex(lockingBytecode)) {
throw new Error(`Invalid locking bytecode: ${lockingBytecode} is not a valid hex string`);
}
Expand All @@ -94,6 +94,7 @@ export default class ElectrumNetworkProvider implements NetworkProvider {
txid: utxo.tx_hash,
vout: utxo.tx_pos,
satoshis: BigInt(utxo.value),
lockingBytecode: binToHex(lockingBytecodeBin),
token: utxo.token_data ? {
...utxo.token_data,
amount: BigInt(utxo.token_data.amount),
Expand Down
Loading
Loading