cashscript 0.11.4 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -6
- package/dist/Argument.js +12 -8
- package/dist/Contract.d.ts +3 -9
- package/dist/Contract.js +4 -31
- package/dist/Errors.d.ts +2 -2
- package/dist/SignatureTemplate.d.ts +1 -0
- package/dist/SignatureTemplate.js +13 -4
- package/dist/TransactionBuilder.d.ts +8 -4
- package/dist/TransactionBuilder.js +74 -22
- package/dist/debugging.js +53 -20
- package/dist/index.d.ts +1 -2
- package/dist/index.js +0 -1
- package/dist/interfaces.d.ts +10 -6
- package/dist/interfaces.js +6 -3
- package/dist/libauth-template/LibauthTemplate.d.ts +6 -0
- package/dist/libauth-template/LibauthTemplate.js +445 -0
- package/dist/libauth-template/utils.d.ts +27 -0
- package/dist/libauth-template/utils.js +89 -0
- package/dist/network/MockNetworkProvider.d.ts +4 -3
- package/dist/network/MockNetworkProvider.js +4 -14
- package/dist/test/JestExtensions.js +5 -26
- package/dist/types/type-inference.d.ts +8 -6
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +3 -23
- package/package.json +4 -7
- package/dist/LibauthTemplate.d.ts +0 -32
- package/dist/LibauthTemplate.js +0 -324
- package/dist/Transaction.d.ts +0 -45
- package/dist/Transaction.js +0 -385
- package/dist/advanced/LibauthTemplate.d.ts +0 -45
- package/dist/advanced/LibauthTemplate.js +0 -426
package/README.md
CHANGED
|
@@ -37,17 +37,29 @@ Using the CashScript SDK, you can import contract artifact files, create new ins
|
|
|
37
37
|
const provider = new ElectrumNetworkProvider('mainnet');
|
|
38
38
|
|
|
39
39
|
// Create a new P2PKH contract with constructor arguments: { pkh: pkh }
|
|
40
|
-
const contract = new Contract(P2PKH, [pkh], provider);
|
|
40
|
+
const contract = new Contract(P2PKH, [pkh], { provider });
|
|
41
41
|
|
|
42
42
|
// Get contract balance & output address + balance
|
|
43
43
|
console.log('contract address:', contract.address);
|
|
44
44
|
console.log('contract balance:', await contract.getBalance());
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
const transactionBuilder = new TransactionBuilder({ provider });
|
|
47
|
+
const contractUtxos = await contract.getUtxos();
|
|
48
|
+
|
|
49
|
+
const sendAmount = 10_000n;
|
|
50
|
+
const destinationAddress = '... some address ...';
|
|
51
|
+
|
|
52
|
+
// Calculate the change amount, accounting for a miner fee of 1000 satoshis
|
|
53
|
+
const changeAmount = contractUtxos[0].satoshis - sendAmount - 1000n;
|
|
54
|
+
|
|
55
|
+
// Construct a transaction with the transaction builder
|
|
56
|
+
const txDetails = await transactionBuilder
|
|
57
|
+
// Add a contract input that spends from the contract using the 'spend' function
|
|
58
|
+
.addInput(contractUtxos[0], contract.unlock.spend(pk, new SignatureTemplate(keypair)))
|
|
59
|
+
// Add an output that sends 0. 000 100 00 BCH back to the destination address
|
|
60
|
+
.addOutput({ to: destinationAddress, amount: sendAmount })
|
|
61
|
+
// Add a change output that sends the change back to the contract's address
|
|
62
|
+
.addOutput({ to: contract.address, amount: changeAmount })
|
|
51
63
|
.send();
|
|
52
64
|
|
|
53
65
|
console.log(txDetails);
|
package/dist/Argument.js
CHANGED
|
@@ -34,15 +34,19 @@ export function encodeFunctionArgument(argument, typeStr) {
|
|
|
34
34
|
if (!(argument instanceof Uint8Array)) {
|
|
35
35
|
throw Error(`Value for type ${type} should be a Uint8Array or hex string`);
|
|
36
36
|
}
|
|
37
|
-
// Redefine SIG as a bytes65
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
// Redefine SIG as a bytes65 (Schnorr) or bytes71, bytes72, bytes73 (ECDSA) or bytes0 (for NULLFAIL)
|
|
38
|
+
if (type === PrimitiveType.SIG) {
|
|
39
|
+
if (![0, 65, 71, 72, 73].includes(argument.byteLength)) {
|
|
40
|
+
throw new TypeError(`bytes${argument.byteLength}`, type);
|
|
41
|
+
}
|
|
42
|
+
type = new BytesType(argument.byteLength);
|
|
41
43
|
}
|
|
42
|
-
// Redefine DATASIG as a bytes64
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
// Redefine DATASIG as a bytes64 (Schnorr) or bytes70, bytes71, bytes72 (ECDSA) or bytes0 (for NULLFAIL)
|
|
45
|
+
if (type === PrimitiveType.DATASIG) {
|
|
46
|
+
if (![0, 64, 70, 71, 72].includes(argument.byteLength)) {
|
|
47
|
+
throw new TypeError(`bytes${argument.byteLength}`, type);
|
|
48
|
+
}
|
|
49
|
+
type = new BytesType(argument.byteLength);
|
|
46
50
|
}
|
|
47
51
|
// Bounded bytes types require a correctly sized argument
|
|
48
52
|
if (type instanceof BytesType && type.bound && argument.byteLength !== type.bound) {
|
package/dist/Contract.d.ts
CHANGED
|
@@ -1,36 +1,30 @@
|
|
|
1
1
|
import { Artifact, Script } from '@cashscript/utils';
|
|
2
|
-
import {
|
|
3
|
-
import { ConstructorArgument, FunctionArgument } from './Argument.js';
|
|
2
|
+
import { ConstructorArgument } from './Argument.js';
|
|
4
3
|
import { Unlocker, ContractOptions, Utxo, AddressType } from './interfaces.js';
|
|
5
4
|
import NetworkProvider from './network/NetworkProvider.js';
|
|
6
5
|
import { ParamsToTuple, AbiToFunctionMap } from './types/type-inference.js';
|
|
7
6
|
export declare class Contract<TArtifact extends Artifact = Artifact, TResolved extends {
|
|
8
7
|
constructorInputs: ConstructorArgument[];
|
|
9
|
-
functions: Record<string, any>;
|
|
10
8
|
unlock: Record<string, any>;
|
|
11
9
|
} = {
|
|
12
10
|
constructorInputs: ParamsToTuple<TArtifact['constructorInputs']>;
|
|
13
|
-
functions: AbiToFunctionMap<TArtifact['abi'], Transaction>;
|
|
14
11
|
unlock: AbiToFunctionMap<TArtifact['abi'], Unlocker>;
|
|
15
12
|
}> {
|
|
16
13
|
artifact: TArtifact;
|
|
17
|
-
private options
|
|
14
|
+
private options;
|
|
18
15
|
name: string;
|
|
19
16
|
address: string;
|
|
20
17
|
tokenAddress: string;
|
|
21
18
|
bytecode: string;
|
|
22
19
|
bytesize: number;
|
|
23
20
|
opcount: number;
|
|
24
|
-
functions: TResolved['functions'];
|
|
25
21
|
unlock: TResolved['unlock'];
|
|
26
22
|
redeemScript: Script;
|
|
27
23
|
provider: NetworkProvider;
|
|
28
24
|
addressType: AddressType;
|
|
29
25
|
encodedConstructorArgs: Uint8Array[];
|
|
30
|
-
constructor(artifact: TArtifact, constructorArgs: TResolved['constructorInputs'], options
|
|
26
|
+
constructor(artifact: TArtifact, constructorArgs: TResolved['constructorInputs'], options: ContractOptions);
|
|
31
27
|
getBalance(): Promise<bigint>;
|
|
32
28
|
getUtxos(): Promise<Utxo[]>;
|
|
33
|
-
private createFunction;
|
|
34
29
|
private createUnlocker;
|
|
35
30
|
}
|
|
36
|
-
export type ContractFunction = (...args: FunctionArgument[]) => Transaction;
|
package/dist/Contract.js
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import { binToHex } from '@bitauth/libauth';
|
|
2
2
|
import { asmToScript, calculateBytesize, countOpcodes, generateRedeemScript, hash256, scriptToBytecode, } from '@cashscript/utils';
|
|
3
|
-
import {
|
|
4
|
-
import { encodeFunctionArgument, encodeConstructorArguments, encodeFunctionArguments, } from './Argument.js';
|
|
3
|
+
import { encodeFunctionArgument, encodeConstructorArguments, } from './Argument.js';
|
|
5
4
|
import { addressToLockScript, createInputScript, createSighashPreimage, scriptToAddress, } from './utils.js';
|
|
6
5
|
import SignatureTemplate from './SignatureTemplate.js';
|
|
7
|
-
import { ElectrumNetworkProvider } from './network/index.js';
|
|
8
6
|
import semver from 'semver';
|
|
9
7
|
export class Contract {
|
|
10
8
|
constructor(artifact, constructorArgs, options) {
|
|
11
9
|
this.artifact = artifact;
|
|
12
10
|
this.options = options;
|
|
13
|
-
this.provider = this.options
|
|
14
|
-
this.addressType = this.options
|
|
11
|
+
this.provider = this.options.provider;
|
|
12
|
+
this.addressType = this.options.addressType ?? 'p2sh32';
|
|
15
13
|
const expectedProperties = ['abi', 'bytecode', 'constructorInputs', 'contractName', 'compiler'];
|
|
16
14
|
if (!expectedProperties.every((property) => property in artifact)) {
|
|
17
15
|
throw new Error('Invalid or incomplete artifact provided');
|
|
@@ -25,21 +23,7 @@ export class Contract {
|
|
|
25
23
|
// Encode arguments (this also performs type checking)
|
|
26
24
|
this.encodedConstructorArgs = encodeConstructorArguments(artifact, constructorArgs);
|
|
27
25
|
this.redeemScript = generateRedeemScript(asmToScript(this.artifact.bytecode), this.encodedConstructorArgs);
|
|
28
|
-
// Populate the
|
|
29
|
-
// (with a special case for single function, which has no "function selector")
|
|
30
|
-
this.functions = {};
|
|
31
|
-
if (artifact.abi.length === 1) {
|
|
32
|
-
const f = artifact.abi[0];
|
|
33
|
-
// @ts-ignore TODO: see if we can use generics to make TypeScript happy
|
|
34
|
-
this.functions[f.name] = this.createFunction(f);
|
|
35
|
-
}
|
|
36
|
-
else {
|
|
37
|
-
artifact.abi.forEach((f, i) => {
|
|
38
|
-
// @ts-ignore TODO: see if we can use generics to make TypeScript happy
|
|
39
|
-
this.functions[f.name] = this.createFunction(f, i);
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
// Populate the functions object with the contract's functions
|
|
26
|
+
// Populate the 'unlock' object with the contract's functions
|
|
43
27
|
// (with a special case for single function, which has no "function selector")
|
|
44
28
|
this.unlock = {};
|
|
45
29
|
if (artifact.abi.length === 1) {
|
|
@@ -67,17 +51,6 @@ export class Contract {
|
|
|
67
51
|
async getUtxos() {
|
|
68
52
|
return this.provider.getUtxos(this.address);
|
|
69
53
|
}
|
|
70
|
-
createFunction(abiFunction, selector) {
|
|
71
|
-
return (...args) => {
|
|
72
|
-
if (abiFunction.inputs.length !== args.length) {
|
|
73
|
-
throw new Error(`Incorrect number of arguments passed to function ${abiFunction.name}. Expected ${abiFunction.inputs.length} arguments (${abiFunction.inputs.map((input) => input.type)}) but got ${args.length}`);
|
|
74
|
-
}
|
|
75
|
-
// Encode passed args (this also performs type checking)
|
|
76
|
-
const encodedArgs = encodeFunctionArguments(abiFunction, args);
|
|
77
|
-
const unlocker = this.createUnlocker(abiFunction, selector)(...args);
|
|
78
|
-
return new Transaction(this, unlocker, abiFunction, encodedArgs, selector);
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
54
|
createUnlocker(abiFunction, selector) {
|
|
82
55
|
return (...args) => {
|
|
83
56
|
if (abiFunction.inputs.length !== args.length) {
|
package/dist/Errors.d.ts
CHANGED
|
@@ -19,8 +19,8 @@ export declare class NoDebugInformationInArtifactError extends Error {
|
|
|
19
19
|
}
|
|
20
20
|
export declare class FailedTransactionError extends Error {
|
|
21
21
|
reason: string;
|
|
22
|
-
bitauthUri
|
|
23
|
-
constructor(reason: string, bitauthUri
|
|
22
|
+
bitauthUri: string;
|
|
23
|
+
constructor(reason: string, bitauthUri: string);
|
|
24
24
|
}
|
|
25
25
|
export declare class FailedTransactionEvaluationError extends FailedTransactionError {
|
|
26
26
|
artifact: Artifact;
|
|
@@ -5,6 +5,7 @@ export default class SignatureTemplate {
|
|
|
5
5
|
privateKey: Uint8Array;
|
|
6
6
|
constructor(signer: Keypair | Uint8Array | string, hashtype?: HashType, signatureAlgorithm?: SignatureAlgorithm);
|
|
7
7
|
generateSignature(payload: Uint8Array, bchForkId?: boolean): Uint8Array;
|
|
8
|
+
signMessageHash(payload: Uint8Array): Uint8Array;
|
|
8
9
|
getHashType(bchForkId?: boolean): number;
|
|
9
10
|
getSignatureAlgorithm(): SignatureAlgorithm;
|
|
10
11
|
getPublicKey(): Uint8Array;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { decodePrivateKeyWif, secp256k1, SigningSerializationFlag } from '@bitauth/libauth';
|
|
1
|
+
import { decodePrivateKeyWif, hexToBin, isHex, secp256k1, SigningSerializationFlag } from '@bitauth/libauth';
|
|
2
2
|
import { hash256, scriptToBytecode } from '@cashscript/utils';
|
|
3
3
|
import { HashType, SignatureAlgorithm, } from './interfaces.js';
|
|
4
4
|
import { createSighashPreimage, publicKeyToP2PKHLockingBytecode } from './utils.js';
|
|
@@ -11,18 +11,27 @@ export default class SignatureTemplate {
|
|
|
11
11
|
this.privateKey = decodeWif(wif);
|
|
12
12
|
}
|
|
13
13
|
else if (typeof signer === 'string') {
|
|
14
|
-
|
|
14
|
+
const maybeHexString = signer.startsWith('0x') ? signer.slice(2) : signer;
|
|
15
|
+
if (isHex(maybeHexString)) {
|
|
16
|
+
this.privateKey = hexToBin(maybeHexString);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
this.privateKey = decodeWif(maybeHexString);
|
|
20
|
+
}
|
|
15
21
|
}
|
|
16
22
|
else {
|
|
17
23
|
this.privateKey = signer;
|
|
18
24
|
}
|
|
19
25
|
}
|
|
20
|
-
// TODO: Allow signing of non-transaction messages (i.e. don't add the hashtype)
|
|
21
26
|
generateSignature(payload, bchForkId) {
|
|
27
|
+
const signature = this.signMessageHash(payload);
|
|
28
|
+
return Uint8Array.from([...signature, this.getHashType(bchForkId)]);
|
|
29
|
+
}
|
|
30
|
+
signMessageHash(payload) {
|
|
22
31
|
const signature = this.signatureAlgorithm === SignatureAlgorithm.SCHNORR
|
|
23
32
|
? secp256k1.signMessageHashSchnorr(this.privateKey, payload)
|
|
24
33
|
: secp256k1.signMessageHashDER(this.privateKey, payload);
|
|
25
|
-
return
|
|
34
|
+
return signature;
|
|
26
35
|
}
|
|
27
36
|
getHashType(bchForkId = true) {
|
|
28
37
|
return bchForkId ? (this.hashtype | SigningSerializationFlag.forkId) : this.hashtype;
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import { Transaction as LibauthTransaction, WalletTemplate } from '@bitauth/libauth';
|
|
2
|
-
import { Unlocker, Output, TransactionDetails, UnlockableUtxo, Utxo, InputOptions } from './interfaces.js';
|
|
2
|
+
import { Unlocker, Output, TransactionDetails, UnlockableUtxo, Utxo, InputOptions, VmResourceUsage } from './interfaces.js';
|
|
3
3
|
import { NetworkProvider } from './network/index.js';
|
|
4
4
|
import { DebugResults } from './debugging.js';
|
|
5
5
|
import { WcTransactionOptions } from './walletconnect-utils.js';
|
|
6
6
|
import { WcTransactionObject } from './walletconnect-utils.js';
|
|
7
7
|
export interface TransactionBuilderOptions {
|
|
8
8
|
provider: NetworkProvider;
|
|
9
|
+
maximumFeeSatoshis?: bigint;
|
|
10
|
+
maximumFeeSatsPerByte?: number;
|
|
11
|
+
allowImplicitFungibleTokenBurn?: boolean;
|
|
9
12
|
}
|
|
10
13
|
export declare class TransactionBuilder {
|
|
11
14
|
provider: NetworkProvider;
|
|
12
15
|
inputs: UnlockableUtxo[];
|
|
13
16
|
outputs: Output[];
|
|
14
17
|
locktime: number;
|
|
15
|
-
|
|
18
|
+
options: TransactionBuilderOptions;
|
|
16
19
|
constructor(options: TransactionBuilderOptions);
|
|
17
20
|
addInput(utxo: Utxo, unlocker: Unlocker, options?: InputOptions): this;
|
|
18
21
|
addInputs(utxos: Utxo[], unlocker: Unlocker, options?: InputOptions): this;
|
|
@@ -21,12 +24,13 @@ export declare class TransactionBuilder {
|
|
|
21
24
|
addOutputs(outputs: Output[]): this;
|
|
22
25
|
addOpReturnOutput(chunks: string[]): this;
|
|
23
26
|
setLocktime(locktime: number): this;
|
|
24
|
-
setMaxFee(maxFee: bigint): this;
|
|
25
27
|
private checkMaxFee;
|
|
28
|
+
private checkFungibleTokenBurn;
|
|
26
29
|
buildLibauthTransaction(): LibauthTransaction;
|
|
27
30
|
build(): string;
|
|
28
31
|
debug(): DebugResults;
|
|
29
|
-
|
|
32
|
+
getVmResourceUsage(verbose?: boolean): Array<VmResourceUsage>;
|
|
33
|
+
getBitauthUri(): string;
|
|
30
34
|
getLibauthTemplate(): WalletTemplate;
|
|
31
35
|
send(): Promise<TransactionDetails>;
|
|
32
36
|
send(raw: true): Promise<string>;
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { binToHex, decodeTransaction, decodeTransactionUnsafe, encodeTransaction, hexToBin, } from '@bitauth/libauth';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import { cashScriptOutputToLibauthOutput, createOpReturnOutput, generateLibauthSourceOutputs, validateInput, validateOutput, } from './utils.js';
|
|
2
|
+
import { isUnlockableUtxo, isStandardUnlockableUtxo, isContractUnlocker, } from './interfaces.js';
|
|
3
|
+
import { cashScriptOutputToLibauthOutput, createOpReturnOutput, delay, generateLibauthSourceOutputs, validateInput, validateOutput, } from './utils.js';
|
|
5
4
|
import { FailedTransactionError } from './Errors.js';
|
|
6
|
-
import { getBitauthUri } from './LibauthTemplate.js';
|
|
7
|
-
import { debugLibauthTemplate, getLibauthTemplates } from './advanced/LibauthTemplate.js';
|
|
5
|
+
import { debugLibauthTemplate, getLibauthTemplate, getBitauthUri } from './libauth-template/LibauthTemplate.js';
|
|
8
6
|
import { getWcContractInfo } from './walletconnect-utils.js';
|
|
9
7
|
import semver from 'semver';
|
|
10
8
|
const DEFAULT_SEQUENCE = 0xfffffffe;
|
|
@@ -14,6 +12,10 @@ export class TransactionBuilder {
|
|
|
14
12
|
this.outputs = [];
|
|
15
13
|
this.locktime = 0;
|
|
16
14
|
this.provider = options.provider;
|
|
15
|
+
this.options = {
|
|
16
|
+
allowImplicitFungibleTokenBurn: options.allowImplicitFungibleTokenBurn ?? false,
|
|
17
|
+
...options,
|
|
18
|
+
};
|
|
17
19
|
}
|
|
18
20
|
addInput(utxo, unlocker, options) {
|
|
19
21
|
return this.addInputs([utxo], unlocker, options);
|
|
@@ -48,22 +50,45 @@ export class TransactionBuilder {
|
|
|
48
50
|
this.locktime = locktime;
|
|
49
51
|
return this;
|
|
50
52
|
}
|
|
51
|
-
|
|
52
|
-
this.maxFee = maxFee;
|
|
53
|
-
return this;
|
|
54
|
-
}
|
|
55
|
-
checkMaxFee() {
|
|
56
|
-
if (!this.maxFee)
|
|
57
|
-
return;
|
|
53
|
+
checkMaxFee(transaction) {
|
|
58
54
|
const totalInputAmount = this.inputs.reduce((total, input) => total + input.satoshis, 0n);
|
|
59
55
|
const totalOutputAmount = this.outputs.reduce((total, output) => total + output.amount, 0n);
|
|
60
56
|
const fee = totalInputAmount - totalOutputAmount;
|
|
61
|
-
if (fee > this.
|
|
62
|
-
throw new Error(`Transaction fee of ${fee} is higher than max fee of ${this.
|
|
57
|
+
if (this.options.maximumFeeSatoshis && fee > this.options.maximumFeeSatoshis) {
|
|
58
|
+
throw new Error(`Transaction fee of ${fee} is higher than max fee of ${this.options.maximumFeeSatoshis}`);
|
|
59
|
+
}
|
|
60
|
+
if (this.options.maximumFeeSatsPerByte) {
|
|
61
|
+
const transactionSize = encodeTransaction(transaction).byteLength;
|
|
62
|
+
const feePerByte = Number((Number(fee) / transactionSize).toFixed(2));
|
|
63
|
+
if (feePerByte > this.options.maximumFeeSatsPerByte) {
|
|
64
|
+
throw new Error(`Transaction fee per byte of ${feePerByte} is higher than max fee per byte of ${this.options.maximumFeeSatsPerByte}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
checkFungibleTokenBurn() {
|
|
69
|
+
if (this.options.allowImplicitFungibleTokenBurn)
|
|
70
|
+
return;
|
|
71
|
+
const tokenInputAmounts = {};
|
|
72
|
+
const tokenOutputAmounts = {};
|
|
73
|
+
for (const input of this.inputs) {
|
|
74
|
+
if (input.token?.amount) {
|
|
75
|
+
tokenInputAmounts[input.token.category] = (tokenInputAmounts[input.token.category] || 0n) + input.token.amount;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
for (const output of this.outputs) {
|
|
79
|
+
if (output.token?.amount) {
|
|
80
|
+
tokenOutputAmounts[output.token.category] = (tokenOutputAmounts[output.token.category] || 0n) + output.token.amount;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
for (const [category, inputAmount] of Object.entries(tokenInputAmounts)) {
|
|
84
|
+
const outputAmount = tokenOutputAmounts[category] || 0n;
|
|
85
|
+
if (outputAmount < inputAmount) {
|
|
86
|
+
throw new Error(`Implicit burning of fungible tokens for category ${category} is not allowed (input amount: ${inputAmount}, output amount: ${outputAmount}). If this is intended, set allowImplicitFungibleTokenBurn to true.`);
|
|
87
|
+
}
|
|
63
88
|
}
|
|
64
89
|
}
|
|
65
90
|
buildLibauthTransaction() {
|
|
66
|
-
this.
|
|
91
|
+
this.checkFungibleTokenBurn();
|
|
67
92
|
const inputs = this.inputs.map((utxo) => ({
|
|
68
93
|
outpointIndex: utxo.vout,
|
|
69
94
|
outpointTransactionHash: hexToBin(utxo.txid),
|
|
@@ -83,6 +108,7 @@ export class TransactionBuilder {
|
|
|
83
108
|
inputScripts.forEach((script, i) => {
|
|
84
109
|
transaction.inputs[i].unlockingBytecode = script;
|
|
85
110
|
});
|
|
111
|
+
this.checkMaxFee(transaction);
|
|
86
112
|
return transaction;
|
|
87
113
|
}
|
|
88
114
|
build() {
|
|
@@ -90,10 +116,6 @@ export class TransactionBuilder {
|
|
|
90
116
|
return binToHex(encodeTransaction(transaction));
|
|
91
117
|
}
|
|
92
118
|
debug() {
|
|
93
|
-
// do not debug a pure P2PKH-spend transaction
|
|
94
|
-
if (this.inputs.every((input) => isP2PKHUnlocker(input.unlocker))) {
|
|
95
|
-
return {};
|
|
96
|
-
}
|
|
97
119
|
if (this.inputs.some((input) => !isStandardUnlockableUtxo(input))) {
|
|
98
120
|
throw new Error('Cannot debug a transaction with custom unlocker');
|
|
99
121
|
}
|
|
@@ -106,12 +128,42 @@ export class TransactionBuilder {
|
|
|
106
128
|
}
|
|
107
129
|
return debugLibauthTemplate(this.getLibauthTemplate(), this);
|
|
108
130
|
}
|
|
109
|
-
|
|
131
|
+
getVmResourceUsage(verbose = false) {
|
|
132
|
+
// Note that only StandardUnlockableUtxo inputs are supported for debugging, so any transaction with custom unlockers
|
|
133
|
+
// cannot be debugged (and therefore cannot return VM resource usage)
|
|
134
|
+
const results = this.debug();
|
|
135
|
+
const vmResourceUsage = [];
|
|
136
|
+
const tableData = [];
|
|
137
|
+
const formatMetric = (value, total, withPercentage = false) => `${formatNumber(value)} / ${formatNumber(total)}${withPercentage ? ` (${(value / total * 100).toFixed(0)}%)` : ''}`;
|
|
138
|
+
const formatNumber = (value) => value.toLocaleString('en');
|
|
139
|
+
const resultEntries = Object.entries(results);
|
|
140
|
+
for (const [index, input] of this.inputs.entries()) {
|
|
141
|
+
const [, result] = resultEntries.find(([entryKey]) => entryKey.includes(`input${index}`)) ?? [];
|
|
142
|
+
const metrics = result?.at(-1)?.metrics;
|
|
143
|
+
// Should not happen
|
|
144
|
+
if (!metrics)
|
|
145
|
+
throw new Error('VM resource could not be calculated');
|
|
146
|
+
vmResourceUsage.push(metrics);
|
|
147
|
+
tableData.push({
|
|
148
|
+
'Contract - Function': isContractUnlocker(input.unlocker) ? `${input.unlocker.contract.name} - ${input.unlocker.abiFunction.name}` : 'P2PKH Input',
|
|
149
|
+
Ops: metrics.evaluatedInstructionCount,
|
|
150
|
+
'Op Cost Budget Usage': formatMetric(metrics.operationCost, metrics.maximumOperationCost, true),
|
|
151
|
+
SigChecks: formatMetric(metrics.signatureCheckCount, metrics.maximumSignatureCheckCount),
|
|
152
|
+
Hashes: formatMetric(metrics.hashDigestIterations, metrics.maximumHashDigestIterations),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
if (verbose) {
|
|
156
|
+
console.log('VM Resource usage by inputs:');
|
|
157
|
+
console.table(tableData);
|
|
158
|
+
}
|
|
159
|
+
return vmResourceUsage;
|
|
160
|
+
}
|
|
161
|
+
getBitauthUri() {
|
|
110
162
|
console.warn('WARNING: it is unsafe to use this Bitauth URI when using real private keys as they are included in the transaction template');
|
|
111
163
|
return getBitauthUri(this.getLibauthTemplate());
|
|
112
164
|
}
|
|
113
165
|
getLibauthTemplate() {
|
|
114
|
-
return
|
|
166
|
+
return getLibauthTemplate(this);
|
|
115
167
|
}
|
|
116
168
|
async send(raw) {
|
|
117
169
|
const tx = this.build();
|
|
@@ -126,7 +178,7 @@ export class TransactionBuilder {
|
|
|
126
178
|
}
|
|
127
179
|
catch (e) {
|
|
128
180
|
const reason = e.error ?? e.message;
|
|
129
|
-
throw new FailedTransactionError(reason);
|
|
181
|
+
throw new FailedTransactionError(reason, this.getBitauthUri());
|
|
130
182
|
}
|
|
131
183
|
}
|
|
132
184
|
async getTxDetails(txid, raw) {
|
package/dist/debugging.js
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
1
|
-
import { AuthenticationErrorCommon, binToHex, createCompiler, createVirtualMachineBch2025, decodeAuthenticationInstructions, encodeAuthenticationInstruction, walletTemplateToCompilerConfiguration } from '@bitauth/libauth';
|
|
1
|
+
import { AuthenticationErrorCommon, binToHex, createCompiler, createVirtualMachineBch2023, createVirtualMachineBch2025, createVirtualMachineBch2026, createVirtualMachineBchSpec, decodeAuthenticationInstructions, encodeAuthenticationInstruction, walletTemplateToCompilerConfiguration } from '@bitauth/libauth';
|
|
2
2
|
import { Op, PrimitiveType, asmToBytecode, bytecodeToAsm, decodeBool, decodeInt, decodeString } from '@cashscript/utils';
|
|
3
3
|
import { findLastIndex, toRegExp } from './utils.js';
|
|
4
4
|
import { FailedRequireError, FailedTransactionError, FailedTransactionEvaluationError } from './Errors.js';
|
|
5
|
-
import { getBitauthUri } from './LibauthTemplate.js';
|
|
5
|
+
import { getBitauthUri } from './libauth-template/LibauthTemplate.js';
|
|
6
|
+
/* eslint-enable @typescript-eslint/indent */
|
|
7
|
+
const createVirtualMachine = (vmTarget) => {
|
|
8
|
+
switch (vmTarget) {
|
|
9
|
+
case 'BCH_2023_05':
|
|
10
|
+
return createVirtualMachineBch2023();
|
|
11
|
+
case 'BCH_2025_05':
|
|
12
|
+
return createVirtualMachineBch2025();
|
|
13
|
+
case 'BCH_2026_05':
|
|
14
|
+
return createVirtualMachineBch2026();
|
|
15
|
+
case 'BCH_SPEC':
|
|
16
|
+
// TODO: This typecast is shitty, but it's hard to fix
|
|
17
|
+
return createVirtualMachineBchSpec();
|
|
18
|
+
default:
|
|
19
|
+
throw new Error(`Debugging is not supported for the ${vmTarget} virtual machine.`);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
6
22
|
// debugs the template, optionally logging the execution data
|
|
7
23
|
export const debugTemplate = (template, artifacts) => {
|
|
8
24
|
// If a contract has the same name, but a different bytecode, then it is considered a name collision
|
|
@@ -14,13 +30,7 @@ export const debugTemplate = (template, artifacts) => {
|
|
|
14
30
|
const unlockingScriptIds = Object.keys(template.scripts).filter((key) => 'unlocks' in template.scripts[key]);
|
|
15
31
|
for (const unlockingScriptId of unlockingScriptIds) {
|
|
16
32
|
const scenarioIds = template.scripts[unlockingScriptId].passes ?? [];
|
|
17
|
-
// There are no scenarios defined for P2PKH placeholder scripts, so we skip them
|
|
18
|
-
if (scenarioIds.length === 0)
|
|
19
|
-
continue;
|
|
20
33
|
const matchingArtifact = artifacts.find((artifact) => unlockingScriptId.startsWith(artifact.contractName));
|
|
21
|
-
if (!matchingArtifact) {
|
|
22
|
-
throw new Error(`No artifact found for unlocking script ${unlockingScriptId}`);
|
|
23
|
-
}
|
|
24
34
|
for (const scenarioId of scenarioIds) {
|
|
25
35
|
results[`${unlockingScriptId}.${scenarioId}`] = debugSingleScenario(template, matchingArtifact, unlockingScriptId, scenarioId);
|
|
26
36
|
}
|
|
@@ -38,10 +48,14 @@ const debugSingleScenario = (template, artifact, unlockingScriptId, scenarioId)
|
|
|
38
48
|
// https://libauth.org/types/AuthenticationProgramStateControlStack.html
|
|
39
49
|
const executedDebugSteps = lockingScriptDebugResult
|
|
40
50
|
.filter((debugStep) => debugStep.controlStack.every(item => item === true));
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
51
|
+
// P2PKH inputs do not have an artifact, so we skip the console.log handling
|
|
52
|
+
if (artifact) {
|
|
53
|
+
const executedLogs = (artifact.debug?.logs ?? [])
|
|
54
|
+
.filter((log) => executedDebugSteps.some((debugStep) => log.ip === debugStep.ip));
|
|
55
|
+
for (const log of executedLogs) {
|
|
56
|
+
const inputIndex = extractInputIndexFromScenario(scenarioId);
|
|
57
|
+
logConsoleLogStatement(log, executedDebugSteps, artifact, inputIndex, vm);
|
|
58
|
+
}
|
|
45
59
|
}
|
|
46
60
|
const lastExecutedDebugStep = executedDebugSteps[executedDebugSteps.length - 1];
|
|
47
61
|
// If an error is present in the last step, that means a require statement in the middle of the function failed
|
|
@@ -59,9 +73,15 @@ const debugSingleScenario = (template, artifact, unlockingScriptId, scenarioId)
|
|
|
59
73
|
// Note that we do NOT use this adjusted IP when passing the failing IP into the FailedRequireError.
|
|
60
74
|
const isNullFail = lastExecutedDebugStep.error.includes(AuthenticationErrorCommon.nonNullSignatureFailure);
|
|
61
75
|
const requireStatementIp = failingIp + (isNullFail && isSignatureCheckWithoutVerify(failingInstruction) ? 1 : 0);
|
|
76
|
+
const { program: { inputIndex }, error } = lastExecutedDebugStep;
|
|
77
|
+
// If there is no artifact, this is a P2PKH debug error, error can occur when final CHECKSIG fails with NULLFAIL or when
|
|
78
|
+
// public key does not match pkh in EQUALVERIFY
|
|
79
|
+
// Note: due to P2PKHUnlocker implementation, the CHECKSIG cannot fail in practice, only the EQUALVERIFY can fail
|
|
80
|
+
if (!artifact) {
|
|
81
|
+
throw new FailedTransactionError(error, getBitauthUri(template));
|
|
82
|
+
}
|
|
62
83
|
const requireStatement = (artifact.debug?.requires ?? [])
|
|
63
84
|
.find((statement) => statement.ip === requireStatementIp);
|
|
64
|
-
const { program: { inputIndex }, error } = lastExecutedDebugStep;
|
|
65
85
|
if (requireStatement) {
|
|
66
86
|
// Note that we use failingIp here rather than requireStatementIp, see comment above
|
|
67
87
|
throw new FailedRequireError(artifact, failingIp, requireStatement, inputIndex, getBitauthUri(template), error);
|
|
@@ -81,9 +101,14 @@ const debugSingleScenario = (template, artifact, unlockingScriptId, scenarioId)
|
|
|
81
101
|
// logDebugSteps(executedDebugSteps, lastExecutedDebugStep.instructions);
|
|
82
102
|
// console.warn('message', finalExecutedVerifyIp);
|
|
83
103
|
// console.warn(artifact.debug?.requires);
|
|
104
|
+
const { program: { inputIndex } } = lastExecutedDebugStep;
|
|
105
|
+
// If there is no artifact, this is a P2PKH debug error, final verify can only occur when final CHECKSIG failed
|
|
106
|
+
// Note: due to P2PKHUnlocker implementation, this cannot happen in practice
|
|
107
|
+
if (!artifact) {
|
|
108
|
+
throw new FailedTransactionError(evaluationResult, getBitauthUri(template));
|
|
109
|
+
}
|
|
84
110
|
const requireStatement = (artifact.debug?.requires ?? [])
|
|
85
111
|
.find((message) => message.ip === finalExecutedVerifyIp);
|
|
86
|
-
const { program: { inputIndex } } = lastExecutedDebugStep;
|
|
87
112
|
if (requireStatement) {
|
|
88
113
|
throw new FailedRequireError(artifact, sourcemapInstructionPointer, requireStatement, inputIndex, getBitauthUri(template));
|
|
89
114
|
}
|
|
@@ -91,10 +116,17 @@ const debugSingleScenario = (template, artifact, unlockingScriptId, scenarioId)
|
|
|
91
116
|
}
|
|
92
117
|
return fullDebugSteps;
|
|
93
118
|
};
|
|
119
|
+
// Note: this relies on the naming convention that the scenario ID is of the form <name>_input<index>_evaluate
|
|
120
|
+
const extractInputIndexFromScenario = (scenarioId) => {
|
|
121
|
+
const match = scenarioId.match(/_input(\d+)_/);
|
|
122
|
+
if (!match)
|
|
123
|
+
throw new Error(`Invalid scenario ID: ${scenarioId}`);
|
|
124
|
+
return parseInt(match[1]);
|
|
125
|
+
};
|
|
94
126
|
// internal util. instantiates the virtual machine and compiles the template into a program
|
|
95
127
|
const createProgram = (template, unlockingScriptId, scenarioId) => {
|
|
96
128
|
const configuration = walletTemplateToCompilerConfiguration(template);
|
|
97
|
-
const vm =
|
|
129
|
+
const vm = createVirtualMachine(template.supported[0]);
|
|
98
130
|
const compiler = createCompiler(configuration);
|
|
99
131
|
if (!template.scripts[unlockingScriptId]) {
|
|
100
132
|
throw new Error(`No unlock script found in template for ID ${unlockingScriptId}`);
|
|
@@ -115,18 +147,18 @@ const createProgram = (template, unlockingScriptId, scenarioId) => {
|
|
|
115
147
|
}
|
|
116
148
|
return { vm, program: scenarioGeneration.scenario.program };
|
|
117
149
|
};
|
|
118
|
-
const logConsoleLogStatement = (log, debugSteps, artifact) => {
|
|
150
|
+
const logConsoleLogStatement = (log, debugSteps, artifact, inputIndex, vm) => {
|
|
119
151
|
let line = `${artifact.contractName}.cash:${log.line}`;
|
|
120
152
|
const decodedData = log.data.map((element) => {
|
|
121
153
|
if (typeof element === 'string')
|
|
122
154
|
return element;
|
|
123
155
|
const debugStep = debugSteps.find((step) => step.ip === element.ip);
|
|
124
|
-
const transformedDebugStep = applyStackItemTransformations(element, debugStep);
|
|
156
|
+
const transformedDebugStep = applyStackItemTransformations(element, debugStep, vm);
|
|
125
157
|
return decodeStackItem(element, transformedDebugStep.stack);
|
|
126
158
|
});
|
|
127
|
-
console.log(
|
|
159
|
+
console.log(`[Input #${inputIndex}] ${line} ${decodedData.join(' ')}`);
|
|
128
160
|
};
|
|
129
|
-
const applyStackItemTransformations = (element, debugStep) => {
|
|
161
|
+
const applyStackItemTransformations = (element, debugStep, vm) => {
|
|
130
162
|
if (!element.transformations)
|
|
131
163
|
return debugStep;
|
|
132
164
|
const transformationsBytecode = asmToBytecode(element.transformations);
|
|
@@ -142,8 +174,9 @@ const applyStackItemTransformations = (element, debugStep) => {
|
|
|
142
174
|
instructions: transformationsAuthenticationInstructions,
|
|
143
175
|
signedMessages: [],
|
|
144
176
|
program: { ...debugStep.program },
|
|
177
|
+
functionTable: debugStep.functionTable ?? {},
|
|
178
|
+
functionCount: debugStep.functionCount ?? 0,
|
|
145
179
|
};
|
|
146
|
-
const vm = createVirtualMachineBch2025();
|
|
147
180
|
const transformationsEndState = vm.stateEvaluate(transformationsStartState);
|
|
148
181
|
return transformationsEndState;
|
|
149
182
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export { default as SignatureTemplate } from './SignatureTemplate.js';
|
|
2
|
-
export { Contract
|
|
3
|
-
export { Transaction } from './Transaction.js';
|
|
2
|
+
export { Contract } from './Contract.js';
|
|
4
3
|
export { TransactionBuilder } from './TransactionBuilder.js';
|
|
5
4
|
export { type ConstructorArgument, type FunctionArgument, type EncodedConstructorArgument, type EncodedFunctionArgument, encodeFunctionArgument, } from './Argument.js';
|
|
6
5
|
export type { Artifact, AbiFunction, AbiInput } from '@cashscript/utils';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export { default as SignatureTemplate } from './SignatureTemplate.js';
|
|
2
2
|
export { Contract } from './Contract.js';
|
|
3
|
-
export { Transaction } from './Transaction.js';
|
|
4
3
|
export { TransactionBuilder } from './TransactionBuilder.js';
|
|
5
4
|
export { encodeFunctionArgument, } from './Argument.js';
|
|
6
5
|
export * as utils from '@cashscript/utils';
|
package/dist/interfaces.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Transaction } from '@bitauth/libauth';
|
|
1
|
+
import { AuthenticationProgramStateResourceLimits, type Transaction } from '@bitauth/libauth';
|
|
2
2
|
import type { NetworkProvider } from './network/index.js';
|
|
3
3
|
import type SignatureTemplate from './SignatureTemplate.js';
|
|
4
4
|
import { Contract } from './Contract.js';
|
|
@@ -47,10 +47,6 @@ export declare function isContractUnlocker(unlocker: Unlocker): unlocker is Cont
|
|
|
47
47
|
export declare function isP2PKHUnlocker(unlocker: Unlocker): unlocker is P2PKHUnlocker;
|
|
48
48
|
export declare function isStandardUnlocker(unlocker: Unlocker): unlocker is StandardUnlocker;
|
|
49
49
|
export declare function isPlaceholderUnlocker(unlocker: Unlocker): unlocker is PlaceholderP2PKHUnlocker;
|
|
50
|
-
export interface UtxoP2PKH extends Utxo {
|
|
51
|
-
template: SignatureTemplate;
|
|
52
|
-
}
|
|
53
|
-
export declare function isUtxoP2PKH(utxo: Utxo): utxo is UtxoP2PKH;
|
|
54
50
|
export interface Recipient {
|
|
55
51
|
to: string;
|
|
56
52
|
amount: bigint;
|
|
@@ -107,12 +103,20 @@ export declare const Network: {
|
|
|
107
103
|
REGTEST: "regtest";
|
|
108
104
|
};
|
|
109
105
|
export type Network = (typeof Network)[keyof typeof Network];
|
|
106
|
+
export declare const VmTarget: {
|
|
107
|
+
BCH_2023_05: "BCH_2023_05";
|
|
108
|
+
BCH_2025_05: "BCH_2025_05";
|
|
109
|
+
BCH_2026_05: "BCH_2026_05";
|
|
110
|
+
BCH_SPEC: "BCH_SPEC";
|
|
111
|
+
};
|
|
112
|
+
export type VmTarget = (typeof VmTarget)[keyof typeof VmTarget];
|
|
110
113
|
export interface TransactionDetails extends Transaction {
|
|
111
114
|
txid: string;
|
|
112
115
|
hex: string;
|
|
113
116
|
}
|
|
114
117
|
export interface ContractOptions {
|
|
115
|
-
provider
|
|
118
|
+
provider: NetworkProvider;
|
|
116
119
|
addressType?: AddressType;
|
|
117
120
|
}
|
|
118
121
|
export type AddressType = 'p2sh20' | 'p2sh32';
|
|
122
|
+
export type VmResourceUsage = AuthenticationProgramStateResourceLimits['metrics'];
|
package/dist/interfaces.js
CHANGED
|
@@ -16,9 +16,6 @@ export function isStandardUnlocker(unlocker) {
|
|
|
16
16
|
export function isPlaceholderUnlocker(unlocker) {
|
|
17
17
|
return 'placeholder' in unlocker;
|
|
18
18
|
}
|
|
19
|
-
export function isUtxoP2PKH(utxo) {
|
|
20
|
-
return 'template' in utxo;
|
|
21
|
-
}
|
|
22
19
|
export var SignatureAlgorithm;
|
|
23
20
|
(function (SignatureAlgorithm) {
|
|
24
21
|
SignatureAlgorithm[SignatureAlgorithm["ECDSA"] = 0] = "ECDSA";
|
|
@@ -43,4 +40,10 @@ export const Network = {
|
|
|
43
40
|
MOCKNET: literal('mocknet'),
|
|
44
41
|
REGTEST: literal('regtest'),
|
|
45
42
|
};
|
|
43
|
+
export const VmTarget = {
|
|
44
|
+
BCH_2023_05: literal('BCH_2023_05'),
|
|
45
|
+
BCH_2025_05: literal('BCH_2025_05'),
|
|
46
|
+
BCH_2026_05: literal('BCH_2026_05'),
|
|
47
|
+
BCH_SPEC: literal('BCH_SPEC'),
|
|
48
|
+
};
|
|
46
49
|
//# sourceMappingURL=interfaces.js.map
|