├── .env.example ├── .eslintignore ├── .eslintrc ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── .vscode ├── launch.json └── tasks.json ├── README.md ├── package-lock.json ├── package.json ├── scripts └── testnet.sh ├── src └── contracts │ ├── accumulatorMultiSig.ts │ ├── ackermann.ts │ ├── acs.ts │ ├── auction.ts │ ├── cltv.ts │ ├── counter.ts │ ├── crowdfund.ts │ ├── demo.ts │ ├── erc20.ts │ ├── erc721.ts │ ├── hashPuzzle.ts │ ├── hashedMapNonState.ts │ ├── hashedMapState.ts │ ├── hashedSetNonState.ts │ ├── hashedSetState.ts │ ├── helloWorld.ts │ ├── mimc7.ts │ ├── montyHall.ts │ ├── p2pkh.ts │ └── recallable.ts ├── tests ├── local │ ├── accumulatorMultiSig.test.ts │ ├── ackerman.test.ts │ ├── acs.test.ts │ ├── auction.test.ts │ ├── cltv.test.ts │ ├── counter.test.ts │ ├── crowdfund.test.ts │ ├── demo.test.ts │ ├── erc20.test.ts │ ├── erc721.test.ts │ ├── hashPuzzle.test.ts │ ├── hashedMapNonState.test.ts │ ├── hashedMapState.test.ts │ ├── hashedSetNonState.test.ts │ ├── hashedSetState.test.ts │ ├── helloWorld.test.ts │ ├── mimc7.test.ts │ ├── multi_contracts_call.test.ts │ ├── p2pkh.test.ts │ └── recallable.test.ts ├── testnet │ ├── accumulatorMultiSig.ts │ ├── ackerman.ts │ ├── acs.ts │ ├── auction.ts │ ├── cltv.ts │ ├── counter.ts │ ├── counterFromTx.ts │ ├── demo.ts │ ├── erc721.ts │ ├── hashPuzzle.ts │ ├── hashedMapState.ts │ ├── helloWorld.ts │ ├── multi_contracts_call.ts │ ├── p2pkh.ts │ ├── p2pkhFromTx.ts │ └── recallable.ts └── utils │ ├── helper.ts │ └── privateKey.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | # You can fund its address from sCrypt faucet https://scrypt.io/#faucet 2 | # `npm run genprivkey` to generate a private key along with a testnet address 3 | PRIVATE_KEY="Your testnet private key in WIF format" -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | out/ 3 | dist/ 4 | artifacts/ 5 | test/out/ 6 | test/dist 7 | test/artifacts 8 | **/.env 9 | **/scrypt.index.json 10 | .vscode/* 11 | !.vscode/launch.json 12 | !.vscode/tasks.json -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint"], 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/eslint-recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "rules": { 11 | "prefer-rest-params": "off" // super(...arguments) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI-Test 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | build: 9 | strategy: 10 | matrix: 11 | os: [ubuntu-latest] 12 | runs-on: ${{ matrix.os }} 13 | steps: 14 | - name: Prepare git 15 | run: git config --global core.autocrlf false 16 | - name: Checkout 17 | uses: actions/checkout@v3 18 | with: 19 | submodules: true 20 | - name: Install Node.js 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: 16 24 | - name: Npm Install 25 | run: npm i 26 | - name: Test Generate private key 27 | run: npm run genprivkey 28 | - name: gen Env 29 | run: echo "PRIVATE_KEY="${{ secrets.PRIVATE_KEY }}"" > .env 30 | - name: Test 31 | run: npm run build && npm t 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | out/ 4 | .vscode/* 5 | !.vscode/launch.json 6 | !.vscode/tasks.json 7 | artifacts/ 8 | .env 9 | scrypt.index.json 10 | .eslintcache 11 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # This loads nvm.sh and sets the correct PATH before running hook 4 | export NVM_DIR="$HOME/.nvm" 5 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" 6 | 7 | . "$(dirname -- "$0")/_/husky.sh" 8 | 9 | npx lint-staged 10 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/node_modules/ 2 | **/out/ 3 | **/dist/ 4 | **/artifacts/ 5 | **/test/out/ 6 | **/test/dist 7 | **/test/artifacts 8 | **/.env 9 | **/scrypt.index.json 10 | **/.vscode/* 11 | !**/.vscode/launch.json 12 | !**/.vscode/tasks.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "auto", 3 | "overrides": [ 4 | { 5 | "files": "*.ts", 6 | "options": { 7 | "semi": false, 8 | "printWidth": 80, 9 | "tabWidth": 4, 10 | "useTabs": false, 11 | "singleQuote": true, 12 | "trailingComma": "es5", 13 | "bracketSpacing": true 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: focal 2 | language: node_js 3 | node_js: 4 | - 16 5 | - 18 6 | - 19 7 | 8 | before_install: 9 | 10 | os: 11 | - linux 12 | - osx 13 | - windows 14 | 15 | osx_image: 16 | - xcode9.4 17 | - xcode14 18 | 19 | jobs: 20 | exclude: 21 | - osx_image: xcode9.4 22 | node_js: 18 23 | - osx_image: xcode9.4 24 | node_js: 19 25 | - osx_image: xcode14 26 | node_js: 16 27 | # Only test on linux for regular test jobs 28 | - if: branch != master 29 | node_js: 18 30 | - if: branch != master 31 | node_js: 19 32 | - if: type = pull_request 33 | os: osx 34 | - if: type = pull_request 35 | os: windows 36 | 37 | script: 38 | - npm run genprivkey 39 | - npm t 40 | - echo "PRIVATE_KEY=$PRIVATE_KEY" > .env 41 | - if [ "$TRAVIS_EVENT_TYPE" == "cron" ] ; then sh ./scripts/testnet.sh; fi 42 | 43 | notifications: 44 | slack: 45 | rooms: 46 | - secure: CbhzWXY5zlDrpX5v3hws7rtO6piA5uJ3nfd4VCoVIvSAjTFX6a1sMbjZG4BUBiCIRQShqYjEbJYuVWRmm/O+xcPAsM08RuQy0TPYt/0EA0LPCO/VMwouuf8bOQ/LO9rKSXLSLR0NaC/SwdWeIWChanAXMqHmGk+TOgOKr9fUzDNYCN8/ELk8ki9C1WmJlUW+HmfkTbMu6lup6hY0vStY0gLoT8/No71MbLJ33yHPZicHYxXee3nGSX/vUdfd+hs9KTsVIo56cecTdWoOSD0KYq6y4nNsYKtRke8fnX32au238jGDWypXBSGU8IxDGIPNE4iwpxQ1WID27xZNGPIYLBwnYRMcGMC3ApSgQLTuXqqNU8ppJQ1UbleWZDNj1zbelt+7aHUNQZy3zuaPcckHF6LjZI8paETQvkzoaOGALwOtS1aWUn0GbqkRHjCFiD+GajZ+L3Zi8CKxpPv//ANHXvHQ+EX+9mPF+/vCBq0ZErIktwDbSN4J2iN19Zuqdzl25OGHkBc6q22r9Q2hWenzwe8iuutqEkHVHUy2KTeVnNn4SqQefXvvf+aKFuPD63HE9nN3PFWB3vlFMPV7/8KFFb1JJw/104z+1VYPQt9cJSIMralofV2JtabAN+ui7lnhvPgYA808spDu7JH7gPMDLMJWRP2rTqfsz4j77eurgQs= 47 | on_success: change 48 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch demo", 11 | "skipFiles": ["/**", "**/node_modules/**"], 12 | "resolveSourceMapLocations": ["!**/node_modules/**"], 13 | "program": "${workspaceRoot}/src/contracts/demo.ts", 14 | "preLaunchTask": "tsc: build - tsconfig.json", 15 | "outFiles": ["${workspaceRoot}/dist/**/*.js"] 16 | }, 17 | { 18 | "type": "node", 19 | "request": "launch", 20 | "name": "Launch demo test", 21 | "skipFiles": ["/**", "**/node_modules/**"], 22 | "resolveSourceMapLocations": ["!**/node_modules/**"], 23 | "program": "${workspaceRoot}/node_modules/.bin/_mocha", 24 | "args": [ 25 | "${workspaceRoot}/dist/tests/**/demo.test.js", 26 | "--colors", 27 | "-t", 28 | "100000" 29 | ], 30 | "preLaunchTask": "tsc: build - tsconfig.json", 31 | "outFiles": [] 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Build", 6 | "detail": "Build the project which includes `scrypt-ts` smart contracts.", 7 | "type": "typescript", 8 | "tsconfig": "./tsconfig.json", 9 | "option": "watch", 10 | "runOptions": { 11 | "runOn": "folderOpen" 12 | }, 13 | "problemMatcher": [ 14 | "$tsc", 15 | "$tsc-watch", 16 | { 17 | "owner": "scrypt-ts", // the last problem may not be removed from problems view due to this bug: https://github.com/microsoft/vscode/issues/164751, restart the task is a temp solution. 18 | "fileLocation": "autoDetect", 19 | "pattern": { 20 | "regexp": "^scrypt-ts (ERROR|WARNING) - (.*):(\\d+):(\\d+):(\\d+):(\\d+) -\\s+(.*)$", 21 | "severity": 1, 22 | "file": 2, 23 | "line": 3, 24 | "column": 4, 25 | "endLine": 5, 26 | "endColumn": 6, 27 | "message": 7 28 | } 29 | } 30 | ], 31 | "group": { 32 | "kind": "build", 33 | "isDefault": true 34 | } 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CI-Test](https://github.com/sCrypt-Inc/scryptTS-examples/actions/workflows/ci.yml/badge.svg)](https://github.com/sCrypt-Inc/scryptTS-examples/actions/workflows/ci.yml) 2 | [![Build Status](https://app.travis-ci.com/sCrypt-Inc/scryptTS-examples.svg?branch=master)](https://app.travis-ci.com/sCrypt-Inc/scryptTS-examples) 3 | 4 | # DEPRECATION WARNING 5 | 6 | This repository is deprecated. Please go to [boilerplate](https://github.com/sCrypt-Inc/boilerplate). 7 | 8 | --- 9 | 10 | A collection of smart contract examples along with tests, implemented in [scryptTS](https://scrypt.io/scryptTS), a Typescript framework to write smart contracts on Bitcoin. 11 | 12 | Install all dependencies first. 13 | 14 | ```sh 15 | npm install 16 | ``` 17 | 18 | ## Local Tests 19 | 20 | In order to run smart contract tests locally, just simply run: 21 | 22 | ```sh 23 | npm test 24 | ``` 25 | 26 | This will run every test defined under `tests/local/`. 27 | 28 | To run tests for a specific smart contract, i.e. the `Counter` smart contract, run the following: 29 | 30 | ```sh 31 | npm run build && npx mocha 'dist/tests/local/counter.test.js' 32 | ``` 33 | 34 | To understand how these tests work, please read the [scryptTS docs](https://scrypt.io/scrypt-ts/how-to-test-a-contract). 35 | 36 | ## Test on the Bitcoin Testnet 37 | 38 | This repository also contains tests for the testnet. They will deploy and call contracts on chain, so you first need to have some testnet coins. 39 | 40 | First, generate a private key along with an address: 41 | 42 | ``` 43 | npm run genprivkey 44 | ``` 45 | 46 | This will store a private key in a file named `.env`. Additionally, it will output its address to the console, which you can then fund from a [faucet](https://scrypt.io/#faucet). 47 | 48 | Once the address is funded, you can either run 49 | 50 | ```sh 51 | npm run testnet 52 | ``` 53 | 54 | to run all defined testnet tests, or: 55 | 56 | ```sh 57 | npm run build && npx mocha 'dist/tests/testnet/.js' 58 | ``` 59 | 60 | to run a specific contract test. 61 | 62 | ## Debug Smart Contract Code 63 | 64 | In order to debug smart contract code in [Visual Studio Code](https://code.visualstudio.com), you need to configure `launch.json` (located under `.vscode/`). This repository already has an example configuration for the `Demo` smart contract. 65 | 66 | See the [docs](https://scrypt.io/scrypt-ts/how-to-debug-a-contract/#use-visual-studio-code-debugger) for more information on how to use the debugger. 67 | 68 | ## Project Structure 69 | 70 | - `src/contracts` - This is where all the smart contract code is. Each file is for a separate smart contract example, e.g. the `P2PKH` smart contract is defined inside `src/contracts/p2pkh.ts`. 71 | - `tests/local` - This is the directory which contains smart contract tests that get executed locally. Each smart contract has its separate test file. 72 | - `tests/testnet` - This is the directory which contains smart contract tests that get broadcast to the Bitcoin testnet. 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scrypt-ts-example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "prebuild": "npm run clean", 8 | "build": "tsc", 9 | "clean": "rimraf scrypt.index.json && rimraf dist && rimraf artifacts", 10 | "pretest": "npm run build", 11 | "test": "mocha 'dist/tests/**/*.test.js' --timeout 1200000", 12 | "testnet": "npm run build && mocha 'dist/tests/testnet/**/*.js' --timeout 1200000", 13 | "lint": "eslint . --ext .js,.ts --fix && prettier --write --ignore-unknown \"**/*\"", 14 | "lint-check": "eslint . --ext .js,.ts && prettier --check --ignore-unknown \"**/*\"", 15 | "prepare": "husky install", 16 | "genprivkey": "npm run build && node dist/tests/utils/privateKey.js" 17 | }, 18 | "lint-staged": { 19 | "**/*": [ 20 | "prettier --write --ignore-unknown" 21 | ], 22 | "**/*.{ts,js}": [ 23 | "eslint --cache --fix" 24 | ] 25 | }, 26 | "author": "", 27 | "license": "MIT", 28 | "dependencies": { 29 | "scrypt-ts": "beta", 30 | "scrypt-ts-lib": "^0.1.12" 31 | }, 32 | "devDependencies": { 33 | "@types/chai": "^4.3.4", 34 | "@types/chai-as-promised": "^7.1.5", 35 | "@types/mocha": "^10.0.0", 36 | "@types/node": "^18.11.9", 37 | "@typescript-eslint/eslint-plugin": "^5.48.1", 38 | "@typescript-eslint/parser": "^5.48.1", 39 | "axios": "^1.3.2", 40 | "chai": "^4.3.7", 41 | "chai-as-promised": "^7.1.1", 42 | "cross-env": "^7.0.3", 43 | "dotenv": "^16.0.3", 44 | "eslint": "^8.31.0", 45 | "eslint-config-prettier": "^8.6.0", 46 | "husky": "^8.0.3", 47 | "lint-staged": "^13.1.0", 48 | "mocha": "^10.1.0", 49 | "prettier": "^2.8.2", 50 | "rimraf": "^3.0.2", 51 | "typescript": "=4.8.4" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /scripts/testnet.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | day_mod=`expr $(date +%d) % 9` 5 | 6 | echo "day_mod: $day_mod" 7 | 8 | if [ $day_mod -eq 0 ]; then 9 | if [ "$TRAVIS_OS_NAME" -eq "linux" ] && [ "$TRAVIS_NODE_VERSION" -eq "16" ] ; then 10 | echo "run on linux, nodejs=16"; 11 | npm run testnet; 12 | fi 13 | elif [ $day_mod -eq 1 ]; then 14 | if [ "$TRAVIS_OS_NAME" -eq "linux" ] && [ "$TRAVIS_NODE_VERSION" -eq "18" ] ; then 15 | echo "run on linux, nodejs=18"; 16 | npm run testnet; 17 | fi 18 | elif [ $day_mod -eq 2 ]; then 19 | if [ "$TRAVIS_OS_NAME" -eq "linux" ] && [ "$TRAVIS_NODE_VERSION" -eq "19" ] ; then 20 | echo "run on linux, nodejs=19"; 21 | npm run testnet; 22 | fi 23 | elif [ $day_mod -eq 3 ]; then 24 | if [ "$TRAVIS_OS_NAME" -eq "osx" ] && [ "$TRAVIS_NODE_VERSION" -eq "16" ] ; then 25 | echo "run on osx, nodejs=16"; 26 | npm run testnet; 27 | fi 28 | elif [ $day_mod -eq 4 ]; then 29 | if [ "$TRAVIS_OS_NAME" -eq "osx" ] && [ "$TRAVIS_NODE_VERSION" -eq "18" ] ; then 30 | echo "run on osx, nodejs=18"; 31 | npm run testnet; 32 | fi 33 | elif [ $day_mod -eq 5 ]; then 34 | if [ "$TRAVIS_OS_NAME" -eq "osx" ] && [ "$TRAVIS_NODE_VERSION" -eq "19" ] ; then 35 | echo "run on osx, nodejs=19"; 36 | npm run testnet; 37 | fi 38 | elif [ $day_mod -eq 6 ]; then 39 | if [ "$TRAVIS_OS_NAME" -eq "windows" ] && [ "$TRAVIS_NODE_VERSION" -eq "16" ] ; then 40 | echo "run on windows, nodejs=16"; 41 | npm run testnet; 42 | fi 43 | elif [ $day_mod -eq 7 ]; then 44 | if [ "$TRAVIS_OS_NAME" -eq "windows" ] && [ "$TRAVIS_NODE_VERSION" -eq "18" ] ; then 45 | echo "run on windows, nodejs=18"; 46 | npm run testnet; 47 | fi 48 | elif [ $day_mod -eq 8 ]; then 49 | if [ "$TRAVIS_OS_NAME" -eq "windows" ] && [ "$TRAVIS_NODE_VERSION" -eq "19" ] ; then 50 | echo "run on windows, nodejs=19"; 51 | npm run testnet; 52 | fi 53 | else 54 | echo "day_mod: error" 55 | exit -1; 56 | fi -------------------------------------------------------------------------------- /src/contracts/accumulatorMultiSig.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assert, 3 | FixedArray, 4 | hash160, 5 | method, 6 | prop, 7 | PubKey, 8 | PubKeyHash, 9 | Sig, 10 | SmartContract, 11 | } from 'scrypt-ts' 12 | 13 | export class AccumulatorMultiSig extends SmartContract { 14 | // Number of multi sig participants. 15 | static readonly N = 3 16 | 17 | // Threshold of the signatures needed. 18 | @prop() 19 | readonly threshold: bigint 20 | 21 | // Addresses. 22 | @prop() 23 | readonly pubKeyHashes: FixedArray 24 | 25 | constructor( 26 | threshold: bigint, 27 | pubKeyHashes: FixedArray 28 | ) { 29 | super(...arguments) 30 | this.threshold = threshold 31 | this.pubKeyHashes = pubKeyHashes 32 | } 33 | 34 | @method() 35 | public main( 36 | pubKeys: FixedArray, 37 | sigs: FixedArray, 38 | masks: FixedArray // Mask the unused signatures with `false` 39 | ) { 40 | let total = 0n 41 | for (let i = 0; i < AccumulatorMultiSig.N; i++) { 42 | if (masks[i]) { 43 | if ( 44 | // Ensure the public key belongs to the specified address. 45 | hash160(pubKeys[i]) == this.pubKeyHashes[i] && 46 | // Check the signature 47 | this.checkSig(sigs[i], pubKeys[i]) 48 | ) { 49 | total++ // Increment the number of successful signature checks. 50 | } 51 | } 52 | } 53 | assert( 54 | total >= this.threshold, 55 | 'the number of signatures does not meet the threshold limit' 56 | ) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/contracts/ackermann.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assert, 3 | ByteString, 4 | byteString2Int, 5 | int2ByteString, 6 | len, 7 | method, 8 | prop, 9 | SmartContract, 10 | } from 'scrypt-ts' 11 | 12 | export class Ackermann extends SmartContract { 13 | // Maximum number of iterations of the Ackermann function. 14 | // This needs to be finite due to the constraints of the Bitcoin virtual machine. 15 | static readonly LOOP_COUNT = 14n 16 | 17 | // Input parameters of the Ackermann function. 18 | @prop() 19 | readonly a: bigint 20 | @prop() 21 | readonly b: bigint 22 | 23 | constructor(a: bigint, b: bigint) { 24 | super(...arguments) 25 | this.a = a 26 | this.b = b 27 | } 28 | 29 | @method() 30 | ackermann(m: bigint, n: bigint): bigint { 31 | let stk: ByteString = int2ByteString(m, 1n) 32 | 33 | for (let i = 0; i < Ackermann.LOOP_COUNT; i++) { 34 | if (len(stk) > 0) { 35 | const top: ByteString = stk.slice(0, 2) 36 | m = byteString2Int(top) 37 | 38 | // pop 39 | stk = stk.slice(2, len(stk) * 2) 40 | 41 | if (m == 0n) { 42 | n = n + m + 1n 43 | } else if (n == 0n) { 44 | n++ 45 | m-- 46 | // push 47 | stk = int2ByteString(m, 1n) + stk 48 | } else { 49 | stk = int2ByteString(m - 1n, 1n) + stk 50 | stk = int2ByteString(m, 1n) + stk 51 | n-- 52 | } 53 | } 54 | } 55 | 56 | return n 57 | } 58 | 59 | // This method can only be unlocked if the right solution to ackermann(a, b) is provided. 60 | @method() 61 | public unlock(y: bigint) { 62 | assert(y == this.ackermann(this.a, this.b), 'Wrong solution.') 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/contracts/acs.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assert, 3 | ByteString, 4 | hash256, 5 | method, 6 | prop, 7 | PubKeyHash, 8 | SigHash, 9 | SmartContract, 10 | Utils, 11 | } from 'scrypt-ts' 12 | 13 | export class AnyoneCanSpend extends SmartContract { 14 | // Address of the recipient. 15 | @prop() 16 | readonly pubKeyHash: PubKeyHash 17 | 18 | constructor(pubKeyHash: PubKeyHash) { 19 | super(...arguments) 20 | this.pubKeyHash = pubKeyHash 21 | } 22 | 23 | @method(SigHash.ANYONECANPAY_SINGLE) 24 | public unlock() { 25 | const output: ByteString = Utils.buildPublicKeyHashOutput( 26 | this.pubKeyHash, 27 | this.changeAmount 28 | ) 29 | assert( 30 | hash256(output) == this.ctx.hashOutputs, 31 | 'hashOutputs check failed' 32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/contracts/auction.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assert, 3 | MethodCallOptions, 4 | ContractTransaction, 5 | ByteString, 6 | hash256, 7 | method, 8 | prop, 9 | PubKey, 10 | Sig, 11 | SmartContract, 12 | Utils, 13 | UTXO, 14 | bsv, 15 | hash160, 16 | } from 'scrypt-ts' 17 | 18 | import Transaction = bsv.Transaction 19 | import Address = bsv.Address 20 | import Script = bsv.Script 21 | 22 | export class Auction extends SmartContract { 23 | static readonly LOCKTIME_BLOCK_HEIGHT_MARKER = 500000000 24 | static readonly UINT_MAX = 0xffffffffn 25 | 26 | // The bidder's public key. 27 | @prop(true) 28 | bidder: PubKey 29 | 30 | // The auctioneer's public key. 31 | @prop() 32 | readonly auctioneer: PubKey 33 | 34 | // Deadline of the auction. Can be block height or timestamp. 35 | @prop() 36 | readonly auctionDeadline: bigint 37 | 38 | constructor(auctioneer: PubKey, auctionDeadline: bigint) { 39 | super(...arguments) 40 | this.bidder = auctioneer 41 | this.auctioneer = auctioneer 42 | this.auctionDeadline = auctionDeadline 43 | } 44 | 45 | // Call this public method to bid with a higher offer. 46 | @method() 47 | public bid(bidder: PubKey, bid: bigint) { 48 | const highestBid: bigint = this.ctx.utxo.value 49 | assert( 50 | bid > highestBid, 51 | 'the auction bid is lower than the current highest bid' 52 | ) 53 | 54 | // Change the public key of the highest bidder. 55 | const highestBidder: PubKey = this.bidder 56 | this.bidder = bidder 57 | 58 | // Auction continues with a higher bidder. 59 | const auctionOutput: ByteString = this.buildStateOutput(bid) 60 | 61 | // Refund previous highest bidder. 62 | const refundOutput: ByteString = Utils.buildPublicKeyHashOutput( 63 | hash160(highestBidder), 64 | highestBid 65 | ) 66 | let outputs: ByteString = auctionOutput + refundOutput 67 | 68 | // Add change output. 69 | if (this.changeAmount > 0) { 70 | outputs += this.buildChangeOutput() 71 | } 72 | 73 | assert( 74 | hash256(outputs) == this.ctx.hashOutputs, 75 | 'hashOutputs check failed' 76 | ) 77 | } 78 | 79 | // Close the auction if deadline is reached. 80 | @method() 81 | public close(sig: Sig) { 82 | // Check if using block height. 83 | if (this.auctionDeadline < Auction.LOCKTIME_BLOCK_HEIGHT_MARKER) { 84 | // Enforce nLocktime field to also use block height. 85 | assert(this.ctx.locktime < Auction.LOCKTIME_BLOCK_HEIGHT_MARKER) 86 | } 87 | assert( 88 | this.ctx.sequence < Auction.UINT_MAX, 89 | 'input sequence should less than UINT_MAX' 90 | ) 91 | assert( 92 | this.ctx.locktime >= this.auctionDeadline, 93 | 'auction is not over yet' 94 | ) 95 | 96 | // Check signature of the auctioneer. 97 | assert(this.checkSig(sig, this.auctioneer), 'signature check failed') 98 | } 99 | 100 | // Customize the deployment tx by overriding `SmartContract.buildDeployTransaction` method 101 | override async buildDeployTransaction( 102 | utxos: UTXO[], 103 | amount: number, 104 | changeAddress?: Address | string 105 | ): Promise { 106 | const deployTx = new Transaction() 107 | // add p2pkh inputs 108 | .from(utxos) 109 | // add contract output 110 | .addOutput( 111 | new Transaction.Output({ 112 | script: this.lockingScript, 113 | satoshis: amount, 114 | }) 115 | ) 116 | // add OP_RETURN output 117 | .addData('Hello World') 118 | 119 | if (changeAddress) { 120 | deployTx.change(changeAddress) 121 | if (this._provider) { 122 | deployTx.feePerKb(await this.provider.getFeePerKb()) 123 | } 124 | } 125 | 126 | return deployTx 127 | } 128 | 129 | // User defined transaction builder for calling function `bid` 130 | static bidTxBuilder( 131 | current: Auction, 132 | options: MethodCallOptions, 133 | bidder: PubKey, 134 | bid: bigint 135 | ): Promise { 136 | const nextInstance = current.next() 137 | nextInstance.bidder = bidder 138 | 139 | const unsignedTx: Transaction = new Transaction() 140 | // add contract input 141 | .addInput(current.buildContractInput(options.fromUTXO)) 142 | // build next instance output 143 | .addOutput( 144 | new Transaction.Output({ 145 | script: nextInstance.lockingScript, 146 | satoshis: Number(bid), 147 | }) 148 | ) 149 | // build refund output 150 | .addOutput( 151 | new Transaction.Output({ 152 | script: Script.fromHex( 153 | Utils.buildPublicKeyHashScript(hash160(current.bidder)) 154 | ), 155 | satoshis: 156 | options.fromUTXO?.satoshis ?? 157 | current.from.tx.outputs[current.from.outputIndex] 158 | .satoshis, 159 | }) 160 | ) 161 | // build change output 162 | .change(options.changeAddress) 163 | 164 | return Promise.resolve({ 165 | tx: unsignedTx, 166 | atInputIndex: 0, 167 | nexts: [ 168 | { 169 | instance: nextInstance, 170 | atOutputIndex: 0, 171 | balance: Number(bid), 172 | }, 173 | ], 174 | }) 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/contracts/cltv.ts: -------------------------------------------------------------------------------- 1 | import { assert, method, prop, SmartContract } from 'scrypt-ts' 2 | 3 | export class CheckLockTimeVerify extends SmartContract { 4 | static readonly LOCKTIME_BLOCK_HEIGHT_MARKER = 500000000 5 | static readonly UINT_MAX = 0xffffffffn 6 | 7 | @prop() 8 | readonly matureTime: bigint // Can be a timestamp or block height. 9 | 10 | constructor(matureTime: bigint) { 11 | super(matureTime) 12 | this.matureTime = matureTime 13 | } 14 | 15 | @method() 16 | public unlock() { 17 | // Ensure nSequence is less than UINT_MAX. 18 | assert( 19 | this.ctx.sequence < CheckLockTimeVerify.UINT_MAX, 20 | 'input sequence should less than UINT_MAX' 21 | ) 22 | 23 | // Check if using block height. 24 | if ( 25 | this.matureTime < CheckLockTimeVerify.LOCKTIME_BLOCK_HEIGHT_MARKER 26 | ) { 27 | // Enforce nLocktime field to also use block height. 28 | assert( 29 | this.ctx.locktime < 30 | CheckLockTimeVerify.LOCKTIME_BLOCK_HEIGHT_MARKER, 31 | 'locktime should be less than 500000000' 32 | ) 33 | } 34 | assert( 35 | this.ctx.locktime >= this.matureTime, 36 | 'locktime has not yet expired' 37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/contracts/counter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assert, 3 | ByteString, 4 | hash256, 5 | method, 6 | prop, 7 | SigHash, 8 | SmartContract, 9 | } from 'scrypt-ts' 10 | 11 | export class Counter extends SmartContract { 12 | // Stateful prop to store counters value. 13 | @prop(true) 14 | count: bigint 15 | 16 | constructor(count: bigint) { 17 | super(...arguments) 18 | this.count = count 19 | } 20 | 21 | // ANYONECANPAY_SINGLE is used here to ignore all inputs and outputs, other than the ones contains the state 22 | // see https://scrypt.io/scrypt-ts/getting-started/what-is-scriptcontext#sighash-type 23 | @method(SigHash.ANYONECANPAY_SINGLE) 24 | public incrementOnChain() { 25 | // Increment counter value 26 | this.increment() 27 | 28 | // make sure balance in the contract does not change 29 | const amount: bigint = this.ctx.utxo.value 30 | // output containing the latest state 31 | const output: ByteString = this.buildStateOutput(amount) 32 | // verify current tx has this single output 33 | assert(this.ctx.hashOutputs == hash256(output), 'hashOutputs mismatch') 34 | } 35 | 36 | @method() 37 | increment(): void { 38 | this.count++ 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/contracts/crowdfund.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assert, 3 | hash160, 4 | hash256, 5 | method, 6 | prop, 7 | PubKey, 8 | Sig, 9 | SmartContract, 10 | Utils, 11 | } from 'scrypt-ts' 12 | 13 | export class Crowdfund extends SmartContract { 14 | static readonly LOCKTIME_BLOCK_HEIGHT_MARKER = 500000000 15 | static readonly UINT_MAX = 0xffffffffn 16 | 17 | @prop() 18 | readonly recipient: PubKey 19 | 20 | @prop() 21 | readonly contributor: PubKey 22 | 23 | @prop() 24 | readonly deadline: bigint 25 | 26 | @prop() 27 | readonly target: bigint 28 | 29 | constructor( 30 | recipient: PubKey, 31 | contributor: PubKey, 32 | deadline: bigint, 33 | target: bigint 34 | ) { 35 | super(...arguments) 36 | this.recipient = recipient 37 | this.contributor = contributor 38 | this.deadline = deadline 39 | this.target = target 40 | } 41 | 42 | // Method to collect pledged fund. 43 | @method() 44 | public collect(sig: Sig) { 45 | // Ensure the collected amount actually reaches the target. 46 | assert(this.ctx.utxo.value >= this.target) 47 | // Funds go to the recipient. 48 | const output = Utils.buildPublicKeyHashOutput( 49 | hash160(this.recipient), 50 | this.changeAmount 51 | ) 52 | // Ensure the payment output to the recipient is actually in the unlocking TX. 53 | assert( 54 | hash256(output) == this.ctx.hashOutputs, 55 | 'hashOutputs check failed' 56 | ) 57 | // Validate signature of recipient 58 | assert( 59 | this.checkSig(sig, this.recipient), 60 | 'recipient signature check failed' 61 | ) 62 | } 63 | 64 | // Contributors can be refunded after the deadline. 65 | @method() 66 | public refund(sig: Sig) { 67 | // Require nLocktime enabled https://wiki.bitcoinsv.io/index.php/NLocktime_and_nSequence 68 | assert( 69 | this.ctx.sequence < Crowdfund.UINT_MAX, 70 | 'require nLocktime enabled' 71 | ) 72 | 73 | // Check if using block height. 74 | if (this.deadline < Crowdfund.LOCKTIME_BLOCK_HEIGHT_MARKER) { 75 | // Enforce nLocktime field to also use block height. 76 | assert(this.ctx.locktime < Crowdfund.LOCKTIME_BLOCK_HEIGHT_MARKER) 77 | } 78 | assert(this.ctx.locktime >= this.deadline, 'fundraising expired') 79 | assert( 80 | this.checkSig(sig, this.contributor), 81 | 'contributor signature check failed' 82 | ) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/contracts/demo.ts: -------------------------------------------------------------------------------- 1 | import { assert, method, prop, SmartContract } from 'scrypt-ts' 2 | 3 | export class Demo extends SmartContract { 4 | @prop() 5 | readonly x: bigint 6 | 7 | @prop() 8 | readonly y: bigint 9 | 10 | // The values of the x and y properties get passed via the 11 | // smart contract's constructor. 12 | constructor(x: bigint, y: bigint) { 13 | super(...arguments) 14 | this.x = x 15 | this.y = y 16 | } 17 | 18 | // Contract internal method to compute x + y 19 | @method() 20 | sum(a: bigint, b: bigint): bigint { 21 | return a + b 22 | } 23 | 24 | // Public method which can be unlocked by providing the solution to x + y 25 | @method() 26 | public add(z: bigint) { 27 | assert(z == this.sum(this.x, this.y), 'add check failed') 28 | } 29 | 30 | // Public method which can be unlocked by providing the solution to x - y 31 | @method() 32 | public sub(z: bigint) { 33 | assert(z == this.x - this.y, 'sub check failed') 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/contracts/erc20.ts: -------------------------------------------------------------------------------- 1 | import { 2 | method, 3 | SigHash, 4 | prop, 5 | SmartContract, 6 | assert, 7 | HashedMap, 8 | ByteString, 9 | PubKeyHash, 10 | toByteString, 11 | hash256, 12 | PubKey, 13 | Sig, 14 | hash160, 15 | } from 'scrypt-ts' 16 | 17 | export type BalanceMap = HashedMap 18 | 19 | export type Allowance = { 20 | owner: PubKeyHash 21 | spender: PubKeyHash 22 | } 23 | 24 | export type AllowanceMap = HashedMap 25 | 26 | export type ERC20Pair = { 27 | address: PubKeyHash 28 | balance: bigint 29 | } 30 | 31 | export class ERC20 extends SmartContract { 32 | @prop(true) 33 | balances: BalanceMap 34 | 35 | @prop(true) 36 | allowances: AllowanceMap 37 | 38 | @prop() 39 | name: ByteString 40 | 41 | @prop() 42 | symbol: ByteString 43 | 44 | @prop() 45 | decimals: bigint 46 | 47 | @prop(true) 48 | totalSupply: bigint 49 | 50 | @prop() 51 | issuer: PubKey 52 | 53 | @prop() 54 | static readonly EMPTY_ADDR: PubKeyHash = PubKeyHash( 55 | toByteString('0000000000000000000000000000000000000000') 56 | ) 57 | 58 | constructor( 59 | name: ByteString, 60 | symbol: ByteString, 61 | issuer: PubKey, 62 | balances: BalanceMap, 63 | allowances: AllowanceMap 64 | ) { 65 | super(...arguments) 66 | this.name = name 67 | this.symbol = symbol 68 | this.decimals = 18n 69 | this.totalSupply = 0n 70 | this.issuer = issuer 71 | this.balances = balances 72 | this.allowances = allowances 73 | } 74 | 75 | /** 76 | * Creates `amount` tokens and assigns them to `issuer`, increasing 77 | * the total supply. 78 | * @param sig 79 | * @param issuerBalance 80 | * @param amount 81 | */ 82 | @method(SigHash.SINGLE) 83 | public mint(sig: Sig, issuerBalance: bigint, amount: bigint) { 84 | const address = hash160(this.issuer) 85 | assert( 86 | this.checkSig(sig, this.issuer), 87 | 'ERC20: check issuer signature failed' 88 | ) 89 | if (this.totalSupply === 0n) { 90 | this.balances.set(address, amount) 91 | this.totalSupply = amount 92 | } else { 93 | assert( 94 | this.balances.canGet(address, issuerBalance), 95 | 'ERC20: can not get balance from issuer address' 96 | ) 97 | this.balances.set(address, issuerBalance + amount) 98 | this.totalSupply += amount 99 | } 100 | assert( 101 | this.ctx.hashOutputs == 102 | hash256(this.buildStateOutput(this.ctx.utxo.value)) 103 | ) 104 | } 105 | 106 | /** 107 | * check the owner's balance 108 | * @param owner 109 | * @param balance 110 | */ 111 | @method(SigHash.SINGLE) 112 | public balanceOf(owner: PubKeyHash, balance: bigint) { 113 | assert( 114 | this.balances.canGet(owner, balance), 115 | 'ERC20: can not get balance from owner address' 116 | ) 117 | assert( 118 | this.ctx.hashOutputs == 119 | hash256(this.buildStateOutput(this.ctx.utxo.value)) 120 | ) 121 | } 122 | 123 | /** 124 | * transfer token from owner to receiver 125 | * @param from owner's address and balance 126 | * @param pubkey owner's public key 127 | * @param sig owner's signature 128 | * @param to receiver's address and balance 129 | * @param amount amount of token, the owner must have a balance of at least `amount`. 130 | */ 131 | @method(SigHash.SINGLE) 132 | public transfer( 133 | from: ERC20Pair, 134 | pubkey: PubKey, 135 | sig: Sig, 136 | to: ERC20Pair, 137 | amount: bigint 138 | ) { 139 | assert( 140 | from.address != ERC20.EMPTY_ADDR, 141 | 'ERC20: transfer from the zero address' 142 | ) 143 | assert( 144 | to.address != ERC20.EMPTY_ADDR, 145 | 'ERC20: transfer to the zero address' 146 | ) 147 | assert( 148 | this.balances.canGet(from.address, from.balance), 149 | 'ERC20: can not get balance from sender address' 150 | ) 151 | assert(from.balance >= amount, 'ERC20: transfer amount exceeds balance') 152 | 153 | assert(hash160(pubkey) == from.address, 'ERC20: check signature failed') 154 | 155 | assert(this.checkSig(sig, pubkey), 'ERC20: check signature failed') 156 | 157 | this.balances.set(from.address, from.balance - amount) 158 | 159 | if (this.balances.canGet(to.address, to.balance)) { 160 | this.balances.set(to.address, to.balance + amount) 161 | } else { 162 | this.balances.set(to.address, amount) 163 | } 164 | 165 | assert( 166 | this.ctx.hashOutputs == 167 | hash256(this.buildStateOutput(this.ctx.utxo.value)), 168 | 'check hashOutputs failed' 169 | ) 170 | } 171 | 172 | /** 173 | * moves `amount` of tokens from `from` to `to` by spender. 174 | * @param spender spender's public key 175 | * @param sig spender's signature 176 | * @param currentAllowance the allowance granted to `spender` by the owner. 177 | * @param from owner's address and balance 178 | * @param to receiver's address and balance 179 | * @param amount amount of token, the owner must have a balance of at least `amount`. 180 | */ 181 | @method(SigHash.SINGLE) 182 | public transferFrom( 183 | spender: PubKey, 184 | sig: Sig, 185 | currentAllowance: bigint, 186 | from: ERC20Pair, 187 | to: ERC20Pair, 188 | amount: bigint 189 | ) { 190 | assert( 191 | to.address != ERC20.EMPTY_ADDR, 192 | 'ERC20: approve to the zero address' 193 | ) 194 | assert(this.checkSig(sig, spender)) 195 | assert( 196 | this.allowances.canGet( 197 | { 198 | owner: from.address, 199 | spender: hash160(spender), 200 | }, 201 | currentAllowance 202 | ) 203 | ) 204 | 205 | assert( 206 | currentAllowance > 0n && currentAllowance >= amount, 207 | 'ERC20: insufficient allowance' 208 | ) 209 | 210 | // update allowances 211 | this.allowances.set( 212 | { 213 | owner: from.address, 214 | spender: hash160(spender), 215 | }, 216 | currentAllowance - amount 217 | ) 218 | 219 | assert( 220 | from.address != ERC20.EMPTY_ADDR, 221 | 'ERC20: transfer from the zero address' 222 | ) 223 | assert( 224 | to.address != ERC20.EMPTY_ADDR, 225 | 'ERC20: transfer to the zero address' 226 | ) 227 | assert( 228 | this.balances.canGet(from.address, from.balance), 229 | 'ERC20: can not get balance from sender address' 230 | ) 231 | assert(from.balance >= amount, 'ERC20: transfer amount exceeds balance') 232 | 233 | this.balances.set(from.address, from.balance - amount) 234 | 235 | if (this.balances.canGet(to.address, to.balance)) { 236 | this.balances.set(to.address, to.balance + amount) 237 | } else { 238 | this.balances.set(to.address, amount) 239 | } 240 | 241 | assert( 242 | this.ctx.hashOutputs == 243 | hash256(this.buildStateOutput(this.ctx.utxo.value)), 244 | 'check hashOutputs failed' 245 | ) 246 | } 247 | 248 | /** 249 | * allows `spender` to withdraw from your account multiple times, up to the `amount`. 250 | * If this function is called again it overwrites the current allowance with `amount`. 251 | * @param owner owner's public key 252 | * @param sig owner's signature 253 | * @param spender spender's address 254 | * @param amount amount of token 255 | */ 256 | @method(SigHash.SINGLE) 257 | public approve( 258 | owner: PubKey, 259 | sig: Sig, 260 | spender: PubKeyHash, 261 | amount: bigint 262 | ) { 263 | assert( 264 | spender != ERC20.EMPTY_ADDR, 265 | 'ERC20: approve to the zero address' 266 | ) 267 | 268 | assert(this.checkSig(sig, owner)) 269 | 270 | this.allowances.set( 271 | { 272 | owner: hash160(owner), 273 | spender: spender, 274 | }, 275 | amount 276 | ) 277 | 278 | assert( 279 | this.ctx.hashOutputs == 280 | hash256(this.buildStateOutput(this.ctx.utxo.value)), 281 | 'check hashOutputs failed' 282 | ) 283 | } 284 | 285 | /** 286 | * check the amount which `spender` is still allowed to withdraw from `owner`. 287 | * @param owner owner's address 288 | * @param spender spender's address 289 | * @param amount amount of token allowed to withdraw 290 | */ 291 | @method(SigHash.SINGLE) 292 | public allowance(owner: PubKeyHash, spender: PubKeyHash, amount: bigint) { 293 | assert( 294 | this.allowances.canGet( 295 | { 296 | owner: owner, 297 | spender: spender, 298 | }, 299 | amount 300 | ) 301 | ) 302 | 303 | assert( 304 | this.ctx.hashOutputs == 305 | hash256(this.buildStateOutput(this.ctx.utxo.value)), 306 | 'check hashOutputs failed' 307 | ) 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /src/contracts/erc721.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assert, 3 | hash256, 4 | HashedMap, 5 | method, 6 | prop, 7 | PubKey, 8 | Sig, 9 | SigHash, 10 | SmartContract, 11 | } from 'scrypt-ts' 12 | 13 | // tokenId: ownerPubKey 14 | type OwnerMap = HashedMap 15 | 16 | // a basic ERC721-like non-fungible token 17 | export class Erc721 extends SmartContract { 18 | @prop() 19 | minter: PubKey 20 | s 21 | @prop(true) 22 | owners: OwnerMap 23 | 24 | constructor(minter: PubKey, owners: OwnerMap) { 25 | super(...arguments) 26 | this.minter = minter 27 | this.owners = owners 28 | } 29 | 30 | // mint a new token to receiver 31 | @method(SigHash.SINGLE) 32 | public mint(tokenId: bigint, mintTo: PubKey, minterSig: Sig) { 33 | // require token was not minted before 34 | assert(!this.owners.has(tokenId), 'token was already minted before') 35 | // require the minter to provide a signature before minting 36 | assert( 37 | this.checkSig(minterSig, this.minter), 38 | 'minter signature check failed' 39 | ) 40 | // set token belongs to the receiver 41 | this.owners.set(tokenId, mintTo) 42 | // validate hashOutputs 43 | assert( 44 | this.ctx.hashOutputs == 45 | hash256(this.buildStateOutput(this.ctx.utxo.value)), 46 | 'hashOutputs check failed' 47 | ) 48 | } 49 | 50 | // burn a token 51 | @method(SigHash.SINGLE) 52 | public burn(tokenId: bigint, sender: PubKey, sig: Sig) { 53 | // verify ownership 54 | assert( 55 | this.owners.canGet(tokenId, sender), 56 | "sender doesn't have the token" 57 | ) 58 | // verify sender's signature 59 | assert(this.checkSig(sig, sender), 'sender signature check failed') 60 | // remove token from owners 61 | assert(this.owners.delete(tokenId), 'token burn failed') 62 | // validate hashOutputs 63 | assert( 64 | this.ctx.hashOutputs == 65 | hash256(this.buildStateOutput(this.ctx.utxo.value)), 66 | 'hashOutputs check failed' 67 | ) 68 | } 69 | 70 | // transfer a token from sender to receiver 71 | @method(SigHash.SINGLE) 72 | public transferFrom( 73 | tokenId: bigint, 74 | sender: PubKey, 75 | sig: Sig, 76 | receiver: PubKey 77 | ) { 78 | // verify ownership 79 | assert( 80 | this.owners.canGet(tokenId, sender), 81 | "sender doesn't have the token" 82 | ) 83 | // verify sender's signature 84 | assert(this.checkSig(sig, sender), 'sender signature check failed') 85 | // change token owner 86 | this.owners.set(tokenId, receiver) 87 | // validate hashOutputs 88 | assert( 89 | this.ctx.hashOutputs == 90 | hash256(this.buildStateOutput(this.ctx.utxo.value)), 91 | 'hashOutputs check failed' 92 | ) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/contracts/hashPuzzle.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assert, 3 | ByteString, 4 | method, 5 | prop, 6 | Sha256, 7 | sha256, 8 | SmartContract, 9 | } from 'scrypt-ts' 10 | 11 | export class HashPuzzle extends SmartContract { 12 | @prop() 13 | readonly sha256: Sha256 14 | 15 | constructor(sha256: Sha256) { 16 | super(...arguments) 17 | this.sha256 = sha256 18 | } 19 | 20 | // This method can only be unlocked if providing the real hash preimage of 21 | // the specified SHA-256 hash. 22 | @method() 23 | public unlock(data: ByteString) { 24 | assert(this.sha256 == sha256(data), 'hashes are not equal') 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/contracts/hashedMapNonState.ts: -------------------------------------------------------------------------------- 1 | import { 2 | method, 3 | prop, 4 | SmartContract, 5 | assert, 6 | HashedMap, 7 | ByteString, 8 | } from 'scrypt-ts' 9 | 10 | type MyMap = HashedMap 11 | 12 | export class HashedMapNonState extends SmartContract { 13 | @prop() 14 | map: MyMap 15 | 16 | constructor(map: MyMap) { 17 | super(map) 18 | this.map = map 19 | } 20 | 21 | @method() 22 | public unlock(key: bigint, val: ByteString) { 23 | this.map.set(key, val) 24 | 25 | for (let i = 0; i < 4; i++) { 26 | if (i < 2) { 27 | this.map.set(key + BigInt(i), val) 28 | assert(this.map.has(key + BigInt(i))) 29 | } else { 30 | this.map.set(key * 2n + BigInt(i), val) 31 | } 32 | } 33 | 34 | assert( 35 | this.map.canGet(key, val), 36 | 'cannot get key-value pair from hashedMap' 37 | ) 38 | assert( 39 | this.map.canGet(key * 2n + 2n, val), 40 | 'cannot get key-value pair from hashedMap' 41 | ) 42 | assert(this.map.size >= 5) 43 | assert(true) 44 | } 45 | 46 | @method() 47 | public delete(key: bigint) { 48 | assert(this.map.has(key), 'hashedMap should have the key before delete') 49 | assert(this.map.delete(key), 'delete key in hashedMap failed') 50 | assert( 51 | !this.map.has(key), 52 | 'hashedMap should not have the key after delete' 53 | ) 54 | assert(true) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/contracts/hashedMapState.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assert, 3 | ByteString, 4 | hash256, 5 | HashedMap, 6 | int2ByteString, 7 | method, 8 | prop, 9 | SigHash, 10 | SmartContract, 11 | } from 'scrypt-ts' 12 | 13 | export class HashedMapState extends SmartContract { 14 | @prop(true) 15 | hashedmap: HashedMap 16 | 17 | constructor(hashedmap: HashedMap) { 18 | super(hashedmap) 19 | this.hashedmap = hashedmap 20 | } 21 | 22 | @method(SigHash.SINGLE) 23 | public insert(key: bigint, val: ByteString) { 24 | this.hashedmap.set(key, val) 25 | assert( 26 | this.ctx.hashOutputs == 27 | hash256(this.buildStateOutput(this.ctx.utxo.value)), 28 | 'hashOutputs check failed' 29 | ) 30 | } 31 | 32 | @method(SigHash.SINGLE) 33 | public canGet(key: bigint, val: ByteString) { 34 | assert(this.hashedmap.has(key), `hashedMap does not have key: ${key}`) 35 | assert( 36 | this.hashedmap.canGet(key, val), 37 | `can not get key-value pair: ${key}-${val}` 38 | ) 39 | assert( 40 | this.ctx.hashOutputs == 41 | hash256(this.buildStateOutput(this.ctx.utxo.value)), 42 | 'hashOutputs check failed' 43 | ) 44 | } 45 | 46 | @method(SigHash.SINGLE) 47 | public notExist(key: bigint) { 48 | assert( 49 | !this.hashedmap.has(key), 50 | `key: ${key} should not exist in hashedmap` 51 | ) 52 | assert( 53 | this.ctx.hashOutputs == 54 | hash256(this.buildStateOutput(this.ctx.utxo.value)), 55 | 'hashOutputs check failed' 56 | ) 57 | } 58 | 59 | @method(SigHash.SINGLE) 60 | public update(key: bigint, val: ByteString) { 61 | this.hashedmap.set(key, val) 62 | assert( 63 | this.ctx.hashOutputs == 64 | hash256(this.buildStateOutput(this.ctx.utxo.value)), 65 | 'hashOutputs check failed' 66 | ) 67 | } 68 | 69 | @method(SigHash.SINGLE) 70 | public delete(key: bigint) { 71 | assert(this.hashedmap.delete(key), 'delete key from hashedMap failed') 72 | assert( 73 | this.ctx.hashOutputs == 74 | hash256(this.buildStateOutput(this.ctx.utxo.value)) 75 | ) 76 | } 77 | 78 | @method(SigHash.SINGLE) 79 | public unlock(key: bigint, val: ByteString) { 80 | for (let i = 0; i < 5; i++) { 81 | this.hashedmap.set(BigInt(i), int2ByteString(BigInt(i), BigInt(i))) 82 | } 83 | 84 | for (let i = 0; i < 5; i++) { 85 | assert( 86 | this.hashedmap.canGet( 87 | BigInt(i), 88 | int2ByteString(BigInt(i), BigInt(i)) 89 | ), 90 | `canGet failed` 91 | ) 92 | 93 | if (i === 3) { 94 | assert( 95 | this.hashedmap.delete(key), 96 | 'delete key from hashedMap failed' 97 | ) 98 | } else if (i == 4) { 99 | this.hashedmap.set(key, val) 100 | } 101 | } 102 | 103 | assert( 104 | this.ctx.hashOutputs == 105 | hash256(this.buildStateOutput(this.ctx.utxo.value)) 106 | ) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/contracts/hashedSetNonState.ts: -------------------------------------------------------------------------------- 1 | import { method, prop, SmartContract, assert, HashedSet } from 'scrypt-ts' 2 | 3 | export class HashedSetNonState extends SmartContract { 4 | @prop() 5 | set: HashedSet 6 | 7 | constructor(set: HashedSet) { 8 | super(set) 9 | this.set = set 10 | } 11 | 12 | @method() 13 | public add(key: bigint) { 14 | this.set.add(key) 15 | assert(this.set.has(key), 'hashedSet should have the key after add') 16 | } 17 | 18 | @method() 19 | public delete(key: bigint) { 20 | assert(this.set.has(key), 'hashedSet should have the key before delete') 21 | assert(this.set.delete(key), 'delete key in hashedSet failed') 22 | assert( 23 | !this.set.has(key), 24 | 'hashedSet should not have the key after delete' 25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/contracts/hashedSetState.ts: -------------------------------------------------------------------------------- 1 | import { 2 | method, 3 | prop, 4 | SmartContract, 5 | assert, 6 | hash256, 7 | HashedSet, 8 | SigHash, 9 | } from 'scrypt-ts' 10 | 11 | export class HashedSetState extends SmartContract { 12 | @prop(true) 13 | hashedset: HashedSet 14 | 15 | constructor(hashedset: HashedSet) { 16 | super(hashedset) 17 | this.hashedset = hashedset 18 | } 19 | 20 | @method(SigHash.SINGLE) 21 | public add(key: bigint) { 22 | this.hashedset.add(key) 23 | assert( 24 | this.ctx.hashOutputs == 25 | hash256(this.buildStateOutput(this.ctx.utxo.value)) 26 | ) 27 | } 28 | 29 | @method(SigHash.SINGLE) 30 | public has(key: bigint) { 31 | assert(this.hashedset.has(key)) 32 | assert( 33 | this.ctx.hashOutputs == 34 | hash256(this.buildStateOutput(this.ctx.utxo.value)) 35 | ) 36 | } 37 | 38 | @method(SigHash.SINGLE) 39 | public notExist(key: bigint) { 40 | assert(!this.hashedset.has(key)) 41 | assert( 42 | this.ctx.hashOutputs == 43 | hash256(this.buildStateOutput(this.ctx.utxo.value)) 44 | ) 45 | } 46 | 47 | @method(SigHash.SINGLE) 48 | public delete(key: bigint) { 49 | assert(this.hashedset.delete(key)) 50 | assert( 51 | this.ctx.hashOutputs == 52 | hash256(this.buildStateOutput(this.ctx.utxo.value)) 53 | ) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/contracts/helloWorld.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assert, 3 | ByteString, 4 | method, 5 | prop, 6 | sha256, 7 | Sha256, 8 | SmartContract, 9 | } from 'scrypt-ts' 10 | 11 | export class HelloWorld extends SmartContract { 12 | @prop() 13 | hash: Sha256 14 | 15 | constructor(hash: Sha256) { 16 | super(...arguments) 17 | this.hash = hash 18 | } 19 | 20 | @method() 21 | public unlock(message: ByteString) { 22 | assert(this.hash === sha256(message), 'Not expected message!') 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/contracts/mimc7.ts: -------------------------------------------------------------------------------- 1 | import { Mimc7 } from 'scrypt-ts-lib' 2 | import { assert, method, SmartContract } from 'scrypt-ts' 3 | 4 | export class Mimc7Test extends SmartContract { 5 | @method() 6 | public unlock(x: bigint, k: bigint, h: bigint) { 7 | // call imported library method 8 | assert(Mimc7.hash(x, k) == h) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/contracts/montyHall.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assert, 3 | ByteString, 4 | FixedArray, 5 | hash256, 6 | method, 7 | prop, 8 | PubKey, 9 | Sig, 10 | Sha256, 11 | SmartContract, 12 | Utils, 13 | byteString2Int, 14 | sha256, 15 | hash160, 16 | } from 'scrypt-ts' 17 | 18 | // this contract simulates the Monty Hall problem 19 | // https://xiaohuiliu.medium.com/the-monty-hall-problem-on-bitcoin-1f9be62b38e8 20 | export class MontyHall extends SmartContract { 21 | @prop() 22 | readonly player: PubKey 23 | 24 | @prop() 25 | readonly host: PubKey 26 | 27 | @prop(true) 28 | step: bigint 29 | 30 | // player's choice 31 | @prop(true) 32 | choice: bigint 33 | 34 | // door opened by host 35 | @prop(true) 36 | openedDoor: bigint 37 | 38 | // number of doors 39 | static readonly N: number = 3 40 | 41 | // what's behind each door 42 | @prop() 43 | doorHashes: FixedArray 44 | 45 | constructor( 46 | player: PubKey, 47 | host: PubKey, 48 | doorHashes: FixedArray 49 | ) { 50 | super(...arguments) 51 | this.player = player 52 | this.host = host 53 | this.step = 0n 54 | this.choice = -1n 55 | this.openedDoor = -1n 56 | this.doorHashes = doorHashes 57 | } 58 | 59 | // step 1: the player chooses initially a random door that s/he believes has the prize 60 | @method() 61 | public choose(choice: bigint, sig: Sig) { 62 | assert(++this.step == 1n, 'step number unexpected') 63 | 64 | this.checkSig(sig, this.player) 65 | this.choice = choice 66 | 67 | // game goes on 68 | assert( 69 | this.ctx.hashOutputs == 70 | hash256(this.buildStateOutput(this.ctx.utxo.value)), 71 | 'hashOutputs check failed' 72 | ) 73 | } 74 | 75 | // step 2: host opens a goat door 76 | @method() 77 | public open(goatDoorNum: bigint, behindDoor: ByteString, sig: Sig) { 78 | assert(++this.step == 2n, 'step number unexpected') 79 | 80 | this.checkSig(sig, this.host) 81 | 82 | this.openedDoor = goatDoorNum 83 | const goatDoorHash = this.doorHashes[Number(goatDoorNum)] 84 | assert(sha256(behindDoor) == goatDoorHash) 85 | assert(!this.isCar(behindDoor), 'expect goat, but got car') 86 | 87 | assert( 88 | this.ctx.hashOutputs == 89 | hash256(this.buildStateOutput(this.ctx.utxo.value)), 90 | 'hashOutputs check failed' 91 | ) 92 | } 93 | 94 | // step 3: player stays or switches 95 | @method() 96 | public stay(stay: boolean, sig: Sig) { 97 | assert(++this.step == 3n, 'step number unexpected') 98 | this.checkSig(sig, this.player) 99 | 100 | if (!stay) { 101 | // switch 102 | this.choice = this.findUnopenedDoor() 103 | } 104 | 105 | assert( 106 | this.ctx.hashOutputs == 107 | hash256(this.buildStateOutput(this.ctx.utxo.value)), 108 | 'hashOutputs check failed' 109 | ) 110 | } 111 | 112 | // step 4: reveal 113 | @method() 114 | public reveal(behindDoor: ByteString) { 115 | assert(++this.step == 4n, 'step number unexpected') 116 | 117 | const doorHash = this.doorHashes[Number(this.choice)] 118 | assert(sha256(behindDoor) == doorHash) 119 | 120 | // does the play choose a door, behind which is a car 121 | const won = this.isCar(behindDoor) 122 | const winner = won ? this.player : this.host 123 | 124 | // pay full amount to winner 125 | const winnerScript: ByteString = Utils.buildPublicKeyHashScript( 126 | hash160(winner) 127 | ) 128 | const payoutOutput: ByteString = Utils.buildOutput( 129 | winnerScript, 130 | this.ctx.utxo.value 131 | ) 132 | assert(this.ctx.hashOutputs == hash256(payoutOutput)) 133 | } 134 | 135 | // if last bit is set, it is a car; otherwise, a goat 136 | @method() 137 | isCar(behindDoor: ByteString): boolean { 138 | return byteString2Int(behindDoor) % 2n == 1n 139 | } 140 | 141 | // find the remaining unopened door 142 | @method() 143 | findUnopenedDoor(): bigint { 144 | let result = -1n 145 | for (let i = 0n; i < MontyHall.N; i++) { 146 | if (i != this.choice && i != this.openedDoor) result = i 147 | } 148 | return result 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/contracts/p2pkh.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assert, 3 | hash160, 4 | method, 5 | prop, 6 | PubKey, 7 | PubKeyHash, 8 | Sig, 9 | SmartContract, 10 | } from 'scrypt-ts' 11 | 12 | export class P2PKH extends SmartContract { 13 | // Address of the recipient. 14 | @prop() 15 | readonly pubKeyHash: PubKeyHash 16 | 17 | constructor(pubKeyHash: PubKeyHash) { 18 | super(...arguments) 19 | this.pubKeyHash = pubKeyHash 20 | } 21 | 22 | @method() 23 | public unlock(sig: Sig, pubkey: PubKey) { 24 | // Check if the passed public key belongs to the specified address. 25 | assert( 26 | hash160(pubkey) == this.pubKeyHash, 27 | 'public key hashes are not equal' 28 | ) 29 | // Check signature validity. 30 | assert(this.checkSig(sig, pubkey), 'signature check failed') 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/contracts/recallable.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assert, 3 | hash256, 4 | method, 5 | prop, 6 | PubKey, 7 | Sig, 8 | SmartContract, 9 | } from 'scrypt-ts' 10 | 11 | /** 12 | * re-callable satoshis demo 13 | * users can transfer these satoshis as wish, and issuer can recall them back to himself at anytime 14 | */ 15 | export class Recallable extends SmartContract { 16 | // the public key of issuer 17 | @prop() 18 | readonly issuerPubKey: PubKey 19 | 20 | // the public key of current user 21 | @prop(true) 22 | userPubKey: PubKey 23 | 24 | constructor(issuer: PubKey) { 25 | super(...arguments) 26 | this.issuerPubKey = issuer 27 | this.userPubKey = issuer // the first user is the issuer himself 28 | } 29 | 30 | @method() 31 | public transfer( 32 | userSig: Sig, // the current user should provide his signature before transfer 33 | receiverPubKey: PubKey, // send to 34 | satoshisSent: bigint // send amount 35 | ) { 36 | // total satoshis locked in this contract utxo 37 | const satoshisTotal = this.ctx.utxo.value 38 | // require the amount requested to be transferred is valid 39 | assert( 40 | satoshisSent > 0 && satoshisSent <= satoshisTotal, 41 | `invalid value of \`satoshisSent\`, should be greater than 0 and less than or equal to ${satoshisTotal}` 42 | ) 43 | 44 | // require the current user to provide signature before transfer 45 | assert( 46 | this.checkSig(userSig, this.userPubKey), 47 | "user's signature check failed" 48 | ) 49 | 50 | // temp record previous user 51 | const previousUserPubKey = this.userPubKey 52 | 53 | // construct all the outputs of the method calling tx 54 | 55 | // the output send to `receiver` 56 | this.userPubKey = receiverPubKey 57 | let outputs = this.buildStateOutput(satoshisSent) 58 | 59 | // the change output back to previous `user` 60 | const satoshisLeft = satoshisTotal - satoshisSent 61 | if (satoshisLeft > 0) { 62 | this.userPubKey = previousUserPubKey 63 | outputs += this.buildStateOutput(satoshisLeft) 64 | } 65 | 66 | // the change output for paying the transaction fee 67 | if (this.changeAmount > 0) { 68 | outputs += this.buildChangeOutput() 69 | } 70 | 71 | // require all of these outputs are actually in the unlocking transaction 72 | assert( 73 | hash256(outputs) == this.ctx.hashOutputs, 74 | 'hashOutputs check failed' 75 | ) 76 | } 77 | 78 | @method() 79 | public recall(issuerSig: Sig) { 80 | // require the issuer to provide signature before recall 81 | assert( 82 | this.checkSig(issuerSig, this.issuerPubKey), 83 | "issuer's signature check failed" 84 | ) 85 | 86 | this.userPubKey = this.issuerPubKey 87 | // the amount is satoshis locked in this UTXO 88 | let outputs = this.buildStateOutput(this.ctx.utxo.value) 89 | 90 | if (this.changeAmount > 0) { 91 | outputs += this.buildChangeOutput() 92 | } 93 | 94 | // require all of these outputs are actually in the unlocking transaction 95 | assert( 96 | hash256(outputs) == this.ctx.hashOutputs, 97 | 'hashOutputs check failed' 98 | ) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tests/local/accumulatorMultiSig.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { 3 | bsv, 4 | findSig, 5 | FixedArray, 6 | getDummySig, 7 | MethodCallOptions, 8 | ContractTransaction, 9 | PubKey, 10 | PubKeyHash, 11 | toHex, 12 | } from 'scrypt-ts' 13 | import { AccumulatorMultiSig } from '../../src/contracts/accumulatorMultiSig' 14 | import { getDummySigner, getDummyUTXO, randomPrivateKey } from '../utils/helper' 15 | 16 | const [privateKey1, publicKey1, publicKeyHash1] = randomPrivateKey() 17 | const [privateKey2, publicKey2, publicKeyHash2] = randomPrivateKey() 18 | const [privateKey3, publicKey3, publicKeyHash3] = randomPrivateKey() 19 | 20 | const pubKeys = [publicKey1, publicKey2, publicKey3].map((pk) => { 21 | return PubKey(pk.toString()) 22 | }) as FixedArray 23 | 24 | const pubKeyHashes = [publicKeyHash1, publicKeyHash2, publicKeyHash3].map( 25 | (pkh) => PubKeyHash(toHex(pkh)) 26 | ) as FixedArray 27 | 28 | let accumulatorMultiSig: AccumulatorMultiSig 29 | 30 | describe('Test SmartContract `AccumulatorMultiSig`', () => { 31 | before(async () => { 32 | await AccumulatorMultiSig.compile() 33 | accumulatorMultiSig = new AccumulatorMultiSig(2n, pubKeyHashes) 34 | 35 | const signer = getDummySigner([privateKey1, privateKey2, privateKey3]) 36 | await accumulatorMultiSig.connect(signer) 37 | }) 38 | 39 | it('should successfully with all three right.', async () => { 40 | const { tx: callTx, atInputIndex } = await call([true, true, true]) 41 | const result = callTx.verifyScript(atInputIndex) 42 | expect(result.success, result.error).to.eq(true) 43 | }) 44 | 45 | it('should successfully with two right.', async () => { 46 | const { tx: callTx, atInputIndex } = await call([true, false, true]) 47 | const result = callTx.verifyScript(atInputIndex) 48 | expect(result.success, result.error).to.eq(true) 49 | }) 50 | 51 | it('should throw with only one right.', async () => { 52 | return expect(call([false, true, false])).to.be.rejectedWith( 53 | /the number of signatures does not meet the threshold limit/ 54 | ) 55 | }) 56 | }) 57 | 58 | async function call( 59 | masks: FixedArray 60 | ): Promise { 61 | return accumulatorMultiSig.methods.main( 62 | pubKeys, 63 | (sigResps) => { 64 | return pubKeys.map((pubKey) => { 65 | try { 66 | return findSig(sigResps, bsv.PublicKey.fromString(pubKey)) 67 | } catch (error) { 68 | return getDummySig() 69 | } 70 | }) 71 | }, 72 | masks, 73 | { 74 | fromUTXO: getDummyUTXO(), 75 | pubKeyOrAddrToSign: pubKeys 76 | .filter((_, idx) => masks[idx]) 77 | .map((pubkey) => bsv.PublicKey.fromString(pubkey)), 78 | } as MethodCallOptions 79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /tests/local/ackerman.test.ts: -------------------------------------------------------------------------------- 1 | import { Ackermann } from '../../src/contracts/ackermann' 2 | import { expect, use } from 'chai' 3 | import chaiAsPromised from 'chai-as-promised' 4 | import { getDummySigner, getDummyUTXO } from '../utils/helper' 5 | import { MethodCallOptions } from 'scrypt-ts' 6 | 7 | use(chaiAsPromised) 8 | 9 | describe('Test SmartContract `Ackermann`', () => { 10 | let ackermann: Ackermann 11 | 12 | before(async () => { 13 | await Ackermann.compile() 14 | ackermann = new Ackermann(2n, 1n) 15 | 16 | await ackermann.connect(getDummySigner()) 17 | }) 18 | 19 | it('should transpile contract `Ackermann` successfully.', async () => { 20 | const { tx: callTx, atInputIndex } = await ackermann.methods.unlock( 21 | 5n, 22 | { 23 | fromUTXO: getDummyUTXO(), 24 | } as MethodCallOptions 25 | ) 26 | const result = callTx.verifyScript(atInputIndex) 27 | expect(result.success, result.error).to.eq(true) 28 | }) 29 | 30 | it('should throw', async () => { 31 | return expect( 32 | ackermann.methods.unlock(4n, { 33 | fromUTXO: getDummyUTXO(), 34 | } as MethodCallOptions) 35 | ).to.be.rejectedWith(/Wrong solution/) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /tests/local/acs.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { MethodCallOptions, PubKeyHash, toHex } from 'scrypt-ts' 3 | import { AnyoneCanSpend } from '../../src/contracts/acs' 4 | import { myPublicKeyHash } from '../utils/privateKey' 5 | import { getDummySigner, getDummyUTXO } from '../utils/helper' 6 | 7 | describe('Test SmartContract `AnyoneCanSpend`', () => { 8 | before(async () => { 9 | await AnyoneCanSpend.compile() 10 | }) 11 | 12 | it('should transpile contract `AnyoneCanSpend` successfully.', async () => { 13 | const anyoneCanSpend = new AnyoneCanSpend( 14 | PubKeyHash(toHex(myPublicKeyHash)) 15 | ) 16 | await anyoneCanSpend.connect(getDummySigner()) 17 | 18 | const { tx: callTx, atInputIndex } = 19 | await anyoneCanSpend.methods.unlock({ 20 | fromUTXO: getDummyUTXO(), 21 | } as MethodCallOptions) 22 | 23 | const result = callTx.verifyScript(atInputIndex) 24 | expect(result.success, result.error).to.eq(true) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /tests/local/auction.test.ts: -------------------------------------------------------------------------------- 1 | import { Auction } from '../../src/contracts/auction' 2 | import { findSig, MethodCallOptions, PubKey, toHex } from 'scrypt-ts' 3 | import { expect } from 'chai' 4 | import { getDummySigner, getDummyUTXO, randomPrivateKey } from '../utils/helper' 5 | 6 | describe('Test SmartContract `Auction` on testnet', () => { 7 | const [privateKeyAuctioneer, publicKeyAuctioneer, ,] = randomPrivateKey() 8 | const [, publicKeyNewBidder, , addressNewBidder] = randomPrivateKey() 9 | 10 | const auctionDeadline = Math.round(new Date('2020-01-03').valueOf() / 1000) 11 | 12 | let auction: Auction 13 | 14 | before(async () => { 15 | await Auction.compile() 16 | 17 | auction = new Auction( 18 | PubKey(toHex(publicKeyAuctioneer)), 19 | BigInt(auctionDeadline) 20 | ) 21 | 22 | auction.bindTxBuilder('bid', Auction.bidTxBuilder) 23 | 24 | await auction.connect(getDummySigner(privateKeyAuctioneer)) 25 | }) 26 | 27 | it('should pass `bid` call', async () => { 28 | const balance = 1 29 | const { tx: callTx, atInputIndex } = await auction.methods.bid( 30 | PubKey(toHex(publicKeyNewBidder)), 31 | BigInt(balance + 1), 32 | { 33 | fromUTXO: getDummyUTXO(balance), 34 | changeAddress: addressNewBidder, 35 | } as MethodCallOptions 36 | ) 37 | 38 | const result = callTx.verifyScript(atInputIndex) 39 | expect(result.success, result.error).to.eq(true) 40 | }) 41 | 42 | it('should pass `close` call', async () => { 43 | const { tx: callTx, atInputIndex } = await auction.methods.close( 44 | (sigResps) => findSig(sigResps, publicKeyAuctioneer), 45 | { 46 | fromUTXO: getDummyUTXO(), 47 | pubKeyOrAddrToSign: publicKeyAuctioneer, 48 | changeAddress: addressNewBidder, 49 | lockTime: auctionDeadline + 1, 50 | } as MethodCallOptions 51 | ) 52 | 53 | const result = callTx.verifyScript(atInputIndex) 54 | expect(result.success, result.error).to.eq(true) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /tests/local/cltv.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { CheckLockTimeVerify } from '../../src/contracts/cltv' 3 | import { getDummySigner, getDummyUTXO } from '../utils/helper' 4 | import { MethodCallOptions } from 'scrypt-ts' 5 | 6 | describe('Test SmartContract `CheckLockTimeVerify`', () => { 7 | let cltv: CheckLockTimeVerify 8 | const lockTimeMin = 1673510000n 9 | 10 | before(async () => { 11 | await CheckLockTimeVerify.compile() 12 | 13 | cltv = new CheckLockTimeVerify(lockTimeMin) 14 | await cltv.connect(getDummySigner()) 15 | }) 16 | 17 | it('should pass the public method unit test successfully.', async () => { 18 | const { tx: callTx, atInputIndex } = await cltv.methods.unlock({ 19 | fromUTXO: getDummyUTXO(), 20 | lockTime: 1673523720, 21 | } as MethodCallOptions) 22 | const result = callTx.verifyScript(atInputIndex) 23 | expect(result.success, result.error).to.eq(true) 24 | }) 25 | 26 | it('should fail when nLocktime is too low.', async () => { 27 | return expect( 28 | cltv.methods.unlock({ 29 | fromUTXO: getDummyUTXO(), 30 | lockTime: 1673500100, 31 | } as MethodCallOptions) 32 | ).to.be.rejectedWith(/locktime has not yet expired/) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /tests/local/counter.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { Counter } from '../../src/contracts/counter' 3 | import { getDummySigner, getDummyUTXO } from '../utils/helper' 4 | import { MethodCallOptions } from 'scrypt-ts' 5 | 6 | describe('Test SmartContract `Counter`', () => { 7 | before(async () => { 8 | await Counter.compile() 9 | }) 10 | 11 | it('should pass the public method unit test successfully.', async () => { 12 | const balance = 1 13 | 14 | const counter = new Counter(0n) 15 | await counter.connect(getDummySigner()) 16 | 17 | // set current instance to be the deployed one 18 | let currentInstance = counter 19 | 20 | // call the method of current instance to apply the updates on chain 21 | for (let i = 0; i < 3; ++i) { 22 | // create the next instance from the current 23 | const nextInstance = currentInstance.next() 24 | 25 | // apply updates on the next instance off chain 26 | nextInstance.increment() 27 | 28 | // call the method of current instance to apply the updates on chain 29 | const { tx: tx_i, atInputIndex } = 30 | await currentInstance.methods.incrementOnChain({ 31 | fromUTXO: getDummyUTXO(balance), 32 | next: { 33 | instance: nextInstance, 34 | balance, 35 | }, 36 | } as MethodCallOptions) 37 | 38 | const result = tx_i.verifyScript(atInputIndex) 39 | expect(result.success, result.error).to.eq(true) 40 | 41 | // update the current instance reference 42 | currentInstance = nextInstance 43 | } 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /tests/local/crowdfund.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { findSig, MethodCallOptions, PubKey, toHex } from 'scrypt-ts' 3 | import { Crowdfund } from '../../src/contracts/crowdfund' 4 | import { 5 | getDummySigner, 6 | getDummyUTXO, 7 | inputSatoshis, 8 | randomPrivateKey, 9 | } from '../utils/helper' 10 | 11 | const [privateKeyRecipient, publicKeyRecipient, ,] = randomPrivateKey() 12 | const [privateKeyContributor, publicKeyContributor, ,] = randomPrivateKey() 13 | 14 | describe('Test SmartContract `Crowdfund`', () => { 15 | // JS timestamps are in milliseconds, so we divide by 1000 to get a UNIX timestamp 16 | const deadline = Math.round(new Date('2020-01-03').valueOf() / 1000) 17 | const target = BigInt(inputSatoshis - 100) 18 | 19 | let crowdfund: Crowdfund 20 | 21 | before(async () => { 22 | await Crowdfund.compile() 23 | crowdfund = new Crowdfund( 24 | PubKey(toHex(publicKeyRecipient)), 25 | PubKey(toHex(publicKeyContributor)), 26 | BigInt(deadline), 27 | target 28 | ) 29 | await crowdfund.connect( 30 | getDummySigner([privateKeyRecipient, privateKeyContributor]) 31 | ) 32 | }) 33 | 34 | it('should collect fund success', async () => { 35 | const { tx: callTx, atInputIndex } = await crowdfund.methods.collect( 36 | (sigResps) => findSig(sigResps, publicKeyRecipient), 37 | { 38 | fromUTXO: getDummyUTXO(), 39 | pubKeyOrAddrToSign: publicKeyRecipient, 40 | changeAddress: publicKeyRecipient.toAddress('testnet'), 41 | } as MethodCallOptions 42 | ) 43 | const result = callTx.verifyScript(atInputIndex) 44 | expect(result.success, result.error).to.eq(true) 45 | }) 46 | 47 | it('should success when refund', async () => { 48 | const today = Math.round(new Date().valueOf() / 1000) 49 | const { tx: callTx, atInputIndex } = await crowdfund.methods.refund( 50 | (sigResps) => findSig(sigResps, publicKeyContributor), 51 | { 52 | fromUTXO: getDummyUTXO(), 53 | pubKeyOrAddrToSign: publicKeyContributor, 54 | lockTime: today, 55 | } as MethodCallOptions 56 | ) 57 | const result = callTx.verifyScript(atInputIndex) 58 | expect(result.success, result.error).to.eq(true) 59 | }) 60 | 61 | it('should fail when refund before deadline', async () => { 62 | return expect( 63 | crowdfund.methods.refund( 64 | (sigResps) => findSig(sigResps, publicKeyContributor), 65 | { 66 | fromUTXO: getDummyUTXO(), 67 | pubKeyOrAddrToSign: publicKeyContributor, 68 | lockTime: deadline - 1, 69 | } as MethodCallOptions 70 | ) 71 | ).to.be.rejectedWith(/fundraising expired/) 72 | }) 73 | }) 74 | -------------------------------------------------------------------------------- /tests/local/demo.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, use } from 'chai' 2 | import { Demo } from '../../src/contracts/demo' 3 | import { MethodCallOptions } from 'scrypt-ts' 4 | import { getDummySigner, getDummyUTXO } from '../utils/helper' 5 | import chaiAsPromised from 'chai-as-promised' 6 | 7 | use(chaiAsPromised) 8 | 9 | describe('Test SmartContract `Demo`', () => { 10 | let demo: Demo 11 | 12 | before(async () => { 13 | await Demo.compile() 14 | 15 | demo = new Demo(-2n, 7n) 16 | console.log(demo.scriptSize) 17 | await demo.connect(getDummySigner()) 18 | }) 19 | 20 | it('should pass `add`', async () => { 21 | const { tx: callTx, atInputIndex } = await demo.methods.add(5n, { 22 | fromUTXO: getDummyUTXO(), 23 | } as MethodCallOptions) 24 | const result = callTx.verifyScript(atInputIndex) 25 | expect(result.success, result.error).to.eq(true) 26 | }) 27 | 28 | it('should pass `sub`', async () => { 29 | const { tx: callTx, atInputIndex } = await demo.methods.sub(-9n, { 30 | fromUTXO: getDummyUTXO(), 31 | } as MethodCallOptions) 32 | const result = callTx.verifyScript(atInputIndex) 33 | expect(result.success, result.error).to.eq(true) 34 | }) 35 | 36 | it('should throw when calling `add`', () => { 37 | return expect( 38 | demo.methods.add(-5n, { 39 | fromUTXO: getDummyUTXO(), 40 | } as MethodCallOptions) 41 | ).to.be.rejectedWith(/add check failed/) 42 | }) 43 | 44 | it('should throw when calling `sub`', () => { 45 | return expect( 46 | demo.methods.sub(9n, { 47 | fromUTXO: getDummyUTXO(), 48 | } as MethodCallOptions) 49 | ).to.be.rejectedWith(/sub check failed/) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /tests/local/erc20.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, use } from 'chai' 2 | import chaiAsPromised from 'chai-as-promised' 3 | import { 4 | Allowance, 5 | AllowanceMap, 6 | BalanceMap, 7 | ERC20, 8 | ERC20Pair, 9 | } from '../../src/contracts/erc20' 10 | import { 11 | bsv, 12 | findSig, 13 | hash160, 14 | HashedMap, 15 | MethodCallOptions, 16 | PubKey, 17 | PubKeyHash, 18 | toByteString, 19 | toHex, 20 | } from 'scrypt-ts' 21 | import { dummyUTXO, getDummySigner, inputSatoshis } from '../utils/helper' 22 | 23 | use(chaiAsPromised) 24 | import Transaction = bsv.Transaction 25 | 26 | const signer = getDummySigner() 27 | 28 | const initialSupply = 1000000n 29 | 30 | describe('Test SmartContract `ERC20`', () => { 31 | let map: BalanceMap, allowances: AllowanceMap, erc20: ERC20 32 | before(async () => { 33 | await ERC20.compile() 34 | 35 | map = new HashedMap() 36 | allowances = new HashedMap() 37 | const issuer = PubKey(toHex(await signer.getDefaultPubKey())) 38 | 39 | erc20 = new ERC20( 40 | toByteString('Gold', true), 41 | toByteString('GLD', true), 42 | issuer, 43 | map, 44 | allowances 45 | ) 46 | await erc20.connect(signer) 47 | }) 48 | 49 | async function mint( 50 | instance: ERC20, 51 | issuer: PubKey, 52 | issuerBalance: bigint, 53 | amount: bigint 54 | ): Promise<{ 55 | tx: Transaction 56 | newInstance: ERC20 57 | }> { 58 | const newInstance = instance.next() 59 | 60 | newInstance.balances.set(hash160(issuer), issuerBalance + amount) 61 | newInstance.totalSupply += amount 62 | const publicKey = bsv.PublicKey.fromString(issuer) 63 | const { nexts, tx } = await instance.methods.mint( 64 | (sigResps) => { 65 | return findSig(sigResps, publicKey) 66 | }, 67 | issuerBalance, 68 | amount, 69 | { 70 | fromUTXO: dummyUTXO, 71 | pubKeyOrAddrToSign: publicKey, 72 | next: { 73 | instance: newInstance, 74 | balance: inputSatoshis, 75 | }, 76 | } as MethodCallOptions 77 | ) 78 | 79 | return { 80 | tx: tx, 81 | newInstance: nexts[0].instance, 82 | } 83 | } 84 | 85 | async function transfer( 86 | instance: ERC20, 87 | from: ERC20Pair, 88 | pubkey: PubKey, 89 | to: ERC20Pair, 90 | amount: bigint 91 | ): Promise<{ 92 | tx: Transaction 93 | newInstance: ERC20 94 | }> { 95 | const newInstance = instance.next() 96 | 97 | newInstance.balances.set(from.address, from.balance - amount) 98 | newInstance.balances.set(to.address, to.balance + amount) 99 | 100 | const publicKey = bsv.PublicKey.fromString(pubkey) 101 | const { nexts, tx } = await instance.methods.transfer( 102 | from, 103 | pubkey, 104 | (sigResps) => { 105 | return findSig(sigResps, publicKey) 106 | }, 107 | to, 108 | amount, 109 | { 110 | fromUTXO: dummyUTXO, 111 | pubKeyOrAddrToSign: publicKey, 112 | next: { 113 | instance: newInstance, 114 | balance: inputSatoshis, 115 | }, 116 | } as MethodCallOptions 117 | ) 118 | 119 | return { 120 | tx: tx, 121 | newInstance: nexts[0].instance, 122 | } 123 | } 124 | 125 | async function approve( 126 | instance: ERC20, 127 | owner: PubKey, 128 | spender: PubKeyHash, 129 | amount: bigint 130 | ): Promise<{ 131 | tx: Transaction 132 | newInstance: ERC20 133 | }> { 134 | const newInstance = instance.next() 135 | 136 | newInstance.allowances.set( 137 | { 138 | owner: hash160(owner), 139 | spender: spender, 140 | }, 141 | amount 142 | ) 143 | 144 | const publicKey = bsv.PublicKey.fromString(owner) 145 | const { nexts, tx } = await instance.methods.approve( 146 | owner, 147 | (sigResps) => { 148 | return findSig(sigResps, publicKey) 149 | }, 150 | spender, 151 | amount, 152 | { 153 | fromUTXO: dummyUTXO, 154 | pubKeyOrAddrToSign: publicKey, 155 | next: { 156 | instance: newInstance, 157 | balance: inputSatoshis, 158 | }, 159 | } as MethodCallOptions 160 | ) 161 | 162 | return { 163 | tx: tx, 164 | newInstance: nexts[0].instance, 165 | } 166 | } 167 | 168 | async function transferFrom( 169 | instance: ERC20, 170 | spender: PubKey, 171 | currentAllowance: bigint, 172 | from: ERC20Pair, 173 | to: ERC20Pair, 174 | amount: bigint 175 | ): Promise<{ 176 | tx: Transaction 177 | newInstance: ERC20 178 | }> { 179 | const newInstance = instance.next() 180 | 181 | newInstance.balances.set(from.address, from.balance - amount) 182 | newInstance.balances.set(to.address, to.balance + amount) 183 | 184 | newInstance.allowances.set( 185 | { 186 | owner: from.address, 187 | spender: hash160(spender), 188 | }, 189 | currentAllowance - amount 190 | ) 191 | 192 | const publicKey = bsv.PublicKey.fromString(spender) 193 | const { nexts, tx } = await instance.methods.transferFrom( 194 | spender, 195 | (sigResps) => { 196 | return findSig(sigResps, publicKey) 197 | }, 198 | currentAllowance, 199 | from, 200 | to, 201 | amount, 202 | { 203 | fromUTXO: dummyUTXO, 204 | pubKeyOrAddrToSign: publicKey, 205 | next: { 206 | instance: newInstance, 207 | balance: inputSatoshis, 208 | }, 209 | } as MethodCallOptions 210 | ) 211 | return { 212 | tx: tx, 213 | newInstance: nexts[0].instance, 214 | } 215 | } 216 | 217 | it('mint,transfer,approve,transferFrom', async () => { 218 | const issuer = PubKey(toHex(await signer.getDefaultPubKey())) 219 | const address = await signer.getDefaultAddress() 220 | const issuerAddress = PubKeyHash(address.toObject().hash) 221 | 222 | const aliceKey = bsv.PrivateKey.fromRandom('testnet') 223 | signer.addPrivateKey(aliceKey) 224 | 225 | const alicePubkey = PubKey(toHex(aliceKey.publicKey)) 226 | 227 | const aliceAddress = PubKeyHash(aliceKey.toAddress().toObject().hash) 228 | 229 | const issuerBalance = initialSupply 230 | 231 | const { tx: tx1, newInstance: erc20_1 } = await mint( 232 | erc20, 233 | issuer, 234 | 0n, 235 | initialSupply 236 | ) 237 | console.log( 238 | `mint ${initialSupply} Gold to issuer: ${address.toString()}` 239 | ) 240 | let result = tx1.verifyScript(0) 241 | expect(result.success, result.error).to.eq(true) 242 | 243 | const { tx: tx2, newInstance: erc20_2 } = await transfer( 244 | erc20_1, 245 | { 246 | address: issuerAddress, 247 | balance: issuerBalance, 248 | }, 249 | issuer, 250 | { 251 | address: aliceAddress, 252 | balance: 0n, 253 | }, 254 | 1000n 255 | ) 256 | result = tx2.verifyScript(0) 257 | expect(result.success, result.error).to.eq(true) 258 | 259 | console.log( 260 | `transfer ${1000n} Gold to alice: ${aliceKey 261 | .toAddress() 262 | .toString()}` 263 | ) 264 | 265 | const bobKey = bsv.PrivateKey.fromRandom('testnet') 266 | signer.addPrivateKey(bobKey) 267 | const bobPubkey = PubKey(toHex(bobKey.publicKey)) 268 | 269 | const bobAddress = PubKeyHash(bobKey.toAddress().toObject().hash) 270 | 271 | const aliceBalance = 1000n 272 | 273 | const { tx: tx3, newInstance: erc20_3 } = await transfer( 274 | erc20_2, 275 | { 276 | address: aliceAddress, 277 | balance: aliceBalance, 278 | }, 279 | alicePubkey, 280 | { 281 | address: bobAddress, 282 | balance: 0n, 283 | }, 284 | 100n 285 | ) 286 | result = tx3.verifyScript(0) 287 | expect(result.success, result.error).to.eq(true) 288 | console.log( 289 | `transfer ${100n} Gold to bob: ${bobKey.toAddress().toString()}` 290 | ) 291 | 292 | const { tx: tx4, newInstance: erc20_4 } = await transfer( 293 | erc20_3, 294 | { 295 | address: bobAddress, 296 | balance: 100n, 297 | }, 298 | bobPubkey, 299 | { 300 | address: aliceAddress, 301 | balance: aliceBalance - 100n, 302 | }, 303 | 10n 304 | ) 305 | result = tx4.verifyScript(0) 306 | expect(result.success, result.error).to.eq(true) 307 | console.log( 308 | `transfer ${10n} Gold to back to alice: ${aliceKey 309 | .toAddress() 310 | .toString()}` 311 | ) 312 | 313 | const { tx: tx5, newInstance: erc20_5 } = await approve( 314 | erc20_4, 315 | alicePubkey, 316 | bobAddress, 317 | 111n 318 | ) 319 | console.log(`alice approve ${111n} Gold to be spend by bob`) 320 | result = tx5.verifyScript(0) 321 | expect(result.success, result.error).to.eq(true) 322 | const lilyKey = bsv.PrivateKey.fromRandom('testnet') 323 | signer.addPrivateKey(lilyKey) 324 | const lilyAddress = PubKeyHash(lilyKey.toAddress().toObject().hash) 325 | 326 | const { tx: tx6 } = await transferFrom( 327 | erc20_5, 328 | bobPubkey, 329 | 111n, 330 | { 331 | address: aliceAddress, 332 | balance: aliceBalance - 100n + 10n, 333 | }, 334 | { 335 | address: lilyAddress, 336 | balance: 0n, 337 | }, 338 | 50n 339 | ) 340 | result = tx6.verifyScript(0) 341 | expect(result.success, result.error).to.eq(true) 342 | console.log(`bob transfer ${50n} Gold from alice balance`) 343 | 344 | return expect( 345 | transferFrom( 346 | erc20_5, 347 | bobPubkey, 348 | 111n, 349 | { 350 | address: aliceAddress, 351 | balance: aliceBalance - 100n + 10n, 352 | }, 353 | { 354 | address: lilyAddress, 355 | balance: 0n, 356 | }, 357 | 150n 358 | ) 359 | ).to.be.rejectedWith(/Execution failed, ERC20: insufficient allowance/) 360 | }) 361 | }) 362 | -------------------------------------------------------------------------------- /tests/local/erc721.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, use } from 'chai' 2 | import { Erc721 } from '../../src/contracts/erc721' 3 | import { myPublicKey } from '../utils/privateKey' 4 | import { 5 | findSig, 6 | getDummySig, 7 | HashedMap, 8 | MethodCallOptions, 9 | PubKey, 10 | toHex, 11 | } from 'scrypt-ts' 12 | import chaiAsPromised from 'chai-as-promised' 13 | import { dummyUTXO, getDummySigner, randomPrivateKey } from '../utils/helper' 14 | 15 | use(chaiAsPromised) 16 | describe('Test SmartContract `Erc721`', () => { 17 | before(async () => { 18 | await Erc721.compile() 19 | }) 20 | 21 | it('should fail `mint` without correct minter sig', async () => { 22 | const owners: HashedMap = new HashedMap< 23 | bigint, 24 | PubKey 25 | >() 26 | const erc721 = new Erc721(PubKey(toHex(myPublicKey)), owners) 27 | await erc721.connect(getDummySigner()) 28 | 29 | const [, alicePubKey, ,] = randomPrivateKey() 30 | 31 | return expect( 32 | erc721.methods.mint( 33 | 1n, // tokenId 34 | PubKey(toHex(alicePubKey)), // mintTo 35 | () => getDummySig(), // mint without correct minter sig 36 | { 37 | fromUTXO: dummyUTXO, 38 | } as MethodCallOptions 39 | ) 40 | ).to.be.rejectedWith(/minter signature check failed/) 41 | }) 42 | 43 | it('should fail `mint` when token was already minted before', async () => { 44 | const [, alicePubKey, ,] = randomPrivateKey() 45 | const tokenId = 1n 46 | 47 | const owners: HashedMap = new HashedMap< 48 | bigint, 49 | PubKey 50 | >() 51 | owners.set(tokenId, PubKey(toHex(alicePubKey))) // token has already in the owners map 52 | 53 | const erc721 = new Erc721(PubKey(toHex(myPublicKey)), owners) 54 | await erc721.connect(getDummySigner()) 55 | 56 | return expect( 57 | erc721.methods.mint( 58 | tokenId, // token already minted before 59 | PubKey(toHex(alicePubKey)), // mintTo 60 | (sigResps) => findSig(sigResps, myPublicKey), // minterSig 61 | { 62 | fromUTXO: dummyUTXO, 63 | pubKeyOrAddrToSign: myPublicKey, 64 | } as MethodCallOptions 65 | ) 66 | ).to.be.rejectedWith(/token was already minted before/) 67 | }) 68 | 69 | it("should fail `burn` when the sender doesn't have the token", async () => { 70 | const [, alicePubKey, ,] = randomPrivateKey() 71 | const [bobPrivateKey, bobPublicKey, ,] = randomPrivateKey() 72 | const tokenId = 1n 73 | 74 | const owners: HashedMap = new HashedMap< 75 | bigint, 76 | PubKey 77 | >() 78 | owners.set(tokenId, PubKey(toHex(alicePubKey))) // alice has the token 79 | 80 | const erc721 = new Erc721(PubKey(toHex(myPublicKey)), owners) 81 | await erc721.connect(getDummySigner(bobPrivateKey)) 82 | 83 | // bob burn the token will fail 84 | return expect( 85 | erc721.methods.burn( 86 | tokenId, 87 | PubKey(toHex(bobPublicKey)), 88 | (sigResps) => findSig(sigResps, bobPublicKey), 89 | { 90 | fromUTXO: dummyUTXO, 91 | pubKeyOrAddrToSign: bobPublicKey, 92 | } as MethodCallOptions 93 | ) 94 | ).to.be.rejectedWith(/sender doesn't have the token/) 95 | }) 96 | 97 | it('should pass `mint`, `transferFrom` then `burn`', async () => { 98 | const [alicePrivateKey, alicePubKey, ,] = randomPrivateKey() 99 | const [bobPrivateKey, bobPubKey, ,] = randomPrivateKey() 100 | const tokenId = 1n 101 | 102 | const owners: HashedMap = new HashedMap< 103 | bigint, 104 | PubKey 105 | >() 106 | 107 | const erc721 = new Erc721(PubKey(toHex(myPublicKey)), owners) 108 | await erc721.connect(getDummySigner([alicePrivateKey, bobPrivateKey])) 109 | 110 | // mint to alice 111 | 112 | const aliceInstance = erc721.next() 113 | aliceInstance.owners.set(tokenId, PubKey(toHex(alicePubKey))) 114 | 115 | const { tx: mintTx, atInputIndex: mintAtInputIndex } = 116 | await erc721.methods.mint( 117 | tokenId, // tokenId 118 | PubKey(toHex(alicePubKey)), // mintTo 119 | (sigResps) => findSig(sigResps, myPublicKey), // minterSig 120 | { 121 | fromUTXO: dummyUTXO, 122 | pubKeyOrAddrToSign: myPublicKey, 123 | next: { 124 | instance: aliceInstance, 125 | balance: dummyUTXO.satoshis, 126 | atOutputIndex: 0, 127 | }, 128 | } as MethodCallOptions 129 | ) 130 | 131 | let result = mintTx.verifyScript(mintAtInputIndex) 132 | expect(result.success, result.error).to.eq(true) 133 | 134 | // transfer from alice to bob 135 | 136 | const bobInstance = aliceInstance.next() 137 | bobInstance.owners.set(tokenId, PubKey(toHex(bobPubKey))) 138 | 139 | const { tx: transferTx, atInputIndex: transferAtInputIndex } = 140 | await aliceInstance.methods.transferFrom( 141 | 1n, // tokenId 142 | PubKey(toHex(alicePubKey)), // sender 143 | (sigResps) => findSig(sigResps, alicePubKey), // sig 144 | PubKey(toHex(bobPubKey)), // receiver 145 | { 146 | pubKeyOrAddrToSign: alicePubKey, 147 | next: { 148 | instance: bobInstance, 149 | balance: dummyUTXO.satoshis, 150 | atOutputIndex: 0, 151 | }, 152 | } as MethodCallOptions 153 | ) 154 | 155 | result = transferTx.verifyScript(transferAtInputIndex) 156 | expect(result.success, result.error).to.eq(true) 157 | 158 | // bob burn 159 | const burnInstance = bobInstance.next() 160 | burnInstance.owners.delete(tokenId) 161 | 162 | const { tx: burnTx, atInputIndex: burnAtInputIndex } = 163 | await bobInstance.methods.burn( 164 | tokenId, // tokenId 165 | PubKey(toHex(bobPubKey)), // sender 166 | (sigResps) => findSig(sigResps, bobPubKey), // sig 167 | { 168 | pubKeyOrAddrToSign: bobPubKey, 169 | next: { 170 | instance: burnInstance, 171 | balance: dummyUTXO.satoshis, 172 | atOutputIndex: 0, 173 | }, 174 | } as MethodCallOptions 175 | ) 176 | 177 | result = burnTx.verifyScript(burnAtInputIndex) 178 | expect(result.success, result.error).to.eq(true) 179 | }) 180 | }) 181 | -------------------------------------------------------------------------------- /tests/local/hashPuzzle.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { MethodCallOptions, sha256, toByteString } from 'scrypt-ts' 3 | import { HashPuzzle } from '../../src/contracts/hashPuzzle' 4 | import { getDummySigner, getDummyUTXO } from '../utils/helper' 5 | 6 | const plainText = 'abc' 7 | const byteString = toByteString(plainText, true) 8 | const sha256Data = sha256(byteString) 9 | 10 | describe('Test SmartContract `HashPuzzle`', () => { 11 | before(async () => { 12 | await HashPuzzle.compile() 13 | }) 14 | 15 | it('should pass the public method unit test successfully.', async () => { 16 | const hashPuzzle = new HashPuzzle(sha256Data) 17 | await hashPuzzle.connect(getDummySigner()) 18 | const { tx: callTx, atInputIndex } = await hashPuzzle.methods.unlock( 19 | byteString, 20 | { 21 | fromUTXO: getDummyUTXO(), 22 | } as MethodCallOptions 23 | ) 24 | 25 | const result = callTx.verifyScript(atInputIndex) 26 | expect(result.success, result.error).to.eq(true) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /tests/local/hashedMapNonState.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { HashedMapNonState } from '../../src/contracts/hashedMapNonState' 3 | import { 4 | ByteString, 5 | HashedMap, 6 | MethodCallOptions, 7 | toByteString, 8 | } from 'scrypt-ts' 9 | import { dummyUTXO, getDummySigner } from '../utils/helper' 10 | 11 | describe('Test SmartContract `HashedMapNonState`', () => { 12 | before(async () => { 13 | await HashedMapNonState.compile() 14 | }) 15 | 16 | it('should unlock `HashedMapNonState` successfully.', async () => { 17 | const map = new HashedMap() 18 | map.set(1n, toByteString('0001')) 19 | map.set(2n, toByteString('0011')) 20 | map.set(10n, toByteString('0111')) 21 | 22 | const hashedMapNonState = new HashedMapNonState(map) 23 | await hashedMapNonState.connect(getDummySigner()) 24 | 25 | const { tx, atInputIndex } = await hashedMapNonState.methods.unlock( 26 | 7n, 27 | toByteString('07'), 28 | { 29 | fromUTXO: dummyUTXO, 30 | } as MethodCallOptions 31 | ) 32 | const result = tx.verifyScript(atInputIndex) 33 | expect(result.success, result.error).to.eq(true) 34 | }) 35 | 36 | it('should delete element successfully.', async () => { 37 | const map = new HashedMap([ 38 | [1n, toByteString('0001')], 39 | ]) 40 | 41 | const hashedMapNonState = new HashedMapNonState(map) 42 | await hashedMapNonState.connect(getDummySigner()) 43 | 44 | const { tx, atInputIndex } = await hashedMapNonState.methods.delete( 45 | 1n, 46 | { 47 | fromUTXO: dummyUTXO, 48 | } as MethodCallOptions 49 | ) 50 | const result = tx.verifyScript(atInputIndex) 51 | expect(result.success, result.error).to.eq(true) 52 | }) 53 | 54 | it('should throw', async () => { 55 | const map = new HashedMap([ 56 | [1n, toByteString('0001')], 57 | ]) 58 | 59 | const hashedMapNonState = new HashedMapNonState(map) 60 | await hashedMapNonState.connect(getDummySigner()) 61 | 62 | return expect( 63 | hashedMapNonState.methods.delete(2n, { 64 | fromUTXO: dummyUTXO, 65 | } as MethodCallOptions) 66 | ).to.be.rejectedWith(/hashedMap should have the key before delete/) 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /tests/local/hashedMapState.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | 3 | import { HashedMapState } from '../../src/contracts/hashedMapState' 4 | import { 5 | ByteString, 6 | HashedMap, 7 | int2ByteString, 8 | MethodCallOptions, 9 | toByteString, 10 | } from 'scrypt-ts' 11 | import { dummyUTXO, getDummySigner, inputSatoshis } from '../utils/helper' 12 | 13 | const signer = getDummySigner() 14 | 15 | describe('Test SmartContract `HashedMapState`', () => { 16 | let map: HashedMap, stateMap: HashedMapState 17 | before(async () => { 18 | await HashedMapState.compile() 19 | 20 | map = new HashedMap() 21 | 22 | stateMap = new HashedMapState(map) 23 | await stateMap.connect(signer) 24 | }) 25 | 26 | async function insert( 27 | instance: HashedMapState, 28 | key: bigint, 29 | val: ByteString 30 | ) { 31 | const newInstance = instance.next() 32 | 33 | newInstance.hashedmap.set(key, val) 34 | 35 | const { nexts, tx } = await instance.methods.insert(key, val, { 36 | fromUTXO: dummyUTXO, 37 | next: { 38 | instance: newInstance, 39 | balance: inputSatoshis, 40 | }, 41 | } as MethodCallOptions) 42 | 43 | return { 44 | tx: tx, 45 | newInstance: nexts[0].instance, 46 | } 47 | } 48 | 49 | async function canGet( 50 | instance: HashedMapState, 51 | key: bigint, 52 | val: ByteString 53 | ) { 54 | const newInstance = instance.next() 55 | 56 | const { nexts, tx } = await instance.methods.canGet(key, val, { 57 | fromUTXO: dummyUTXO, 58 | next: { 59 | instance: newInstance, 60 | balance: inputSatoshis, 61 | }, 62 | } as MethodCallOptions) 63 | 64 | return { 65 | tx: tx, 66 | newInstance: nexts[0].instance, 67 | } 68 | } 69 | 70 | async function notExist(instance: HashedMapState, key: bigint) { 71 | const newInstance = instance.next() 72 | 73 | const { nexts, tx } = await instance.methods.notExist(key, { 74 | fromUTXO: dummyUTXO, 75 | next: { 76 | instance: newInstance, 77 | balance: inputSatoshis, 78 | }, 79 | } as MethodCallOptions) 80 | 81 | return { 82 | tx: tx, 83 | newInstance: nexts[0].instance, 84 | } 85 | } 86 | 87 | async function update( 88 | instance: HashedMapState, 89 | key: bigint, 90 | val: ByteString 91 | ) { 92 | const newInstance = instance.next() 93 | newInstance.hashedmap.set(key, val) 94 | 95 | const { nexts, tx } = await instance.methods.update(key, val, { 96 | fromUTXO: dummyUTXO, 97 | next: { 98 | instance: newInstance, 99 | balance: inputSatoshis, 100 | }, 101 | } as MethodCallOptions) 102 | 103 | return { 104 | tx: tx, 105 | newInstance: nexts[0].instance, 106 | } 107 | } 108 | 109 | async function deleteKey(instance: HashedMapState, key: bigint) { 110 | const newInstance = instance.next() 111 | newInstance.hashedmap.delete(key) 112 | const { nexts, tx } = await instance.methods.delete(key, { 113 | fromUTXO: dummyUTXO, 114 | next: { 115 | instance: newInstance, 116 | balance: inputSatoshis, 117 | }, 118 | } as MethodCallOptions) 119 | 120 | return { 121 | tx: tx, 122 | newInstance: nexts[0].instance, 123 | } 124 | } 125 | 126 | it('insert, canGet, update, delete should pass', async () => { 127 | const { tx: tx1, newInstance: newInstance1 } = await insert( 128 | stateMap, 129 | 1n, 130 | toByteString('0001') 131 | ) 132 | let result = tx1.verifyScript(0) 133 | expect(result.success, result.error).to.eq(true) 134 | 135 | const { tx: tx2, newInstance: newInstance2 } = await insert( 136 | newInstance1, 137 | 2n, 138 | toByteString('0002') 139 | ) 140 | 141 | result = tx2.verifyScript(0) 142 | expect(result.success, result.error).to.eq(true) 143 | 144 | const { tx: tx3, newInstance: newInstance3 } = await canGet( 145 | newInstance2, 146 | 2n, 147 | toByteString('0002') 148 | ) 149 | result = tx3.verifyScript(0) 150 | expect(result.success, result.error).to.eq(true) 151 | 152 | const { tx: tx4, newInstance: newInstance4 } = await canGet( 153 | newInstance3, 154 | 1n, 155 | toByteString('0001') 156 | ) 157 | result = tx4.verifyScript(0) 158 | expect(result.success, result.error).to.eq(true) 159 | 160 | const { tx: tx5, newInstance: newInstance5 } = await update( 161 | newInstance4, 162 | 1n, 163 | toByteString('000001') 164 | ) 165 | result = tx5.verifyScript(0) 166 | expect(result.success, result.error).to.eq(true) 167 | 168 | const { tx: tx6, newInstance: newInstance6 } = await update( 169 | newInstance5, 170 | 2n, 171 | toByteString('000002') 172 | ) 173 | result = tx6.verifyScript(0) 174 | expect(result.success, result.error).to.eq(true) 175 | 176 | const { tx: tx7, newInstance: newInstance7 } = await canGet( 177 | newInstance6, 178 | 1n, 179 | toByteString('000001') 180 | ) 181 | result = tx7.verifyScript(0) 182 | expect(result.success, result.error).to.eq(true) 183 | 184 | const { tx: tx8, newInstance: newInstance8 } = await canGet( 185 | newInstance7, 186 | 2n, 187 | toByteString('000002') 188 | ) 189 | result = tx8.verifyScript(0) 190 | expect(result.success, result.error).to.eq(true) 191 | 192 | const { tx: tx9, newInstance: newInstance9 } = await deleteKey( 193 | newInstance8, 194 | 2n 195 | ) 196 | result = tx9.verifyScript(0) 197 | expect(result.success, result.error).to.eq(true) 198 | 199 | const { tx: tx10 } = await notExist(newInstance9, 2n) 200 | result = tx10.verifyScript(0) 201 | expect(result.success, result.error).to.eq(true) 202 | }) 203 | 204 | it('unlock should pass', async () => { 205 | const map = new HashedMap() 206 | 207 | const key = 2n 208 | const val = toByteString('0a0a0a0a0a') 209 | 210 | const instance = new HashedMapState(map) 211 | await instance.connect(signer) 212 | 213 | const newInstance = instance.next() 214 | 215 | for (let i = 0; i < 5; i++) { 216 | newInstance.hashedmap.set( 217 | BigInt(i), 218 | int2ByteString(BigInt(i), BigInt(i)) 219 | ) 220 | } 221 | 222 | for (let i = 0; i < 5; i++) { 223 | if (i === 3) { 224 | newInstance.hashedmap.delete(key) 225 | } else if (i == 4) { 226 | newInstance.hashedmap.set(key, val) 227 | } 228 | } 229 | 230 | const { tx } = await instance.methods.unlock(key, val, { 231 | fromUTXO: dummyUTXO, 232 | next: { 233 | instance: newInstance, 234 | balance: inputSatoshis, 235 | }, 236 | } as MethodCallOptions) 237 | 238 | const result = tx.verifyScript(0) 239 | expect(result.success, result.error).to.eq(true) 240 | }) 241 | }) 242 | -------------------------------------------------------------------------------- /tests/local/hashedSetNonState.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { HashedSetNonState } from '../../src/contracts/hashedSetNonState' 3 | import { HashedSet, MethodCallOptions } from 'scrypt-ts' 4 | import { dummyUTXO, getDummySigner } from '../utils/helper' 5 | 6 | describe('Test SmartContract `HashedSetNonState`', () => { 7 | before(async () => { 8 | await HashedSetNonState.compile() 9 | }) 10 | 11 | it('should unlock contract `HashedSetNonState` successfully.', async () => { 12 | const set = new HashedSet() 13 | set.add(1n) 14 | set.add(2n) 15 | set.add(3n) 16 | 17 | const hashedSetNonState = new HashedSetNonState(set) 18 | await hashedSetNonState.connect(getDummySigner()) 19 | 20 | const { tx: tx1, atInputIndex: atInputIndex1 } = 21 | await hashedSetNonState.methods.add(1n, { 22 | fromUTXO: dummyUTXO, 23 | } as MethodCallOptions) 24 | let result = tx1.verifyScript(atInputIndex1) 25 | expect(result.success, result.error).to.eq(true) 26 | 27 | const { tx: tx2, atInputIndex: atInputIndex2 } = 28 | await hashedSetNonState.methods.add(7n, { 29 | fromUTXO: dummyUTXO, 30 | } as MethodCallOptions) 31 | result = tx2.verifyScript(atInputIndex2) 32 | expect(result.success, result.error).to.eq(true) 33 | }) 34 | 35 | it('should delete element successfully.', async () => { 36 | const set = new HashedSet() 37 | set.add(1n) 38 | 39 | const hashedSetNonState = new HashedSetNonState(set) 40 | await hashedSetNonState.connect(getDummySigner()) 41 | 42 | const { tx, atInputIndex } = await hashedSetNonState.methods.delete( 43 | 1n, 44 | { 45 | fromUTXO: dummyUTXO, 46 | } as MethodCallOptions 47 | ) 48 | const result = tx.verifyScript(atInputIndex) 49 | expect(result.success, result.error).to.eq(true) 50 | }) 51 | 52 | it('should throw', async () => { 53 | const set = new HashedSet() 54 | set.add(1n) 55 | 56 | const hashedSetNonState = new HashedSetNonState(set) 57 | await hashedSetNonState.connect(getDummySigner()) 58 | 59 | return expect( 60 | hashedSetNonState.methods.delete(2n, { 61 | fromUTXO: dummyUTXO, 62 | } as MethodCallOptions) 63 | ).to.be.rejectedWith(/hashedSet should have the key before delete/) 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /tests/local/hashedSetState.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | 3 | import { HashedSetState } from '../../src/contracts/hashedSetState' 4 | import { HashedSet, MethodCallOptions } from 'scrypt-ts' 5 | import { dummyUTXO, getDummySigner, inputSatoshis } from '../utils/helper' 6 | 7 | const signer = getDummySigner() 8 | 9 | describe('Test SmartContract `HashedSetState`', () => { 10 | let set: HashedSet, stateSet: HashedSetState 11 | before(async () => { 12 | await HashedSetState.compile() 13 | 14 | set = new HashedSet() 15 | 16 | stateSet = new HashedSetState(set) 17 | await stateSet.connect(signer) 18 | }) 19 | 20 | async function add(instance: HashedSetState, key: bigint) { 21 | const newInstance = instance.next() 22 | newInstance.hashedset.add(key) 23 | 24 | const { nexts, tx, atInputIndex } = await instance.methods.add(key, { 25 | fromUTXO: dummyUTXO, 26 | next: { 27 | instance: newInstance, 28 | balance: inputSatoshis, 29 | }, 30 | } as MethodCallOptions) 31 | 32 | return { 33 | tx, 34 | atInputIndex, 35 | newInstance: nexts[0].instance, 36 | } 37 | } 38 | 39 | async function has(instance: HashedSetState, key: bigint) { 40 | const newInstance = instance.next() 41 | 42 | const { nexts, tx, atInputIndex } = await instance.methods.has(key, { 43 | fromUTXO: dummyUTXO, 44 | next: { 45 | instance: newInstance, 46 | balance: inputSatoshis, 47 | }, 48 | } as MethodCallOptions) 49 | 50 | return { 51 | tx, 52 | atInputIndex, 53 | newInstance: nexts[0].instance, 54 | } 55 | } 56 | 57 | async function notExist(instance: HashedSetState, key: bigint) { 58 | const newInstance = instance.next() 59 | 60 | const { nexts, tx, atInputIndex } = await instance.methods.notExist( 61 | key, 62 | { 63 | fromUTXO: dummyUTXO, 64 | next: { 65 | instance: newInstance, 66 | balance: inputSatoshis, 67 | }, 68 | } as MethodCallOptions 69 | ) 70 | 71 | return { 72 | tx, 73 | atInputIndex, 74 | newInstance: nexts[0].instance, 75 | } 76 | } 77 | 78 | async function _delete(instance: HashedSetState, key: bigint) { 79 | const newInstance = instance.next() 80 | newInstance.hashedset.delete(key) 81 | 82 | const { nexts, tx, atInputIndex } = await instance.methods.delete(key, { 83 | fromUTXO: dummyUTXO, 84 | next: { 85 | instance: newInstance, 86 | balance: inputSatoshis, 87 | }, 88 | } as MethodCallOptions) 89 | 90 | return { 91 | tx, 92 | atInputIndex, 93 | newInstance: nexts[0].instance, 94 | } 95 | } 96 | 97 | it('add, has, delete should pass', async () => { 98 | const { 99 | tx: tx1, 100 | newInstance: newInstance1, 101 | atInputIndex: index1, 102 | } = await add(stateSet, 1n) 103 | let result = tx1.verifyScript(index1) 104 | expect(result.success, result.error).to.eq(true) 105 | 106 | const { 107 | tx: tx2, 108 | newInstance: newInstance2, 109 | atInputIndex: index2, 110 | } = await add(newInstance1, 2n) 111 | result = tx2.verifyScript(index2) 112 | expect(result.success, result.error).to.eq(true) 113 | 114 | const { 115 | tx: tx3, 116 | newInstance: newInstance3, 117 | atInputIndex: index3, 118 | } = await has(newInstance2, 2n) 119 | result = tx3.verifyScript(index3) 120 | expect(result.success, result.error).to.eq(true) 121 | 122 | const { 123 | tx: tx4, 124 | newInstance: newInstance4, 125 | atInputIndex: index4, 126 | } = await has(newInstance3, 1n) 127 | result = tx4.verifyScript(index4) 128 | expect(result.success, result.error).to.eq(true) 129 | 130 | const { 131 | tx: tx5, 132 | newInstance: newInstance5, 133 | atInputIndex: index5, 134 | } = await _delete(newInstance4, 2n) 135 | result = tx5.verifyScript(index5) 136 | expect(result.success, result.error).to.eq(true) 137 | 138 | const { tx: tx6, atInputIndex: index6 } = await notExist( 139 | newInstance5, 140 | 2n 141 | ) 142 | result = tx6.verifyScript(index6) 143 | expect(result.success, result.error).to.eq(true) 144 | }) 145 | }) 146 | -------------------------------------------------------------------------------- /tests/local/helloWorld.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, use } from 'chai' 2 | import { MethodCallOptions, sha256, toByteString } from 'scrypt-ts' 3 | import { HelloWorld } from '../../src/contracts/helloWorld' 4 | import { getDummySigner, getDummyUTXO } from '../utils/helper' 5 | import chaiAsPromised from 'chai-as-promised' 6 | 7 | use(chaiAsPromised) 8 | 9 | describe('Test SmartContract `HelloWorld`', () => { 10 | let helloWorld: HelloWorld 11 | 12 | before(async () => { 13 | await HelloWorld.compile() 14 | helloWorld = new HelloWorld(sha256(toByteString('hello world', true))) 15 | await helloWorld.connect(getDummySigner()) 16 | }) 17 | 18 | it('should pass the public method unit test successfully.', async () => { 19 | const { tx: callTx, atInputIndex } = await helloWorld.methods.unlock( 20 | toByteString('hello world', true), 21 | { 22 | fromUTXO: getDummyUTXO(), 23 | } as MethodCallOptions 24 | ) 25 | 26 | const result = callTx.verifyScript(atInputIndex) 27 | expect(result.success, result.error).to.eq(true) 28 | }) 29 | 30 | it('should throw with wrong message.', async () => { 31 | return expect( 32 | helloWorld.methods.unlock(toByteString('wrong message', true), { 33 | fromUTXO: getDummyUTXO(), 34 | } as MethodCallOptions) 35 | ).to.be.rejectedWith(/Not expected message!/) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /tests/local/mimc7.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { Mimc7Test } from '../../src/contracts/mimc7' 3 | import { getDummySigner, getDummyUTXO } from '../utils/helper' 4 | import { MethodCallOptions } from 'scrypt-ts' 5 | 6 | describe('Test SmartContract `Mimc7Test`', () => { 7 | before(async () => { 8 | await Mimc7Test.compile() 9 | }) 10 | 11 | it('should pass the public method unit test successfully.', async () => { 12 | const mimc7 = new Mimc7Test() 13 | await mimc7.connect(getDummySigner()) 14 | 15 | const { tx: callTx, atInputIndex } = await mimc7.methods.unlock( 16 | 1n, 17 | 2n, 18 | 10594780656576967754230020536574539122676596303354946869887184401991294982664n, 19 | { 20 | fromUTXO: getDummyUTXO(), 21 | } as MethodCallOptions 22 | ) 23 | 24 | const result = callTx.verifyScript(atInputIndex) 25 | expect(result.success, result.error).to.eq(true) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /tests/local/multi_contracts_call.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MethodCallOptions, 3 | SmartContract, 4 | bsv, 5 | ContractTransaction, 6 | toByteString, 7 | sha256, 8 | } from 'scrypt-ts' 9 | import { Counter } from '../../src/contracts/counter' 10 | import { getDummySigner, getDummyUTXO } from '../utils/helper' 11 | import { expect } from 'chai' 12 | import { HashPuzzle } from '../../src/contracts/hashPuzzle' 13 | 14 | describe('Test SmartContract `Counter, HashPuzzle` multi call on local', () => { 15 | before(async () => { 16 | await Counter.compile() 17 | await HashPuzzle.compile() 18 | }) 19 | 20 | it('should succeed', async () => { 21 | const signer = getDummySigner() 22 | let counter1 = new Counter(1n) 23 | 24 | // connect to a signer 25 | await counter1.connect(signer) 26 | 27 | counter1.bindTxBuilder( 28 | 'incrementOnChain', 29 | ( 30 | current: Counter, 31 | options: MethodCallOptions, 32 | ...args: any 33 | ): Promise => { 34 | // create the next instance from the current 35 | const nextInstance = current.next() 36 | // apply updates on the next instance locally 37 | nextInstance.count++ 38 | 39 | const tx = new bsv.Transaction() 40 | tx.addInput( 41 | current.buildContractInput(options.fromUTXO) 42 | ).addOutput( 43 | new bsv.Transaction.Output({ 44 | script: nextInstance.lockingScript, 45 | satoshis: current.balance, 46 | }) 47 | ) 48 | 49 | return Promise.resolve({ 50 | tx: tx, 51 | atInputIndex: 0, 52 | nexts: [ 53 | { 54 | instance: nextInstance, 55 | balance: current.balance, 56 | atOutputIndex: 0, 57 | }, 58 | ], 59 | }) 60 | } 61 | ) 62 | 63 | const plainText = 'abc' 64 | const byteString = toByteString(plainText, true) 65 | const sha256Data = sha256(byteString) 66 | 67 | const hashPuzzle = new HashPuzzle(sha256Data) 68 | 69 | // connect to a signer 70 | await hashPuzzle.connect(signer) 71 | hashPuzzle.bindTxBuilder( 72 | 'unlock', 73 | ( 74 | current: HashPuzzle, 75 | options: MethodCallOptions, 76 | ...args: any 77 | ): Promise => { 78 | if (options.partialContractTransaction) { 79 | const unSignedTx = options.partialContractTransaction.tx 80 | unSignedTx.addInput( 81 | current.buildContractInput(options.fromUTXO) 82 | ) 83 | 84 | return Promise.resolve({ 85 | tx: unSignedTx, 86 | atInputIndex: 1, 87 | nexts: [], 88 | }) 89 | } 90 | 91 | throw new Error('no partialContractTransaction found') 92 | } 93 | ) 94 | 95 | const partialContractTransaction1 = 96 | await counter1.methods.incrementOnChain({ 97 | multiContractCall: true, 98 | fromUTXO: getDummyUTXO(1, true), 99 | } as MethodCallOptions) 100 | 101 | const partialContractTransaction2 = await hashPuzzle.methods.unlock( 102 | byteString, 103 | { 104 | fromUTXO: getDummyUTXO(1, true), 105 | multiContractCall: true, 106 | partialContractTransaction: partialContractTransaction1, 107 | } as MethodCallOptions 108 | ) 109 | 110 | const { tx: callTx, nexts } = await SmartContract.multiContractCall( 111 | partialContractTransaction2, 112 | signer 113 | ) 114 | 115 | const result = callTx.verify() 116 | expect(result).to.be.true 117 | 118 | // hashPuzzle has terminated, but counter can still be called 119 | counter1 = nexts[0].instance 120 | }) 121 | }) 122 | -------------------------------------------------------------------------------- /tests/local/p2pkh.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, use } from 'chai' 2 | import chaiAsPromised from 'chai-as-promised' 3 | import { 4 | findSig, 5 | MethodCallOptions, 6 | PubKey, 7 | PubKeyHash, 8 | toHex, 9 | } from 'scrypt-ts' 10 | import { P2PKH } from '../../src/contracts/p2pkh' 11 | import { getDummySigner, getDummyUTXO, randomPrivateKey } from '../utils/helper' 12 | import { myPublicKey, myPublicKeyHash } from '../utils/privateKey' 13 | 14 | use(chaiAsPromised) 15 | 16 | describe('Test SmartContract `P2PKH`', () => { 17 | before(async () => { 18 | await P2PKH.compile() 19 | }) 20 | 21 | it('should pass if using right private key', async () => { 22 | // create a new P2PKH contract instance 23 | // this instance was paid to `myPublicKeyHash` 24 | const p2pkh = new P2PKH(PubKeyHash(toHex(myPublicKeyHash))) 25 | // connect contract instance to a signer 26 | // dummySigner() has one private key in it by default, it's `myPrivateKey` 27 | await p2pkh.connect(getDummySigner()) 28 | // call public function `unlock` of this contract 29 | const { tx: callTx, atInputIndex } = await p2pkh.methods.unlock( 30 | // pass signature, the first parameter, to `unlock` 31 | // after the signer signs the transaction, the signatures are returned in `SignatureResponse[]` 32 | // you need to find the signature or signatures you want in the return through the public key or address 33 | // here we use `myPublicKey` to find the signature because we signed the transaction with `myPrivateKey` before 34 | (sigResps) => findSig(sigResps, myPublicKey), 35 | // pass public key, the second parameter, to `unlock` 36 | PubKey(toHex(myPublicKey)), 37 | // method call options 38 | { 39 | fromUTXO: getDummyUTXO(), 40 | // tell the signer to use the private key corresponding to `myPublicKey` to sign this transaction 41 | // that is using `myPrivateKey` to sign the transaction 42 | pubKeyOrAddrToSign: myPublicKey, 43 | } as MethodCallOptions 44 | ) 45 | // check if the unlock transaction built above is correct 46 | const result = callTx.verifyScript(atInputIndex) 47 | expect(result.success, result.error).to.eq(true) 48 | }) 49 | 50 | it('should fail if using wrong private key', async () => { 51 | const [wrongPrivateKey, wrongPublicKey] = randomPrivateKey() 52 | // contract instance was paid to `myPublicKeyHash` 53 | const p2pkh = new P2PKH(PubKeyHash(toHex(myPublicKeyHash))) 54 | // add a new private key, `wrongPrivateKey`, into the signer 55 | // now the signer has two private keys in it 56 | await p2pkh.connect(getDummySigner(wrongPrivateKey)) 57 | return expect( 58 | p2pkh.methods.unlock( 59 | // pass the signature signed by `wrongPrivateKey` 60 | (sigResps) => findSig(sigResps, wrongPublicKey), 61 | // pass the correct public key 62 | PubKey(toHex(myPublicKey)), 63 | { 64 | fromUTXO: getDummyUTXO(), 65 | pubKeyOrAddrToSign: wrongPublicKey, // use `wrongPrivateKey` to sign 66 | } as MethodCallOptions 67 | ) 68 | ).to.be.rejectedWith(/signature check failed/) 69 | }) 70 | 71 | it('should fail if passing wrong public key', async () => { 72 | const [, wrongPublicKey, ,] = randomPrivateKey() 73 | // contract instance was paid to `myPublicKeyHash` 74 | const p2pkh = new P2PKH(PubKeyHash(toHex(myPublicKeyHash))) 75 | await p2pkh.connect(getDummySigner()) 76 | return expect( 77 | p2pkh.methods.unlock( 78 | // pass the correct signature signed by `myPrivateKey` 79 | (sigResps) => findSig(sigResps, myPublicKey), 80 | // but pass the wrong public key 81 | PubKey(toHex(wrongPublicKey)), 82 | { 83 | fromUTXO: getDummyUTXO(), 84 | pubKeyOrAddrToSign: myPublicKey, // use the correct private key, `myPrivateKey`, to sign 85 | } as MethodCallOptions 86 | ) 87 | ).to.be.rejectedWith(/public key hashes are not equal/) 88 | }) 89 | }) 90 | -------------------------------------------------------------------------------- /tests/local/recallable.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, use } from 'chai' 2 | import chaiAsPromised from 'chai-as-promised' 3 | import { 4 | findSig, 5 | getDummySig, 6 | MethodCallOptions, 7 | PubKey, 8 | toHex, 9 | } from 'scrypt-ts' 10 | import { Recallable } from '../../src/contracts/recallable' 11 | import { getDummySigner, getDummyUTXO, randomPrivateKey } from '../utils/helper' 12 | 13 | use(chaiAsPromised) 14 | 15 | describe('Test SmartContract `Recallable`', () => { 16 | // alice is the issuer 17 | const [alicePrivateKey, alicePublicKey, ,] = randomPrivateKey() 18 | // bob is a user 19 | const [, bobPublicKey, ,] = randomPrivateKey() 20 | 21 | let recallable: Recallable 22 | 23 | before(async () => { 24 | await Recallable.compile() 25 | 26 | recallable = new Recallable(PubKey(toHex(alicePublicKey))) 27 | await recallable.connect(getDummySigner(alicePrivateKey)) 28 | }) 29 | 30 | it('should fail with `satoshisSent` that is less than 1', () => { 31 | return expect( 32 | recallable.methods.transfer( 33 | (sigResps) => findSig(sigResps, alicePublicKey), 34 | PubKey(toHex(bobPublicKey)), 35 | BigInt(0), // less than 1 36 | { 37 | fromUTXO: getDummyUTXO(), 38 | pubKeyOrAddrToSign: alicePublicKey, 39 | } as MethodCallOptions 40 | ) 41 | ).to.be.rejectedWith(/invalid value of `satoshisSent`/) 42 | }) 43 | 44 | it('should fail with `satoshisSent` that is greater than total satoshis', () => { 45 | return expect( 46 | recallable.methods.transfer( 47 | (sigResps) => findSig(sigResps, alicePublicKey), 48 | PubKey(toHex(bobPublicKey)), 49 | BigInt(getDummyUTXO().satoshis + 1), // more than the total satoshis 50 | { 51 | fromUTXO: getDummyUTXO(), 52 | pubKeyOrAddrToSign: alicePublicKey, 53 | } as MethodCallOptions 54 | ) 55 | ).to.be.rejectedWith(/invalid value of `satoshisSent`/) 56 | }) 57 | 58 | it('should fail with invalid signature', () => { 59 | return expect( 60 | recallable.methods.transfer( 61 | () => getDummySig(), 62 | PubKey(toHex(bobPublicKey)), 63 | BigInt(1), 64 | { 65 | fromUTXO: getDummyUTXO(), 66 | } as MethodCallOptions 67 | ) 68 | ).to.be.rejectedWith(/user's signature check failed/) 69 | }) 70 | 71 | it('should pass 3000/7000 and recall', async () => { 72 | /** 73 | * alice transfers 3000 to bob, keeps 7000 left 74 | * */ 75 | 76 | const aliceNextInstance = recallable.next() 77 | 78 | const bobNextInstance = recallable.next() 79 | bobNextInstance.userPubKey = PubKey(toHex(bobPublicKey)) 80 | 81 | const dummyUTXO = getDummyUTXO() 82 | const satoshiSent = 3000 83 | const satoshisLeft = dummyUTXO.satoshis - satoshiSent 84 | 85 | // transfer method calling tx 86 | const { tx: transferTx, atInputIndex: transferAtInputIndex } = 87 | await recallable.methods.transfer( 88 | (sigResps) => findSig(sigResps, alicePublicKey), 89 | PubKey(toHex(bobPublicKey)), 90 | BigInt(satoshiSent), 91 | { 92 | fromUTXO: dummyUTXO, 93 | pubKeyOrAddrToSign: alicePublicKey, 94 | next: [ 95 | { 96 | instance: bobNextInstance, 97 | balance: satoshiSent, 98 | }, 99 | { 100 | instance: aliceNextInstance, 101 | balance: satoshisLeft, 102 | }, 103 | ], 104 | } as MethodCallOptions 105 | ) 106 | 107 | let result = transferTx.verifyScript(transferAtInputIndex) 108 | expect(result.success, result.error).to.eq(true) 109 | 110 | /** 111 | * alice recall 3000 from bob 112 | */ 113 | 114 | const aliceRecallInstance = bobNextInstance.next() 115 | aliceRecallInstance.userPubKey = PubKey(toHex(alicePublicKey)) 116 | 117 | // recall method calling tx 118 | const { tx: recallTx, atInputIndex: recallAtInputIndex } = 119 | await bobNextInstance.methods.recall( 120 | (sigResps) => findSig(sigResps, alicePublicKey), 121 | { 122 | pubKeyOrAddrToSign: alicePublicKey, 123 | next: { 124 | instance: aliceRecallInstance, 125 | balance: bobNextInstance.balance, 126 | }, 127 | } as MethodCallOptions 128 | ) 129 | 130 | result = recallTx.verifyScript(recallAtInputIndex) 131 | expect(result.success, result.error).to.eq(true) 132 | }) 133 | 134 | it('should pass 10000/0', async () => { 135 | // alice transfers 10000 to bob, keeps nothing left 136 | 137 | const bobNextInstance = recallable.next() 138 | bobNextInstance.userPubKey = PubKey(toHex(bobPublicKey)) 139 | 140 | const dummyUTXO = getDummyUTXO() 141 | const satoshiSent = dummyUTXO.satoshis 142 | 143 | // transfer method calling tx 144 | const { tx: callTx, atInputIndex } = await recallable.methods.transfer( 145 | (sigResps) => findSig(sigResps, alicePublicKey), 146 | PubKey(toHex(bobPublicKey)), 147 | BigInt(satoshiSent), 148 | { 149 | fromUTXO: dummyUTXO, 150 | pubKeyOrAddrToSign: alicePublicKey, 151 | next: { 152 | instance: bobNextInstance, 153 | balance: satoshiSent, 154 | atOutputIndex: 0, 155 | }, 156 | } as MethodCallOptions 157 | ) 158 | 159 | const result = callTx.verifyScript(atInputIndex) 160 | expect(result.success, result.error).to.eq(true) 161 | }) 162 | }) 163 | -------------------------------------------------------------------------------- /tests/testnet/accumulatorMultiSig.ts: -------------------------------------------------------------------------------- 1 | import { AccumulatorMultiSig } from '../../src/contracts/accumulatorMultiSig' 2 | import { 3 | getDefaultSigner, 4 | inputSatoshis, 5 | randomPrivateKey, 6 | } from '../utils/helper' 7 | import { 8 | bsv, 9 | findSig, 10 | FixedArray, 11 | getDummySig, 12 | MethodCallOptions, 13 | PubKey, 14 | PubKeyHash, 15 | toHex, 16 | } from 'scrypt-ts' 17 | 18 | async function main() { 19 | await AccumulatorMultiSig.compile() 20 | 21 | const [privateKey1, publicKey1, publicKeyHash1] = randomPrivateKey() 22 | const [privateKey2, publicKey2, publicKeyHash2] = randomPrivateKey() 23 | const [privateKey3, publicKey3, publicKeyHash3] = randomPrivateKey() 24 | 25 | const pubKeyHashes = [publicKeyHash1, publicKeyHash2, publicKeyHash3].map( 26 | (pkh) => PubKeyHash(toHex(pkh)) 27 | ) as FixedArray 28 | 29 | const accumulatorMultiSig = new AccumulatorMultiSig(2n, pubKeyHashes) 30 | 31 | const signer = await getDefaultSigner([ 32 | privateKey1, 33 | privateKey2, 34 | privateKey3, 35 | ]) 36 | 37 | // connect to a signer 38 | await accumulatorMultiSig.connect(signer) 39 | 40 | // deploy 41 | const deployTx = await accumulatorMultiSig.deploy(inputSatoshis) 42 | console.log('AccumulatorMultiSig contract deployed: ', deployTx.id) 43 | 44 | // set one random mask index to be false to mark an invalid signature. 45 | const masks = [true, false, true] 46 | 47 | const pubKeys = [publicKey1, publicKey2, publicKey3].map((pk) => { 48 | return PubKey(pk.toString()) 49 | }) as FixedArray 50 | 51 | // call 52 | const { tx: callTx } = await accumulatorMultiSig.methods.main( 53 | pubKeys, 54 | (sigResps) => { 55 | return pubKeys.map((pubKey) => { 56 | try { 57 | return findSig(sigResps, bsv.PublicKey.fromString(pubKey)) 58 | } catch (error) { 59 | return getDummySig() 60 | } 61 | }) 62 | }, 63 | masks, 64 | { 65 | pubKeyOrAddrToSign: pubKeys 66 | .filter((_, idx) => masks[idx]) 67 | .map((pubkey) => bsv.PublicKey.fromString(pubkey)), 68 | } as MethodCallOptions 69 | ) 70 | console.log('AccumulatorMultiSig contract called: ', callTx.id) 71 | } 72 | 73 | describe('Test SmartContract `AccumulatorMultiSig` on testnet', () => { 74 | it('should succeed', async () => { 75 | await main() 76 | }) 77 | }) 78 | -------------------------------------------------------------------------------- /tests/testnet/ackerman.ts: -------------------------------------------------------------------------------- 1 | import { getDefaultSigner, inputSatoshis } from '../utils/helper' 2 | import { Ackermann } from '../../src/contracts/ackermann' 3 | 4 | async function main() { 5 | await Ackermann.compile() 6 | const ackermann = new Ackermann(2n, 1n) 7 | 8 | // connect to a signer 9 | await ackermann.connect(getDefaultSigner()) 10 | 11 | // contract deploy 12 | const deployTx = await ackermann.deploy(inputSatoshis) 13 | console.log('Ackermann contract deployed: ', deployTx.id) 14 | 15 | // contract call 16 | const { tx: callTx } = await ackermann.methods.unlock(5n) 17 | console.log('Ackermann contract called: ', callTx.id) 18 | } 19 | 20 | describe('Test SmartContract `Ackermann` on testnet', () => { 21 | it('should succeed', async () => { 22 | await main() 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /tests/testnet/acs.ts: -------------------------------------------------------------------------------- 1 | import { AnyoneCanSpend } from '../../src/contracts/acs' 2 | import { getDefaultSigner, inputSatoshis } from '../utils/helper' 3 | import { PubKeyHash, toHex } from 'scrypt-ts' 4 | import { myPublicKeyHash } from '../utils/privateKey' 5 | 6 | async function main() { 7 | await AnyoneCanSpend.compile() 8 | const anyoneCanSpend = new AnyoneCanSpend( 9 | PubKeyHash(toHex(myPublicKeyHash)) 10 | ) 11 | 12 | // connect to a signer 13 | await anyoneCanSpend.connect(getDefaultSigner()) 14 | 15 | // contract deployment 16 | const deployTx = await anyoneCanSpend.deploy(inputSatoshis) 17 | console.log('AnyoneCanSpend contract deployed: ', deployTx.id) 18 | 19 | // contract call 20 | const { tx: callTx } = await anyoneCanSpend.methods.unlock() 21 | console.log('AnyoneCanSpend contract called: ', callTx.id) 22 | } 23 | 24 | describe('Test SmartContract `AnyoneCanSpend` on testnet', () => { 25 | it('should succeed', async () => { 26 | await main() 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /tests/testnet/auction.ts: -------------------------------------------------------------------------------- 1 | import { Auction } from '../../src/contracts/auction' 2 | import { getDefaultSigner } from '../utils/helper' 3 | import { findSig, MethodCallOptions, PubKey, toHex } from 'scrypt-ts' 4 | import { myAddress, myPrivateKey, myPublicKey } from '../utils/privateKey' 5 | 6 | async function main() { 7 | await Auction.compile() 8 | 9 | const privateKeyAuctioneer = myPrivateKey 10 | const publicKeyAuctioneer = myPublicKey 11 | 12 | const publicKeyNewBidder = myPublicKey 13 | const addressNewBidder = myAddress 14 | 15 | const auctionDeadline = Math.round(new Date('2020-01-03').valueOf() / 1000) 16 | 17 | const auction = new Auction( 18 | PubKey(toHex(publicKeyAuctioneer)), 19 | BigInt(auctionDeadline) 20 | ) 21 | auction.bindTxBuilder('bid', Auction.bidTxBuilder) 22 | await auction.connect(getDefaultSigner(privateKeyAuctioneer)) 23 | 24 | // contract deployment 25 | const minBid = 1 26 | const deployTx = await auction.deploy(minBid) 27 | console.log('Auction contract deployed: ', deployTx.id) 28 | 29 | // contract call `bid` 30 | const { tx: bidTx, next } = await auction.methods.bid( 31 | PubKey(toHex(publicKeyNewBidder)), 32 | BigInt(minBid + 1), 33 | { 34 | changeAddress: addressNewBidder, 35 | } as MethodCallOptions 36 | ) 37 | console.log('Bid Tx: ', bidTx.id) 38 | 39 | // contract call `close` 40 | // call `close` 41 | const { tx: closeTx } = await next.instance.methods.close( 42 | (sigReps) => findSig(sigReps, publicKeyAuctioneer), 43 | { 44 | pubKeyOrAddrToSign: publicKeyAuctioneer, 45 | changeAddress: addressNewBidder, 46 | lockTime: Math.round(Date.now() / 1000), 47 | } as MethodCallOptions 48 | ) 49 | console.log('Close Tx: ', closeTx.id) 50 | } 51 | 52 | describe('Test SmartContract `Auction` on testnet', () => { 53 | it('should succeed', async () => { 54 | await main() 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /tests/testnet/cltv.ts: -------------------------------------------------------------------------------- 1 | import { CheckLockTimeVerify } from '../../src/contracts/cltv' 2 | import { getDefaultSigner, inputSatoshis } from '../utils/helper' 3 | import { MethodCallOptions } from 'scrypt-ts' 4 | 5 | async function main() { 6 | await CheckLockTimeVerify.compile() 7 | 8 | const lockTimeMin = 1673510000n 9 | const checkLockTimeVerify = new CheckLockTimeVerify(lockTimeMin) 10 | 11 | // connect to a signer 12 | await checkLockTimeVerify.connect(getDefaultSigner()) 13 | 14 | // contract deployment 15 | const deployTx = await checkLockTimeVerify.deploy(inputSatoshis) 16 | console.log('CLTV contract deployed: ', deployTx.id) 17 | 18 | // contract call 19 | const { tx: callTx } = await checkLockTimeVerify.methods.unlock({ 20 | lockTime: 1673523720, 21 | } as MethodCallOptions) 22 | console.log('CLTV contract called: ', callTx.id) 23 | } 24 | 25 | describe('Test SmartContract `CheckLockTimeVerify` on testnet', () => { 26 | it('should succeed', async () => { 27 | await main() 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /tests/testnet/counter.ts: -------------------------------------------------------------------------------- 1 | import { Counter } from '../../src/contracts/counter' 2 | import { getDefaultSigner, sleep } from '../utils/helper' 3 | import { MethodCallOptions } from 'scrypt-ts' 4 | 5 | async function main() { 6 | await Counter.compile() 7 | 8 | const counter = new Counter(0n) 9 | 10 | // connect to a signer 11 | await counter.connect(getDefaultSigner()) 12 | 13 | const balance = 1 14 | 15 | // contract deployment 16 | const deployTx = await counter.deploy(balance) 17 | console.log('Counter deploy tx:', deployTx.id) 18 | 19 | // set current instance to be the deployed one 20 | let currentInstance = counter 21 | 22 | // call the method of current instance to apply the updates on chain 23 | for (let i = 0; i < 3; ++i) { 24 | // avoid mempool conflicts, sleep to allow previous tx "sink-into" the network 25 | await sleep(2) 26 | 27 | // create the next instance from the current 28 | const nextInstance = currentInstance.next() 29 | 30 | // apply updates on the next instance off chain 31 | nextInstance.increment() 32 | 33 | // call the method of current instance to apply the updates on chain 34 | const { tx: tx_i } = await currentInstance.methods.incrementOnChain({ 35 | next: { 36 | instance: nextInstance, 37 | balance, 38 | }, 39 | } as MethodCallOptions) 40 | 41 | console.log( 42 | `Counter call tx: ${tx_i.id}, count updated to: ${nextInstance.count}` 43 | ) 44 | 45 | // update the current instance reference 46 | currentInstance = nextInstance 47 | } 48 | } 49 | 50 | describe('Test SmartContract `Counter` on testnet', () => { 51 | it('should succeed', async () => { 52 | await main() 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /tests/testnet/counterFromTx.ts: -------------------------------------------------------------------------------- 1 | import { Counter } from '../../src/contracts/counter' 2 | import { getDefaultSigner } from '../utils/helper' 3 | import { bsv, MethodCallOptions } from 'scrypt-ts' 4 | import Transaction = bsv.Transaction 5 | 6 | async function compileContract() { 7 | await Counter.compile() 8 | } 9 | 10 | async function deploy(initialCount = 100n): Promise { 11 | const instance = new Counter(initialCount) 12 | await instance.connect(getDefaultSigner()) 13 | const tx = await instance.deploy(1) 14 | console.log(`Counter deployed: ${tx.id}, the count is: ${instance.count}`) 15 | return tx 16 | } 17 | 18 | async function callIncrementOnChain( 19 | tx: Transaction, 20 | atOutputIndex = 0 21 | ): Promise { 22 | // recover instance from tx 23 | const instance = Counter.fromTx(tx, atOutputIndex) 24 | 25 | await instance.connect(getDefaultSigner()) 26 | 27 | const nextInstance = instance.next() 28 | nextInstance.increment() 29 | 30 | const { tx: callTx } = await instance.methods.incrementOnChain({ 31 | next: { 32 | instance: nextInstance, 33 | balance: 34 | instance.from.tx.outputs[instance.from.outputIndex].satoshis, 35 | }, 36 | } as MethodCallOptions) 37 | console.log( 38 | `Counter incrementOnChain called: ${callTx.id}, the count now is: ${nextInstance.count}` 39 | ) 40 | return callTx 41 | } 42 | 43 | async function main() { 44 | await compileContract() 45 | let lastTx = await deploy() 46 | for (let i = 0; i < 5; ++i) { 47 | lastTx = await callIncrementOnChain(lastTx) 48 | } 49 | } 50 | 51 | describe('Test SmartContract `Counter` on testnet using `SmartContract.fromTx`', () => { 52 | it('should succeed', async () => { 53 | await main() 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /tests/testnet/demo.ts: -------------------------------------------------------------------------------- 1 | import { Demo } from '../../src/contracts/demo' 2 | import { getDefaultSigner, inputSatoshis } from '../utils/helper' 3 | 4 | async function main() { 5 | await Demo.compile() 6 | const demo = new Demo(1n, 2n) 7 | 8 | // connect to a signer 9 | await demo.connect(getDefaultSigner()) 10 | 11 | // contract deployment 12 | const deployTx = await demo.deploy(inputSatoshis) 13 | console.log('Demo contract deployed: ', deployTx.id) 14 | 15 | // contract call 16 | const { tx: callTx } = await demo.methods.add(3n) 17 | console.log('Demo contract `add` called: ', callTx.id) 18 | } 19 | 20 | describe('Test SmartContract `Demo` on testnet', () => { 21 | it('should succeed', async () => { 22 | await main() 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /tests/testnet/erc721.ts: -------------------------------------------------------------------------------- 1 | import { Erc721 } from '../../src/contracts/erc721' 2 | import { getDefaultSigner, randomPrivateKey } from '../utils/helper' 3 | import { findSig, HashedMap, MethodCallOptions, PubKey, toHex } from 'scrypt-ts' 4 | import { myPublicKey } from '../utils/privateKey' 5 | 6 | async function main() { 7 | await Erc721.compile() 8 | 9 | const [alicePrivateKey, alicePubKey, ,] = randomPrivateKey() 10 | const [bobPrivateKey, bobPubKey, ,] = randomPrivateKey() 11 | const tokenId = 1n 12 | 13 | const owners: HashedMap = new HashedMap() 14 | 15 | const erc721 = new Erc721(PubKey(toHex(myPublicKey)), owners) 16 | await erc721.connect(getDefaultSigner([alicePrivateKey, bobPrivateKey])) 17 | 18 | // contract deployment 19 | 20 | const lockedSatoshi = 1 21 | const deployTx = await erc721.deploy(lockedSatoshi) 22 | console.log(`Erc721 contract deployed: ${deployTx.id}`) 23 | 24 | // mint to alice 25 | 26 | const aliceInstance = erc721.next() 27 | aliceInstance.owners.set(tokenId, PubKey(toHex(alicePubKey))) 28 | 29 | const { tx: mintTx } = await erc721.methods.mint( 30 | tokenId, // tokenId 31 | PubKey(toHex(alicePubKey)), // mintTo 32 | (sigResps) => findSig(sigResps, myPublicKey), // minterSig 33 | { 34 | pubKeyOrAddrToSign: myPublicKey, 35 | next: { 36 | instance: aliceInstance, 37 | balance: lockedSatoshi, 38 | atOutputIndex: 0, 39 | }, 40 | } as MethodCallOptions 41 | ) 42 | console.log(`Erc721 contract called, mint to alice: ${mintTx.id}`) 43 | 44 | // transfer from alice to bob 45 | 46 | const bobInstance = aliceInstance.next() 47 | bobInstance.owners.set(tokenId, PubKey(toHex(bobPubKey))) 48 | 49 | const { tx: transferTx } = await aliceInstance.methods.transferFrom( 50 | 1n, // tokenId 51 | PubKey(toHex(alicePubKey)), // sender 52 | (sigResps) => findSig(sigResps, alicePubKey), // sig 53 | PubKey(toHex(bobPubKey)), // receiver 54 | { 55 | pubKeyOrAddrToSign: alicePubKey, 56 | next: { 57 | instance: bobInstance, 58 | balance: lockedSatoshi, 59 | atOutputIndex: 0, 60 | }, 61 | } as MethodCallOptions 62 | ) 63 | console.log( 64 | `Erc721 contract called, transfer from alice to bob: ${transferTx.id}` 65 | ) 66 | 67 | // bob burn 68 | const burnInstance = bobInstance.next() 69 | burnInstance.owners.delete(tokenId) 70 | 71 | const { tx: burnTx } = await bobInstance.methods.burn( 72 | tokenId, // tokenId 73 | PubKey(toHex(bobPubKey)), // sender 74 | (sigResps) => findSig(sigResps, bobPubKey), // sig 75 | { 76 | pubKeyOrAddrToSign: bobPubKey, 77 | next: { 78 | instance: burnInstance, 79 | balance: lockedSatoshi, 80 | atOutputIndex: 0, 81 | }, 82 | } as MethodCallOptions 83 | ) 84 | console.log(`Erc721 contract called, bob burn: ${burnTx.id}`) 85 | } 86 | 87 | describe('Test SmartContract `Erc721` on testnet', () => { 88 | it('should succeed', async () => { 89 | await main() 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /tests/testnet/hashPuzzle.ts: -------------------------------------------------------------------------------- 1 | import { HashPuzzle } from '../../src/contracts/hashPuzzle' 2 | import { getDefaultSigner, inputSatoshis } from '../utils/helper' 3 | import { sha256, toByteString } from 'scrypt-ts' 4 | 5 | async function main() { 6 | await HashPuzzle.compile() 7 | 8 | const plainText = 'abc' 9 | const byteString = toByteString(plainText, true) 10 | const sha256Data = sha256(byteString) 11 | 12 | const hashPuzzle = new HashPuzzle(sha256Data) 13 | 14 | await hashPuzzle.connect(getDefaultSigner()) 15 | 16 | // contract deployment 17 | const deployTx = await hashPuzzle.deploy(inputSatoshis) 18 | console.log('HashPuzzle contract deployed: ', deployTx.id) 19 | 20 | // contract call 21 | const { tx: callTx } = await hashPuzzle.methods.unlock(byteString) 22 | console.log('HashPuzzle contract called: ', callTx.id) 23 | } 24 | 25 | describe('Test SmartContract `HashPuzzle` on testnet', () => { 26 | it('should succeed', async () => { 27 | await main() 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /tests/testnet/hashedMapState.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ByteString, 3 | HashedMap, 4 | MethodCallOptions, 5 | toByteString, 6 | } from 'scrypt-ts' 7 | import { HashedMapState } from '../../src/contracts/hashedMapState' 8 | import { getDefaultSigner } from '../utils/helper' 9 | 10 | const initBalance = 1 11 | 12 | function insert(instance: HashedMapState, key: bigint, val: ByteString) { 13 | const newInstance = instance.next() 14 | 15 | newInstance.hashedmap.set(key, val) 16 | 17 | return instance.methods.insert(key, val, { 18 | next: { 19 | instance: newInstance, 20 | balance: initBalance, 21 | }, 22 | } as MethodCallOptions) 23 | } 24 | 25 | function update(instance: HashedMapState, key: bigint, val: ByteString) { 26 | const newInstance = instance.next() 27 | 28 | newInstance.hashedmap.set(key, val) 29 | 30 | return instance.methods.update(key, val, { 31 | next: { 32 | instance: newInstance, 33 | balance: initBalance, 34 | }, 35 | } as MethodCallOptions) 36 | } 37 | 38 | function canGet(instance: HashedMapState, key: bigint, val: ByteString) { 39 | const newInstance = instance.next() 40 | 41 | return instance.methods.canGet(key, val, { 42 | next: { 43 | instance: newInstance, 44 | balance: initBalance, 45 | }, 46 | } as MethodCallOptions) 47 | } 48 | 49 | function deleteKey(instance: HashedMapState, key: bigint) { 50 | const newInstance = instance.next() 51 | newInstance.hashedmap.delete(key) 52 | return instance.methods.delete(key, { 53 | next: { 54 | instance: newInstance, 55 | balance: initBalance, 56 | }, 57 | } as MethodCallOptions) 58 | } 59 | 60 | function notExist(instance: HashedMapState, key: bigint) { 61 | const newInstance = instance.next() 62 | newInstance.hashedmap.delete(key) 63 | return instance.methods.notExist(key, { 64 | next: { 65 | instance: newInstance, 66 | balance: initBalance, 67 | }, 68 | } as MethodCallOptions) 69 | } 70 | 71 | async function main() { 72 | await HashedMapState.compile() 73 | const signer = getDefaultSigner() 74 | const map = new HashedMap() 75 | 76 | const stateMap = new HashedMapState(map) 77 | 78 | await stateMap.connect(signer) 79 | 80 | // deploy 81 | const deployTx = await stateMap.deploy(1) 82 | console.log('contract deployed: ', deployTx.id) 83 | 84 | //call 85 | const { 86 | tx: tx1, 87 | next: { instance: instance1 }, 88 | } = await insert(stateMap, 1n, toByteString('0001')) 89 | 90 | console.log('contract insert called: ', tx1.id) 91 | const { 92 | tx: tx2, 93 | next: { instance: instance2 }, 94 | } = await insert(instance1, 2n, toByteString('0002')) 95 | console.log('contract insert called: ', tx2.id) 96 | const { 97 | tx: tx3, 98 | next: { instance: instance3 }, 99 | } = await canGet(instance2, 2n, toByteString('0002')) 100 | console.log('contract canGet called: ', tx3.id) 101 | const { 102 | tx: tx4, 103 | next: { instance: instance4 }, 104 | } = await canGet(instance3, 1n, toByteString('0001')) 105 | console.log('contract canGet called: ', tx4.id) 106 | const { 107 | tx: tx5, 108 | next: { instance: instance5 }, 109 | } = await update(instance4, 1n, toByteString('000001')) 110 | console.log('contract update called: ', tx5.id) 111 | const { 112 | tx: tx6, 113 | next: { instance: instance6 }, 114 | } = await update(instance5, 2n, toByteString('000002')) 115 | console.log('contract update called: ', tx6.id) 116 | const { 117 | tx: tx7, 118 | next: { instance: instance7 }, 119 | } = await canGet(instance6, 2n, toByteString('000002')) 120 | console.log('contract canGet called: ', tx7.id) 121 | const { 122 | tx: tx8, 123 | next: { instance: instance8 }, 124 | } = await canGet(instance7, 1n, toByteString('000001')) 125 | console.log('contract canGet called: ', tx8.id) 126 | const { 127 | tx: tx9, 128 | next: { instance: instance9 }, 129 | } = await deleteKey(instance8, 1n) 130 | console.log('contract delete called: ', tx9.id) 131 | const { tx: tx10 } = await notExist(instance9, 1n) 132 | console.log('contract notExist called: ', tx10.id) 133 | } 134 | 135 | describe('Test SmartContract `HashedMapState` on testnet', () => { 136 | it('should succeed', async () => { 137 | await main() 138 | }) 139 | }) 140 | -------------------------------------------------------------------------------- /tests/testnet/helloWorld.ts: -------------------------------------------------------------------------------- 1 | import { HelloWorld } from '../../src/contracts/helloWorld' 2 | import { getDefaultSigner, inputSatoshis } from '../utils/helper' 3 | import { sha256, toByteString } from 'scrypt-ts' 4 | 5 | const message = 'hello world, sCrypt!' 6 | 7 | async function main() { 8 | await HelloWorld.compile() 9 | const helloWorld = new HelloWorld(sha256(toByteString(message, true))) 10 | 11 | // connect to a signer 12 | await helloWorld.connect(getDefaultSigner()) 13 | 14 | // contract deployment 15 | const deployTx = await helloWorld.deploy(inputSatoshis) 16 | console.log('HelloWorld contract deployed: ', deployTx.id) 17 | 18 | // contract call 19 | const { tx: callTx } = await helloWorld.methods.unlock( 20 | toByteString(message, true) 21 | ) 22 | console.log('HelloWorld contract `unlock` called: ', callTx.id) 23 | } 24 | 25 | describe('Test SmartContract `HelloWorld` on testnet', () => { 26 | it('should succeed', async () => { 27 | await main() 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /tests/testnet/multi_contracts_call.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MethodCallOptions, 3 | SmartContract, 4 | bsv, 5 | ContractTransaction, 6 | toByteString, 7 | sha256, 8 | } from 'scrypt-ts' 9 | import { Counter } from '../../src/contracts/counter' 10 | import { getDefaultSigner } from '../utils/helper' 11 | import { HashPuzzle } from '../../src/contracts/hashPuzzle' 12 | 13 | async function main() { 14 | await Counter.compile() 15 | await HashPuzzle.compile() 16 | 17 | const signer = getDefaultSigner() 18 | let counter = new Counter(1n) 19 | 20 | // connect to a signer 21 | await counter.connect(signer) 22 | 23 | // contract deployment 24 | const deployTx = await counter.deploy(1) 25 | console.log('Counter contract deployed: ', deployTx.id) 26 | 27 | counter.bindTxBuilder( 28 | 'incrementOnChain', 29 | ( 30 | current: Counter, 31 | options: MethodCallOptions, 32 | ...args: any 33 | ): Promise => { 34 | // create the next instance from the current 35 | const nextInstance = current.next() 36 | // apply updates on the next instance locally 37 | nextInstance.count++ 38 | 39 | const tx = new bsv.Transaction() 40 | tx.addInput(current.buildContractInput(options.fromUTXO)).addOutput( 41 | new bsv.Transaction.Output({ 42 | script: nextInstance.lockingScript, 43 | satoshis: current.balance, 44 | }) 45 | ) 46 | 47 | return Promise.resolve({ 48 | tx: tx, 49 | atInputIndex: 0, 50 | nexts: [ 51 | { 52 | instance: nextInstance, 53 | balance: current.balance, 54 | atOutputIndex: 0, 55 | }, 56 | ], 57 | }) 58 | } 59 | ) 60 | 61 | const plainText = 'abc' 62 | const byteString = toByteString(plainText, true) 63 | const sha256Data = sha256(byteString) 64 | 65 | const hashPuzzle = new HashPuzzle(sha256Data) 66 | 67 | // connect to a signer 68 | await hashPuzzle.connect(signer) 69 | 70 | const deployTx1 = await hashPuzzle.deploy(1) 71 | console.log('HashPuzzle contract deployed: ', deployTx1.id) 72 | 73 | hashPuzzle.bindTxBuilder( 74 | 'unlock', 75 | ( 76 | current: HashPuzzle, 77 | options: MethodCallOptions, 78 | ...args: any 79 | ): Promise => { 80 | if (options.partialContractTransaction) { 81 | const unSignedTx = options.partialContractTransaction.tx 82 | unSignedTx.addInput( 83 | current.buildContractInput(options.fromUTXO) 84 | ) 85 | 86 | return Promise.resolve({ 87 | tx: unSignedTx, 88 | atInputIndex: 1, 89 | nexts: [], 90 | }) 91 | } 92 | 93 | throw new Error('no partialContractTransaction found') 94 | } 95 | ) 96 | 97 | const partialContractTransaction1 = await counter.methods.incrementOnChain({ 98 | multiContractCall: true, 99 | } as MethodCallOptions) 100 | 101 | const partialContractTransaction2 = await hashPuzzle.methods.unlock( 102 | byteString, 103 | { 104 | multiContractCall: true, 105 | partialContractTransaction: partialContractTransaction1, 106 | } as MethodCallOptions 107 | ) 108 | 109 | const { tx: callTx, nexts } = await SmartContract.multiContractCall( 110 | partialContractTransaction2, 111 | signer 112 | ) 113 | 114 | console.log('Counter, HashPuzzle contract `unlock` called: ', callTx.id) 115 | 116 | // hashPuzzle has terminated, but counter can still be called 117 | counter = nexts[0].instance 118 | } 119 | 120 | describe('Test SmartContract `Counter, HashPuzzle ` multi called on testnet', () => { 121 | it('should succeed', async () => { 122 | await main() 123 | }) 124 | }) 125 | -------------------------------------------------------------------------------- /tests/testnet/p2pkh.ts: -------------------------------------------------------------------------------- 1 | import { P2PKH } from '../../src/contracts/p2pkh' 2 | import { getDefaultSigner, inputSatoshis } from '../utils/helper' 3 | import { myPublicKey, myPublicKeyHash } from '../utils/privateKey' 4 | 5 | import { 6 | findSig, 7 | MethodCallOptions, 8 | PubKey, 9 | PubKeyHash, 10 | toHex, 11 | } from 'scrypt-ts' 12 | 13 | async function main() { 14 | await P2PKH.compile() 15 | const p2pkh = new P2PKH(PubKeyHash(toHex(myPublicKeyHash))) 16 | 17 | // connect to a signer 18 | await p2pkh.connect(getDefaultSigner()) 19 | 20 | // deploy 21 | const deployTx = await p2pkh.deploy(inputSatoshis) 22 | console.log('P2PKH contract deployed: ', deployTx.id) 23 | 24 | // call 25 | const { tx: callTx } = await p2pkh.methods.unlock( 26 | // pass signature, the first parameter, to `unlock` 27 | // after the signer signs the transaction, the signatures are returned in `SignatureResponse[]` 28 | // you need to find the signature or signatures you want in the return through the public key or address 29 | // here we use `myPublicKey` to find the signature because we signed the transaction with `myPrivateKey` before 30 | (sigResps) => findSig(sigResps, myPublicKey), 31 | // pass public key, the second parameter, to `unlock` 32 | PubKey(toHex(myPublicKey)), 33 | // method call options 34 | { 35 | // tell the signer to use the private key corresponding to `myPublicKey` to sign this transaction 36 | // that is using `myPrivateKey` to sign the transaction 37 | pubKeyOrAddrToSign: myPublicKey, 38 | } as MethodCallOptions 39 | ) 40 | console.log('P2PKH contract called: ', callTx.id) 41 | } 42 | 43 | describe('Test SmartContract `P2PKH` on testnet', () => { 44 | it('should succeed', async () => { 45 | await main() 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /tests/testnet/p2pkhFromTx.ts: -------------------------------------------------------------------------------- 1 | import { P2PKH } from '../../src/contracts/p2pkh' 2 | import { getDefaultSigner, inputSatoshis } from '../utils/helper' 3 | import { myPublicKey, myPublicKeyHash } from '../utils/privateKey' 4 | 5 | import { 6 | bsv, 7 | findSig, 8 | MethodCallOptions, 9 | PubKey, 10 | PubKeyHash, 11 | toHex, 12 | } from 'scrypt-ts' 13 | import Transaction = bsv.Transaction 14 | 15 | let deployTx: Transaction 16 | const atOutputIndex = 0 17 | 18 | async function deploy() { 19 | await P2PKH.compile() 20 | const p2pkh = new P2PKH(PubKeyHash(toHex(myPublicKeyHash))) 21 | await p2pkh.connect(getDefaultSigner()) 22 | 23 | deployTx = await p2pkh.deploy(inputSatoshis) 24 | console.log('P2PKH contract deployed: ', deployTx.id) 25 | } 26 | 27 | async function call() { 28 | // recover instance from tx 29 | const p2pkh = P2PKH.fromTx(deployTx, atOutputIndex) 30 | 31 | await p2pkh.connect(getDefaultSigner()) 32 | 33 | const { tx } = await p2pkh.methods.unlock( 34 | (sigResps) => findSig(sigResps, myPublicKey), 35 | PubKey(toHex(myPublicKey)), 36 | { 37 | pubKeyOrAddrToSign: myPublicKey, 38 | } as MethodCallOptions 39 | ) 40 | console.log('P2PKH contract called: ', tx.id) 41 | } 42 | 43 | describe('Test SmartContract `P2PKH` on testnet using `SmartContract.fromTx`', () => { 44 | it('should succeed', async () => { 45 | await deploy() 46 | await call() 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /tests/testnet/recallable.ts: -------------------------------------------------------------------------------- 1 | import { bsv, findSig, MethodCallOptions, PubKey, toHex } from 'scrypt-ts' 2 | import { Recallable } from '../../src/contracts/recallable' 3 | import { getDefaultSigner, randomPrivateKey } from '../utils/helper' 4 | import { myPublicKey } from '../utils/privateKey' 5 | import Transaction = bsv.Transaction 6 | 7 | // 3 players, alice, bob, and me 8 | // I am the issuer 9 | const [alicePrivateKey, alicePublicKey, ,] = randomPrivateKey() 10 | const [, bobPublicKey, ,] = randomPrivateKey() 11 | 12 | // contract deploy transaction 13 | let deployTx: Transaction 14 | // last contract calling transaction 15 | let lastCallTx: Transaction 16 | // contract output index 17 | const atOutputIndex = 0 18 | 19 | const satoshisIssued = 10 20 | const satoshisSendToAlice = 7 21 | const satoshisSendToBob = 7 22 | 23 | async function deploy() { 24 | await Recallable.compile() 25 | 26 | // I am the issuer, and the first user as well 27 | const initialInstance = new Recallable(PubKey(toHex(myPublicKey))) 28 | 29 | // there is one key in the signer, that is `myPrivateKey` (added by default) 30 | await initialInstance.connect(getDefaultSigner()) 31 | 32 | // I issue 10 re-callable satoshis 33 | deployTx = await initialInstance.deploy(satoshisIssued) 34 | console.log(`I issue ${satoshisIssued}: ${deployTx.id}`) 35 | 36 | // the current balance of each player: 37 | // - me 10 (1 utxo) 38 | // - alice 0 39 | // - bob 0 40 | } 41 | 42 | async function recoverAfterDeployed() { 43 | // recover instance from contract deploy transaction 44 | const meInstance = Recallable.fromTx(deployTx, atOutputIndex) 45 | // connect a signer 46 | await meInstance.connect(getDefaultSigner()) 47 | 48 | // now `meInstance` is good to use 49 | console.log('Contract `Recallable` recovered after deployed') 50 | 51 | // I send 7 to alice, keep 3 left 52 | const meNextInstance = meInstance.next() 53 | 54 | const aliceNextInstance = meInstance.next() 55 | aliceNextInstance.userPubKey = PubKey(toHex(alicePublicKey)) 56 | 57 | const { tx: transferToAliceTx } = await meInstance.methods.transfer( 58 | (sigResps) => findSig(sigResps, myPublicKey), 59 | PubKey(toHex(alicePublicKey)), 60 | BigInt(satoshisSendToAlice), 61 | { 62 | // sign with the private key corresponding to `myPublicKey` (which is `myPrivateKey` in the signer) 63 | // since I am the current user 64 | pubKeyOrAddrToSign: myPublicKey, 65 | next: [ 66 | { 67 | // outputIndex 0: UTXO of alice 68 | instance: aliceNextInstance, 69 | balance: satoshisSendToAlice, 70 | }, 71 | { 72 | // outputIndex 1: the change UTXO back to me 73 | instance: meNextInstance, 74 | balance: satoshisIssued - satoshisSendToAlice, 75 | }, 76 | ], 77 | } as MethodCallOptions 78 | ) 79 | console.log( 80 | `I send ${satoshisSendToAlice} to Alice: ${transferToAliceTx.id}` 81 | ) 82 | lastCallTx = transferToAliceTx 83 | 84 | // the current balance of each player: 85 | // - me 3 (1 utxo) 86 | // - alice 7 (1 utxo) 87 | // - bob 0 88 | } 89 | 90 | async function recoverAfterCalled() { 91 | // recover instance from contract calling transaction 92 | const aliceInstance = Recallable.fromTx(lastCallTx, atOutputIndex) 93 | // connect a signer 94 | await aliceInstance.connect(getDefaultSigner(alicePrivateKey)) 95 | 96 | // now `aliceInstance` is good to use 97 | console.log('Contract `Recallable` recovered after calling') 98 | 99 | // alice sends all the 7 to bob, keeps nothing left 100 | const bobNextInstance = aliceInstance.next() 101 | bobNextInstance.userPubKey = PubKey(toHex(bobPublicKey)) 102 | 103 | const { tx: transferToBobTx } = await aliceInstance.methods.transfer( 104 | (sigResps) => findSig(sigResps, alicePublicKey), 105 | PubKey(toHex(bobPublicKey)), 106 | BigInt(satoshisSendToBob), 107 | { 108 | // sign with the private key corresponding to `alicePublicKey` (which is `alicePrivateKey` in the signer) 109 | // since she is the current user 110 | pubKeyOrAddrToSign: alicePublicKey, 111 | next: { 112 | instance: bobNextInstance, 113 | balance: satoshisSendToBob, 114 | atOutputIndex: 0, 115 | }, 116 | } as MethodCallOptions 117 | ) 118 | console.log( 119 | `Alice sends ${satoshisSendToBob} to Bob: ${transferToBobTx.id}` 120 | ) 121 | 122 | // the current balance of each player: 123 | // - me 3 (1 utxo) 124 | // - alice 0 125 | // - bob 7 (1 utxo) 126 | 127 | // I recall all the 7 from bob 128 | const meRecallInstance = bobNextInstance.next() 129 | meRecallInstance.userPubKey = PubKey(toHex(myPublicKey)) 130 | 131 | const { tx: recallTx } = await bobNextInstance.methods.recall( 132 | (sigResps) => findSig(sigResps, myPublicKey), 133 | { 134 | // sign with the private key corresponding to `myPublicKey` (which is `myPrivateKey` in the signer) 135 | // since I am the issuer at the beginning 136 | pubKeyOrAddrToSign: myPublicKey, 137 | next: { 138 | instance: meRecallInstance, 139 | balance: satoshisSendToBob, 140 | atOutputIndex: 0, 141 | }, 142 | } as MethodCallOptions 143 | ) 144 | console.log(`I recall ${satoshisSendToBob} from Bob: ${recallTx.id}`) 145 | 146 | // the current balance of each player: 147 | // - me 10 (2 utxos) 148 | // - alice 0 149 | // - bob 0 150 | } 151 | 152 | describe('Test SmartContract `Recallable` on testnet', () => { 153 | it('should succeed', async () => { 154 | await deploy() 155 | await recoverAfterDeployed() 156 | await recoverAfterCalled() 157 | }) 158 | }) 159 | -------------------------------------------------------------------------------- /tests/utils/helper.ts: -------------------------------------------------------------------------------- 1 | import { 2 | bsv, 3 | DefaultProvider, 4 | DummyProvider, 5 | TestWallet, 6 | UTXO, 7 | } from 'scrypt-ts' 8 | import { randomBytes } from 'crypto' 9 | import { myPrivateKey } from './privateKey' 10 | 11 | export const inputSatoshis = 10000 12 | 13 | export const sleep = async (seconds: number) => { 14 | return new Promise((resolve) => { 15 | setTimeout(() => { 16 | resolve({}) 17 | }, seconds * 1000) 18 | }) 19 | } 20 | 21 | export function randomPrivateKey() { 22 | const privateKey = bsv.PrivateKey.fromRandom('testnet') 23 | const publicKey = bsv.PublicKey.fromPrivateKey(privateKey) 24 | const publicKeyHash = bsv.crypto.Hash.sha256ripemd160(publicKey.toBuffer()) 25 | const address = publicKey.toAddress() 26 | return [privateKey, publicKey, publicKeyHash, address] as const 27 | } 28 | 29 | export function getDefaultSigner( 30 | privateKey?: bsv.PrivateKey | bsv.PrivateKey[] 31 | ): TestWallet { 32 | if (global.testnetSigner === undefined) { 33 | global.testnetSigner = new TestWallet( 34 | myPrivateKey, 35 | new DefaultProvider() 36 | ) 37 | } 38 | if (privateKey !== undefined) { 39 | global.testnetSigner.addPrivateKey(privateKey) 40 | } 41 | return global.testnetSigner 42 | } 43 | 44 | export const dummyUTXO = { 45 | txId: randomBytes(32).toString('hex'), 46 | outputIndex: 0, 47 | script: '', // placeholder 48 | satoshis: inputSatoshis, 49 | } 50 | 51 | export function getDummyUTXO( 52 | satoshis: number = inputSatoshis, 53 | unique = false 54 | ): UTXO { 55 | if (unique) { 56 | return Object.assign({}, dummyUTXO, { 57 | satoshis, 58 | txId: randomBytes(32).toString('hex'), 59 | }) 60 | } 61 | return Object.assign({}, dummyUTXO, { satoshis }) 62 | } 63 | 64 | export function getDummySigner( 65 | privateKey?: bsv.PrivateKey | bsv.PrivateKey[] 66 | ): TestWallet { 67 | if (global.dummySigner === undefined) { 68 | global.dummySigner = new TestWallet(myPrivateKey, new DummyProvider()) 69 | } 70 | if (privateKey !== undefined) { 71 | global.dummySigner.addPrivateKey(privateKey) 72 | } 73 | return global.dummySigner 74 | } 75 | -------------------------------------------------------------------------------- /tests/utils/privateKey.ts: -------------------------------------------------------------------------------- 1 | import { bsv } from 'scrypt-ts' 2 | import * as dotenv from 'dotenv' 3 | import * as fs from 'fs' 4 | 5 | const dotenvConfigPath = '.env' 6 | dotenv.config({ path: dotenvConfigPath }) 7 | 8 | // fill in private key on testnet in WIF here 9 | let privKey = process.env.PRIVATE_KEY 10 | if (!privKey) { 11 | genPrivKey() 12 | } else { 13 | showAddr(bsv.PrivateKey.fromWIF(privKey)) 14 | } 15 | 16 | export function genPrivKey() { 17 | const newPrivKey = bsv.PrivateKey.fromRandom('testnet') 18 | console.log(`Missing private key, generating a new one ... 19 | Private key generated: '${newPrivKey.toWIF()}' 20 | You can fund its address '${newPrivKey.toAddress()}' from the sCrypt faucet https://scrypt.io/#faucet`) 21 | // auto generate .env file with new generated key 22 | fs.writeFileSync(dotenvConfigPath, `PRIVATE_KEY="${newPrivKey}"`) 23 | privKey = newPrivKey.toWIF() 24 | } 25 | 26 | export function showAddr(privKey: bsv.PrivateKey) { 27 | console.log(`Private key already present ... 28 | You can fund its address '${privKey.toAddress()}' from the sCrypt faucet https://scrypt.io/#faucet`) 29 | } 30 | 31 | export const myPrivateKey = bsv.PrivateKey.fromWIF(privKey) 32 | export const myPublicKey = bsv.PublicKey.fromPrivateKey(myPrivateKey) 33 | export const myPublicKeyHash = bsv.crypto.Hash.sha256ripemd160( 34 | myPublicKey.toBuffer() 35 | ) 36 | export const myAddress = myPublicKey.toAddress() 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Language and Environment */ 4 | "target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 5 | "lib": [ 6 | "es2021" 7 | ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, 8 | "experimentalDecorators": true /* Enable experimental support for TC39 stage 2 draft decorators. */, 9 | /* Modules */ 10 | "module": "commonjs" /* Specify what module code is generated. */, 11 | "rootDir": "./" /* Specify the root folder within your source files. */, 12 | "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, 13 | "outDir": "./dist" /* Specify an output folder for all emitted files. */, 14 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 15 | /* Type Checking */ 16 | /* "strict": true, Enable all strict type-checking options. */ 17 | "skipLibCheck": true /* Skip type checking all .d.ts files. */, 18 | "sourceMap": true, 19 | "plugins": [ 20 | { 21 | "transform": "scrypt-ts/dist/transformation/transformer", 22 | "outDir": "./artifacts", 23 | "transformProgram": true 24 | } 25 | ] 26 | }, 27 | "include": ["src/**/*.ts", "tests/**/*.ts"] 28 | } 29 | --------------------------------------------------------------------------------