diff --git a/.cspell.json b/.cspell.json index e7ea603c..565a20cc 100644 --- a/.cspell.json +++ b/.cspell.json @@ -23,6 +23,7 @@ "bitcore", "bitfield", "bitfield's", + "bitshift", "blackie", "blockbook", "booland", @@ -33,6 +34,7 @@ "cashc", "cashproof", "cashscript", + "cashtokens", "castable", "checkdatasig", "checkdatasigverify", @@ -45,6 +47,7 @@ "chipnet", "cleanstack", "cryptocurrency", + "collateralized", "datasig", "deserialisation", "docblock", @@ -79,6 +82,9 @@ "INPUTBYTECODE", "INPUTINDEX", "INPUTSEQUENCENUMBER", + "ints", + "introspected", + "introspecting", "kalis", "keypair", "keypairs", @@ -92,12 +98,14 @@ "linebreak", "listunspent", "locktime", + "locktimes", "lokad", "lshift", "mecenas", "meep", "minimaldata", "minimalif", + "mocknet", "n", "noncompressed", "nonschnorr", @@ -109,6 +117,7 @@ "numequal", "numequalverify", "numnotequal", + "NOTEQUAL", "op", "opcode", "opcodes", @@ -132,6 +141,7 @@ "prevouts", "priv", "pubkey", + "pubkeys", "pubkeytype", "pushbytes", "pushdata", @@ -149,21 +159,28 @@ "scripthash", "sdks", "secp", + "segwit", "setup", "sig", "sighash", "signable", "sigs", "spedn", + "stablecoins", "startup", + "standardness", "tagline", "teardown", "tendo", "timeout", + "timeops", + "timelock", + "timelocks", "toaltstack", "troutner", "tuple", "tuples", + "typeof", "txid", "TXINPUTCOUNT", "TXLOCKTIME", @@ -171,6 +188,9 @@ "txvalue", "TXVERSION", "unary", + "unlocker", + "unlockers", + "unreached", "utxo", "utxo's", "UTXOBYTECODE", @@ -185,29 +205,59 @@ "workdir" ], "ignoreWords": [ + "addinput", + "bitcats", "bitcoincashjs", + "branchup", + "bchguru", + "bchn", + "c0ffee", "cashcompiler", + "cashninjas", + "chaingraph", "cherian", + "CSCriptNum", "docu", + "fundme", + "getblockcount", + "getrawtransaction", + "hardhat", + "hodlvault", + "incl", "infima", "jedex", "lichos", + "mainnetjs", "maxdepth", + "moria", "networkprovider", "outputnulldata", "outputp", + "pako", + "paytaca", + "popd", + "pushd", + "sendrawtransaction", "setfiletype", + "setlocktime", "tada", + "tapswap", + "tokenaut", "txage", "txbytecode", "txhashoutputs", "txtime", + "vite", + "walletconnect", "withage", "withfeeperbyte", "withhardcodedfee", "withminchange", "withopreturn", - "withtime" + "withtime", + "XBVJRKV", + "zapit", + "zwets" ], "ignorePaths": [ // Do not spellcheck NPM files @@ -233,6 +283,10 @@ "**/grammar/**", // Do not spellcheck vendor code "**/node_modules/**", - "**/bitcoin-rpc-promise-retry.d.ts" + ], + "ignoreRegExpList": [ + "bitcoincash:.*", + "bchreg:.*", + "bchtest:.*" ] } diff --git a/.eslintrc.cjs b/.eslintrc.cjs index ec009b1c..84ab1988 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -15,10 +15,10 @@ module.exports = { 'jest': true, }, rules: { - 'max-len': ['error', { code: 120, ignoreStrings: true, ignoreTemplateLiterals: true }], + 'max-len': ['error', { code: 125, ignoreStrings: true, ignoreTemplateLiterals: true }], 'import/no-cycle': 0, // Needed for AST -> AstVisitor -> AST 'class-methods-use-this': 0, // I don't like this rule - 'no-underscore-dangle': 0, // antlr4ts automatically uses this + 'no-underscore-dangle': 0, // antlr automatically uses this 'no-param-reassign': 0, // Makes visitors returning the node object easier '@typescript-eslint/lines-between-class-members': [ // Makes defining interfaces / abstract classes easier 'error', @@ -46,6 +46,7 @@ module.exports = { 'max-classes-per-file': 0, // Multiple classes in one file are allowed (e.g. Errors) '@typescript-eslint/no-redeclare': 0, // I sometimes name variables an types the same 'linebreak-style': 0, // Ignore linebreak lints https://stackoverflow.com/a/43008668/1129108 - 'import/extensions': ['error', 'always'], // ESM requires file extensions + 'import/extensions': ['error', 'ignorePackages'], // ESM requires file extensions + '@typescript-eslint/only-throw-error': 'error', // We should only throw Error objects }, } diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index a536b5d2..59d0aa3e 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -22,11 +22,26 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 + - name: Setup node uses: actions/setup-node@v3 with: - node-version: 16 - - run: yarn - - run: yarn test -- -- --coverage - - run: yarn lint - - run: yarn spellcheck + node-version: 20 + + - name: Install dependencies + run: yarn + + - name: Run tests + run: TESTS_USE_MOCKNET=true yarn test -- -- --coverage --coverageProvider=v8 + + - name: Run linter + run: yarn lint + + - name: Run spellchecker + run: yarn spellcheck + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + slug: CashScript/cashscript diff --git a/.gitignore b/.gitignore index f2b6e1e1..1945b137 100644 --- a/.gitignore +++ b/.gitignore @@ -105,3 +105,5 @@ typings/ # DynamoDB Local files .dynamodb/ + +manual-test.ts diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 00000000..54e633eb --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,75 @@ +We use yarn workspaces + lerna for monorepo management. So to get started, clone this repository and run `yarn` in the root directory to install all dependencies for all packages. + +When updating code in one package, you can run `yarn build` in the root directory to build all packages so the changes get propagated to the other packages as well. If you're already in a package directory, you can run the following command to do so: + +```bash +pushd ../.. && yarn build && popd +``` + +### Publishing a release + +To publish a new release, we use `yarn update-version 'x.x.x'` in the root directory to bump the version before release, and then `yarn publish-all` in the root directory to publish the release. In case of a tagged release (such as `next`), we use `TESTS_USE_MOCKNET=true yarn publish-all --dist-tag ` to publish the release with the specified tag. + +## cashc + +### Prerequisites + +In order to use the `antlr` command line tool when updating CashScript's grammar file, you need to have it installed. + +On macOS you can install it using Homebrew: + +```bash +brew install antlr +``` + +On Linux you can install it using apt: + +```bash +sudo apt install antlr4 +``` + +For other platforms, refer to the [Antlr website](https://www.antlr.org/). + +### Updating the grammar + +When updating the grammar file in `src/grammar/CashScript.g4`, we also need to make sure that the generated parser is updated. To do this, run the following command in the `packages/cashc` directory: + +```bash +yarn antlr +``` + +### Running `cashproof` + +Most of the bytecode optimisations that the `cashc` compiler uses can be verified for correctness using the [`cashproof` tool](https://github.com/EyeOfPython/cashproof). This tool needs to be installed separately by installing its dependencies using `pip` and cloning its repository from GitHub. + +From there, you can run `python [filenames]` to verify that the optimisations contained in these files are provably correct. + +Example: +```bash +python packages/cashc/test/cashproof/0.1.2=0.2.0.equiv +``` + +Note that if you want to run `cashproof` on the "main" CashScript optimisations file, you need to first extract the optimisations from the `cashc` compiler and save them in a separate file. This can be done using the following commands: + +```bash +cp packages/utils/src/cashproof-optimisations.ts opt.equiv && sed -i '' '/`/d' opt.equiv +python opt.equiv +``` + +## cashscript + +### Running tests + +By default, running tests in the `cashscript` package uses chipnet contracts, which requires the test accounts to have some chipnet BCH. To run the tests against a local "mock network", you can use the `TESTS_USE_MOCKNET` environment variable. + +```bash +# Run all tests using the mock network +TESTS_USE_MOCKNET=true yarn test +``` + +To run specific tests, you can use the `-t` flag to match the name mentioned in the `it` or `describe` block: + +```bash +# Run all tests in the 'Transaction Builder' describe block (test/e2e/transaction-builder/TransactionBuilder.test.ts) +yarn test -t 'Transaction Builder' +``` diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 08352cb2..00000000 --- a/Dockerfile +++ /dev/null @@ -1,30 +0,0 @@ -# BUILD IMAGE -FROM node:10 as build - -WORKDIR /app - -# Invalidate cache -ADD "http://worldtimeapi.org/api/timezone/Europe/Amsterdam.txt" skipCache - -# Add app -COPY website /app -RUN yarn - -# Remove potentially cached Docusaurus files -RUN rm -rf /app/.docusaurus -RUN rm -rf /app/build - -# Generate build -RUN yarn build - -# ############################################################################### - -# PROD IMAGE -FROM nginx:1.17.0-alpine - -# Invalidate cache -ADD "http://worldtimeapi.org/api/timezone/Europe/Amsterdam.txt" skipCache - -# Copy build artifacts from the 'build environment' -RUN rm -rf /usr/share/nginx/html/** -COPY --from=build /app/build /usr/share/nginx/html diff --git a/README.md b/README.md index fb3519bb..a1aa4e24 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ # CashScript -[![Build Status](https://travis-ci.org/Bitcoin-com/cashscript.svg)](https://travis-ci.org/Bitcoin-com/cashscript) -[![Coverage Status](https://img.shields.io/codecov/c/github/Bitcoin-com/cashscript.svg)](https://codecov.io/gh/Bitcoin-com/cashscript/) +[![Build Status](https://github.com/CashScript/cashscript/actions/workflows/github-actions.yml/badge.svg)](https://github.com/CashScript/cashscript/actions/workflows/github-actions.yml) +[![Coverage Status](https://img.shields.io/codecov/c/github/CashScript/cashscript.svg)](https://codecov.io/gh/CashScript/cashscript/) [![NPM Version](https://img.shields.io/npm/v/cashscript.svg)](https://www.npmjs.com/package/cashscript) [![NPM Monthly Downloads](https://img.shields.io/npm/dm/cashscript.svg)](https://www.npmjs.com/package/cashscript) [![NPM License](https://img.shields.io/npm/l/cashscript.svg)](https://www.npmjs.com/package/cashscript) CashScript is a high-level programming language for smart contracts on Bitcoin Cash. It offers a strong abstraction layer over Bitcoin Cash' native virtual machine, Bitcoin Script. Its syntax is based on Ethereum's smart contract language Solidity, but its functionality is very different since smart contracts on Bitcoin Cash differ greatly from smart contracts on Ethereum. For a detailed comparison of them, refer to the blog post [_Smart Contracts on Ethereum, Bitcoin and Bitcoin Cash_](https://kalis.me/smart-contracts-eth-btc-bch/). -This repository contains the code for the CashScript compiler & command line tool under [`packages/cashc/`](/packages/cashc). This repository also contains the code for the CashScript JavaScript SDK under [`packages/cashscript/`](/packages/cashscript). The source code of the [CashScript.org](https://cashscript.org) website is included under [`website/`](/website). Visit the website for a detailed [Documentation](https://cashscript.org/docs/) on the CashScript language and SDK. +This repository contains the code for the CashScript compiler & command line tool under [`packages/cashc/`](/packages/cashc). This repository also contains the code for the CashScript TypeScript SDK under [`packages/cashscript/`](/packages/cashscript). The source code of the [CashScript.org](https://cashscript.org) website is included under [`website/`](/website). Visit the website for a detailed [Documentation](https://cashscript.org/docs/) on the CashScript language and SDK. ## The CashScript Language @@ -16,7 +16,7 @@ CashScript is a high-level language that allows you to write Bitcoin Cash smart ## The CashScript Compiler -CashScript features a compiler as a standalone command line tool, called `cashc`. It can be installed through npm and used to compile `.cash` files into `.json` artifact files. These artifact files can be imported into the CashScript JavaScript SDK (or other SDKs in the future). The `cashc` NPM package can also be imported inside JavaScript files to compile `.cash` files without using the command line tool. +CashScript features a compiler as a standalone command line tool, called `cashc`. It can be installed through npm and used to compile `.cash` files into `.json` (or `.ts`) artifact files. These artifact files can be imported into the CashScript TypeScript SDK (or other SDKs in the future). The `cashc` NPM package can also be imported inside JavaScript files to compile `.cash` files without using the command line tool. ### Installation @@ -30,18 +30,19 @@ npm install -g cashc Usage: cashc [options] [source_file] Options: - -V, --version Output the version number. - -o, --output Specify a file to output the generated artifact. - -h, --hex Compile the contract to hex format rather than a full artifact. - -A, --asm Compile the contract to ASM format rather than a full artifact. - -c, --opcount Display the number of opcodes in the compiled bytecode. - -s, --size Display the size in bytes of the compiled bytecode. - -?, --help Display help + -V, --version Output the version number. + -o, --output Specify a file to output the generated artifact. + -h, --hex Compile the contract to hex format rather than a full artifact. + -A, --asm Compile the contract to ASM format rather than a full artifact. + -c, --opcount Display the number of opcodes in the compiled bytecode. + -s, --size Display the size in bytes of the compiled bytecode. + -f, --format Specify the format of the output. (choices: "json", "ts", default: "json") + -?, --help Display help ``` ## The CashScript SDK -The main way to interact with CashScript contracts and integrate them into applications is using the CashScript SDK. This SDK allows you to import `.json` artifact files that were compiled using the `cashc` compiler and convert them to `Contract` objects. These objects are used to create new contract instances. These instances are used to interact with the contracts using the functions that were implemented in the `.cash` file. For more information on the CashScript SDK, refer to the [SDK documentation](https://cashscript.org/docs/sdk/). +The main way to interact with CashScript contracts and integrate them into applications is using the CashScript SDK. This SDK allows you to import `.json` (or `.ts`) artifact files that were compiled using the `cashc` compiler and convert them to `Contract` objects. These objects are used to create new contract instances. These instances are used to interact with the contracts using the functions that were implemented in the `.cash` file. For more information on the CashScript SDK, refer to the [SDK documentation](https://cashscript.org/docs/sdk/). ### Installation @@ -60,27 +61,36 @@ Using the CashScript SDK, you can import contract artifact files, create new ins ```ts ... // Import the P2PKH artifact - import P2PKH from './p2pkh-artifact.json' assert { type: 'json' }; + import P2PKH from './p2pkh-artifact.json' with { type: 'json' }; // Instantiate a network provider for CashScript's network operations - const provider = new ElectrumNetworkProvider('mainnet'); + const provider = new ElectrumNetworkProvider('chipnet'); // Create a new P2PKH contract with constructor arguments: { pkh: pkh } - const contract = new Contract(P2PKH, [pkh], provider); + const contract = new Contract(P2PKH, [pkh], {provider}); - // Get contract balance & output address + balance + // Fetch contract utxos + const contractUtxos = await contract.getUtxos(); + + // Log contract output address + contract utxos console.log('contract address:', contract.address); - console.log('contract balance:', await contract.getBalance()); + console.log('contract utxos', contractUtxos); + + // Specify the contract UTXO + const selectedContractUtxo = contractUtxos[0] + // Start building the transaction // Call the spend function with the owner's signature // And use it to send 0. 000 100 00 BCH back to the contract's address - const txDetails = await contract.functions - .spend(pk, new SignatureTemplate(keypair)) - .to(contract.address, 10000) + const txDetails = await new TransactionBuilder({ provider }) + .addInput(selectedContractUtxo, contract.unlock.spend(new SignatureTemplate(keypair))) + .addOutput({ + to: contract.address, + amount: 10000n + }) .send(); console.log(txDetails); -... ``` ## Examples @@ -94,16 +104,16 @@ The "Hello World" of CashScript contracts is defining the P2PKH pattern inside a To run the examples, clone this repository and navigate to the `examples/` directory. Since the examples depend on the SDK, be sure to run `npm install` or `yarn` inside the `examples/` directory, which installs all required packages. ```bash -git clone git@github.com:Bitcoin-com/cashscript.git +git clone git@github.com:CashScript/cashscript.git cd cashscript/examples npm install ``` -All `.ts` files in the [`examples/`](/examples) directory can then be executed with `ts-node-esm`. +All `.ts` files in the [`examples/`](/examples) directory can then be executed with `tsx`. ```bash -npm install -g ts-node -ts-node-esm p2pkh.ts +npm install -g tsx +tsx p2pkh.ts ``` All `.js` files can be executed with `node`. diff --git a/examples/README.md b/examples/README.md index 242c6a79..803a8015 100644 --- a/examples/README.md +++ b/examples/README.md @@ -12,11 +12,11 @@ cd cashscript/examples yarn ``` -All `.ts` files can then be executed with `ts-node-esm`. +All `.ts` files can then be executed with `tsx`. ```bash -npm install -g ts-node -ts-node-esm p2pkh.ts +npm install -g tsx +tsx p2pkh.ts ``` All `.js` files can be executed with `node`. diff --git a/examples/announcement.cash b/examples/announcement.cash index e10ccea4..a678e80a 100644 --- a/examples/announcement.cash +++ b/examples/announcement.cash @@ -1,4 +1,4 @@ -pragma cashscript ^0.9.0; +pragma cashscript ^0.11.0; /* This is a contract showcasing covenants outside of regular transactional use. * It enforces the contract to make an "announcement" on Memo.cash, and send the diff --git a/examples/announcement.ts b/examples/announcement.ts index dac7fa1c..a7710c03 100644 --- a/examples/announcement.ts +++ b/examples/announcement.ts @@ -1,4 +1,4 @@ -import { Contract, ElectrumNetworkProvider } from 'cashscript'; +import { Contract, ElectrumNetworkProvider, Output, TransactionBuilder } from 'cashscript'; import { compileFile } from 'cashc'; import { stringify } from '@bitauth/libauth'; import { URL } from 'url'; @@ -16,20 +16,36 @@ const contract = new Contract(artifact, [], { provider, addressType }); // Get contract balance & output address + balance console.log('contract address:', contract.address); +const contractUtxos = await contract.getUtxos(); +console.log('contract utxos:', contractUtxos); console.log('contract balance:', await contract.getBalance()); console.log('contract opcount:', contract.opcount); console.log('contract bytesize:', contract.bytesize); -// Send the announcement. Trying to send any other announcement will fail because -// the contract's covenant logic. Uses a hardcoded fee and minChange so that -// change is only sent back to the contract if there's enough leftover -// for another announcement. +// Select a contract UTXO to spend from +const contractInputUtxo = contractUtxos.find(utxo => utxo.satoshis > 10_000n); +if (!contractInputUtxo) throw new Error('No contract UTXO with enough satoshis found'); +const inputAmount = contractInputUtxo.satoshis; + +// Announcement string const str = 'A contract may not injure a human being or, through inaction, allow a human being to come to harm.'; -const tx = await contract.functions - .announce() - .withOpReturn(['0x6d02', str]) - .withHardcodedFee(1000n) - .withMinChange(1000n) - .send(); + +// Construct a changeOutput so the leftover BCH can be send back for another announcement +const minerFee = 1000n; +const changeAmount = inputAmount - minerFee; +const contractChangeOutput: Output = { + amount: changeAmount, + to: contract.address, +}; + +// Send the announcement. Trying to send any other announcement or other change output +// will fail because of the contract's covenant logic +const transactionBuilder = new TransactionBuilder({ provider }); + +transactionBuilder.addInput(contractInputUtxo, contract.unlock.announce()); +transactionBuilder.addOpReturnOutput(['0x6d02', str]); +if (changeAmount > 1000n) transactionBuilder.addOutput(contractChangeOutput); + +const tx = await transactionBuilder.send(); console.log('transaction details:', stringify(tx)); diff --git a/examples/common-js.js b/examples/common-js.js index 7539c2c1..a7913f1b 100644 --- a/examples/common-js.js +++ b/examples/common-js.js @@ -4,14 +4,14 @@ import { deriveHdPath, secp256k1, encodeCashAddress, + deriveSeedFromBip39Mnemonic, } from '@bitauth/libauth'; -import bip39 from 'bip39'; // This is duplicated from common.ts because it is not possible to import from a .ts file in p2pkh.js // Generate entropy from BIP39 mnemonic phrase and initialise a root HD-wallet node -const seed = await bip39.mnemonicToSeed('CashScript Examples'); -const rootNode = deriveHdPrivateNodeFromSeed(seed, true); +const seed = deriveSeedFromBip39Mnemonic('CashScript Examples'); +const rootNode = deriveHdPrivateNodeFromSeed(seed, { assumeValidity: true, throwErrors: true }); const baseDerivationPath = "m/44'/145'/0'/0"; // Derive Alice's private key, public key, public key hash and address @@ -20,4 +20,5 @@ if (typeof aliceNode === 'string') throw new Error(); export const alicePub = secp256k1.derivePublicKeyCompressed(aliceNode.privateKey); export const alicePriv = aliceNode.privateKey; export const alicePkh = hash160(alicePub); -export const aliceAddress = encodeCashAddress('bchtest', 'p2pkh', alicePkh); +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; diff --git a/examples/common.ts b/examples/common.ts index f1559a40..c8b43231 100644 --- a/examples/common.ts +++ b/examples/common.ts @@ -4,13 +4,13 @@ import { deriveHdPath, secp256k1, encodeCashAddress, + deriveSeedFromBip39Mnemonic, } from '@bitauth/libauth'; -import bip39 from 'bip39'; import { PriceOracle } from './PriceOracle.js'; // Generate entropy from BIP39 mnemonic phrase and initialise a root HD-wallet node -const seed = await bip39.mnemonicToSeed('CashScript Examples'); -const rootNode = deriveHdPrivateNodeFromSeed(seed, true); +const seed = deriveSeedFromBip39Mnemonic('CashScript Examples'); +const rootNode = deriveHdPrivateNodeFromSeed(seed, { assumeValidity: true, throwErrors: true }); const baseDerivationPath = "m/44'/145'/0'/0"; // Derive Alice's private key, public key, public key hash and address @@ -19,7 +19,7 @@ if (typeof aliceNode === 'string') throw new Error(); export const alicePub = secp256k1.derivePublicKeyCompressed(aliceNode.privateKey) as Uint8Array; export const alicePriv = aliceNode.privateKey; export const alicePkh = hash160(alicePub); -export const aliceAddress = encodeCashAddress('bchtest', 'p2pkhWithTokens', alicePkh); +export const aliceAddress = encodeCashAddress({ prefix: 'bchtest', type: 'p2pkhWithTokens', payload: alicePkh, throwErrors: true }).address; // Derive Bob's private key, public key, public key hash and address const bobNode = deriveHdPath(rootNode, `${baseDerivationPath}/1`); @@ -27,7 +27,7 @@ if (typeof bobNode === 'string') throw new Error(); export const bobPub = secp256k1.derivePublicKeyCompressed(bobNode.privateKey) as Uint8Array; export const bobPriv = bobNode.privateKey; export const bobPkh = hash160(bobPub); -export const bobAddress = encodeCashAddress('bchtest', 'p2pkhWithTokens', bobPkh); +export const bobAddress = encodeCashAddress({ prefix: 'bchtest', type: 'p2pkhWithTokens', payload: bobPkh, throwErrors: true }).address; // Initialise a price oracle with a private key const oracleNode = deriveHdPath(rootNode, `${baseDerivationPath}/2`); @@ -36,4 +36,4 @@ export const oraclePub = secp256k1.derivePublicKeyCompressed(oracleNode.privateK export const oraclePriv = oracleNode.privateKey; export const oracle = new PriceOracle(oracleNode.privateKey); export const oraclePkh = hash160(oraclePub); -export const oracleAddress = encodeCashAddress('bchtest', 'p2pkhWithTokens', oraclePkh); +export const oracleAddress = encodeCashAddress({ prefix: 'bchtest', type: 'p2pkhWithTokens', payload: oraclePkh, throwErrors: true }).address; diff --git a/examples/hodl_vault.cash b/examples/hodl_vault.cash index a687a4c2..1af8c96b 100644 --- a/examples/hodl_vault.cash +++ b/examples/hodl_vault.cash @@ -1,4 +1,4 @@ -pragma cashscript ^0.9.0; +pragma cashscript ^0.11.0; // This contract forces HODLing until a certain price target has been reached // A minimum block is provided to ensure that oracle price entries from before this block are disregarded diff --git a/examples/hodl_vault.ts b/examples/hodl_vault.ts index ff29721c..a151451d 100644 --- a/examples/hodl_vault.ts +++ b/examples/hodl_vault.ts @@ -1,5 +1,5 @@ import { stringify } from '@bitauth/libauth'; -import { Contract, SignatureTemplate, ElectrumNetworkProvider } from 'cashscript'; +import { Contract, SignatureTemplate, ElectrumNetworkProvider, TransactionBuilder, Output } from 'cashscript'; import { compileFile } from 'cashc'; import { URL } from 'url'; @@ -24,16 +24,44 @@ const contract = new Contract(artifact, parameters, { provider }); // Get contract balance & output address + balance console.log('contract address:', contract.address); +const contractUtxos = await contract.getUtxos(); +console.log('contract utxos:', contractUtxos); console.log('contract balance:', await contract.getBalance()); +const currentBlockHeight = await provider.getBlockHeight(); +console.log('current block height:', currentBlockHeight); + +// Select a contract UTXO to spend from +const contractInputUtxo = contractUtxos.find(utxo => utxo.satoshis > 10_000n); +if (!contractInputUtxo) throw new Error('No contract UTXO with enough satoshis found'); +const inputAmount = contractInputUtxo.satoshis; + // Produce new oracle message and signature const oracleMessage = oracle.createMessage(100000n, 30000n); const oracleSignature = oracle.signMessage(oracleMessage); +// construct an output +const outputAmount = 1000n; +const output: Output = { amount: outputAmount, to: contract.address }; + +// Construct a changeOutput +const minerFee = 1000n; +const changeAmount = inputAmount - outputAmount - minerFee; +const changeOutput: Output = { + amount: changeAmount, + to: contract.address, +}; + +const aliceTemplate = new SignatureTemplate(alicePriv); + // Spend from the vault -const tx = await contract.functions - .spend(new SignatureTemplate(alicePriv), oracleSignature, oracleMessage) - .to(contract.address, 1000n) - .send(); +const transactionBuilder = new TransactionBuilder({ provider }); + +transactionBuilder.setLocktime(currentBlockHeight); +transactionBuilder.addInput(contractInputUtxo, contract.unlock.spend(aliceTemplate, oracleSignature, oracleMessage)); +transactionBuilder.addOutput(output); +if (changeAmount > 1000n) transactionBuilder.addOutput(changeOutput); + +const tx = await transactionBuilder.send(); console.log(stringify(tx)); diff --git a/examples/mecenas.cash b/examples/mecenas.cash index 82e1a274..341a523d 100644 --- a/examples/mecenas.cash +++ b/examples/mecenas.cash @@ -1,4 +1,4 @@ -pragma cashscript ^0.9.0; +pragma cashscript ^0.11.0; /* This is an unofficial CashScript port of Licho's Mecenas contract. It is * not compatible with Licho's EC plugin, but rather meant as a demonstration @@ -7,7 +7,7 @@ pragma cashscript ^0.9.0; */ contract Mecenas(bytes20 recipient, bytes20 funder, int pledge/*, int period */) { function receive() { - // require(tx.age >= period); + // require(this.age >= period); // Check that the first output sends to the recipient require(tx.outputs[0].lockingBytecode == new LockingBytecodeP2PKH(recipient)); diff --git a/examples/mecenas.ts b/examples/mecenas.ts index 650a754e..cf28c126 100644 --- a/examples/mecenas.ts +++ b/examples/mecenas.ts @@ -1,5 +1,5 @@ import { stringify } from '@bitauth/libauth'; -import { Contract, ElectrumNetworkProvider } from 'cashscript'; +import { Contract, ElectrumNetworkProvider, Output, TransactionBuilder } from 'cashscript'; import { compileFile } from 'cashc'; import { URL } from 'url'; @@ -15,21 +15,44 @@ const provider = new ElectrumNetworkProvider('chipnet'); // Instantiate a new contract using the compiled artifact and network provider // AND providing the constructor parameters: // (recipient: alicePkh, funder: bobPkh, pledge: 10000) -const contract = new Contract(artifact, [alicePkh, bobPkh, 10000n], { provider }); +const pledgeAmount = 10_000n; +const contract = new Contract(artifact, [alicePkh, bobPkh, pledgeAmount], { provider }); // Get contract balance & output address + balance console.log('contract address:', contract.address); +const contractUtxos = await contract.getUtxos(); +console.log('contract utxos:', contractUtxos); console.log('contract balance:', await contract.getBalance()); console.log('contract opcount:', contract.opcount); console.log('contract bytesize:', contract.bytesize); +// Select a contract UTXO to spend from +const contractInputUtxo = contractUtxos.find(utxo => utxo.satoshis > 10_000n); +if (!contractInputUtxo) throw new Error('No contract UTXO with enough satoshis found'); +const inputAmount = contractInputUtxo.satoshis; + +const receiverOutput: Output = { + amount: 10_000n, + to: aliceAddress, +}; + +// Construct the changeOutput +const minerFee = 1000n; +const changeAmount = inputAmount - pledgeAmount - minerFee; +const contractChangeOutput: Output = { + amount: changeAmount, + to: contract.address, +}; + // Call the transfer function with any signature // Will send one pledge amount to alice, and send change back to the contract // Manually set fee to 1000 because this is hardcoded in the contract -const tx = await contract.functions - .receive() - .to(aliceAddress, 10000n) - .withHardcodedFee(1000n) - .send(); +const transactionBuilder = new TransactionBuilder({ provider }); + +transactionBuilder.addInput(contractInputUtxo, contract.unlock.receive()); +transactionBuilder.addOutput(receiverOutput); +if (changeAmount > pledgeAmount + minerFee) transactionBuilder.addOutput(contractChangeOutput); + +const tx = await transactionBuilder.send(); console.log('transaction details:', stringify(tx)); diff --git a/examples/mecenas_locktime.cash b/examples/mecenas_locktime.cash index 471f9173..cb0e56ff 100644 --- a/examples/mecenas_locktime.cash +++ b/examples/mecenas_locktime.cash @@ -1,4 +1,4 @@ -pragma cashscript ^0.9.0; +pragma cashscript ^0.11.0; // This is an experimental contract for a more "streaming" Mecenas experience // Completely untested, just a concept diff --git a/examples/p2pkh.cash b/examples/p2pkh.cash index 02e6ad68..6cc64059 100644 --- a/examples/p2pkh.cash +++ b/examples/p2pkh.cash @@ -1,4 +1,4 @@ -pragma cashscript ^0.9.0; +pragma cashscript ^0.11.0; contract P2PKH(bytes20 pkh) { // Require pk to match stored pkh and signature to match diff --git a/examples/p2pkh.js b/examples/p2pkh.js index 7fe516e2..b10dc6f8 100644 --- a/examples/p2pkh.js +++ b/examples/p2pkh.js @@ -1,10 +1,10 @@ import { URL } from 'url'; import { compileFile } from 'cashc'; -import { ElectrumNetworkProvider, Contract, SignatureTemplate } from 'cashscript'; +import { ElectrumNetworkProvider, Contract, SignatureTemplate, TransactionBuilder } from 'cashscript'; import { stringify } from '@bitauth/libauth'; -// Import Alice's keys from common-js.js -import { alicePkh, alicePriv, alicePub } from './common-js.js'; +// Import Alice's keys from common.ts +import { alicePkh, alicePriv, aliceAddress, alicePub } from './common.js'; // Compile the P2PKH contract to an artifact object const artifact = compileFile(new URL('p2pkh.cash', import.meta.url)); @@ -18,23 +18,38 @@ const contract = new Contract(artifact, [alicePkh], { provider }); // Get contract balance & output address + balance console.log('contract address:', contract.address); +const contractUtxos = await contract.getUtxos(); +console.log('contract utxos:', contractUtxos); console.log('contract balance:', await contract.getBalance()); -// Call the spend function with alice's signature + pk -// And use it to send 0. 000 100 00 BCH back to the contract's address -const tx = await contract.functions - .spend(alicePub, new SignatureTemplate(alicePriv)) - .to(contract.address, 10000n) - .send(); +// Select a contract UTXO to spend from +const contractInputUtxo = contractUtxos.find(utxo => utxo.satoshis > 12_000n); +if (!contractInputUtxo) throw new Error('No contract UTXO with enough satoshis found'); +const inputAmount = contractInputUtxo.satoshis; -console.log('transaction details:', stringify(tx)); +// construct an output +const outputAmount = 10_000n; +const output = { amount: outputAmount, to: aliceAddress }; -// Call the spend function with alice's signature + pk -// And use it to send two outputs of 0. 000 150 00 BCH back to the contract's address -const tx2 = await contract.functions - .spend(alicePub, new SignatureTemplate(alicePriv)) - .to(contract.address, 15000n) - .to(contract.address, 15000n) - .send(); +// Construct a changeOutput +const minerFee = 1000n; +const changeAmount = inputAmount - outputAmount - minerFee; +const changeOutput = { + amount: changeAmount, + to: contract.address, +}; -console.log('transaction details:', stringify(tx2)); +const aliceTemplate = new SignatureTemplate(alicePriv); + +// Call the spend() function with alice's signature + pk +// And use it to send 0. 000 100 00 BCH to aliceAddress and the remainder +// back to the contract's address +const transactionBuilder = new TransactionBuilder({ provider }); + +transactionBuilder.addInput(contractInputUtxo, contract.unlock.spend(alicePub, aliceTemplate)); +transactionBuilder.addOutput(output); +if (changeAmount > 1000n) transactionBuilder.addOutput(changeOutput); + +const tx = await transactionBuilder.send(); + +console.log('transaction details:', stringify(tx)); \ No newline at end of file diff --git a/examples/p2pkh.ts b/examples/p2pkh.ts index 54e10a3d..867cb694 100644 --- a/examples/p2pkh.ts +++ b/examples/p2pkh.ts @@ -1,10 +1,10 @@ import { stringify } from '@bitauth/libauth'; import { compileFile } from 'cashc'; -import { ElectrumNetworkProvider, SignatureTemplate, Contract } from 'cashscript'; +import { ElectrumNetworkProvider, SignatureTemplate, Contract, TransactionBuilder, Output } from 'cashscript'; import { URL } from 'url'; // Import Alice's keys from common.ts -import { alicePkh, alicePriv, alicePub } from './common.js'; +import { alicePkh, alicePriv, aliceAddress, alicePub } from './common.js'; // Compile the P2PKH contract to an artifact object const artifact = compileFile(new URL('p2pkh.cash', import.meta.url)); @@ -18,23 +18,38 @@ const contract = new Contract(artifact, [alicePkh], { provider }); // Get contract balance & output address + balance console.log('contract address:', contract.address); +const contractUtxos = await contract.getUtxos(); +console.log('contract utxos:', contractUtxos); console.log('contract balance:', await contract.getBalance()); -// Call the spend() function with alice's signature + pk -// And use it to send 0. 000 100 00 BCH back to the contract's address -const tx = await contract.functions - .spend(alicePub, new SignatureTemplate(alicePriv)) - .to(contract.address, 10000n) - .send(); +// Select a contract UTXO to spend from +const contractInputUtxo = contractUtxos.find(utxo => utxo.satoshis > 12_000n); +if (!contractInputUtxo) throw new Error('No contract UTXO with enough satoshis found'); +const inputAmount = contractInputUtxo.satoshis; -console.log('transaction details:', stringify(tx)); +// construct an output +const outputAmount = 10_000n; +const output: Output = { amount: outputAmount, to: aliceAddress }; + +// Construct a changeOutput +const minerFee = 1000n; +const changeAmount = inputAmount - outputAmount - minerFee; +const changeOutput: Output = { + amount: changeAmount, + to: contract.address, +}; + +const aliceTemplate = new SignatureTemplate(alicePriv); // Call the spend() function with alice's signature + pk -// And use it to send two outputs of 0. 000 150 00 BCH back to the contract's address -const tx2 = await contract.functions - .spend(alicePub, new SignatureTemplate(alicePriv)) - .to(contract.address, 15000n) - .to(contract.address, 15000n) - .send(); - -console.log('transaction details:', stringify(tx2)); +// And use it to send 0. 000 100 00 BCH to aliceAddress and the remainder +// back to the contract's address +const transactionBuilder = new TransactionBuilder({ provider }); + +transactionBuilder.addInput(contractInputUtxo, contract.unlock.spend(alicePub, aliceTemplate)); +transactionBuilder.addOutput(output); +if (changeAmount > 1000n) transactionBuilder.addOutput(changeOutput); + +const tx = await transactionBuilder.send(); + +console.log('transaction details:', stringify(tx)); diff --git a/examples/package.json b/examples/package.json index e839702d..4562e047 100644 --- a/examples/package.json +++ b/examples/package.json @@ -1,7 +1,7 @@ { "name": "cashscript-examples", "private": true, - "version": "0.10.0-next.0", + "version": "0.11.0", "description": "Usage examples of the CashScript SDK", "main": "p2pkh.js", "type": "module", @@ -11,12 +11,11 @@ "lint": "eslint . --ext .ts --ignore-path ../.eslintignore" }, "dependencies": { - "@bitauth/libauth": "^2.0.0-alpha.8", - "@types/node": "^12.7.8", - "bip39": "^3.0.4", - "cashc": "^0.10.0-next.0", - "cashscript": "^0.10.0-next.0", + "@bitauth/libauth": "^3.1.0-next.2", + "@types/node": "^22.10.7", + "cashc": "^0.11.0", + "cashscript": "^0.11.0", "eslint": "^8.56.0", - "typescript": "^4.9.5" + "typescript": "^5.7.3" } } diff --git a/examples/testing-suite/artifacts/example.artifact.ts b/examples/testing-suite/artifacts/example.artifact.ts new file mode 100644 index 00000000..6670b71d --- /dev/null +++ b/examples/testing-suite/artifacts/example.artifact.ts @@ -0,0 +1,47 @@ +export default { + contractName: 'Example', + constructorInputs: [], + abi: [ + { + name: 'test', + inputs: [ + { + name: 'value', + type: 'int', + }, + ], + }, + ], + bytecode: 'OP_1 OP_NUMEQUAL', + source: 'contract Example() {\n function test(int value) {\n console.log(value, \'test\');\n require(value == 1, \'Wrong value passed\');\n }\n}\n', + debug: { + bytecode: '007a519c', + sourceMap: '4:12:4:17;;:21::22;:12:::1', + logs: [ + { + ip: 0, + line: 3, + data: [ + { + stackIndex: 0, + type: 'int', + ip: 0, + }, + 'test', + ], + }, + ], + requires: [ + { + ip: 4, + line: 4, + message: 'Wrong value passed', + }, + ], + }, + compiler: { + name: 'cashc', + version: '0.11.0', + }, + updatedAt: '2025-04-11T09:08:09.750Z', +} as const; diff --git a/examples/testing-suite/artifacts/example.json b/examples/testing-suite/artifacts/example.json index 50b4db3d..7104e362 100644 --- a/examples/testing-suite/artifacts/example.json +++ b/examples/testing-suite/artifacts/example.json @@ -24,13 +24,14 @@ "data": [ { "stackIndex": 0, - "type": "int" + "type": "int", + "ip": 0 }, "test" ] } ], - "requireMessages": [ + "requires": [ { "ip": 4, "line": 4, @@ -40,7 +41,7 @@ }, "compiler": { "name": "cashc", - "version": "0.10.0-next.0" + "version": "0.10.0" }, - "updatedAt": "2023-12-13T15:04:57.625Z" + "updatedAt": "2024-09-10T09:55:42.448Z" } \ No newline at end of file diff --git a/examples/testing-suite/package.json b/examples/testing-suite/package.json index bff4660e..089e7587 100644 --- a/examples/testing-suite/package.json +++ b/examples/testing-suite/package.json @@ -1,6 +1,6 @@ { "name": "testing-suite", - "version": "0.10.0", + "version": "0.11.0", "description": "Example project to develop and test CashScript contracts", "main": "index.js", "type": "module", @@ -16,8 +16,8 @@ "build:test": "yarn clean:test && yarn compile:test", "clean": "rm -rf ./dist", "clean:test": "rm -rf ./dist-test", - "compile": "tsc -p tsconfig.json && ts-node-esm tasks/index.ts compile", - "compile:test": "tsc -p tsconfig.test.json && ts-node-esm tasks/index.ts compile", + "compile": "tsc -p tsconfig.json && tsx tasks/index.ts compile", + "compile:test": "tsc -p tsconfig.test.json && tsx tasks/index.ts compile", "lint": "eslint . --ext .ts --ignore-path ../../.eslintignore", "prepare": "yarn build", "prepublishOnly": "yarn test && yarn lint", @@ -25,14 +25,16 @@ "test": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest" }, "dependencies": { - "cashc": "^0.10.0", - "cashscript": "^0.10.0" + "@bitauth/libauth": "^3.1.0-next.2", + "cashc": "^0.11.0", + "cashscript": "^0.11.0", + "url-join": "^5.0.0" }, "devDependencies": { - "@jest/globals": "^29.4.1", - "@types/jest": "^29.4.1", - "jest": "^29.4.1", - "ts-node": "^10.9.1", - "typescript": "^4.9.5" + "@jest/globals": "^29.7.0", + "@types/jest": "^29.5.14", + "jest": "^29.7.0", + "tsx": "^4.19.2", + "typescript": "^5.7.3" } } diff --git a/examples/testing-suite/test/example.test.ts b/examples/testing-suite/test/example.test.ts index 1ab83f28..866e2a9f 100644 --- a/examples/testing-suite/test/example.test.ts +++ b/examples/testing-suite/test/example.test.ts @@ -1,18 +1,28 @@ -import artifact from '../artifacts/example.json' assert { type: 'json' }; -import { Contract, MockNetworkProvider, randomUtxo } from 'cashscript'; -import 'cashscript/dist/test/JestExtensions.js'; +import artifact from '../artifacts/example.artifact.js'; +import { Contract, MockNetworkProvider, TransactionBuilder, randomUtxo } from 'cashscript'; +import 'cashscript/jest'; describe('test example contract functions', () => { it('should check for output logs and error messages', async () => { const provider = new MockNetworkProvider(); const contract = new Contract(artifact, [], { provider }); - provider.addUtxo(contract.address, randomUtxo()); - let transaction = contract.functions.test(0n).to(contract.address, 10000n); - await (expect(transaction)).toLog(/0 test/); - await (expect(transaction)).toFailRequireWith(/Wrong value passed/); + // Create a contract Utxo + const contractUtxo = randomUtxo(); + provider.addUtxo(contract.address, contractUtxo); - transaction = contract.functions.test(1n).to(contract.address, 10000n); - await expect(transaction.send()).resolves.not.toThrow(); + const transactionWrongValuePassed = new TransactionBuilder({ provider }) + .addInput(contractUtxo, contract.unlock.test(0n)) + .addOutput({ to: contract.address, amount: 10000n }); + + expect(transactionWrongValuePassed).toLog(/0 test/); + expect(transactionWrongValuePassed).toFailRequireWith(/Wrong value passed/); + + const transactionRightValuePassed = new TransactionBuilder({ provider }) + .addInput(contractUtxo, contract.unlock.test(1n)) + .addOutput({ to: contract.address, amount: 10000n }) + .send(); + + await expect(transactionRightValuePassed).resolves.not.toThrow(); }); }); diff --git a/examples/testing-suite/yarn.lock b/examples/testing-suite/yarn.lock index d43fdd4a..c731accf 100644 --- a/examples/testing-suite/yarn.lock +++ b/examples/testing-suite/yarn.lock @@ -297,25 +297,133 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@bitauth/libauth@^2.0.0-alpha.8": - version "2.0.0-alpha.8" - resolved "https://registry.yarnpkg.com/@bitauth/libauth/-/libauth-2.0.0-alpha.8.tgz#352ade2075517f1548e05553e7bb1c5e1023d1fe" - integrity sha512-KINAJbzg5pKs+WM8wCjWw6Ct/CeZG1JMrRXsIyPFztjNtwA5eeQL8eFkyquePsf8aZ5peRYK2eFPUDo/1rkg+w== +"@bitauth/libauth@^2.0.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@bitauth/libauth/-/libauth-2.1.0.tgz#e618706092fe9ea5e4103a2cf427cd5ae5287e1e" + integrity sha512-O0cIN7/UPUHg6gaWHuePXIYRkvNPrc3zHfi80guZkkO4Ke9nSaN5mSITqhC5lKFBwAakr6OSToClXHN/IxiOfA== -"@cashscript/utils@^0.10.0-next.0": - version "0.10.0-next.0" - resolved "https://registry.yarnpkg.com/@cashscript/utils/-/utils-0.10.0-next.0.tgz#d8a47c2e9b18840678e62c58c3190afb86794f9a" - integrity sha512-cqTniDARlZqqThMTd33LtwSFhRdVe15tTq1K4Zs0gYFgBcT1l40B3P57szN+2gXsffGmN77I33IXv+wPoObcDg== +"@cashscript/utils@^0.10.0-next.3": + version "0.10.0-next.3" + resolved "https://registry.yarnpkg.com/@cashscript/utils/-/utils-0.10.0-next.3.tgz#53bcdfae252734696c8158d4f479c8c60e2a0f7f" + integrity sha512-eS16y0OyZ0m2OitsvsKwPX+fwI8Lc4imJioM+hRjedSZ0ioqO+6qZUJoeLAkdpCvEzkRo2I68vuRCbelH0wQMQ== dependencies: - "@bitauth/libauth" "^2.0.0-alpha.8" + "@bitauth/libauth" "^2.0.0" hash.js "^1.1.7" -"@cspotcode/source-map-support@^0.8.0": - version "0.8.1" - resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" - integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== - dependencies: - "@jridgewell/trace-mapping" "0.3.9" +"@esbuild/aix-ppc64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz#d1bc06aedb6936b3b6d313bf809a5a40387d2b7f" + integrity sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA== + +"@esbuild/android-arm64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz#7ad65a36cfdb7e0d429c353e00f680d737c2aed4" + integrity sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA== + +"@esbuild/android-arm@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.12.tgz#b0c26536f37776162ca8bde25e42040c203f2824" + integrity sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w== + +"@esbuild/android-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.12.tgz#cb13e2211282012194d89bf3bfe7721273473b3d" + integrity sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew== + +"@esbuild/darwin-arm64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz#cbee41e988020d4b516e9d9e44dd29200996275e" + integrity sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g== + +"@esbuild/darwin-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz#e37d9633246d52aecf491ee916ece709f9d5f4cd" + integrity sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A== + +"@esbuild/freebsd-arm64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz#1ee4d8b682ed363b08af74d1ea2b2b4dbba76487" + integrity sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA== + +"@esbuild/freebsd-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz#37a693553d42ff77cd7126764b535fb6cc28a11c" + integrity sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg== + +"@esbuild/linux-arm64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz#be9b145985ec6c57470e0e051d887b09dddb2d4b" + integrity sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA== + +"@esbuild/linux-arm@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz#207ecd982a8db95f7b5279207d0ff2331acf5eef" + integrity sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w== + +"@esbuild/linux-ia32@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz#d0d86b5ca1562523dc284a6723293a52d5860601" + integrity sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA== + +"@esbuild/linux-loong64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz#9a37f87fec4b8408e682b528391fa22afd952299" + integrity sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA== + +"@esbuild/linux-mips64el@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz#4ddebd4e6eeba20b509d8e74c8e30d8ace0b89ec" + integrity sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w== + +"@esbuild/linux-ppc64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz#adb67dadb73656849f63cd522f5ecb351dd8dee8" + integrity sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg== + +"@esbuild/linux-riscv64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz#11bc0698bf0a2abf8727f1c7ace2112612c15adf" + integrity sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg== + +"@esbuild/linux-s390x@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz#e86fb8ffba7c5c92ba91fc3b27ed5a70196c3cc8" + integrity sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg== + +"@esbuild/linux-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz#5f37cfdc705aea687dfe5dfbec086a05acfe9c78" + integrity sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg== + +"@esbuild/netbsd-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz#29da566a75324e0d0dd7e47519ba2f7ef168657b" + integrity sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA== + +"@esbuild/openbsd-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz#306c0acbdb5a99c95be98bdd1d47c916e7dc3ff0" + integrity sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw== + +"@esbuild/sunos-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz#0933eaab9af8b9b2c930236f62aae3fc593faf30" + integrity sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA== + +"@esbuild/win32-arm64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz#773bdbaa1971b36db2f6560088639ccd1e6773ae" + integrity sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A== + +"@esbuild/win32-ia32@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz#000516cad06354cc84a73f0943a4aa690ef6fd67" + integrity sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ== + +"@esbuild/win32-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz#c57c8afbb4054a3ab8317591a0b7320360b444ae" + integrity sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA== "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" @@ -534,7 +642,7 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" -"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": +"@jridgewell/resolve-uri@^3.1.0": version "3.1.1" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== @@ -549,14 +657,6 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== -"@jridgewell/trace-mapping@0.3.9": - version "0.3.9" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" - integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== - dependencies: - "@jridgewell/resolve-uri" "^3.0.3" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.9": version "0.3.20" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f" @@ -584,26 +684,6 @@ dependencies: "@sinonjs/commons" "^3.0.0" -"@tsconfig/node10@^1.0.7": - version "1.0.9" - resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" - integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== - -"@tsconfig/node12@^1.0.7": - version "1.0.11" - resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" - integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== - -"@tsconfig/node14@^1.0.0": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" - integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== - -"@tsconfig/node16@^1.0.2": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" - integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== - "@types/babel__core@^7.1.14": version "7.20.5" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" @@ -702,16 +782,6 @@ dependencies: "@types/yargs-parser" "*" -acorn-walk@^8.1.1: - version "8.3.0" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.0.tgz#2097665af50fd0cf7a2dfccd2b9368964e66540f" - integrity sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA== - -acorn@^8.4.1: - version "8.11.2" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b" - integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w== - ansi-escapes@^4.2.1: version "4.3.2" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" @@ -743,10 +813,10 @@ ansi-styles@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== -antlr4ts@^0.5.0-alpha.4: - version "0.5.0-alpha.4" - resolved "https://registry.yarnpkg.com/antlr4ts/-/antlr4ts-0.5.0-alpha.4.tgz#71702865a87478ed0b40c0709f422cf14d51652a" - integrity sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ== +antlr4@^4.13.1-patch-1: + version "4.13.1-patch-1" + resolved "https://registry.yarnpkg.com/antlr4/-/antlr4-4.13.1-patch-1.tgz#946176f863f890964a050c4f18c47fd6f7e57602" + integrity sha512-OjFLWWLzDMV9rdFhpvroCWR4ooktNg9/nvVYSA5z28wuVpU36QUNuioR1XLnQtcjVlf8npjyz593PxnU/f/Cow== anymatch@^3.0.3: version "3.1.3" @@ -756,11 +826,6 @@ anymatch@^3.0.3: normalize-path "^3.0.0" picomatch "^2.0.4" -arg@^4.1.0: - version "4.1.3" - resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" - integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== - argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -915,23 +980,23 @@ caniuse-lite@^1.0.30001541: integrity sha512-xrE//a3O7TP0vaJ8ikzkD2c2NgcVUvsEe2IvFTntV4Yd1Z9FVzh+gW+enX96L0psrbaFMcVcH2l90xNuGDWc8w== cashc@^0.10.0: - version "0.10.0-next.0" - resolved "https://registry.yarnpkg.com/cashc/-/cashc-0.10.0-next.0.tgz#ba8f803abca77ad1ac629cfa3e794fa691906c81" - integrity sha512-PeOiU/5u+MyugcICuzb1aD5TitVRKbXD/4cBz9WNR7PCjdMX+3NpYDZi0Ql61YrcHP1s4z8a3Xw+w4FKcCL5LQ== + version "0.10.0-next.3" + resolved "https://registry.yarnpkg.com/cashc/-/cashc-0.10.0-next.3.tgz#25e82b55c7a11848ba25c45a05c24b263a322fba" + integrity sha512-TfZvCSi840txb2XTuf7H1OgFWB+nraHVeibwsFLfQZu1n1VfW9Ff6tNkEqzxa6EpXuyJfgZAgM9zS9B7J0P2Yg== dependencies: - "@bitauth/libauth" "^2.0.0-alpha.8" - "@cashscript/utils" "^0.10.0-next.0" - antlr4ts "^0.5.0-alpha.4" + "@bitauth/libauth" "^2.0.0" + "@cashscript/utils" "^0.10.0-next.3" + antlr4 "^4.13.1-patch-1" commander "^7.1.0" - semver "^7.3.4" + semver "^7.5.4" cashscript@^0.10.0: - version "0.10.0-next.0" - resolved "https://registry.yarnpkg.com/cashscript/-/cashscript-0.10.0-next.0.tgz#6959a5d0872660094e10a77fb59a8aec52ead3bf" - integrity sha512-ph6gmsLA7q7birLyFIE6gAAS4QRnA5Msa+T4Dx5sxLoQERIUHn8nJ8w/QjE1+UdcwimK43YV6fwaEVRXYm0UZQ== + version "0.10.0-next.3" + resolved "https://registry.yarnpkg.com/cashscript/-/cashscript-0.10.0-next.3.tgz#2d186a7fa1f8ca496016dba1fb077a1956662407" + integrity sha512-eXGZu1jpsJDWpoJq+iaN9XXBuLefJP8GGgv4mR7mBxaRfW/+IKeIvvu9lcD98wWggwQcJqom2/e43cB7K5Wonw== dependencies: - "@bitauth/libauth" "^2.0.0-alpha.8" - "@cashscript/utils" "^0.10.0-next.0" + "@bitauth/libauth" "^2.0.0" + "@cashscript/utils" "^0.10.0-next.3" bip68 "^1.0.4" bitcoin-rpc-promise-retry "^1.3.0" delay "^5.0.0" @@ -1042,11 +1107,6 @@ create-jest@^29.7.0: jest-util "^29.7.0" prompts "^2.0.1" -create-require@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" - integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== - cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -1088,11 +1148,6 @@ diff-sequences@^29.6.3: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== -diff@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" - integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== - electron-to-chromium@^1.4.535: version "1.4.594" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.594.tgz#f69f207fba80735a44a988df42f3f439115d0515" @@ -1126,6 +1181,35 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" +esbuild@~0.19.10: + version "0.19.12" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.12.tgz#dc82ee5dc79e82f5a5c3b4323a2a641827db3e04" + integrity sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg== + optionalDependencies: + "@esbuild/aix-ppc64" "0.19.12" + "@esbuild/android-arm" "0.19.12" + "@esbuild/android-arm64" "0.19.12" + "@esbuild/android-x64" "0.19.12" + "@esbuild/darwin-arm64" "0.19.12" + "@esbuild/darwin-x64" "0.19.12" + "@esbuild/freebsd-arm64" "0.19.12" + "@esbuild/freebsd-x64" "0.19.12" + "@esbuild/linux-arm" "0.19.12" + "@esbuild/linux-arm64" "0.19.12" + "@esbuild/linux-ia32" "0.19.12" + "@esbuild/linux-loong64" "0.19.12" + "@esbuild/linux-mips64el" "0.19.12" + "@esbuild/linux-ppc64" "0.19.12" + "@esbuild/linux-riscv64" "0.19.12" + "@esbuild/linux-s390x" "0.19.12" + "@esbuild/linux-x64" "0.19.12" + "@esbuild/netbsd-x64" "0.19.12" + "@esbuild/openbsd-x64" "0.19.12" + "@esbuild/sunos-x64" "0.19.12" + "@esbuild/win32-arm64" "0.19.12" + "@esbuild/win32-ia32" "0.19.12" + "@esbuild/win32-x64" "0.19.12" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -1214,7 +1298,7 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@^2.3.2: +fsevents@^2.3.2, fsevents@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== @@ -1244,6 +1328,13 @@ get-stream@^6.0.0: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== +get-tsconfig@^4.7.2: + version "4.7.3" + resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.7.3.tgz#0498163d98f7b58484dd4906999c0c9d5f103f83" + integrity sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg== + dependencies: + resolve-pkg-maps "^1.0.0" + glob@^7.1.3, glob@^7.1.4: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -1851,11 +1942,6 @@ make-dir@^4.0.0: dependencies: semver "^7.5.3" -make-error@^1.1.1: - version "1.3.6" - resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" - integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== - makeerror@1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" @@ -2066,6 +2152,11 @@ resolve-from@^5.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== +resolve-pkg-maps@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" + integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== + resolve.exports@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800" @@ -2085,7 +2176,7 @@ semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.4, semver@^7.5.3, semver@^7.5.4: +semver@^7.5.3, semver@^7.5.4: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== @@ -2235,30 +2326,21 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -ts-node@^10.9.1: - version "10.9.1" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" - integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== - dependencies: - "@cspotcode/source-map-support" "^0.8.0" - "@tsconfig/node10" "^1.0.7" - "@tsconfig/node12" "^1.0.7" - "@tsconfig/node14" "^1.0.0" - "@tsconfig/node16" "^1.0.2" - acorn "^8.4.1" - acorn-walk "^8.1.1" - arg "^4.1.0" - create-require "^1.1.0" - diff "^4.0.1" - make-error "^1.1.1" - v8-compile-cache-lib "^3.0.1" - yn "3.1.1" - tslib@^2.3.1: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== +tsx@^4.7.2: + version "4.7.2" + resolved "https://registry.yarnpkg.com/tsx/-/tsx-4.7.2.tgz#a108b1a6e16876cd4c9a4b4ba263f2a07f9cf562" + integrity sha512-BCNd4kz6fz12fyrgCTEdZHGJ9fWTGeUzXmQysh0RVocDY3h4frk05ZNCXSy4kIenF7y/QnrdiVpTsyNRn6vlAw== + dependencies: + esbuild "~0.19.10" + get-tsconfig "^4.7.2" + optionalDependencies: + fsevents "~2.3.3" + type-detect@4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" @@ -2287,11 +2369,6 @@ update-browserslist-db@^1.0.13: escalade "^3.1.1" picocolors "^1.0.0" -v8-compile-cache-lib@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" - integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== - v8-to-istanbul@^9.0.1: version "9.2.0" resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz#2ed7644a245cddd83d4e087b9b33b3e62dfd10ad" @@ -2375,11 +2452,6 @@ yargs@^17.3.1: y18n "^5.0.5" yargs-parser "^21.1.1" -yn@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" - integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== - yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" diff --git a/examples/transfer_with_timeout.cash b/examples/transfer_with_timeout.cash index ea04b5f8..f49b0e9d 100644 --- a/examples/transfer_with_timeout.cash +++ b/examples/transfer_with_timeout.cash @@ -1,4 +1,4 @@ -pragma cashscript ^0.9.0; +pragma cashscript ^0.11.0; contract TransferWithTimeout( pubkey sender, diff --git a/examples/transfer_with_timeout.ts b/examples/transfer_with_timeout.ts index 0ccd4855..12a55918 100644 --- a/examples/transfer_with_timeout.ts +++ b/examples/transfer_with_timeout.ts @@ -1,6 +1,6 @@ import { stringify } from '@bitauth/libauth'; import { compileFile } from 'cashc'; -import { Contract, ElectrumNetworkProvider, SignatureTemplate } from 'cashscript'; +import { Contract, ElectrumNetworkProvider, Output, SignatureTemplate, TransactionBuilder } from 'cashscript'; import { URL } from 'url'; // Import Bob and Alice's keys from common.ts @@ -24,22 +24,63 @@ const contract = new Contract(artifact, [alicePub, bobPub, 100000n], { provider // Get contract balance & output address + balance console.log('contract address:', contract.address); +const contractUtxos = await contract.getUtxos(); +console.log('contract utxos:', contractUtxos); console.log('contract balance:', await contract.getBalance()); -// Call the transfer function with bob's signature +// Select a contract UTXO to spend from +const contractInputUtxo = contractUtxos.find(utxo => utxo.satoshis > 5_000n); +if (!contractInputUtxo) throw new Error('No contract UTXO with enough satoshis found'); +const inputAmount = contractInputUtxo.satoshis; + +// construct an output +const outputAmount = 1000n; +const output: Output = { amount: outputAmount, to: contract.address }; + +// Construct a changeOutput +const minerFee = 1000n; +const changeAmount = inputAmount - outputAmount - minerFee; +const changeOutput: Output = { + amount: changeAmount, + to: contract.address, +}; + +// Call the contract's transfer unlock-function with bob's signature // Allows bob to claim the money that alice sent him -const transferTx = await contract.functions - .transfer(new SignatureTemplate(bobPriv)) - .to(contract.address, 10000n) - .send(); +const transactionBuilder = new TransactionBuilder({ provider }); + +transactionBuilder.addInput(contractInputUtxo, contract.unlock.transfer(new SignatureTemplate(bobPriv))); +transactionBuilder.addOutput(output); +if (changeAmount > 1000n) transactionBuilder.addOutput(changeOutput); + +const transferTx = await transactionBuilder.send(); console.log('transfer transaction details:', stringify(transferTx)); -// Call the timeout function with alice's signature +// Select a 2nd contract UTXO to spend from +const updateContractUtxos = await contract.getUtxos(); +const contractInputUtxo2 = updateContractUtxos.find(utxo => utxo.satoshis > 5_000n); +if (!contractInputUtxo2) throw new Error('No contract UTXO with enough satoshis found'); +const inputAmount2 = contractInputUtxo2.satoshis; + +const changeAmount2 = inputAmount2 - outputAmount - minerFee; +const changeOutput2: Output = { + amount: changeAmount2, + to: contract.address, +}; + +const currentBlockHeight = await provider.getBlockHeight(); +console.log('current block height:', currentBlockHeight); + +// Call the contract's timeout unlock-function with alice's signature // Allows alice to reclaim the money she sent as the timeout is in the past -const timeoutTx = await contract.functions - .timeout(new SignatureTemplate(alicePriv)) - .to(contract.address, 10000n) - .send(); +const transactionBuilder2 = new TransactionBuilder({ provider }); + +transactionBuilder2.setLocktime(currentBlockHeight); +transactionBuilder2.addInput(contractInputUtxo2, contract.unlock.timeout(new SignatureTemplate(alicePriv))); +transactionBuilder2.addOutput(output); +if (changeAmount2 > 1000n) transactionBuilder2.addOutput(changeOutput2); + +const timeoutTx = await transactionBuilder2.send(); console.log('timeout transaction details:', stringify(timeoutTx)); diff --git a/package.json b/package.json index d9ba8647..a9c4e6d3 100644 --- a/package.json +++ b/package.json @@ -4,40 +4,31 @@ "type": "module", "workspaces": [ "packages/*", - "examples" + "examples", + "examples/testing-suite" ], "devDependencies": { "@jest/reporters": "^26.6.2", - "@types/jest": "^29.4.0", - "@types/node": "^14.14.28", - "@typescript-eslint/eslint-plugin": "^6.12.0", - "@typescript-eslint/parser": "^6.12.0", - "cashproof": "https://github.com/eyeofpython/cashproof", - "chalk": "^4.1.0", - "codecov": "^3.8.1", - "cspell": "^5.2.4", + "@types/jest": "^29.5.14", + "@types/node": "^22.10.7", + "@typescript-eslint/eslint-plugin": "^7.0.0", + "@typescript-eslint/parser": "^7.0.0", + "cspell": "^8.17.2", "eslint": "^8.54.0", - "eslint-config-airbnb-typescript": "^17.1.0", - "eslint-plugin-import": "^2.29.0", + "eslint-config-airbnb-typescript": "^18.0.0", + "eslint-plugin-import": "^2.31.0", "lerna": "^3.22.1", - "ts-node": "^10.9.1", - "typescript": "^4.9.5" + "tsx": "^4.19.2", + "typescript": "^5.7.3" }, "scripts": { - "test": "lerna run test --ignore cashscript-examples", - "lint": "lerna run lint --ignore cashscript-examples", - "preproof:opt": "cp packages/utils/src/cashproof-optimisations.ts opt.equiv && sed -i '' '/`/d' opt.equiv", - "proof:opt": "yarn cashproof opt.equiv", - "postproof:opt": "rm opt.equiv", - "proof:0.2.0": "yarn cashproof packages/cashc/test/cashproof/0.1.2=0.2.0.equiv", - "proof": "yarn proof:opt && yarn proof:0.2.0", - "cashproof": "python3 node_modules/cashproof/run.py", - "examples": "ts-node-esm examples/p2pkh.ts && ts-node-esm examples/transfer_with_timeout.ts && ts-node-esm examples/hodl_vault.ts", - "coverage": "codecov", - "postinstall": "lerna bootstrap && lerna run build --ignore cashscript-examples", + "test": "lerna run test --ignore cashscript-examples --ignore testing-suite", + "lint": "lerna run lint --ignore cashscript-examples --ignore testing-suite", + "examples": "tsx examples/p2pkh.ts && tsx examples/transfer_with_timeout.ts && tsx examples/hodl_vault.ts", + "postinstall": "lerna bootstrap && yarn build", "spellcheck": "cspell lint '**' --no-progress --must-find-files", - "update-version": "ts-node-esm update-version.ts", + "update-version": "tsx update-version.ts", "publish-all": "lerna publish from-package", - "build": "lerna run build" + "build": "lerna run build --ignore cashscript-examples --ignore testing-suite" } } diff --git a/packages/cashc/README.md b/packages/cashc/README.md index 6f6165c7..d39c8b50 100644 --- a/packages/cashc/README.md +++ b/packages/cashc/README.md @@ -1,20 +1,20 @@ # CashScript -[![Build Status](https://travis-ci.org/Bitcoin-com/cashscript.svg)](https://travis-ci.org/Bitcoin-com/cashscript) -[![Coverage Status](https://img.shields.io/codecov/c/github/Bitcoin-com/cashscript.svg)](https://codecov.io/gh/Bitcoin-com/cashscript/) +[![Build Status](https://github.com/CashScript/cashscript/actions/workflows/github-actions.yml/badge.svg)](https://github.com/CashScript/cashscript/actions/workflows/github-actions.yml) +[![Coverage Status](https://img.shields.io/codecov/c/github/CashScript/cashscript.svg)](https://codecov.io/gh/CashScript/cashscript/) [![NPM Version](https://img.shields.io/npm/v/cashscript.svg)](https://www.npmjs.com/package/cashscript) [![NPM Monthly Downloads](https://img.shields.io/npm/dm/cashscript.svg)](https://www.npmjs.com/package/cashscript) [![NPM License](https://img.shields.io/npm/l/cashscript.svg)](https://www.npmjs.com/package/cashscript) CashScript is a high-level programming language for smart contracts on Bitcoin Cash. It offers a strong abstraction layer over Bitcoin Cash' native virtual machine, Bitcoin Script. Its syntax is based on Ethereum's smart contract language Solidity, but its functionality is very different since smart contracts on Bitcoin Cash differ greatly from smart contracts on Ethereum. For a detailed comparison of them, refer to the blog post [*Smart Contracts on Ethereum, Bitcoin and Bitcoin Cash*](https://kalis.me/smart-contracts-eth-btc-bch/). -See the [GitHub repository](https://github.com/Bitcoin-com/cashscript) and the [CashScript website](https://cashscript.org) for full documentation and usage examples. +See the [GitHub repository](https://github.com/CashScript/cashscript) and the [CashScript website](https://cashscript.org) for full documentation and usage examples. ## The CashScript Language CashScript is a high-level language that allows you to write Bitcoin Cash smart contracts in a straightforward and familiar way. Its syntax is inspired by Ethereum's Solidity language, but its functionality is different since the underlying systems have very different fundamentals. See the [language documentation](https://cashscript.org/docs/language/) for a full reference of the language. ## The CashScript Compiler -CashScript features a compiler as a standalone command line tool, called `cashc`. It can be installed through npm and used to compile `.cash` files into `.json` artifact files. These artifact files can be imported into the CashScript JavaScript SDK (or other SDKs in the future). The `cashc` NPM package can also be imported inside JavaScript files to compile `.cash` files without using the command line tool. +CashScript features a compiler as a standalone command line tool, called `cashc`. It can be installed through npm and used to compile `.cash` files into `.json` (or `.ts`)artifact files. These artifact files can be imported into the CashScript TypeScript SDK (or other SDKs in the future). The `cashc` NPM package can also be imported inside JavaScript files to compile `.cash` files without using the command line tool. ### Installation ```bash @@ -26,11 +26,12 @@ npm install -g cashc Usage: cashc [options] [source_file] Options: - -V, --version Output the version number. - -o, --output Specify a file to output the generated artifact. - -h, --hex Compile the contract to hex format rather than a full artifact. - -A, --asm Compile the contract to ASM format rather than a full artifact. - -c, --opcount Display the number of opcodes in the compiled bytecode. - -s, --size Display the size in bytes of the compiled bytecode. - -?, --help Display help + -V, --version Output the version number. + -o, --output Specify a file to output the generated artifact. + -h, --hex Compile the contract to hex format rather than a full artifact. + -A, --asm Compile the contract to ASM format rather than a full artifact. + -c, --opcount Display the number of opcodes in the compiled bytecode. + -s, --size Display the size in bytes of the compiled bytecode. + -f, --format Specify the format of the output. (choices: "json", "ts", default: "json") + -?, --help Display help ``` diff --git a/packages/cashc/package.json b/packages/cashc/package.json index f6509938..f903c87c 100644 --- a/packages/cashc/package.json +++ b/packages/cashc/package.json @@ -1,25 +1,27 @@ { "name": "cashc", - "version": "0.10.0-next.0", + "version": "0.11.0", "description": "Compile Bitcoin Cash contracts to Bitcoin Cash Script or artifacts", "keywords": [ "bitcoin", "bitcoin cash", "cashscript", "compiler", - "smart contracts" + "smart contracts", + "cashtokens" ], "homepage": "https://cashscript.org", "bugs": { - "url": "https://github.com/Bitcoin-com/cashscript/issues" + "url": "https://github.com/CashScript/cashscript/issues" }, "repository": { "type": "git", - "url": "git+https://github.com/Bitcoin-com/cashscript.git" + "url": "git+https://github.com/CashScript/cashscript.git" }, "license": "MIT", "author": "Rosco Kalis ", "contributors": [ + "Mathieu Geukens ", "Gabriel Cardona " ], "main": "dist/index.js", @@ -34,8 +36,8 @@ "test": "test" }, "scripts": { - "antlr": "antlr4ts -visitor -no-listener src/grammar/CashScript.g4", - "postantlr": "find src/grammar -type f -name '*.ts' | xargs sed -i '' 's|\\(import .* \".*/.*\\)\";|\\1\\.js\";|g'", + "antlr": "antlr -Dlanguage=TypeScript -visitor -no-listener src/grammar/CashScript.g4", + "postantlr": "find src/grammar -type f -name 'CashScriptVisitor.ts' | xargs sed -i '' 's|\\(import .* \".*/.*\\)\";|\\1\\.js\";|g'", "build": "yarn clean && yarn compile", "build:test": "yarn clean:test && yarn compile:test && cpy './test/**/*.cash' ./dist-test/test", "clean": "rm -rf ./dist", @@ -49,21 +51,23 @@ "test": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest" }, "dependencies": { - "@bitauth/libauth": "^2.0.0-alpha.8", - "@cashscript/utils": "^0.10.0-next.0", - "antlr4ts": "^0.5.0-alpha.4", - "commander": "^7.1.0", - "semver": "^7.3.4" + "@bitauth/libauth": "^3.1.0-next.2", + "@cashscript/utils": "^0.11.0", + "antlr4": "^4.13.2", + "commander": "^13.1.0", + "semver": "^7.6.3" }, "devDependencies": { - "@jest/globals": "^29.4.1", - "@types/node": "^18.11.18", - "@types/semver": "^7.3.4", - "antlr4ts-cli": "^0.5.0-alpha.4", - "cpy-cli": "^4.2.0", + "@jest/globals": "^29.7.0", + "@types/jest": "^29.5.14", + "@types/node": "^22.10.7", + "@types/semver": "^7.5.8", + "cpy-cli": "^5.0.0", "eslint": "^8.54.0", - "jest": "^29.4.1", - "typescript": "^4.1.5", + "eslint-plugin-import": "^2.31.0", + "jest": "^29.7.0", + "tsx": "^4.19.2", + "typescript": "^5.7.3", "url-join": "^5.0.0" }, "gitHead": "bf02a4b641d5d03c035d052247a545109c17b708" diff --git a/packages/cashc/src/artifact/Artifact.ts b/packages/cashc/src/artifact/Artifact.ts index a5161b6f..b5e584f6 100644 --- a/packages/cashc/src/artifact/Artifact.ts +++ b/packages/cashc/src/artifact/Artifact.ts @@ -1,21 +1,10 @@ import { - Artifact, LogEntry, RequireMessage, Script, scriptToAsm, scriptToBytecode, + Artifact, DebugInformation, Script, scriptToAsm, } from '@cashscript/utils'; import { version } from '../index.js'; import { Ast } from '../ast/AST.js'; -import { binToHex } from '@bitauth/libauth'; -export function generateArtifact( - ast: Ast, - script: Script, - source: string, - debug: { - script: Script, - sourceMap: string, - logs: LogEntry[], - requireMessages: RequireMessage[], - }, -): Artifact { +export function generateArtifact(ast: Ast, script: Script, source: string, debug: DebugInformation): Artifact { const { contract } = ast; const constructorInputs = contract.parameters @@ -30,7 +19,6 @@ export function generateArtifact( })); const bytecode = scriptToAsm(script); - const debugBytecode = binToHex(scriptToBytecode(debug.script)); return { contractName: contract.name, @@ -38,12 +26,7 @@ export function generateArtifact( abi, bytecode, source, - debug: { - bytecode: debugBytecode, - sourceMap: debug.sourceMap, - logs: debug.logs, - requireMessages: debug.requireMessages, - }, + debug, compiler: { name: 'cashc', version, diff --git a/packages/cashc/src/ast/AST.ts b/packages/cashc/src/ast/AST.ts index b12081dd..f8c7b762 100644 --- a/packages/cashc/src/ast/AST.ts +++ b/packages/cashc/src/ast/AST.ts @@ -4,11 +4,12 @@ import AstVisitor from './AstVisitor.js'; import { BinaryOperator, NullaryOperator, UnaryOperator } from './Operator.js'; import { Location } from './Location.js'; import { SymbolTable, Symbol } from './SymbolTable.js'; +import { binToHex } from '@bitauth/libauth'; export type Ast = SourceFileNode; export abstract class Node { - location?: Location; + location: Location; abstract accept(visitor: AstVisitor): T; } @@ -97,8 +98,8 @@ export class VariableDefinitionNode extends StatementNode implements Named, Type export class TupleAssignmentNode extends StatementNode { constructor( - public var1: { name:string, type:Type }, - public var2: { name:string, type:Type }, + public var1: { name: string, type: Type }, + public var2: { name: string, type: Type }, public tuple: ExpressionNode, ) { super(); @@ -299,9 +300,15 @@ export class IdentifierNode extends ExpressionNode implements Named { } } -export abstract class LiteralNode extends ExpressionNode {} +export abstract class LiteralNode extends ExpressionNode { + public value: T; -export class BoolLiteralNode extends LiteralNode { + toString(): string { + return `${this.value}`; + } +} + +export class BoolLiteralNode extends LiteralNode { constructor( public value: boolean, ) { @@ -314,7 +321,7 @@ export class BoolLiteralNode extends LiteralNode { } } -export class IntLiteralNode extends LiteralNode { +export class IntLiteralNode extends LiteralNode { constructor( public value: bigint, ) { @@ -327,7 +334,7 @@ export class IntLiteralNode extends LiteralNode { } } -export class StringLiteralNode extends LiteralNode { +export class StringLiteralNode extends LiteralNode { constructor( public value: string, public quote: string, @@ -341,7 +348,7 @@ export class StringLiteralNode extends LiteralNode { } } -export class HexLiteralNode extends LiteralNode { +export class HexLiteralNode extends LiteralNode { constructor( public value: Uint8Array, ) { @@ -349,6 +356,10 @@ export class HexLiteralNode extends LiteralNode { this.type = new BytesType(value.byteLength); } + toString(): string { + return `0x${binToHex(this.value)}`; + } + accept(visitor: AstVisitor): T { return visitor.visitHexLiteral(this); } @@ -366,16 +377,4 @@ export class ConsoleStatementNode extends Node { } } -// TODO: Couldn't we just use existing nodes for this? e.g. IdentifierNode + StringLiteralNode etc. -export class ConsoleParameterNode extends Node { - constructor( - public message?: string, - public identifier?: string, - ) { - super(); - } - - accept(visitor: AstVisitor): T { - return visitor.visitConsoleParameter(this); - } -} +export type ConsoleParameterNode = LiteralNode | IdentifierNode; diff --git a/packages/cashc/src/ast/AstBuilder.ts b/packages/cashc/src/ast/AstBuilder.ts index 1621ac1c..50d0083a 100644 --- a/packages/cashc/src/ast/AstBuilder.ts +++ b/packages/cashc/src/ast/AstBuilder.ts @@ -1,8 +1,6 @@ +import { ParseTree, ParseTreeVisitor } from 'antlr4'; import { hexToBin } from '@bitauth/libauth'; import { parseType } from '@cashscript/utils'; -import { AbstractParseTreeVisitor } from 'antlr4ts/tree/AbstractParseTreeVisitor.js'; -import { ParseTree } from 'antlr4ts/tree/ParseTree.js'; -import { TerminalNode } from 'antlr4ts/tree/TerminalNode.js'; import semver from 'semver'; import { Node, @@ -48,7 +46,6 @@ import type { FunctionCallContext, CastContext, LiteralContext, - NumberLiteralContext, SourceFileContext, BlockContext, TimeOpStatementContext, @@ -67,8 +64,10 @@ import type { UnaryIntrospectionOpContext, ConsoleStatementContext, ConsoleParameterContext, + StatementContext, + RequireMessageContext, } from '../grammar/CashScriptParser.js'; -import type { CashScriptVisitor } from '../grammar/CashScriptVisitor.js'; +import CashScriptVisitor from '../grammar/CashScriptVisitor.js'; import { Location } from './Location.js'; import { NumberUnit, @@ -79,7 +78,7 @@ import { version } from '../index.js'; import { ParseError, VersionError } from '../Errors.js'; export default class AstBuilder - extends AbstractParseTreeVisitor + extends ParseTreeVisitor implements CashScriptVisitor { constructor(private tree: ParseTree) { super(); @@ -94,7 +93,7 @@ export default class AstBuilder } visitSourceFile(ctx: SourceFileContext): SourceFileNode { - ctx.pragmaDirective().forEach((pragma) => { + ctx.pragmaDirective_list().forEach((pragma) => { this.processPragma(pragma); }); @@ -105,15 +104,15 @@ export default class AstBuilder } processPragma(ctx: PragmaDirectiveContext): void { - const pragmaName = getPragmaName(ctx.pragmaName().text); + const pragmaName = getPragmaName(ctx.pragmaName().getText()); if (pragmaName !== PragmaName.CASHSCRIPT) throw new Error(); // Shouldn't happen // Strip any -beta tags const actualVersion = version.replace(/-.*/g, ''); - ctx.pragmaValue().versionConstraint().forEach((constraint) => { + ctx.pragmaValue().versionConstraint_list().forEach((constraint) => { const op = getVersionOpFromCtx(constraint.versionOperator()); - const versionConstraint = `${op}${constraint.VersionLiteral().text}`; + const versionConstraint = `${op}${constraint.VersionLiteral().getText()}`; if (!semver.satisfies(actualVersion, versionConstraint)) { throw new VersionError(actualVersion, versionConstraint); } @@ -121,18 +120,18 @@ export default class AstBuilder } visitContractDefinition(ctx: ContractDefinitionContext): ContractNode { - const name = ctx.Identifier().text; - const parameters = ctx.parameterList().parameter().map((p) => this.visit(p) as ParameterNode); - const functions = ctx.functionDefinition().map((f) => this.visit(f) as FunctionDefinitionNode); + const name = ctx.Identifier().getText(); + const parameters = ctx.parameterList().parameter_list().map((p) => this.visit(p) as ParameterNode); + const functions = ctx.functionDefinition_list().map((f) => this.visit(f) as FunctionDefinitionNode); const contract = new ContractNode(name, parameters, functions); contract.location = Location.fromCtx(ctx); return contract; } visitFunctionDefinition(ctx: FunctionDefinitionContext): FunctionDefinitionNode { - const name = ctx.Identifier().text; - const parameters = ctx.parameterList().parameter().map((p) => this.visit(p) as ParameterNode); - const statements = ctx.statement().map((s) => this.visit(s) as StatementNode); + const name = ctx.Identifier().getText(); + const parameters = ctx.parameterList().parameter_list().map((p) => this.visit(p) as ParameterNode); + const statements = ctx.statement_list().map((s) => this.visit(s) as StatementNode); const block = new BlockNode(statements); block.location = Location.fromCtx(ctx); @@ -142,17 +141,22 @@ export default class AstBuilder } visitParameter(ctx: ParameterContext): ParameterNode { - const type = parseType(ctx.typeName().text); - const name = ctx.Identifier().text; + const type = parseType(ctx.typeName().getText()); + const name = ctx.Identifier().getText(); const parameter = new ParameterNode(type, name); parameter.location = Location.fromCtx(ctx); return parameter; } + visitStatement(ctx: StatementContext): StatementNode { + // Statement nodes only have a single child, so we can just visit that child + return this.visit(ctx.getChild(0)); + } + visitVariableDefinition(ctx: VariableDefinitionContext): VariableDefinitionNode { - const type = parseType(ctx.typeName().text); - const modifiers = ctx.modifier().map((modifier) => modifier.text); - const name = ctx.Identifier().text; + const type = parseType(ctx.typeName().getText()); + const modifiers = ctx.modifier_list().map((modifier) => modifier.getText()); + const name = ctx.Identifier().getText(); const expression = this.visit(ctx.expression()); const variableDefinition = new VariableDefinitionNode(type, modifiers, name, expression); variableDefinition.location = Location.fromCtx(ctx); @@ -161,18 +165,19 @@ export default class AstBuilder visitTupleAssignment(ctx: TupleAssignmentContext): TupleAssignmentNode { const expression = this.visit(ctx.expression()); - const names = ctx.Identifier(); - const types = ctx.typeName(); - const [var1, var2] = names.map((name, i) => ( - { name: name.text, type: parseType(types[i].text) } - )); + const names = ctx.Identifier_list(); + const types = ctx.typeName_list(); + const [var1, var2] = names.map((name, i) => ({ + name: name.getText(), + type: parseType(types[i].getText()), + })); const tupleAssignment = new TupleAssignmentNode(var1, var2, expression); tupleAssignment.location = Location.fromCtx(ctx); return tupleAssignment; } visitAssignStatement(ctx: AssignStatementContext): AssignNode { - const identifier = new IdentifierNode(ctx.Identifier().text); + const identifier = new IdentifierNode(ctx.Identifier().getText()); identifier.location = Location.fromToken(ctx.Identifier().symbol); const expression = this.visit(ctx.expression()); @@ -183,8 +188,8 @@ export default class AstBuilder visitTimeOpStatement(ctx: TimeOpStatementContext): TimeOpNode { const expression = this.visit(ctx.expression()); - const message = ctx.StringLiteral() && this.createStringLiteral(ctx as any)?.value; - const timeOp = new TimeOpNode(ctx.TxVar().text as TimeOp, expression, message); + const message = ctx.requireMessage() ? this.createStringLiteral(ctx.requireMessage()).value : undefined; + const timeOp = new TimeOpNode(ctx.TxVar().getText() as TimeOp, expression, message); timeOp.location = Location.fromCtx(ctx); return timeOp; @@ -192,7 +197,7 @@ export default class AstBuilder visitRequireStatement(ctx: RequireStatementContext): RequireNode { const expression = this.visit(ctx.expression()); - const message = ctx.StringLiteral() && this.createStringLiteral(ctx as any)?.value; + const message = ctx.requireMessage() ? this.createStringLiteral(ctx.requireMessage()).value : undefined; const require = new RequireNode(expression, message); require.location = Location.fromCtx(ctx); return require; @@ -208,7 +213,7 @@ export default class AstBuilder } visitBlock(ctx: BlockContext): BlockNode { - const statements = ctx.statement().map((s) => this.visit(s) as StatementNode); + const statements = ctx.statement_list().map((s) => this.visit(s) as StatementNode); const block = new BlockNode(statements); block.location = Location.fromCtx(ctx); return block; @@ -219,7 +224,7 @@ export default class AstBuilder } visitCast(ctx: CastContext): CastNode { - const type = parseType(ctx.typeName().text); + const type = parseType(ctx.typeName().getText()); const expression = this.visit(ctx._castable); const size = ctx._size && this.visit(ctx._size); const cast = new CastNode(type, expression, size); @@ -232,18 +237,18 @@ export default class AstBuilder } visitFunctionCall(ctx: FunctionCallContext): FunctionCallNode { - const identifier = new IdentifierNode(ctx.Identifier().text as string); + const identifier = new IdentifierNode(ctx.Identifier().getText()); identifier.location = Location.fromToken(ctx.Identifier().symbol); - const parameters = ctx.expressionList().expression().map((e) => this.visit(e)); + const parameters = ctx.expressionList().expression_list().map((e) => this.visit(e)); const functionCall = new FunctionCallNode(identifier, parameters); functionCall.location = Location.fromCtx(ctx); return functionCall; } visitInstantiation(ctx: InstantiationContext): InstantiationNode { - const identifier = new IdentifierNode(ctx.Identifier().text as string); + const identifier = new IdentifierNode(ctx.Identifier().getText()); identifier.location = Location.fromToken(ctx.Identifier().symbol); - const parameters = ctx.expressionList().expression().map((e) => this.visit(e)); + const parameters = ctx.expressionList().expression_list().map((e) => this.visit(e)); const instantiation = new InstantiationNode(identifier, parameters); instantiation.location = Location.fromCtx(ctx); return instantiation; @@ -251,14 +256,14 @@ export default class AstBuilder visitTupleIndexOp(ctx: TupleIndexOpContext): TupleIndexOpNode { const tuple = this.visit(ctx.expression()); - const index = parseInt(ctx._index.text as string, 10); + const index = parseInt(ctx._index.text, 10); const tupleIndexOp = new TupleIndexOpNode(tuple, index); tupleIndexOp.location = Location.fromCtx(ctx); return tupleIndexOp; } visitNullaryOp(ctx: NullaryOpContext): NullaryOpNode { - const operator = ctx.text as NullaryOperator; + const operator = ctx.getText() as NullaryOperator; const nullaryOp = new NullaryOpNode(operator); nullaryOp.location = Location.fromCtx(ctx); return nullaryOp; @@ -290,14 +295,14 @@ export default class AstBuilder } visitArray(ctx: ArrayContext): ArrayNode { - const elements = ctx.expression().map((e) => this.visit(e)); + const elements = ctx.expression_list().map((e) => this.visit(e)); const array = new ArrayNode(elements); array.location = Location.fromCtx(ctx); return array; } visitIdentifier(ctx: IdentifierContext): IdentifierNode { - const identifier = new IdentifierNode((ctx.Identifier() as TerminalNode).text); + const identifier = new IdentifierNode(ctx.Identifier().getText()); identifier.location = Location.fromCtx(ctx); return identifier; } @@ -331,7 +336,7 @@ export default class AstBuilder } createBooleanLiteral(ctx: LiteralContext): BoolLiteralNode { - const boolString = (ctx.BooleanLiteral() as TerminalNode).text; + const boolString = ctx.BooleanLiteral().getText(); const boolValue = boolString === 'true'; const booleanLiteral = new BoolLiteralNode(boolValue); booleanLiteral.location = Location.fromCtx(ctx); @@ -339,17 +344,17 @@ export default class AstBuilder } createIntLiteral(ctx: LiteralContext): IntLiteralNode { - const numberCtx = ctx.numberLiteral() as NumberLiteralContext; - const numberString = numberCtx.NumberLiteral().text; - const numberUnit = numberCtx.NumberUnit()?.text; - const numberValue = BigInt(numberString) * BigInt(numberUnit ? NumberUnit[numberUnit.toUpperCase()] : 1); + const numberCtx = ctx.numberLiteral(); + const numberString = numberCtx.NumberLiteral().getText(); + const numberUnit = numberCtx.NumberUnit()?.getText(); + const numberValue = parseNumberString(numberString) * BigInt(numberUnit ? NumberUnit[numberUnit.toUpperCase()] : 1); const intLiteral = new IntLiteralNode(numberValue); intLiteral.location = Location.fromCtx(ctx); return intLiteral; } - createStringLiteral(ctx: LiteralContext): StringLiteralNode { - const rawString = (ctx.StringLiteral() as TerminalNode).text; + createStringLiteral(ctx: LiteralContext | RequireMessageContext): StringLiteralNode { + const rawString = ctx.StringLiteral().getText(); const stringValue = rawString.substring(1, rawString.length - 1); const quote = rawString.substring(0, 1); const stringLiteral = new StringLiteralNode(stringValue, quote); @@ -358,7 +363,7 @@ export default class AstBuilder } createDateLiteral(ctx: LiteralContext): IntLiteralNode { - const rawString = (ctx.DateLiteral() as TerminalNode).text; + const rawString = ctx.DateLiteral().getText(); const stringValue = rawString.substring(6, rawString.length - 2).trim(); if (!/^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d$/.test(stringValue)) { @@ -377,7 +382,7 @@ export default class AstBuilder } createHexLiteral(ctx: LiteralContext): HexLiteralNode { - const hexString = (ctx.HexLiteral() as TerminalNode).text; + const hexString = ctx.HexLiteral().getText(); const hexValue = hexToBin(hexString.substring(2)); const hexLiteral = new HexLiteralNode(hexValue); hexLiteral.location = Location.fromCtx(ctx); @@ -385,22 +390,34 @@ export default class AstBuilder } visitConsoleStatement(ctx: ConsoleStatementContext): ConsoleStatementNode { - const parameters = ctx.consoleParameterList().consoleParameter().map((p) => this.visit(p) as ConsoleParameterNode); + const parameters = ctx.consoleParameterList() + .consoleParameter_list() + .map((p) => this.visit(p) as ConsoleParameterNode); + const node = new ConsoleStatementNode(parameters); node.location = Location.fromCtx(ctx); - return node; } visitConsoleParameter(ctx: ConsoleParameterContext): ConsoleParameterNode { - let message = (ctx.BooleanLiteral() ?? ctx.HexLiteral() ?? ctx.NumberLiteral() ?? ctx.StringLiteral())?.text; - if (message?.[0] === '"') { - message = message.slice(1, -1); - } - const identifier = ctx.Identifier()?.text; - - const node = new ConsoleParameterNode(message, identifier); + const node = ctx.literal() ? this.createLiteral(ctx.literal()) : new IdentifierNode(ctx.Identifier().getText()); node.location = Location.fromCtx(ctx); return node; } + + // For safety reasons, we throw an error when the "default" visitChildren is called. *All* nodes + // must have a custom visit method, so that we can be sure that we've covered all cases. + visitChildren(): Node { + throw new Error('Safety Warning: Unhandled node in AST builder'); + } } + +const parseNumberString = (numberString: string): bigint => { + const cleanedNumberString = numberString.replace(/_/g, ''); + + const isScientificNotation = /[eE]/.test(cleanedNumberString); + if (!isScientificNotation) return BigInt(cleanedNumberString); + + const [coefficient, exponent] = cleanedNumberString.split(/[eE]/); + return BigInt(coefficient) * BigInt(10) ** BigInt(exponent); +}; diff --git a/packages/cashc/src/ast/AstTraversal.ts b/packages/cashc/src/ast/AstTraversal.ts index 15c99829..81e3f340 100644 --- a/packages/cashc/src/ast/AstTraversal.ts +++ b/packages/cashc/src/ast/AstTraversal.ts @@ -157,8 +157,4 @@ export default class AstTraversal extends AstVisitor { node.parameters = this.visitList(node.parameters) as ConsoleParameterNode[]; return node; } - - visitConsoleParameter(node: ConsoleParameterNode): Node { - return node; - } } diff --git a/packages/cashc/src/ast/AstVisitor.ts b/packages/cashc/src/ast/AstVisitor.ts index e846d3c2..e154b5d1 100644 --- a/packages/cashc/src/ast/AstVisitor.ts +++ b/packages/cashc/src/ast/AstVisitor.ts @@ -25,7 +25,6 @@ import { TupleAssignmentNode, NullaryOpNode, ConsoleStatementNode, - ConsoleParameterNode, } from './AST.js'; export default abstract class AstVisitor { @@ -54,7 +53,6 @@ export default abstract class AstVisitor { abstract visitStringLiteral(node: StringLiteralNode): T; abstract visitHexLiteral(node: HexLiteralNode): T; abstract visitConsoleStatement(node: ConsoleStatementNode): T; - abstract visitConsoleParameter(node: ConsoleParameterNode): T; visit(node: Node): T { return node.accept(this); diff --git a/packages/cashc/src/ast/Globals.ts b/packages/cashc/src/ast/Globals.ts index 4a09951a..cec809e4 100644 --- a/packages/cashc/src/ast/Globals.ts +++ b/packages/cashc/src/ast/Globals.ts @@ -30,7 +30,7 @@ export enum GlobalFunction { } export enum TimeOp { - CHECK_SEQUENCE = 'tx.age', + CHECK_SEQUENCE = 'this.age', CHECK_LOCKTIME = 'tx.time', } diff --git a/packages/cashc/src/ast/Location.ts b/packages/cashc/src/ast/Location.ts index 1ef15714..a3054606 100644 --- a/packages/cashc/src/ast/Location.ts +++ b/packages/cashc/src/ast/Location.ts @@ -1,25 +1,24 @@ -import type { ParserRuleContext } from 'antlr4ts/ParserRuleContext.js'; -import type { Token } from 'antlr4ts'; +import type { ParserRuleContext, Token } from 'antlr4'; import { LocationI } from '@cashscript/utils'; export class Location implements LocationI { constructor(public start: Point, public end: Point) {} - static fromCtx(ctx: ParserRuleContext): Location | undefined { + static fromCtx(ctx: ParserRuleContext): Location { const stop = ctx.stop?.text ? ctx.stop : ctx.start; const textLength = (stop.text ?? '').length; - const start = new Point(ctx.start.line, ctx.start.charPositionInLine); - const end = new Point(stop.line, stop.charPositionInLine + textLength); + const start = new Point(ctx.start.line, ctx.start.column); + const end = new Point(stop.line, stop.column + textLength); return new Location(start, end); } - static fromToken(token: Token): Location | undefined { + static fromToken(token: Token): Location { const textLength = (token.text ?? '').length; - const start = new Point(token.line, token.charPositionInLine); - const end = new Point(token.line, token.charPositionInLine + textLength); + const start = new Point(token.line, token.column); + const end = new Point(token.line, token.column + textLength); return new Location(start, end); } diff --git a/packages/cashc/src/ast/Pragma.ts b/packages/cashc/src/ast/Pragma.ts index 2b67694c..be0c9b19 100644 --- a/packages/cashc/src/ast/Pragma.ts +++ b/packages/cashc/src/ast/Pragma.ts @@ -1,4 +1,4 @@ -import { VersionOperatorContext } from '../grammar/CashScriptParser.js'; +import type { VersionOperatorContext } from '../grammar/CashScriptParser.js'; export enum PragmaName { CASHSCRIPT = 'cashscript', @@ -19,5 +19,5 @@ export function getPragmaName(name: string): PragmaName { } export function getVersionOpFromCtx(ctx?: VersionOperatorContext): VersionOp { - return (ctx ? ctx.text : '='); + return (ctx ? ctx.getText() : '='); } diff --git a/packages/cashc/src/ast/ThrowingErrorListener.ts b/packages/cashc/src/ast/ThrowingErrorListener.ts index dafe6530..9d07f81a 100644 --- a/packages/cashc/src/ast/ThrowingErrorListener.ts +++ b/packages/cashc/src/ast/ThrowingErrorListener.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { ANTLRErrorListener, RecognitionException, Recognizer } from 'antlr4ts'; +import { ErrorListener, RecognitionException, Recognizer } from 'antlr4'; import { ParseError } from '../Errors.js'; import { Point } from './Location.js'; @@ -7,12 +7,12 @@ import { Point } from './Location.js'; * ANTLR Error Listener that immediately throws on error. This is used so that * ANTLR doesn't attempt any error recovery during lexing/parsing and fails early. */ -export default class ThrowingErrorListener implements ANTLRErrorListener { +export default class ThrowingErrorListener extends ErrorListener { static readonly INSTANCE = new ThrowingErrorListener(); - syntaxError( - recognizer: Recognizer, - offendingSymbol: T, + syntaxError( + recognizer: Recognizer, + offendingSymbol: TSymbol, line: number, charPositionInLine: number, message: string, diff --git a/packages/cashc/src/cashc-cli.ts b/packages/cashc/src/cashc-cli.ts index 5ba31a45..61b63bb7 100644 --- a/packages/cashc/src/cashc-cli.ts +++ b/packages/cashc/src/cashc-cli.ts @@ -4,25 +4,31 @@ import { asmToScript, calculateBytesize, countOpcodes, - exportArtifact, + formatArtifact, scriptToAsm, scriptToBytecode, } from '@cashscript/utils'; -import { program } from 'commander'; +import { program, Option } from 'commander'; import fs from 'fs'; import path from 'path'; import { compileFile, version } from './index.js'; +import { MAX_INPUT_BYTESIZE } from './constants.js'; program .storeOptionsAsProperties(false) .name('cashc') .version(version, '-V, --version', 'Output the version number.') - .usage('[options] [source_file]') + .argument('', 'The source file to compile.') .option('-o, --output ', 'Specify a file to output the generated artifact.') .option('-h, --hex', 'Compile the contract to hex format rather than a full artifact.') .option('-A, --asm', 'Compile the contract to ASM format rather than a full artifact.') .option('-c, --opcount', 'Display the number of opcodes in the compiled bytecode.') .option('-s, --size', 'Display the size in bytes of the compiled bytecode.') + .addOption( + new Option('-f, --format ', 'Specify the format of the output.') + .choices(['json', 'ts']) + .default('json'), + ) .helpOption('-?, --help', 'Display help') .parse(); @@ -48,11 +54,8 @@ function run(): void { const opcount = countOpcodes(script); const bytesize = calculateBytesize(script); - if (opcount > 201) { - console.warn('Warning: Your contract\'s opcount is over the limit of 201 and will not be accepted by the BCH network'); - } - if (bytesize > 520) { - console.warn('Warning: Your contract\'s bytesize is over the limit of 520 and will not be accepted by the BCH network'); + if (bytesize > MAX_INPUT_BYTESIZE) { + console.warn(`Warning: Your contract is ${bytesize} bytes, which is over the limit of ${MAX_INPUT_BYTESIZE} bytes and will not be accepted by the BCH network`); } if (opts.asm) { @@ -76,16 +79,18 @@ function run(): void { return; } + const formattedArtifact = formatArtifact(artifact, opts.format); + if (outputFile) { // Create output file and write the artifact to it const outputDir = path.dirname(outputFile); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } - exportArtifact(artifact, outputFile); + fs.writeFileSync(outputFile, formattedArtifact); } else { // Output artifact to STDOUT - console.log(JSON.stringify(artifact, null, 2)); + console.log(formattedArtifact); } } catch (e: any) { abort(e.message); diff --git a/packages/cashc/src/compiler.ts b/packages/cashc/src/compiler.ts index 3621a5ee..2840accd 100644 --- a/packages/cashc/src/compiler.ts +++ b/packages/cashc/src/compiler.ts @@ -1,13 +1,14 @@ -import { Artifact, optimiseBytecode } from '@cashscript/utils'; -import { ANTLRInputStream, CommonTokenStream } from 'antlr4ts'; +import { CharStream, CommonTokenStream } from 'antlr4'; +import { binToHex } from '@bitauth/libauth'; +import { Artifact, generateSourceMap, optimiseBytecode, optimiseBytecodeOld, scriptToAsm, scriptToBytecode, sourceMapToLocationData } from '@cashscript/utils'; import fs, { PathLike } from 'fs'; import { generateArtifact } from './artifact/Artifact.js'; import { Ast } from './ast/AST.js'; import AstBuilder from './ast/AstBuilder.js'; import ThrowingErrorListener from './ast/ThrowingErrorListener.js'; import GenerateTargetTraversal from './generation/GenerateTargetTraversal.js'; -import { CashScriptLexer } from './grammar/CashScriptLexer.js'; -import { CashScriptParser } from './grammar/CashScriptParser.js'; +import CashScriptLexer from './grammar/CashScriptLexer.js'; +import CashScriptParser from './grammar/CashScriptParser.js'; import SymbolTableTraversal from './semantic/SymbolTableTraversal.js'; import TypeCheckTraversal from './semantic/TypeCheckTraversal.js'; import EnsureFinalRequireTraversal from './semantic/EnsureFinalRequireTraversal.js'; @@ -17,24 +18,41 @@ export function compileString(code: string): Artifact { let ast = parseCode(code); // Semantic analysis - const symbolTableTraversal = new SymbolTableTraversal(); - ast = ast.accept(symbolTableTraversal) as Ast; + ast = ast.accept(new SymbolTableTraversal()) as Ast; ast = ast.accept(new TypeCheckTraversal()) as Ast; ast = ast.accept(new EnsureFinalRequireTraversal()) as Ast; // Code generation - const traversal = new GenerateTargetTraversal(symbolTableTraversal.logSymbols); + const traversal = new GenerateTargetTraversal(); ast = ast.accept(traversal) as Ast; + const constructorParamLength = ast.contract.parameters.length; + // Bytecode optimisation - const optimisedBytecode = optimiseBytecode(traversal.output); - - return generateArtifact(ast, optimisedBytecode, code, { - script: traversal.output, - sourceMap: traversal.souceMap, - logs: traversal.consoleLogs, - requireMessages: traversal.requireMessages, - }); + const optimisedBytecodeOld = optimiseBytecodeOld(traversal.output); + const optimisationResult = optimiseBytecode( + traversal.output, + sourceMapToLocationData(traversal.sourceMap), + traversal.consoleLogs, + traversal.requires, + constructorParamLength, + ); + + if (scriptToAsm(optimisedBytecodeOld) !== scriptToAsm(optimisationResult.script)) { + console.error(scriptToAsm(optimisedBytecodeOld)); + console.error(scriptToAsm(optimisationResult.script)); + throw new Error('New bytecode optimisation is not backwards compatible, please report this issue to the CashScript team'); + } + + // Attach debug information + const debug = { + bytecode: binToHex(scriptToBytecode(optimisationResult.script)), + sourceMap: generateSourceMap(optimisationResult.locationData), + logs: optimisationResult.logs, + requires: optimisationResult.requires, + }; + + return generateArtifact(ast, optimisationResult.script, code, debug); } export function compileFile(codeFile: PathLike): Artifact { @@ -44,7 +62,7 @@ export function compileFile(codeFile: PathLike): Artifact { export function parseCode(code: string): Ast { // Lexing (throwing on errors) - const inputStream = new ANTLRInputStream(code); + const inputStream = new CharStream(code); const lexer = new CashScriptLexer(inputStream); lexer.removeErrorListeners(); lexer.addErrorListener(ThrowingErrorListener.INSTANCE); diff --git a/packages/cashc/src/constants.ts b/packages/cashc/src/constants.ts new file mode 100644 index 00000000..097767c3 --- /dev/null +++ b/packages/cashc/src/constants.ts @@ -0,0 +1 @@ +export const MAX_INPUT_BYTESIZE = 1650; diff --git a/packages/cashc/src/generation/GenerateTargetTraversal.ts b/packages/cashc/src/generation/GenerateTargetTraversal.ts index 5bc1ea2f..44730dba 100644 --- a/packages/cashc/src/generation/GenerateTargetTraversal.ts +++ b/packages/cashc/src/generation/GenerateTargetTraversal.ts @@ -11,10 +11,12 @@ import { Script, scriptToAsm, generateSourceMap, - LocationI, - LocationData, + FullLocationData, LogEntry, - RequireMessage, + RequireStatement, + PositionHint, + SingleLocationData, + StackItem, } from '@cashscript/utils'; import { ContractNode, @@ -56,33 +58,27 @@ import { compileTimeOp, compileUnaryOp, } from './utils.js'; -import { Location } from '../ast/Location.js'; -import { ParseError } from '../Errors.js'; -import { Symbol } from '../ast/SymbolTable.js'; export default class GenerateTargetTraversalWithLocation extends AstTraversal { - private locationData: LocationData = []; // detailed location data needed for sourcemap creation - souceMap: string; + private locationData: FullLocationData = []; // detailed location data needed for sourcemap creation + sourceMap: string; output: Script = []; stack: string[] = []; consoleLogs: LogEntry[] = []; - requireMessages: RequireMessage[] = []; + requires: RequireStatement[] = []; + finalStackUsage: Record = {}; private scopeDepth = 0; private currentFunction: FunctionDefinitionNode; private constructorParameterCount: number; - constructor(private logSymbols: Symbol[]) { - super(); - } - - private emit(op: OpOrData | OpOrData[], location: LocationI, positionHint?: number): void { + private emit(op: OpOrData | OpOrData[], locationData: SingleLocationData): void { if (Array.isArray(op)) { op.forEach((element) => this.output.push(element as Op)); - op.forEach(() => this.locationData.push([location, positionHint])); + op.forEach(() => this.locationData.push(locationData)); } else { this.output.push(op); - this.locationData.push([location, positionHint]); + this.locationData.push(locationData); } } @@ -108,62 +104,78 @@ export default class GenerateTargetTraversalWithLocation extends AstTraversal { this.stack.splice(1, 1); } - private getStackIndex(value: string): number { + private getStackIndex(value: string, canBeUndefined?: boolean): number { const index = this.stack.indexOf(value); - if (index === -1) throw new Error(); // Should not happen + + if (index === -1 && !canBeUndefined) { + throw new Error(`Expected variable '${value}' does not exist on the stack`); + } + return index; } + private getMostRecentInstructionPointer(): number { + // instruction pointer is the count of emitted opcodes + number of constructor data pushes (minus 1 for 0-indexing) + return this.output.length + this.constructorParameterCount - 1; + } + visitSourceFile(node: SourceFileNode): Node { node.contract = this.visit(node.contract) as ContractNode; // Minimally encode output by going Script -> ASM -> Script this.output = asmToScript(scriptToAsm(this.output)); - this.souceMap = generateSourceMap(this.locationData); + this.sourceMap = generateSourceMap(this.locationData); return node; } visitContract(node: ContractNode): Node { node.parameters = this.visitList(node.parameters) as ParameterNode[]; + + // Keep track of constructor parameter count for instructor pointer calculation this.constructorParameterCount = node.parameters.length; + if (node.functions.length === 1) { node.functions = this.visitList(node.functions) as FunctionDefinitionNode[]; } else { this.pushToStack('$$', true); node.functions = node.functions.map((f, i) => { + const locationData = { location: f.location, positionHint: PositionHint.START }; + const stackCopy = [...this.stack]; const selectorIndex = this.getStackIndex('$$'); - this.emit(encodeInt(BigInt(selectorIndex)), f.location!); + + this.emit(encodeInt(BigInt(selectorIndex)), locationData); if (i === node.functions.length - 1) { - this.emit(Op.OP_ROLL, f.location!); + this.emit(Op.OP_ROLL, locationData); this.removeFromStack(selectorIndex); } else { - this.emit(Op.OP_PICK, f.location!); + this.emit(Op.OP_PICK, locationData); } - // All functions are if-else statements, except the final one which is - // enforced with NUMEQUALVERIFY - this.emit(encodeInt(BigInt(i)), f.location!); - this.emit(Op.OP_NUMEQUAL, f.location!); + // All functions are if-else statements, except the final one which is enforced with NUMEQUALVERIFY + this.emit(encodeInt(BigInt(i)), locationData); + this.emit(Op.OP_NUMEQUAL, locationData); + if (i < node.functions.length - 1) { - this.emit(Op.OP_IF, f.location!); + this.emit(Op.OP_IF, locationData); } else { - this.emit(Op.OP_VERIFY, f.location!); + this.emit(Op.OP_VERIFY, locationData); } f = this.visit(f) as FunctionDefinitionNode; if (i < node.functions.length - 1) { - this.emit(Op.OP_ELSE, f.location!, 1); + this.emit(Op.OP_ELSE, { ...locationData, positionHint: PositionHint.END }); } this.stack = [...stackCopy]; return f; }); + for (let i = 0; i < node.functions.length - 1; i += 1) { - this.emit(Op.OP_ENDIF, node.location!, 1); + this.emit(Op.OP_ENDIF, { location: node.location, positionHint: PositionHint.END }); } } @@ -176,18 +188,18 @@ export default class GenerateTargetTraversalWithLocation extends AstTraversal { node.parameters = this.visitList(node.parameters) as ParameterNode[]; node.body = this.visit(node.body) as BlockNode; - this.removeFinalVerify(node.body); + this.removeFinalVerifyFromFunction(node.body); this.cleanStack(node.body); return node; } - removeFinalVerify(node: Node): void { + removeFinalVerifyFromFunction(functionBodyNode: Node): void { // After EnsureFinalRequireTraversal, we know that the final opcodes are either // "OP_VERIFY", "OP_CHECK{LOCKTIME|SEQUENCE}VERIFY OP_DROP" or "OP_ENDIF" const finalOp = this.output.pop() as Op; - const [location] = this.locationData.pop() as [Location, number?]; + const { location, positionHint } = this.locationData.pop()!; // If the final op is OP_VERIFY and the stack size is less than 4 we remove it from the script // - We have the stack size check because it is more efficient to use 2DROP rather than NIP @@ -196,23 +208,30 @@ export default class GenerateTargetTraversalWithLocation extends AstTraversal { // Since the final value is no longer popped from the stack by OP_VERIFY, // we add it back to the stack this.pushToStack('(value)'); + + // Replace the location data of the final check (e.g. (x == 1)) with the location data of the + // full require statement including the removed OP_VERIFY (e.g. require(x == 1)), because + // the check opcode (e.g. OP_EQUAL) now represents the entire require statement (including implicit OP_VERIFY) + this.locationData.pop(); + this.locationData.push({ location, positionHint }); } else { - this.emit(finalOp, location!, 1); + this.emit(finalOp, { location, positionHint: PositionHint.END }); // At this point there is no verification value left on the stack: // - scoped stack is cleared inside branch ended by OP_ENDIF // - OP_CHECK{LOCKTIME|SEQUENCE}VERIFY OP_DROP does not leave a verification value + // - OP_VERIFY does not leave a verification value // so we add OP_1 to the script (indicating success) - this.emit(Op.OP_1, node.location!, 1); + this.emit(Op.OP_1, { location: functionBodyNode.location, positionHint: PositionHint.END }); this.pushToStack('(value)'); } } - cleanStack(node: Node): void { + cleanStack(functionBodyNode: Node): void { // Keep final verification value, OP_NIP the other stack values const stackSize = this.stack.length; for (let i = 0; i < stackSize - 1; i += 1) { - this.emit(Op.OP_NIP, node.location!, 1); + this.emit(Op.OP_NIP, { location: functionBodyNode.location, positionHint: PositionHint.END }); this.nipFromStack(); } } @@ -252,33 +271,34 @@ export default class GenerateTargetTraversalWithLocation extends AstTraversal { // This algorithm can be optimised for hardcoded depths // See thesis for explanation emitReplace(index: number, node: Node): void { - this.emit(encodeInt(BigInt(index)), node.location!); - this.emit(Op.OP_ROLL, node.location!); - this.emit(Op.OP_DROP, node.location!); + const locationData = { location: node.location, positionHint: PositionHint.END }; + + this.emit(encodeInt(BigInt(index)), locationData); + this.emit(Op.OP_ROLL, locationData); + this.emit(Op.OP_DROP, locationData); for (let i = 0; i < index - 1; i += 1) { - this.emit(Op.OP_SWAP, node.location!); + this.emit(Op.OP_SWAP, locationData); if (i < index - 2) { - this.emit(Op.OP_TOALTSTACK, node.location!); + this.emit(Op.OP_TOALTSTACK, locationData); } } for (let i = 0; i < index - 2; i += 1) { - this.emit(Op.OP_FROMALTSTACK, node.location!); + this.emit(Op.OP_FROMALTSTACK, locationData); } } visitTimeOp(node: TimeOpNode): Node { // const countBefore = this.output.length; node.expression = this.visit(node.expression); - this.emit(compileTimeOp(node.timeOp), node.location!, 1); - - // add debug require message - if (node.message) { - this.requireMessages.push({ - ip: this.output.length + this.constructorParameterCount - 1, - line: node.location!.start.line, - message: node.message, - }); - } + this.emit(compileTimeOp(node.timeOp), { location: node.location, positionHint: PositionHint.END }); + + this.requires.push({ + // We're removing 1 from the IP because the error message needs to match the OP_XXX_VERIFY, not the OP_DROP that + // is emitted directly after + ip: this.getMostRecentInstructionPointer() - 1, + line: node.location.start.line, + message: node.message, + }); this.popFromStack(); return node; @@ -287,40 +307,74 @@ export default class GenerateTargetTraversalWithLocation extends AstTraversal { visitRequire(node: RequireNode): Node { node.expression = this.visit(node.expression); - this.emit(Op.OP_VERIFY, node.location!, 1); + this.emit(Op.OP_VERIFY, { location: node.location, positionHint: PositionHint.END }); - // add debug require message - if (node.message) { - this.requireMessages.push({ - ip: this.output.length + this.constructorParameterCount - 1, - line: node.location!.start.line, - message: node.message, - }); - } + this.requires.push({ + ip: this.getMostRecentInstructionPointer(), + line: node.location.start.line, + message: node.message, + }); this.popFromStack(); return node; } + visitConsoleStatement(node: ConsoleStatementNode): Node { + // We add a plus 1 to the most recent instruction pointer, because we want to log the state + // of the stack *after* the most recent instruction has been executed + const ip = this.getMostRecentInstructionPointer() + 1; + const { line } = node.location.start; + + const data = node.parameters.map((parameter: ConsoleParameterNode) => { + if (parameter instanceof IdentifierNode) { + const symbol = parameter.definition!; + + // If the variable is not on the stack, then we add the final stack usage to the console log + const stackIndex = this.getStackIndex(parameter.name, true); + if (stackIndex === -1) { + if (!this.finalStackUsage[parameter.name]) { + throw new Error(`Expected variable '${parameter.name}' does not exist on the stack or in final stack usage`); + } + return this.finalStackUsage[parameter.name]; + } + + // If the variable is on the stack, we add the stack index and type to the console log + const type = symbol.type.toString(); + return { stackIndex, type, ip }; + } + + return parameter.toString(); + }); + + this.consoleLogs.push({ ip, line, data }); + + return node; + } + visitBranch(node: BranchNode): Node { node.condition = this.visit(node.condition); this.popFromStack(); this.scopeDepth += 1; - this.emit(Op.OP_IF, node.ifBlock.location!); + this.emit(Op.OP_IF, { location: node.ifBlock.location, positionHint: PositionHint.START }); let stackDepth = this.stack.length; node.ifBlock = this.visit(node.ifBlock); - this.removeScopedVariables(stackDepth, node); + this.removeScopedVariables(stackDepth, node.ifBlock); if (node.elseBlock) { - this.emit(Op.OP_ELSE, node.elseBlock.location!, 1); + this.emit(Op.OP_ELSE, { location: node.elseBlock.location, positionHint: PositionHint.START }); stackDepth = this.stack.length; node.elseBlock = this.visit(node.elseBlock); - this.removeScopedVariables(stackDepth, node); + this.removeScopedVariables(stackDepth, node.elseBlock); } - this.emit(Op.OP_ENDIF, node.elseBlock ? node.elseBlock.location! : node.ifBlock.location!, 1); + const endLocationData = { + location: node.elseBlock ? node.elseBlock.location : node.ifBlock.location, + positionHint: PositionHint.END, + }; + + this.emit(Op.OP_ENDIF, endLocationData); this.scopeDepth -= 1; return node; @@ -329,7 +383,7 @@ export default class GenerateTargetTraversalWithLocation extends AstTraversal { removeScopedVariables(depthBeforeScope: number, node: Node): void { const dropCount = this.stack.length - depthBeforeScope; for (let i = 0; i < dropCount; i += 1) { - this.emit(Op.OP_DROP, node.location!); + this.emit(Op.OP_DROP, { location: node.location, positionHint: PositionHint.END }); this.popFromStack(); } } @@ -340,11 +394,14 @@ export default class GenerateTargetTraversalWithLocation extends AstTraversal { // Special case for sized bytes cast, since it has another node to traverse if (node.size) { node.size = this.visit(node.size); - this.emit(Op.OP_NUM2BIN, node.location!); + this.emit(Op.OP_NUM2BIN, { location: node.location, positionHint: PositionHint.END }); this.popFromStack(); } - this.emit(compileCast(node.expression.type as PrimitiveType, node.type), node.location!, 1); + this.emit( + compileCast(node.expression.type as PrimitiveType, node.type), + { location: node.location, positionHint: PositionHint.END }, + ); this.popFromStack(); this.pushToStack('(value)'); return node; @@ -357,7 +414,10 @@ export default class GenerateTargetTraversalWithLocation extends AstTraversal { node.parameters = this.visitList(node.parameters); - this.emit(compileGlobalFunction(node.identifier.name as GlobalFunction), node.location!, 1); + this.emit( + compileGlobalFunction(node.identifier.name as GlobalFunction), + { location: node.location, positionHint: PositionHint.END }, + ); this.popFromStack(node.parameters.length); this.pushToStack('(value)'); @@ -365,10 +425,10 @@ export default class GenerateTargetTraversalWithLocation extends AstTraversal { } visitMultiSig(node: FunctionCallNode): Node { - this.emit(encodeBool(false), node.location!); + this.emit(encodeBool(false), { location: node.location, positionHint: PositionHint.START }); this.pushToStack('(value)'); node.parameters = this.visitList(node.parameters); - this.emit(Op.OP_CHECKMULTISIG, node.location!, 1); + this.emit(Op.OP_CHECKMULTISIG, { location: node.location, positionHint: PositionHint.END }); const sigs = node.parameters[0] as ArrayNode; const pks = node.parameters[1] as ArrayNode; this.popFromStack(sigs.elements.length + pks.elements.length + 3); @@ -378,72 +438,77 @@ export default class GenerateTargetTraversalWithLocation extends AstTraversal { } visitInstantiation(node: InstantiationNode): Node { + if (node.identifier.name === Class.LOCKING_BYTECODE_P2PKH) { // OP_DUP OP_HASH160 OP_PUSH<20> - this.emit(hexToBin('76a914'), node.location!); + this.emit(hexToBin('76a914'), { location: node.location, positionHint: PositionHint.START }); this.pushToStack('(value)'); // this.visit(node.parameters[0]); - this.emit(Op.OP_CAT, node.location!); + this.emit(Op.OP_CAT, { location: node.location, positionHint: PositionHint.END }); // OP_EQUAL OP_CHECKSIG - this.emit(hexToBin('88ac'), node.location!); - this.emit(Op.OP_CAT, node.location!); + this.emit(hexToBin('88ac'), { location: node.location, positionHint: PositionHint.END }); + this.emit(Op.OP_CAT, { location: node.location, positionHint: PositionHint.END }); this.popFromStack(2); } else if (node.identifier.name === Class.LOCKING_BYTECODE_P2SH20) { // OP_HASH160 OP_PUSH<20> - this.emit(hexToBin('a914'), node.location!); + this.emit(hexToBin('a914'), { location: node.location, positionHint: PositionHint.START }); this.pushToStack('(value)'); //