├── .changeset ├── README.md ├── config.json └── dry-needles-tease.md ├── .dockerignore ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .gitlab-ci.yml ├── CHANGELOG.md ├── Dockerfile.test ├── LICENSE.md ├── README.md ├── docs ├── cli.md ├── configuration.md ├── core.md ├── index.md └── quickstart.md ├── example ├── contract.ts ├── index.ts └── sandbox │ ├── .env.example │ ├── README.md │ ├── config.ts │ ├── logger.ts │ ├── package-lock.json │ ├── package.json │ ├── run.sh │ ├── run.ts │ ├── sandbox.Dockerfile │ ├── src │ ├── contract.ts │ └── index.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── utils.ts ├── lerna.json ├── package-lock.json ├── package.json ├── packages ├── cli │ ├── CHANGELOG.md │ ├── LICENSE.md │ ├── README.md │ ├── cmd │ │ └── docker.js │ ├── index.js │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── cache.js │ │ ├── config.js │ │ ├── context.js │ │ ├── contract.js │ │ ├── log.js │ │ ├── tx-observer.js │ │ ├── update.js │ │ └── utils.js │ └── test │ │ ├── Dockerfile │ │ └── contract.config.js ├── contract-core │ ├── .npmrc │ ├── CHANGELOG.md │ ├── LICENSE.md │ ├── README.md │ ├── features │ │ └── typed-state.md │ ├── jest.config.js │ ├── migration-guide.md │ ├── package.json │ ├── src │ │ ├── api │ │ │ ├── assets │ │ │ │ ├── asset.ts │ │ │ │ ├── assets-service.ts │ │ │ │ ├── assets-storage.ts │ │ │ │ └── index.ts │ │ │ ├── container.ts │ │ │ ├── contants.ts │ │ │ ├── decorators │ │ │ │ ├── action.ts │ │ │ │ ├── common.ts │ │ │ │ ├── contract.ts │ │ │ │ ├── index.ts │ │ │ │ ├── param.ts │ │ │ │ ├── services.ts │ │ │ │ ├── state.ts │ │ │ │ └── var.ts │ │ │ ├── index.ts │ │ │ ├── logger.ts │ │ │ ├── meta.ts │ │ │ └── state │ │ │ │ ├── contract-state.ts │ │ │ │ ├── index.ts │ │ │ │ ├── preload.ts │ │ │ │ ├── storage.ts │ │ │ │ └── types │ │ │ │ ├── mapping.ts │ │ │ │ └── value.ts │ │ ├── entrypoint.ts │ │ ├── execution │ │ │ ├── constants.ts │ │ │ ├── contract-processor.ts │ │ │ ├── contract-service.ts │ │ │ ├── converter.ts │ │ │ ├── exceptions.ts │ │ │ ├── execution-context.ts │ │ │ ├── index.ts │ │ │ ├── params-extractor.ts │ │ │ ├── reflect.ts │ │ │ ├── types.ts │ │ │ └── worker.ts │ │ ├── grpc │ │ │ ├── config.ts │ │ │ └── grpc-client.ts │ │ ├── index.ts │ │ ├── intefaces │ │ │ ├── contract.ts │ │ │ └── helpers.ts │ │ ├── test-utils │ │ │ ├── environment │ │ │ │ ├── local-contract-processor.ts │ │ │ │ ├── local-contract-state.ts │ │ │ │ ├── local.ts │ │ │ │ └── response-factory.ts │ │ │ ├── example │ │ │ │ ├── example-test.spec.ts │ │ │ │ └── test-wrapper.ts │ │ │ ├── executor.ts │ │ │ ├── index.ts │ │ │ ├── sandbox.ts │ │ │ ├── types.ts │ │ │ └── utils │ │ │ │ └── address.ts │ │ └── utils │ │ │ ├── base58.ts │ │ │ ├── index.ts │ │ │ ├── marshal.ts │ │ │ └── workers │ │ │ └── static-pool.ts │ ├── test │ │ ├── assets.spec.ts │ │ ├── contract-processor.spec.ts │ │ ├── converter.spec.ts │ │ ├── metadata.spec.ts │ │ ├── mocks │ │ │ ├── contract-processor.ts │ │ │ ├── contract-transaction-response.ts │ │ │ └── grpc-client.ts │ │ ├── params-extractor.spec.ts │ │ ├── state.spec.ts │ │ └── tsconfig.json │ ├── tsconfig.json │ └── tsconfig.spec.json └── create-we-contract │ ├── CHANGELOG.md │ ├── LICENSE.md │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── index.ts │ └── install-cmd.ts │ ├── template │ ├── .dockerignore │ ├── Dockerfile │ ├── _gitignore │ ├── contract.config.js │ ├── entrypoint.sh │ ├── index │ ├── package.json │ ├── src │ │ └── contract │ └── tsconfig.json │ └── tsconfig.json └── tsconfig.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.2.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.changeset/dry-needles-tease.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@wavesenterprise/contract-core": patch 3 | "@wavesenterprise/contract-cli": patch 4 | "create-we-contract": patch 5 | --- 6 | 7 | Native tokens compatibility 8 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | /packages/contract-core/node_modules/long 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | .eslintrc.js 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ignorePatterns: ["**/*.spec.ts", "./example/*.ts"], 3 | parserOptions: { 4 | project: './tsconfig.json', 5 | requireConfigFile: false, 6 | }, 7 | extends: [ 8 | '@wavesenterprise/eslint-config/typescript-mixed', 9 | ], 10 | rules: { 11 | 'no-empty-function': 'off', 12 | '@typescript-eslint/no-empty-function': 'off', 13 | 'no-redeclare': 'off', 14 | '@typescript-eslint/no-explicit-any': 'off', 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | proto 4 | pb 5 | vendor 6 | .idea 7 | .env 8 | developer-keys.json 9 | participants.json 10 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: ${REGISTRY}/it/docker:19.03.1 2 | 3 | services: 4 | - name: ${REGISTRY}/it/docker:19.03.1-dind 5 | alias: docker 6 | 7 | variables: 8 | DOCKER_HOST: tcp://docker:2375 9 | DOCKER_TLS_CERTDIR: "" 10 | 11 | stages: 12 | - test 13 | 14 | run-test-ci: 15 | stage: test 16 | before_script: 17 | - mkdir -p $HOME/.docker 18 | - echo $DOCKER_AUTH_CONFIG > $HOME/.docker/config.json 19 | script: 20 | # - docker login -u "${REGISTRY_USER}" -p "${REGISTRY_PASSWORD}" "${REGISTRY}" 21 | - cat Dockerfile.test | sed "s/\${REGISTRY}/${REGISTRY}/g" | docker build --no-cache --tag test-image -f - . 22 | - docker run --hostname test-0 --rm --name test-container-0 test-image 23 | tags: 24 | - voting 25 | 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [1.1.0](https://github.com/waves-enterprise/js-contract-sdk/compare/v1.0.0-rc.1...v1.1.0) (2022-11-08) 6 | 7 | 8 | ### Features 9 | 10 | * **contract-core:** execute incoming txs in worker-pool ([b127634](https://github.com/waves-enterprise/js-contract-sdk/commit/b12763438a7eabf1f048d440d0e1aeb3a6f0a3ec)) 11 | * **create-we-contract:** refactor scaffold cli ([aa1f22d](https://github.com/waves-enterprise/js-contract-sdk/commit/aa1f22d7a0bf1826f3ed94c060bdd1e73b7d1042)) 12 | * implement asset contract methods ([f30f60e](https://github.com/waves-enterprise/js-contract-sdk/commit/f30f60e8c07050e835f00406df48e0acace04bd8)) 13 | * native tokens implementation ([c000d53](https://github.com/waves-enterprise/js-contract-sdk/commit/c000d53ca3eff23dde480e435702363ce821cdfc)) 14 | * native tokens integration ([57a2960](https://github.com/waves-enterprise/js-contract-sdk/commit/57a2960b6b3c0a07579558571c304450b2bfb913)) 15 | * native tokens integration fix transfer in ([fabe99f](https://github.com/waves-enterprise/js-contract-sdk/commit/fabe99ff01aec7d12e62b6680813137045996f52)) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * **contract-core:** add preload helper ([02d20bc](https://github.com/waves-enterprise/js-contract-sdk/commit/02d20bcd4c0ad6c95ba1e5eab6e8e6472899173b)) 21 | * **contract-core:** edit folder structure ([509a028](https://github.com/waves-enterprise/js-contract-sdk/commit/509a0289cad9399651f320a5bd50c46102464078)) 22 | * **contract-core:** fix cross process messages long interoperability ([9a3a1b0](https://github.com/waves-enterprise/js-contract-sdk/commit/9a3a1b09075720d9ea51a02468ed7412e9383e2b)) 23 | * **contract-core:** fix params extractor ([0ea803a](https://github.com/waves-enterprise/js-contract-sdk/commit/0ea803a4ab09467384475f7815d8550ada6c1c2e)) 24 | * **contract-core:** handle entry cache ([34d4f26](https://github.com/waves-enterprise/js-contract-sdk/commit/34d4f26b925b4a69c72a60498ed4fc238c8e8499)) 25 | * **contract-core:** resolver not found error ([755f691](https://github.com/waves-enterprise/js-contract-sdk/commit/755f6916126bcf23efafc345b755e1c833d1f69c)) 26 | * **core:** os.cpus() is undefined ([c0bcc36](https://github.com/waves-enterprise/js-contract-sdk/commit/c0bcc36b333f91ef60173594750b3d35b3fdf102)) 27 | * **create-contract:** pascal case contract name ([7457273](https://github.com/waves-enterprise/js-contract-sdk/commit/7457273ca3cf3162276229f86fb21b8a65379303)) 28 | * **create-we-contract:** bump versions, add we-cli to dependencies ([e4f574a](https://github.com/waves-enterprise/js-contract-sdk/commit/e4f574ada1c57d845799e499209cee2ee233abb0)) 29 | * **create-we-contract:** fix npm create compatibility ([fd2a736](https://github.com/waves-enterprise/js-contract-sdk/commit/fd2a736bea3c178ee4b12e2dc168712c3bd8eb2c)) 30 | * **root:** add useWorkspaces option ([ec80993](https://github.com/waves-enterprise/js-contract-sdk/commit/ec8099355f3f906badf850f461650643f489e303)) 31 | * **root:** ignore ide files ([1689e62](https://github.com/waves-enterprise/js-contract-sdk/commit/1689e623b728b2b45de36979f528249b53e9aad6)) 32 | * **root:** update lerna config ([145956a](https://github.com/waves-enterprise/js-contract-sdk/commit/145956a48d566c142f15d42396ac1d1f8a0d53e1)) 33 | -------------------------------------------------------------------------------- /Dockerfile.test: -------------------------------------------------------------------------------- 1 | FROM ${REGISTRY}/it/node:16-alpine 2 | RUN apk update && apk add --no-cache make bash 3 | WORKDIR /app 4 | COPY package.json package.json 5 | COPY package-lock.json package-lock.json 6 | 7 | COPY tsconfig.json tsconfig.json 8 | COPY .eslintignore .eslintignore 9 | COPY .eslintrc.js .eslintrc.js 10 | COPY packages packages 11 | COPY example example 12 | COPY lerna.json lerna.json 13 | RUN npm --prefix example/sandbox ci -q 14 | RUN npm ci -q 15 | RUN npm --prefix packages/contract-core run build 16 | COPY packages/contract-core example/sandbox/node_modules/@wavesenterprise/contract-core 17 | CMD npm run test-ci 18 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2022 Waves Enterprise 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JS Contract SDK 2 | 3 | Toolkit for development, test and create smart-contracts on Waves Enterprise ecosystem. 4 | 5 | 6 | ## Documentation 7 | 8 | All JS contract SDK documentation, can be found at: 9 | https://docs.wavesenterprise.com/ru/1.8.4/usage/docker-sc/sc-opensource.html 10 | 11 | 12 | ## Quickstart 13 | 14 | The fastest way to get started with JS Contract SDK is to use contract starter CLI commands. 15 | 16 | To create a new project using CLI, run the following command and follow the instructions 17 | 18 | ```npm 19 | npm create we-contract [options] MyContract 20 | ``` 21 | 22 | The next step is to install new project dependencies: 23 | 24 | ```npm 25 | cd contract-project-path 26 | npm install 27 | ``` 28 | 29 | After you have all dependencies installed, edit the `src/contract.ts` file and write actions you need in your contract: 30 | 31 | ```typescript 32 | @Contract() 33 | export default class MyContract { 34 | @State state: ContractState 35 | 36 | 37 | @Action 38 | async greet( 39 | @Param('name') name: string 40 | ) { 41 | const Greeting = await this.state.tryGet('Greeting'); 42 | 43 | assert(Greeting === null, 'you already greeted'); 44 | 45 | this.state.set('Greeting', `Hello, ${name}`); 46 | } 47 | } 48 | ``` 49 | 50 | 51 | Once you finish write your contract actions, you can create your contract to network, i.e to create to local sandbox network run following: 52 | 53 | ```npm 54 | npm run create:sandbox 55 | ``` 56 | 57 | Make sure you setup your network credentials in ```contract.config.js``` 58 | 59 | ## License 60 | 61 | This project is licensed under the [MIT License](LICENSE). -------------------------------------------------------------------------------- /docs/cli.md: -------------------------------------------------------------------------------- 1 | 2 | # Command line interface 3 | 4 | ```we-cli``` - tool that helps you to create contract to different environments. 5 | 6 | 7 | ## Deploy 8 | 9 | Smart contracts are executed once they are deployed in the blockchain. To create a contract run the create command in WE Contract CLI: 10 | 11 | ```bash 12 | we-cli create -n testnet 13 | ``` 14 | where ```testnet``` is the name of the network specified in the configuration file. For example, to create a contract to the sandbox network run the following command: 15 | 16 | ```bash 17 | we-cli create -n sandbox 18 | ``` 19 | 20 | Make sure you setup your network credentials in ```contract.config.js``` 21 | 22 | ## Update 23 | 24 | To update image of your contract run 25 | 26 | ```bash 27 | we-cli update 28 | ``` 29 | 30 | This command increment your contract version and update image hash on node -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | The configuration file is used to set up the image name and the contract name to be displayed in the explorer. You can also set the image tag (the ```name``` property) which will be used to send the contract to the registry in the configuration file. 4 | 5 | Add the ```contract.config.js``` file to the root directory of your project to initialize your contract configuration. 6 | 7 | If you scaffolded the project with the ``create-we-contract`` command as described above in the Quickstart section, the configuration is set by default. 8 | 9 | 10 | ### Default configuration 11 | 12 | An example of default configuration is given below: 13 | 14 | ```js 15 | module.exports = { 16 | image: "my-contract", 17 | name: 'My Contract Name', 18 | version: '1.0.1', 19 | networks: { 20 | /// ... 21 | } 22 | } 23 | ``` 24 | 25 | ### Networks configuration 26 | 27 | In the ```networks``` section, provide specific configuration for your network: 28 | 29 | ```js 30 | module.exports = { 31 | networks: { 32 | "sandbox": { 33 | seed: "#your secret seed phrase" // or get it from env process.env.MY_SECRET_SEED 34 | 35 | // also you can provide 36 | registry: 'localhost:5000', 37 | nodeAddress: 'http://localhost:6862', 38 | params: { 39 | init: () => ({ 40 | paramName: 'paramValue' 41 | }) 42 | } 43 | } 44 | } 45 | } 46 | ``` 47 | 48 | * ```seed``` – if you are going to create a contract to the sandbox network, provide the contract initiator seed phrase; 49 | * ```registry``` – if you used a specific Docker registry, provide the registry name; 50 | * ```nodeAddress``` – provide specific nodeAddress to create to. 51 | * ```params.init``` – to specify initialization parameters, set a function 52 | -------------------------------------------------------------------------------- /docs/core.md: -------------------------------------------------------------------------------- 1 | ## Core concepts 2 | 3 | The basics of making a contract class is to specify class annotations per method. The most important annotations are: 4 | 5 | * `Contract` - register class as contract 6 | * `Action` - register action handler of contract 7 | * `State` - decorates a class property for access to contract state 8 | * `Param` - param decorator that map transaction params to contract class action params 9 | 10 | ```ts 11 | @Contract 12 | export class ExampleContract { 13 | @State state: ContractState; 14 | 15 | @Action 16 | greeting(@Param('name') name: string) { 17 | this.state.set('Greeting', `Hello, ${name}`); 18 | } 19 | } 20 | ``` 21 | 22 | ## Methods 23 | 24 | ## Methods to manage smart contract state 25 | 26 | ```ContractState``` class exposes useful methods to write to contract state. You can find the list of data types currently available in contract state in the node documentation. Contract SDK supports all the data types currently available in the contract state. 27 | 28 | ### Write 29 | 30 | The easiest way to write the state is to use ```set``` method. This method automatically casts data type. 31 | ```ts 32 | this.state.set('key', 'value') 33 | ``` 34 | 35 | For explicit type casting you should use methods in example below: 36 | 37 | ```ts 38 | // for binary 39 | this.state.setBinary('binary', Buffer.from('example', 'base64')); 40 | 41 | // for boolean 42 | this.state.setBool('boolean', true); 43 | 44 | // for integer 45 | this.state.setInt('integer', 102); 46 | 47 | // for string 48 | this.state.setString('string', 'example'); 49 | ``` 50 | 51 | 52 | ### Read 53 | 54 | 55 | Reading the state is currently asynchronous, and reading behavior depends on the contract configuration. 56 | 57 | ```ts 58 | @Contract 59 | export class ExampleContract { 60 | @State state: ContractState; 61 | 62 | @Action 63 | async exampleAction(@Param('name') name: string) { 64 | const stateValue: string = await this.state.get('value', 'default-value'); 65 | } 66 | } 67 | ``` 68 | 69 | Caution: Method state.get can't know about internal state type in runtime, for explicit type casting use casted method 70 | `getBinary`, `getString`, `getBool`, `getNum` 71 | 72 | 73 | ## Write Actions 74 | 75 | The key decorators is `Action` and `Param` 76 | 77 | ### Init action 78 | 79 | To describe create contract action set the ```onInit``` action decorator parameter to ```true```. 80 | 81 | ```ts 82 | @Contract 83 | export class ExampleContract { 84 | @State state: ContractState; 85 | 86 | @Action({onInit: true}) 87 | exampleAction(@Param('name') name: string) { 88 | 89 | this.state.set('state-initial-value', 'initialized') 90 | } 91 | } 92 | ``` 93 | 94 | By default action is used as the name of contract method. To set a different action name, assign it to the ```name``` parameter of the decorator. 95 | 96 | 97 | ```ts 98 | @Contract 99 | export class ExampleContract { 100 | @State state: ContractState; 101 | 102 | @Action({name: 'specificActionName'}) 103 | exampleAction() { 104 | // Your code 105 | } 106 | } 107 | ``` -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | This Directory contains all JS Contract SDK documentation. See docs on [website](https://docs.wavesenterprise.com/en/1.8.4/usage/docker-sc/sc-opensource.html#) 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /docs/quickstart.md: -------------------------------------------------------------------------------- 1 | # Quickstart 2 | 3 | This section describes JS Сontract SDK Toolkit – a toolkit for development, testing and deploying smart contracts in Waves Enterprise public blockchain networks. Use the toolkit to fast take off with the Waves Enterprise ecosystem without using any specific programming language, because smart contracts are deployed in a Docker container. You can create a smart contract using any of the most commonly used languages, such as Typescript. 4 | 5 | Smart contracts are often deployed into different environments and networks. For example, you can scaffold local environment based on a sandbox node and create contracts to this network for test use-cases. 6 | 7 | Deploy your smart contract to different environments using WE Contract Command line interface (CLI). 8 | 9 | 10 | ## Requirements 11 | 12 | * Node.js 13 | * Docker 14 | 15 | ## Getting Started 16 | 17 | The fastest way to get started with JS Contract SDK is to use contract starter CLI commands. 18 | 19 | To create a new project using CLI, run the following command and follow the instructions 20 | 21 | ```npm 22 | npm create we-contract [options] MyContract 23 | ``` 24 | 25 | The next step is to install new project dependencies: 26 | 27 | ```npm 28 | cd contract-project-path 29 | npm install 30 | ``` 31 | 32 | After you have all dependencies installed, edit the `src/contract.ts` file and write actions you need in your contract: 33 | 34 | ```typescript 35 | @Contract() 36 | export default class MyContract { 37 | @State state: ContractState 38 | 39 | 40 | @Action 41 | async greet( 42 | @Param('name') name: string 43 | ) { 44 | const Greeting = await this.state.tryGet('Greeting'); 45 | 46 | assert(Greeting === null, 'you already greeted'); 47 | 48 | this.state.set('Greeting', `Hello, ${name}`); 49 | } 50 | } 51 | ``` 52 | 53 | 54 | Once you finish write your contract actions, you can create your contract to network, i.e to create to local sandbox network run following: 55 | 56 | ```npm 57 | npm run create:sandbox 58 | ``` 59 | 60 | Make sure you setup your network credentials in ```contract.config.js``` 61 | -------------------------------------------------------------------------------- /example/contract.ts: -------------------------------------------------------------------------------- 1 | import { Action, AttachedPayments, Contract, ContractState, Param, Payments, State } from '../packages/contract-core/src' 2 | 3 | @Contract() 4 | export class TestContract { 5 | @State state: ContractState 6 | 7 | @Action({ onInit: true }) 8 | init() { 9 | this.state.set('moveNum', 0) 10 | this.state.set('currentMover', 'x') 11 | } 12 | 13 | @Action 14 | move( 15 | @Param('player') player: string, 16 | @Param('cell') cell: number, 17 | @Payments attachedPayments: AttachedPayments, 18 | ) { 19 | 20 | 21 | this.state.set('moveNum', cell) 22 | this.state.set('currentMover', attachedPayments[0].amount.toString()) 23 | } 24 | } -------------------------------------------------------------------------------- /example/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata' 2 | 3 | import { initContract } from '../packages/contract-core/src' 4 | 5 | initContract({ contractPath: './contract', concurrencyLevel: 8 }) 6 | -------------------------------------------------------------------------------- /example/sandbox/.env.example: -------------------------------------------------------------------------------- 1 | NODE_ADDRESS=http://localhost:6862 2 | NODE_BLOCKCHAIN_ADDRESS=3NeGLTC4XBp5hJri493j861VhcHD8dmX3Jn 3 | NODE_KEYPAIR_PASSWORD=qiu8nG5any9sotEgpeGGZA 4 | NODE_API_KEY=we 5 | -------------------------------------------------------------------------------- /example/sandbox/README.md: -------------------------------------------------------------------------------- 1 | ## How to dev 2 | 3 | - Setup local nodes https://docs.wavesenterprise.com/en/latest/get-started/sandbox/sandbox.html. Do not start. 4 | - Modify nodes config `functionality` section with: 5 | ``` 6 | functionality { 7 | feature-check-blocks-period = 1500 8 | blocks-for-feature-activation = 1000 9 | pre-activated-features { 10 | 2 = 0 11 | 3 = 0 12 | 4 = 0 13 | 5 = 0 14 | 6 = 0 15 | 7 = 0 16 | 9 = 0 17 | 10 = 0 18 | 100 = 0 19 | 101 = 0 20 | 120 = 0 21 | 130 = 0 22 | 140 = 0 23 | 160 = 0 24 | 162 = 0 25 | } 26 | } 27 | ``` 28 | State of nodes should be clean to take effect. Feature 120 must be enabled. 29 | - Modify nodes config `docker-engine` section like this 30 | ``` 31 | docker-engine { 32 | enable = "yes" 33 | use-node-docker-host = "yes" 34 | default-registry-domain = "registry.wavesenterprise.com/waves-enterprise-public" 35 | docker-host = "unix:///var/run/docker.sock" 36 | execution-limits { 37 | timeout = "100s" 38 | memory = 1024 39 | memory-swap = 0 40 | startup-timeout = "100s" 41 | } 42 | remove-container-on-fail = "no" 43 | reuse-containers = "yes" 44 | remove-container-after = "30m" 45 | remote-registries = [] 46 | check-registry-auth-on-startup = "yes" 47 | contract-execution-messages-cache { 48 | expire-after = "60m" 49 | max-buffer-size = 10 50 | max-buffer-time = "100ms" 51 | } 52 | } 53 | ``` 54 | Optionaly add `contracts-parallelism = 8` in docker engine section to embrace the power of parallel execution 55 | - start nodes 56 | - Setup local docker registry `docker run -d -p 5000:5000 --name registry registry:2` 57 | - `cp example/sandbox/.env.example example/sandbox/.env` 58 | - Fill in .env values 59 | ``` 60 | NODE_ADDRESS=http://localhost:6862 61 | NODE_BLOCKCHAIN_ADDRESS= 62 | NODE_KEYPAIR_PASSWORD= 63 | NODE_API_KEY= 64 | ``` 65 | - In separete terminal `npm run logger` 66 | - `npm run start` 67 | -------------------------------------------------------------------------------- /example/sandbox/config.ts: -------------------------------------------------------------------------------- 1 | import { Env } from '@wavesenterprise/env-extractor' 2 | import { config } from 'dotenv' 3 | 4 | config() 5 | 6 | export const NODE_ADDRESS = Env.string('NODE_ADDRESS').required().get() 7 | export const NODE_BLOCKCHAIN_ADDRESS = Env.string('NODE_BLOCKCHAIN_ADDRESS').required().get() 8 | export const NODE_KEYPAIR_PASSWORD = Env.string('NODE_KEYPAIR_PASSWORD').required().get() 9 | export const NODE_API_KEY = Env.string('NODE_API_KEY').required().get() 10 | export const CONTRACT_NAME = Env.string('CONTRACT_NAME').default('TEST_CONTRACT').get() 11 | -------------------------------------------------------------------------------- /example/sandbox/logger.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import * as express from 'express' 3 | import * as body from 'body-parser' 4 | 5 | const app = express() 6 | 7 | app.use(body.json()) 8 | 9 | app.post('/', (req, res) => { 10 | const [type, ...data] = req.body 11 | // @ts-ignore 12 | console[type || 'log'](...data) 13 | res.send('') 14 | }) 15 | 16 | app.listen(5050, '0.0.0.0', () => { 17 | console.log('Logger is ready') 18 | }) 19 | -------------------------------------------------------------------------------- /example/sandbox/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sandbox", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "tsc -p tsconfig.build.json", 8 | "build:all": "npm --prefix ../../packages/contract-core run build && tsc -p tsconfig.build.json", 9 | "logger": "ts-node logger.ts", 10 | "start": "ts-node run.ts" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "@types/dotenv": "^8.2.0", 17 | "@types/node": "^18.11.9", 18 | "@wavesenterprise/env-extractor": "^1.0.0", 19 | "@wavesenterprise/voting-blockchain-tools": "^1.2.4", 20 | "@wavesenterprise/voting-contract-api": "^2.8.1", 21 | "axios": "0.27.2", 22 | "body-parser": "^1.20.1", 23 | "dotenv": "^16.0.3", 24 | "express": "^4.18.2", 25 | "ts-node": "^10.9.1" 26 | }, 27 | "dependencies": { 28 | "@grpc/grpc-js": "^1.6.2", 29 | "@types/bn.js": "^5.1.1", 30 | "@wavesenterprise/js-contract-grpc-client": "1.12.3", 31 | "@wavesenterprise/sdk": "^1.0.3", 32 | "@wavesenterprise/we-node-grpc-api": "^1.1.1", 33 | "bn.js": "^5.2.1", 34 | "google-protobuf": "^3.21.0", 35 | "long": "^5.2.1", 36 | "reflect-metadata": "^0.1.13", 37 | "typescript": "^4.8.4" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /example/sandbox/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | eval $SET_ENV_CMD 4 | node /dist/index.js 5 | -------------------------------------------------------------------------------- /example/sandbox/run.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { exec } from 'child_process' 3 | import { NetworkInterfaceInfo, networkInterfaces } from 'os' 4 | import { broadcast, getContractDeveloper, getParticipants } from './utils' 5 | import { CallContractTx, CreateContractTx } from '@wavesenterprise/voting-blockchain-tools/transactions' 6 | import { CONTRACT_NAME, NODE_ADDRESS } from './config' 7 | import * as path from 'path' 8 | import { WaitTransactionMining } from '@wavesenterprise/voting-contract-api' 9 | import axios from 'axios' 10 | 11 | const contractName = CONTRACT_NAME.toLowerCase() 12 | 13 | function getLocalHostNetworkIp() { 14 | const network = networkInterfaces() 15 | for (const key in network) { 16 | for (const net of (network[key] as NetworkInterfaceInfo[])) { 17 | if (net.address.includes('192.168.')) { 18 | return net.address 19 | } 20 | } 21 | } 22 | throw new Error('Local network ip was not found') 23 | } 24 | 25 | const imageName = `localhost:5000/${contractName}` 26 | const ip = getLocalHostNetworkIp() 27 | 28 | const execute = (command: string): Promise => { 29 | return new Promise((resolve, reject) => { 30 | exec(command, (err, stdout) => { 31 | if (err) { 32 | reject(err) 33 | } else { 34 | resolve(stdout) 35 | } 36 | }) 37 | }) 38 | } 39 | 40 | const run = async () => { 41 | console.log('Building sdk') 42 | await execute(`npm --prefix ${path.resolve(__dirname, '..', '..', 'packages', 'contract-core')} run build`) 43 | console.log(`Building docker image, debug=1, host_network=${ip}`) 44 | const buildCommand = 45 | // eslint-disable-next-line max-len 46 | `docker build --build-arg DEBUG=1 --build-arg REMOTE_LOG=1 --build-arg HOST_NETWORK=${ip} -t ${contractName} -f sandbox.Dockerfile ../../` 47 | console.log(buildCommand) 48 | await execute(buildCommand) 49 | console.log('Done building') 50 | await execute(`docker image tag ${contractName} ${imageName}`) 51 | console.log('Tagged') 52 | console.log(`Start pushing to repo ${imageName}`) 53 | await execute(`docker push ${imageName}`) 54 | console.log('Done pushing') 55 | const inspectResult = await execute(`docker inspect ${imageName}`) 56 | const inspectData = JSON.parse(inspectResult)[0] 57 | const imageHash = inspectData.Id.replace('sha256:', '') 58 | console.log(`New image hash is ${imageHash}`) 59 | 60 | const developerKeys = await getContractDeveloper() 61 | const result = await broadcast(new CreateContractTx({ 62 | contractName: CONTRACT_NAME, 63 | image: imageName, 64 | imageHash, 65 | fee: 0, 66 | senderPublicKey: developerKeys.publicKey, 67 | apiVersion: '1.0', 68 | validationPolicy: { 69 | type: 'any', 70 | }, 71 | params: [ 72 | { 73 | key: 'initial', 74 | type: 'integer', 75 | value: 10, 76 | }, 77 | { 78 | key: 'nonce', 79 | type: 'string', 80 | value: 'nonce', 81 | }, 82 | { 83 | key: 'invisible', 84 | type: 'boolean', 85 | value: false, 86 | }, 87 | ], 88 | }), developerKeys) 89 | 90 | console.log(`Contract id ${result.id}`) 91 | 92 | const waiter = new WaitTransactionMining( 93 | NODE_ADDRESS, 94 | axios, 95 | 100000, 96 | 2, 97 | result.id, 98 | ) 99 | 100 | console.log('Waiting create contract mining') 101 | await waiter.waitMining() 102 | console.log('Create tx mined') 103 | 104 | const participants = await getParticipants() 105 | await Promise.all(participants.slice(0, 20).map((p) => { 106 | return broadcast(new CallContractTx({ 107 | fee: 0, 108 | senderPublicKey: p.publicKey, 109 | params: [ 110 | { 111 | key: 'action', 112 | value: 'increment', 113 | type: 'string', 114 | }, 115 | { 116 | key: 'by', 117 | value: Math.trunc(Math.random() * 10) + 1, 118 | type: 'integer', 119 | }, 120 | ], 121 | contractId: result.id, 122 | contractVersion: 1, 123 | }), p) 124 | })) 125 | console.log('All tx`s sent') 126 | 127 | } 128 | 129 | run().catch(console.error) 130 | -------------------------------------------------------------------------------- /example/sandbox/sandbox.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-buster-slim 2 | ARG DEBUG=0 3 | ENV DEBUG ${DEBUG} 4 | ARG HOST_NETWORK=0.0.0.0 5 | ENV HOST_NETWORK ${HOST_NETWORK} 6 | ARG VERBOSE_LOG=0 7 | ENV VERBOSE_LOG ${VERBOSE_LOG} 8 | ARG REMOTE_LOG=0 9 | ENV REMOTE_LOG ${REMOTE_LOG} 10 | RUN apt update && apt install -yq dnsutils 11 | ADD /example/sandbox/src/ /src 12 | ADD /example/sandbox/tsconfig.build.json / 13 | ADD /example/sandbox/package.json / 14 | ADD /example/sandbox/package-lock.json / 15 | RUN npm install 16 | ADD /packages/contract-core /node_modules/@wavesenterprise/contract-core 17 | RUN npm run build 18 | ADD /example/sandbox/run.sh / 19 | RUN chmod +x run.sh 20 | ENTRYPOINT ["/run.sh"] 21 | -------------------------------------------------------------------------------- /example/sandbox/src/contract.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Action, 3 | Assets, 4 | AssetsService, 5 | Contract, 6 | ContractMapping, 7 | ContractValue, 8 | IncomingTx, 9 | JsonVar, 10 | logger, 11 | Param, 12 | Params, 13 | preload, 14 | Tx, 15 | Var, 16 | } from '@wavesenterprise/contract-core' 17 | import Long from 'long' 18 | 19 | type UserData = { 20 | publicKey: string, 21 | address: string, 22 | amount: number, 23 | } 24 | 25 | @Contract() 26 | export default class MyContract { 27 | 28 | log = logger(this) 29 | 30 | @Var({ key: 'COUNTER' }) 31 | counter!: ContractValue 32 | 33 | @JsonVar({ key: 'PARTICIPANTS' }) 34 | participants!: ContractMapping 35 | 36 | @JsonVar({ key: 'ARR' }) 37 | arr!: ContractValue 38 | 39 | @Var({ key: 'NICE_ASSET_ID' }) 40 | niceAssetId!: ContractValue 41 | 42 | @Assets() 43 | assets!: AssetsService 44 | 45 | @Action({ onInit: true }) 46 | async init(@Params() params: Record) { 47 | this.counter.set(0) 48 | this.arr.set([Math.trunc(Math.random() * 10)]) 49 | this.log.info('all params', params) 50 | const asset = await this.assets.issueAsset({ 51 | name: 'NICE', 52 | description: 'nice asset', 53 | quantity: Long.fromNumber(100_000_000_000), 54 | decimals: 6, 55 | isReissuable: true, 56 | }) 57 | this.niceAssetId.set(asset.getId()!) 58 | } 59 | 60 | @Action() 61 | async increment(@Tx() tx: IncomingTx, @Param('by') by: Long) { 62 | const { senderPublicKey, sender } = tx 63 | await preload(this, ['counter', ['participants', senderPublicKey], 'niceAssetId']) 64 | const counter = await this.counter.get() 65 | let participant = await this.participants.tryGet(senderPublicKey) 66 | if (!participant) { 67 | participant = { 68 | publicKey: senderPublicKey, 69 | address: sender, 70 | amount: 0, 71 | } 72 | } 73 | participant.amount += by.toNumber() 74 | this.counter.set(counter + by.toNumber()) 75 | this.participants.set(senderPublicKey, participant) 76 | this.assets.transferAsset(await this.niceAssetId.get(), { 77 | recipient: sender, 78 | amount: Long.fromNumber(10), 79 | }) 80 | this.log.info(`Transfer to ${sender}`) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /example/sandbox/src/index.ts: -------------------------------------------------------------------------------- 1 | import { initContract } from '@wavesenterprise/contract-core' 2 | 3 | initContract({ 4 | contractPath: __dirname + '/contract.js', 5 | concurrencyLevel: 1, 6 | }) 7 | -------------------------------------------------------------------------------- /example/sandbox/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "commonjs", 5 | "declaration": false, 6 | "removeComments": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "allowSyntheticDefaultImports": true, 10 | "target": "es2020", 11 | "lib": [ 12 | "es2019", 13 | "dom" 14 | ], 15 | "sourceMap": false, 16 | "rootDir": "./src", 17 | "outDir": "./dist", 18 | "baseUrl": "./", 19 | "moduleResolution": "node", 20 | "skipLibCheck": true, 21 | "resolveJsonModule": true, 22 | "esModuleInterop": true, 23 | }, 24 | "include": [ 25 | "src" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /example/sandbox/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "commonjs", 5 | "declaration": false, 6 | "removeComments": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "allowSyntheticDefaultImports": true, 10 | "target": "es2020", 11 | "lib": [ 12 | "es2019", 13 | "dom" 14 | ], 15 | "sourceMap": false, 16 | "rootDir": "./src", 17 | "outDir": "./dist", 18 | "baseUrl": "./", 19 | "moduleResolution": "node", 20 | "skipLibCheck": true, 21 | "resolveJsonModule": true, 22 | "esModuleInterop": true, 23 | }, 24 | "include": [ 25 | "src" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /example/sandbox/utils.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { NODE_ADDRESS, NODE_API_KEY, NODE_BLOCKCHAIN_ADDRESS, NODE_KEYPAIR_PASSWORD } from './config' 3 | /* eslint-disable */ 4 | import { resolve } from 'path' 5 | import { constants as fsConstants, promises as fs } from 'fs' 6 | import { WavesNodeCrypto } from '@wavesenterprise/voting-blockchain-tools/node/seed/waves' 7 | import { SeedManager } from '@wavesenterprise/voting-blockchain-tools/node/seed/common' 8 | import { GostCrypto } from '@wavesenterprise/voting-blockchain-tools/node/seed/gost' 9 | import { KeyPair, TransactionBroadcaster } from '@wavesenterprise/voting-blockchain-tools/transactions' 10 | import { WavesSignature } from '@wavesenterprise/voting-blockchain-tools/node/signature/waves' 11 | import { GostSignature } from '@wavesenterprise/voting-blockchain-tools/node/signature/gost' 12 | 13 | export type NodeConfig = { 14 | additionalFee: { [key: string]: number }, 15 | minimumFee: { [key: string]: number }, 16 | gostCrypto: boolean, 17 | chainId: string, 18 | } 19 | 20 | let nodeConfig: NodeConfig 21 | 22 | export const getNodeConfig = async (): Promise => { 23 | if (nodeConfig) { 24 | return nodeConfig 25 | } 26 | try { 27 | const { data } = await axios.get(`${NODE_ADDRESS}/node/config`) 28 | nodeConfig = data 29 | return data 30 | } catch (e) { 31 | // eslint-disable-next-line no-console 32 | console.log('failed to get node config', (e as Error).message) 33 | process.exit(1) 34 | } 35 | } 36 | 37 | export const checkFileExists = async (path: string) => { 38 | try { 39 | await fs.access(path, fsConstants.F_OK) 40 | return true 41 | } catch (e) { 42 | return false 43 | } 44 | } 45 | 46 | export const readJsonFile = async (path: string): Promise => { 47 | try { 48 | const data = await fs.readFile(path) 49 | return JSON.parse(data.toString()) 50 | } catch (e) { 51 | if (isError(e)) { 52 | throw new Error(`Failed to read json file ${path} ${e.message}`) 53 | } 54 | throw e 55 | } 56 | } 57 | 58 | export function isError unknown = ErrorConstructor>( 59 | value: unknown, 60 | // @ts-ignore 61 | errorType: T = Error, 62 | ): value is InstanceType & NodeJS.ErrnoException { 63 | // @ts-ignore 64 | return value instanceof errorType || value.constructor.name === 'Error' 65 | } 66 | 67 | 68 | export const sleep = (delay: number) => new Promise((resolve) => setTimeout(resolve, delay)) 69 | 70 | 71 | type DeveloperKeyPair = { 72 | address: string, 73 | privateKey: string, 74 | publicKey: string, 75 | } 76 | 77 | export async function getContractDeveloper() { 78 | const nodeConfig = await getNodeConfig() 79 | const keysFile = resolve(__dirname, 'developer-keys.json') 80 | 81 | 82 | const exists = await checkFileExists(keysFile) 83 | if (exists) { 84 | return readJsonFile(keysFile) 85 | } 86 | 87 | const seedManager = new SeedManager({ 88 | networkByte: nodeConfig.chainId.charCodeAt(0), 89 | keysModule: nodeConfig.gostCrypto ? GostCrypto : WavesNodeCrypto, 90 | }) 91 | 92 | const seed = await seedManager.create() 93 | 94 | const keys = { address: seed.address, ...seed.keyPair } 95 | 96 | try { 97 | const { data } = await axios.post(`${NODE_ADDRESS}/transactions/signAndBroadcast`, { 98 | 'type': 102, 99 | 'sender': NODE_BLOCKCHAIN_ADDRESS, 100 | 'password': NODE_KEYPAIR_PASSWORD, 101 | 'fee': nodeConfig.minimumFee['102'], 102 | 'proofs': [''], 103 | 'target': keys.address, 104 | 'opType': 'add', 105 | 'role': 'contract_developer', 106 | 'dueTimestamp': null, 107 | }, { 108 | headers: { 109 | 'X-Api-Token': NODE_API_KEY, 110 | }, 111 | }) 112 | console.log('Wait 20s contract role add tx', data.id) 113 | } catch (e) { 114 | throw new Error(`failed to append contract developer role ${(e as Error).message}`) 115 | } 116 | 117 | await sleep(20000) 118 | 119 | await fs.writeFile(keysFile, JSON.stringify(keys)) 120 | 121 | console.log('Developer keys generated', keys) 122 | 123 | return keys 124 | } 125 | 126 | export async function getParticipants() { 127 | const nodeConfig = await getNodeConfig() 128 | const keysFile = resolve(__dirname, 'participants.json') 129 | 130 | const exists = await checkFileExists(keysFile) 131 | if (exists) { 132 | return readJsonFile(keysFile) 133 | } 134 | 135 | const seedManager = new SeedManager({ 136 | networkByte: nodeConfig.chainId.charCodeAt(0), 137 | keysModule: nodeConfig.gostCrypto ? GostCrypto : WavesNodeCrypto, 138 | }) 139 | 140 | const keys = await Promise.all(Array(1000).fill(undefined).map(async () => { 141 | const seed = await seedManager.create() 142 | return { address: seed.address, ...seed.keyPair } 143 | })) 144 | await fs.writeFile(keysFile, JSON.stringify(keys)) 145 | return keys 146 | } 147 | 148 | 149 | export const broadcast = async (tx: any, keys: KeyPair) => { 150 | const nodeConfig = await getNodeConfig() 151 | const broadcaster = new TransactionBroadcaster({ 152 | networkByte: nodeConfig.chainId.charCodeAt(0), 153 | signModule: nodeConfig.gostCrypto ? GostSignature : WavesSignature, 154 | nodeAddress: NODE_ADDRESS, 155 | axiosInstance: axios, 156 | }) 157 | return broadcaster.broadcast(tx, keys) 158 | } 159 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "npmClient": "npm", 6 | "version": "independent", 7 | "useWorkspaces": true 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wavesenterprise/js-contract-sdk", 3 | "version": "1.1.0", 4 | "description": "Toolkit for development, test and deploy smart-contracts on Waves Enterprise ecosystem.", 5 | "private": false, 6 | "type": "commonjs", 7 | "scripts": { 8 | "bootstrap": "lerna bootstrap", 9 | "typecheck": "lerna run typecheck --stream", 10 | "lint": "npx eslint . --ext .ts,.tsx", 11 | "lint:fix": "npx eslint . --ext .ts,.tsx --fix", 12 | "test-ci": "npm run lint && npm run typecheck && lerna run test --stream" 13 | }, 14 | "repository": { 15 | "url": "git@github.com:waves-enterprise/js-contract-sdk.git" 16 | }, 17 | "author": { 18 | "name": "Timofey Semenyuk", 19 | "email": "rustfy@gmail.com", 20 | "url": "https://github.com/stfy" 21 | }, 22 | "license": "MIT", 23 | "devDependencies": { 24 | "@changesets/cli": "^2.25.0", 25 | "@typescript-eslint/eslint-plugin": "5.39.0", 26 | "@typescript-eslint/parser": "5.39.0", 27 | "@wavesenterprise/eslint-config": "^0.3.1", 28 | "eslint": "8.22.0", 29 | "lerna": "^4.0.0" 30 | }, 31 | "dependencies": { 32 | "@wavesenterprise/sdk": "^1.0.3", 33 | "reflect-metadata": "^0.1.13" 34 | }, 35 | "workspaces": [ 36 | "packages/*" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /packages/cli/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [0.0.3](https://github.com/waves-enterprise/js-contract-sdk/compare/@wavesenterprise/contract-cli@0.0.2...@wavesenterprise/contract-cli@0.0.3) (2022-07-26) 7 | 8 | **Note:** Version bump only for package @wavesenterprise/contract-cli 9 | -------------------------------------------------------------------------------- /packages/cli/LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2022 Waves Enterprise 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /packages/cli/README.md: -------------------------------------------------------------------------------- 1 | # @wavesenterprise/contract-cli 2 | 3 | Command line interface for deploying, testing, and updating Waves Enterprise contracts. 4 | 5 | ### Getting started 6 | 7 | ``` 8 | npm install -D @wavesenterprise/contract-cli 9 | ``` 10 | 11 | ### Overview 12 | 13 | | Command | Description | 14 | | ------------------------------ | ------------------------------------------------------------------------------------------- | 15 | | `we-cli create` | Deploy your contract to Waves Enterprise network based on config | 16 | | `we-cli update` | Update your contract container | 17 | | | More commands soon... | 18 | 19 | ## License 20 | 21 | This project is licensed under the [MIT License](LICENSE). -------------------------------------------------------------------------------- /packages/cli/cmd/docker.js: -------------------------------------------------------------------------------- 1 | const childProcess = require('child_process'); 2 | const {info} = require("../src/log"); 3 | 4 | 5 | function run( 6 | command, 7 | runArgs, 8 | opts, 9 | options = {executable: "docker"} 10 | ) { 11 | return new Promise((resolve, reject) => { 12 | const cwd = options.cwd 13 | const env = options.env || undefined 14 | const executablePath = options.executable; 15 | 16 | const execParams = Object.keys(opts) 17 | .reduce((acc, k) => { 18 | if (opts[k] === '') { 19 | return [...acc, opts[k]]; 20 | } else { 21 | 22 | return [...acc, k, opts[k]] 23 | } 24 | }, []) 25 | 26 | 27 | const cmd = Array.isArray(command) ? [...command] : [command] 28 | 29 | const execArgs = [ 30 | ...cmd, 31 | ...execParams, 32 | ...runArgs 33 | ]; 34 | 35 | info('Execute command: ', [executablePath, ...execArgs].join(' ')); 36 | 37 | const childProc = childProcess.spawn(executablePath, execArgs, { 38 | cwd, 39 | env 40 | }) 41 | 42 | const result = { 43 | exitCode: null, 44 | err: '', 45 | out: '' 46 | } 47 | 48 | childProc.on('error', (err) => { 49 | reject(err) 50 | }); 51 | 52 | childProc.stdout.on('data', (chunk) => { 53 | result.out += chunk.toString() 54 | options.callback?.(chunk, 'stdout') 55 | }) 56 | 57 | childProc.stderr.on('data', (chunk) => { 58 | result.err += chunk.toString() 59 | options.callback?.(chunk, 'stderr') 60 | }) 61 | 62 | childProc.on('exit', (exitCode) => { 63 | result.exitCode = exitCode 64 | if (exitCode === 0) { 65 | resolve(result) 66 | } else { 67 | reject(result) 68 | } 69 | }) 70 | }) 71 | } 72 | 73 | 74 | module.exports = { 75 | 76 | run 77 | } -------------------------------------------------------------------------------- /packages/cli/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const process = require('process'); 4 | 5 | const docker = require('./cmd/docker'); 6 | 7 | 8 | const {Command} = require('commander'); 9 | const {resolveConfig} = require("./src/config"); 10 | const {deploy, update, getImageHash} = require("./src/contract"); 11 | const {info} = require("./src/log"); 12 | const {cache} = require("./src/cache"); 13 | const {context} = require("./src/context"); 14 | 15 | const SANDBOX_CONFIG = { 16 | registry: 'localhost:5000', 17 | nodeUrl: 'http://localhost:6862', 18 | seed: 'admin seed phrase' 19 | } 20 | 21 | 22 | const program = new Command(); 23 | 24 | program 25 | .name('we-contract-toolkit') 26 | .description('Command line toolkit for deploying smart contracts') 27 | .version('0.1.0'); 28 | 29 | 30 | program 31 | .command('deploy') 32 | .option('-n, --network ', 'Network', 'testnet') 33 | .description('Deploys contract to network') 34 | .action(async (options) => { 35 | const config = resolveConfig(); 36 | const imageName = config.image + ':' + (config.version ? config.version : 'latest'); 37 | 38 | try { 39 | await docker.run('build', ['.'], { 40 | '-t': imageName, 41 | }) 42 | 43 | const imageHash = await getImageHash(imageName); 44 | 45 | if (!config.networks.hasOwnProperty(options.network)) { 46 | throw new Error('Network config not founded'); 47 | } 48 | 49 | const networkConfig = { 50 | ...config.networks[options.network] 51 | } 52 | 53 | const nodeConfig = { 54 | ...MAINNET_CONFIG, 55 | nodeAddress: networkConfig.nodeAddress 56 | } 57 | 58 | context({ 59 | network: options.network, 60 | networkConfig: networkConfig, 61 | nodeConfig: nodeConfig 62 | }); 63 | 64 | await docker.run(['image', 'tag'], [imageName, `${networkConfig.registry}/${imageName}`], {}); 65 | await docker.run(['image', 'push'], [`${networkConfig.registry}/${imageName}`], {}); 66 | 67 | const Tx = await deploy(nodeConfig, 68 | { 69 | seed: networkConfig.seed, 70 | imageName, 71 | imageHash, 72 | name: config.name, 73 | params: networkConfig.params.init() 74 | }); 75 | 76 | cache().addDeployedImage(imageHash, options.network, Tx.id, Date.now()); 77 | cache().persist(); 78 | 79 | info('Successfully deployed to network'); 80 | } catch (e) { 81 | console.error(e); 82 | } 83 | }) 84 | 85 | 86 | program 87 | .command('update') 88 | .option('-n, --network ', 'Network', 'testnet') 89 | .description('Update contract to network') 90 | .action(async (options) => { 91 | const config = resolveConfig(); 92 | const dbCache = cache(); 93 | 94 | if (!config.networks.hasOwnProperty(options.network)) { 95 | throw new Error('Network config not founded'); 96 | } 97 | 98 | const networkConfig = { 99 | ...config.networks[options.network] 100 | } 101 | 102 | const nodeConfig = { 103 | ...MAINNET_CONFIG, 104 | nodeAddress: networkConfig.nodeAddress 105 | } 106 | 107 | context({ 108 | network: options.network, 109 | networkConfig: networkConfig, 110 | nodeConfig: nodeConfig 111 | }); 112 | 113 | const imageName = config.image + ':' + (config.version ? config.version : 'latest'); 114 | 115 | await docker.run('build', ['.'], { 116 | '-t': imageName, 117 | }) 118 | 119 | const imageHash = await getImageHash(imageName); 120 | 121 | await docker.run(['image', 'tag'], [imageName, `${networkConfig.registry}/${imageName}`], {}); 122 | await docker.run(['image', 'push'], [`${networkConfig.registry}/${imageName}`], {}); 123 | 124 | const lastDeploy = dbCache.getLastContractVersion(); 125 | 126 | if (!lastDeploy) { 127 | throw new Error('Contract was not deployed from this machine'); 128 | } 129 | 130 | const Tx = await update(nodeConfig, 131 | { 132 | seed: networkConfig.seed, 133 | image: imageName, 134 | imageHash, 135 | contractId: lastDeploy.contractId 136 | } 137 | ) 138 | 139 | dbCache.updateImage(Tx.contractId, Tx.imageHash, Tx.id); 140 | dbCache.persist(); 141 | 142 | info('Contract successfully updated'); 143 | }); 144 | 145 | program 146 | .command('compile') 147 | .option('-n, --network ', 'Network', 'testnet') 148 | .description('Compile contract') 149 | 150 | 151 | program.parse(process.argv); -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wavesenterprise/contract-cli", 3 | "version": "0.0.4", 4 | "description": "Waves Enterprise Contracts CLI", 5 | "main": "index.js", 6 | "private": false, 7 | "publishConfig": { 8 | "access": "public" 9 | }, 10 | "bin": { 11 | "we-cli": "index.js" 12 | }, 13 | "author": { 14 | "name": "Timofey Semenyuk", 15 | "email": "rustfy@gmail.com", 16 | "url": "https://github.com/stfy" 17 | }, 18 | "license": "MIT", 19 | "dependencies": { 20 | "@wavesenterprise/js-sdk": "^3.4.7", 21 | "commander": "^9.1.0", 22 | "node-fetch": "^2.6.7" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/cli/src/cache.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require('fs'); 3 | const {ctx} = require("./context"); 4 | const {resolveConfig} = require("./config"); 5 | 6 | const cacheRoot = path.join(process.cwd(), '.we-cache'); 7 | const dbPath = path.join(cacheRoot, 'db.json'); 8 | 9 | function initCache() { 10 | console.log(ctx); 11 | const isCacheRootExists = fs.existsSync(cacheRoot); 12 | const isDBExists = fs.existsSync(dbPath); 13 | 14 | if (!isCacheRootExists) { 15 | fs.mkdirSync(cacheRoot); 16 | } 17 | 18 | if (!isDBExists) { 19 | const cfg = resolveConfig() 20 | 21 | const cache = { 22 | name: cfg.name, 23 | image: cfg.image, 24 | deployed: [] 25 | }; 26 | 27 | fs.writeFileSync(dbPath, JSON.stringify(cache, null, 2)); 28 | } 29 | 30 | const dbFile = fs.readFileSync(dbPath); 31 | 32 | return JSON.parse(dbFile); 33 | } 34 | 35 | function cache() { 36 | const db = initCache(); 37 | 38 | const addDeployedImage = (hash, network, contractId, timestamp) => { 39 | db.deployed.push({ 40 | hash, network, contractId, timestamp, updates: [] 41 | }); 42 | } 43 | 44 | const updateImage = (contractId, hash, txId) => { 45 | const contractIndex = db.deployed.findIndex(t => t.contractId); 46 | 47 | db.deployed.splice(contractIndex, 1, { 48 | ...db.deployed[contractIndex], 49 | updates: [ 50 | ...db.deployed[contractIndex].updates, 51 | { 52 | hash, txId 53 | } 54 | ] 55 | }); 56 | } 57 | 58 | return { 59 | getLastContractVersion: () => { 60 | const [last] = db.deployed 61 | .filter(c => ctx.network === c.network) 62 | .sort((p, c) => p.timestamp < c.timestamp); 63 | 64 | return last; 65 | }, 66 | addDeployedImage, 67 | updateImage, 68 | prune: () => { 69 | db.deployed = []; 70 | }, 71 | 72 | persist: () => { 73 | fs.writeFileSync(dbPath, JSON.stringify(db, null, 2)); 74 | } 75 | } 76 | } 77 | 78 | 79 | module.exports = { 80 | cache 81 | } -------------------------------------------------------------------------------- /packages/cli/src/config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | function resolveConfig() { 4 | const configPath = path.join(process.cwd(), 'contract.config.js'); 5 | 6 | return require(configPath); 7 | } 8 | 9 | 10 | module.exports = { 11 | resolveConfig 12 | } -------------------------------------------------------------------------------- /packages/cli/src/context.js: -------------------------------------------------------------------------------- 1 | let ctx = {}; 2 | 3 | function context({network, networkConfig, nodeConfig}) { 4 | ctx.network = network; 5 | ctx.networkConfig = networkConfig; 6 | ctx.nodeConfig = nodeConfig; 7 | } 8 | 9 | module.exports = { 10 | context, 11 | ctx 12 | } -------------------------------------------------------------------------------- /packages/cli/src/contract.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch') 2 | const docker = require("../cmd/docker"); 3 | 4 | const {We} = require("@wavesenterprise/sdk"); 5 | const {paramType} = require("./utils"); 6 | const {info} = require("./log"); 7 | const {contractStatus} = require("./tx-observer"); 8 | 9 | async function deploy(nodeConfig, {seed, imageName, imageHash, name, params}) { 10 | const {chainId, minimumFee} = await (await fetch(`${nodeConfig.nodeAddress}/node/config`)).json(); 11 | 12 | const WE = new We() 13 | 14 | const signer = WE.Seed.fromExistingPhrase(seed) 15 | 16 | const txBody = { 17 | image: imageName, 18 | imageHash: imageHash, 19 | contractName: name, 20 | timestamp: Date.now(), 21 | params: transformParams(params) 22 | }; 23 | 24 | const tx = WE.API.Transactions.CreateContract.V2(txBody); 25 | 26 | const Tx = await tx.broadcast(signer.keyPair); 27 | 28 | info(`ContractId "${Tx.id}"`); 29 | 30 | await contractStatus(Tx.id); 31 | 32 | return Tx; 33 | } 34 | 35 | async function update(nodeConfig, {seed, image, imageHash, contractId}) { 36 | const nodeApi = await getNodeApi(nodeConfig); 37 | 38 | const signer = nodeApi.Seed.fromExistingPhrase(seed) 39 | const tx = nodeApi.API.Transactions.UpdateContract.V2({ 40 | image, 41 | imageHash, 42 | contractId, 43 | timestamp: Date.now() 44 | }) 45 | 46 | const Tx = await tx.broadcast(signer.keyPair); 47 | 48 | info(`Contract updated at address ${contractId}`); 49 | 50 | return Tx; 51 | } 52 | 53 | 54 | async function getImageHash(imageName) { 55 | const inspectCommand = await docker.run('inspect', [imageName], {}) 56 | 57 | const imageHash = JSON.parse(inspectCommand.out)[0].Id.replace('sha256:', '') 58 | 59 | return imageHash; 60 | } 61 | 62 | async function getNodeApi(nodeConfig) { 63 | const {chainId, minimumFee} = await (await fetch(`${nodeConfig.nodeAddress}/node/config`)).json(); 64 | 65 | return create({ 66 | initialConfiguration: { 67 | ...MAINNET_CONFIG, 68 | ...nodeConfig, 69 | networkByte: chainId.charCodeAt(0), 70 | minimumFee, 71 | }, 72 | fetchInstance: fetch 73 | }); 74 | } 75 | 76 | function transformParams(params) { 77 | return Object.entries(params).map(([key, value]) => { 78 | return { 79 | type: paramType(value), 80 | value: value, 81 | key: key 82 | } 83 | }); 84 | } 85 | 86 | module.exports = { 87 | deploy, 88 | update, 89 | getImageHash 90 | } -------------------------------------------------------------------------------- /packages/cli/src/log.js: -------------------------------------------------------------------------------- 1 | function info(...args) { 2 | console.info(...args); 3 | } 4 | 5 | 6 | module.exports = { 7 | info 8 | } -------------------------------------------------------------------------------- /packages/cli/src/tx-observer.js: -------------------------------------------------------------------------------- 1 | const fetch = require("node-fetch"); 2 | 3 | const {ctx} = require("./context"); 4 | 5 | const POLL_TTL = 7000; 6 | const MAX_RETRIES = 15; 7 | 8 | async function observe(method) { 9 | const {nodeConfig} = ctx; 10 | 11 | if (!nodeConfig) { 12 | throw new Error('node config was not provided'); 13 | } 14 | 15 | let retries = 0; 16 | let lastError; 17 | 18 | const f = async () => { 19 | if (++retries > MAX_RETRIES) { 20 | 21 | throw new Error(lastError.message || 'Unhandled error'); 22 | } 23 | 24 | const res = await fetch(`${nodeConfig.nodeAddress}${method}`); 25 | const resp = await res.json(); 26 | 27 | if (res.status !== 200) { 28 | lastError = resp; 29 | 30 | await sleep(POLL_TTL); 31 | return f(); 32 | } else { 33 | return resp; 34 | } 35 | } 36 | 37 | const resp = await f(); 38 | 39 | const [lastMessage] = resp.slice(-1); 40 | 41 | if (lastMessage.status === 'Failure') { 42 | throw new Error(lastMessage.message); 43 | } 44 | 45 | return { 46 | message: lastMessage.message, 47 | txId: lastMessage.txId 48 | } 49 | } 50 | 51 | async function contractStatus(contractId) { 52 | return observe(`/contracts/status/${contractId}`); 53 | } 54 | 55 | async function contractInfo(contractId) { 56 | return observe(`/contracts/info/${contractId}`); 57 | } 58 | 59 | function sleep(tm) { 60 | return new Promise((resolve, reject) => { 61 | 62 | setTimeout(resolve, tm); 63 | }) 64 | } 65 | 66 | module.exports = { 67 | contractStatus, 68 | contractInfo, 69 | } -------------------------------------------------------------------------------- /packages/cli/src/update.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waves-enterprise/js-contract-sdk/c7118460983cc9ddde926973b91708752159ef79/packages/cli/src/update.js -------------------------------------------------------------------------------- /packages/cli/src/utils.js: -------------------------------------------------------------------------------- 1 | const isBool = (v) => { 2 | return typeof v === 'boolean'; 3 | } 4 | 5 | const isString = (v) => { 6 | return typeof v === 'string'; 7 | } 8 | 9 | const isNumber = (v) => { 10 | return typeof v === 'number'; 11 | } 12 | 13 | const isBinary = (v) => { 14 | return v.startsWith('base64:'); 15 | } 16 | 17 | 18 | const paramType = (v) => { 19 | if (isBool(v)) { 20 | return 'boolean'; 21 | } 22 | 23 | if (isString(v)) { 24 | return 'string'; 25 | } 26 | 27 | if (isNumber(v)) { 28 | return 'integer'; 29 | } 30 | 31 | if (isBinary(v)) { 32 | return 'binary'; 33 | } 34 | 35 | throw new Error('Unexpected value type'); 36 | } 37 | 38 | 39 | module.exports = { 40 | isBool, 41 | isString, 42 | isNumber, 43 | isBinary, 44 | paramType 45 | } -------------------------------------------------------------------------------- /packages/cli/test/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch -------------------------------------------------------------------------------- /packages/cli/test/contract.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | image: "test-contract", 3 | name: 'JS build Contract name', 4 | version: '1.2.3', // default=latest 5 | networks: { 6 | testnet: { 7 | seed: 'admin seed phrase', 8 | }, 9 | 10 | mainnet: { 11 | seed: 'mainnet env seed' 12 | }, 13 | sandbox: { 14 | registry: 'localhost:5000', 15 | nodeAddress: 'http://localhost:6862', 16 | seed: 'admin seed phrase', 17 | 18 | params: { 19 | init: () => ({ 20 | test: 'string' 21 | }) 22 | } 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /packages/contract-core/.npmrc: -------------------------------------------------------------------------------- 1 | git-checks=false 2 | registry.npmjs.org/:_authToken=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 -------------------------------------------------------------------------------- /packages/contract-core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [1.2.3](https://gitlab.wvservices.com/waves-enterprise/js-contract-sdk/compare/@wavesenterprise/contract-core@1.2.2...@wavesenterprise/contract-core@1.2.3) (2023-02-02) 7 | 8 | **Note:** Version bump only for package @wavesenterprise/contract-core 9 | 10 | 11 | 12 | 13 | 14 | ## [1.2.2](https://gitlab.wvservices.com/waves-enterprise/js-contract-sdk/compare/@wavesenterprise/contract-core@1.2.1...@wavesenterprise/contract-core@1.2.2) (2023-02-02) 15 | 16 | **Note:** Version bump only for package @wavesenterprise/contract-core 17 | 18 | 19 | 20 | 21 | 22 | ## [1.2.1](https://gitlab.wvservices.com/waves-enterprise/js-contract-sdk/compare/@wavesenterprise/contract-core@1.1.14...@wavesenterprise/contract-core@1.2.1) (2023-01-20) 23 | 24 | **Note:** Version bump only for package @wavesenterprise/contract-core 25 | 26 | 27 | 28 | 29 | 30 | ## [1.1.14](https://gitlab.wvservices.com/waves-enterprise/js-contract-sdk/compare/@wavesenterprise/contract-core@1.1.13...@wavesenterprise/contract-core@1.1.14) (2023-01-13) 31 | 32 | **Note:** Version bump only for package @wavesenterprise/contract-core 33 | 34 | 35 | 36 | 37 | 38 | ## [1.1.13](https://gitlab.wvservices.com/waves-enterprise/js-contract-sdk/compare/@wavesenterprise/contract-core@1.1.12...@wavesenterprise/contract-core@1.1.13) (2023-01-10) 39 | 40 | **Note:** Version bump only for package @wavesenterprise/contract-core 41 | 42 | 43 | 44 | 45 | 46 | ## [1.1.12](https://gitlab.wvservices.com/waves-enterprise/js-contract-sdk/compare/@wavesenterprise/contract-core@1.1.11...@wavesenterprise/contract-core@1.1.12) (2023-01-10) 47 | 48 | **Note:** Version bump only for package @wavesenterprise/contract-core 49 | 50 | 51 | 52 | 53 | 54 | ## [1.1.11](https://gitlab.wvservices.com/waves-enterprise/js-contract-sdk/compare/@wavesenterprise/contract-core@1.1.10...@wavesenterprise/contract-core@1.1.11) (2023-01-10) 55 | 56 | **Note:** Version bump only for package @wavesenterprise/contract-core 57 | 58 | 59 | 60 | 61 | 62 | ## [1.1.10](https://gitlab.wvservices.com/waves-enterprise/js-contract-sdk/compare/@wavesenterprise/contract-core@1.1.9...@wavesenterprise/contract-core@1.1.10) (2023-01-09) 63 | 64 | **Note:** Version bump only for package @wavesenterprise/contract-core 65 | 66 | 67 | 68 | 69 | 70 | ## [1.1.9](https://gitlab.wvservices.com/waves-enterprise/js-contract-sdk/compare/@wavesenterprise/contract-core@1.1.8...@wavesenterprise/contract-core@1.1.9) (2022-12-02) 71 | 72 | **Note:** Version bump only for package @wavesenterprise/contract-core 73 | 74 | 75 | 76 | 77 | 78 | ## [1.1.8](https://gitlab.wvservices.com/waves-enterprise/js-contract-sdk/compare/@wavesenterprise/contract-core@1.1.7...@wavesenterprise/contract-core@1.1.8) (2022-12-02) 79 | 80 | **Note:** Version bump only for package @wavesenterprise/contract-core 81 | 82 | 83 | 84 | 85 | 86 | # [1.1.0-rc.4](https://github.com/waves-enterprise/js-contract-sdk/compare/@wavesenterprise/contract-core@1.1.0-rc.3...@wavesenterprise/contract-core@1.1.0-rc.4) (2022-11-09) 87 | 88 | **Note:** Version bump only for package @wavesenterprise/contract-core 89 | 90 | 91 | 92 | 93 | 94 | # [1.1.0-rc.3](https://github.com/waves-enterprise/js-contract-sdk/compare/@wavesenterprise/contract-core@1.1.0-rc.2...@wavesenterprise/contract-core@1.1.0-rc.3) (2022-11-09) 95 | 96 | 97 | ### Bug Fixes 98 | 99 | * **contract-core:** contract transactions serialization ([3851675](https://github.com/waves-enterprise/js-contract-sdk/commit/3851675eceb4e0a2e3367b4d40d81c568da23d04)) 100 | 101 | 102 | ### Features 103 | 104 | * **contract-core:** add @Payments, @Ctx to action params ([9c7c3f2](https://github.com/waves-enterprise/js-contract-sdk/commit/9c7c3f2c073c23eb8081513c0c7f1e9639f82b67)) 105 | 106 | 107 | 108 | 109 | 110 | # [1.1.0-rc.2](https://github.com/waves-enterprise/js-contract-sdk/compare/@wavesenterprise/contract-core@1.1.0-rc.1...@wavesenterprise/contract-core@1.1.0-rc.2) (2022-11-09) 111 | 112 | 113 | ### Bug Fixes 114 | 115 | * **contract-core:** build fix ([25ede04](https://github.com/waves-enterprise/js-contract-sdk/commit/25ede04b4d3a9c0a4caee676a6f771fb773a318d)) 116 | 117 | 118 | 119 | 120 | 121 | # [1.1.0](https://github.com/waves-enterprise/js-contract-sdk/compare/@wavesenterprise/contract-core@1.0.15...@wavesenterprise/contract-core@1.1.0) (2022-11-08) 122 | 123 | 124 | ### Bug Fixes 125 | 126 | * **contract-core:** add preload helper ([02d20bc](https://github.com/waves-enterprise/js-contract-sdk/commit/02d20bcd4c0ad6c95ba1e5eab6e8e6472899173b)) 127 | * **contract-core:** edit folder structure ([509a028](https://github.com/waves-enterprise/js-contract-sdk/commit/509a0289cad9399651f320a5bd50c46102464078)) 128 | * **contract-core:** fix params extractor ([0ea803a](https://github.com/waves-enterprise/js-contract-sdk/commit/0ea803a4ab09467384475f7815d8550ada6c1c2e)) 129 | * **contract-core:** handle entry cache ([34d4f26](https://github.com/waves-enterprise/js-contract-sdk/commit/34d4f26b925b4a69c72a60498ed4fc238c8e8499)) 130 | 131 | 132 | 133 | 134 | 135 | ## [1.0.5](https://github.com/waves-enterprise/js-contract-sdk/compare/@wavesenterprise/contract-core@1.0.2...@wavesenterprise/contract-core@1.0.5) (2022-07-26) 136 | 137 | 138 | ### Bug Fixes 139 | 140 | * **contract-core:** resolver not found error ([755f691](https://github.com/waves-enterprise/js-contract-sdk/commit/755f6916126bcf23efafc345b755e1c833d1f69c)) 141 | 142 | 143 | 144 | 145 | 146 | ## [1.0.3](https://github.com/waves-enterprise/js-contract-sdk/compare/@wavesenterprise/contract-core@1.0.2...@wavesenterprise/contract-core@1.0.3) (2022-07-26) 147 | 148 | 149 | ### Bug Fixes 150 | 151 | * **contract-core:** resolver not found error ([755f691](https://github.com/waves-enterprise/js-contract-sdk/commit/755f6916126bcf23efafc345b755e1c833d1f69c)) 152 | -------------------------------------------------------------------------------- /packages/contract-core/LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2022 Waves Enterprise 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /packages/contract-core/README.md: -------------------------------------------------------------------------------- 1 | # @wavesenterpise/contract-core 2 | 3 | Implements JS Contract SDK core functionality, rpc services, tools and utilities. 4 | 5 | ### Getting Started 6 | 7 | Run following in command line: 8 | 9 | ```bash 10 | npm i @wavesenterpise/contract-core 11 | ``` 12 | 13 | Create `contract.ts` as follows 14 | 15 | ```ts 16 | import { 17 | Action, 18 | Contract, 19 | ContractMapping, 20 | ContractValue, 21 | IncomingTx, 22 | JsonVar, 23 | logger, 24 | Param, 25 | Params, 26 | Tx, 27 | Var, 28 | Block, 29 | Sender, 30 | BlockInfo, 31 | } from '@wavesenterprise/contract-core' 32 | import Long from 'long' 33 | 34 | @Contract() 35 | export default class #{contractName} { 36 | 37 | log = logger(this) 38 | 39 | @Var() 40 | counter!: ContractValue 41 | 42 | @JsonVar() 43 | participants!: ContractMapping 44 | 45 | @Action({ onInit: true }) 46 | init(@Params() params: Record) { 47 | this.counter.set(0) 48 | this.log.info('all params', params) 49 | } 50 | 51 | @Action({ preload: ['counter'] }) 52 | async increment(@Tx() tx: IncomingTx, @Param('by') by: Long) { 53 | const { senderPublicKey, sender } = tx 54 | const counter = await this.counter.get() 55 | let participant = await this.participants.tryGet(senderPublicKey) 56 | if (!participant) { 57 | participant = { 58 | publicKey: senderPublicKey, 59 | address: sender, 60 | amount: 0, 61 | } 62 | } 63 | participant.amount += by.toNumber() 64 | this.counter.set(counter + by.toNumber()) 65 | this.participants.set(senderPublicKey, participant) 66 | } 67 | 68 | async saveInfo(@Sender() sender: string, @Block() currentBlock: BlockInfo) { 69 | // 70 | } 71 | } 72 | ``` 73 | 74 | To initialize contract use: 75 | ```ts 76 | import { initContract } from '@wavesenterprise/contract-core' 77 | 78 | initContract({ 79 | contractPath: __dirname + '/contract.js', // path to compiled contract 80 | concurrencyLevel: 1, 81 | }) 82 | ``` 83 | 84 | ## License 85 | 86 | This project is licensed under the [MIT License](LICENSE). 87 | 88 | -------------------------------------------------------------------------------- /packages/contract-core/features/typed-state.md: -------------------------------------------------------------------------------- 1 | ## Typed State 2 | `@Var` decorates any contract entry 3 | 4 | ```typescript 5 | @Contract() 6 | class Example { 7 | @Var({ mutable: false }) adminKey: TVar; 8 | @Var({ name: 'k_quoteAssetWeight'}) quoteWeight: TVar; 9 | @Var({ name: 'k_baseAssetWeight'}) baseWeight: TVar; 10 | 11 | @Action setAdminKey( 12 | @Param('adminKey') adminKey: string 13 | ) { 14 | this.adminKey.set(adminKey) 15 | } 16 | 17 | @Action setQuoteWeight( 18 | @Param('adminKey') adminKey: string 19 | ) { 20 | this.quoteWeight.set(100000000)// map to `k_quoteAssetWeight` key 21 | this.quoteWeight.set(900000000)// map to `k_baseAssetWeight` key 22 | } 23 | } 24 | ``` 25 | By default `@Var` uses property name to assign key of data entry, to specify 26 | entry key pass `name` param. 27 | 28 | Mutablility of entries defines by param `mutable` (true by default), 29 | If specify `mutable: false`, change of this property is allows only in CreateContractTransaction (103) with Action annotated with `@Action({onInit: true})`. 30 | 31 | Example: 32 | 33 | ```typescript 34 | import {Contract, TVar} from "@wavesenterprise/contract-core"; 35 | 36 | @Contract 37 | class EntriesExample { 38 | @Var({ mutable: false }) 39 | todaysmood: TVar 40 | 41 | @Action({onInit: true}) 42 | _contstructor() { 43 | 44 | // Correct. Sets value 45 | this.constantValue.set('Everything ok!'); 46 | } 47 | 48 | @Action 49 | makeSad() { 50 | // Throws Non Retryable ContractError 51 | this.constantValue.set('Sad :('); 52 | } 53 | } 54 | ``` 55 | 56 | ### Preload 57 | 58 | Node uses RPC calls on every read entry, make rpc call sometimes may cause performance issues. 59 | For perfomance issues use method ```preload```, 60 | to batch preload contract entries. 61 | 62 | Usage: 63 | 64 | ```typescript 65 | import {Contract, preload, TVar} from "@wavesenterprise/contract-core"; 66 | 67 | @Contract 68 | class EntriesExample { 69 | @Var() angryFriend: TVar 70 | @Var() funnyFriend: TVar 71 | 72 | @Action 73 | async makeSad() { 74 | await preload(this, ['angryFriend', 'funnyFriend']); 75 | 76 | // uses cached values by tx context, doesn't make request on every call 77 | const funnyFriend = await this.funnyFriend.get(); 78 | const angryFriend = await this.angryFriend.get(); 79 | } 80 | } 81 | ``` 82 | -------------------------------------------------------------------------------- /packages/contract-core/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | testPathIgnorePatterns: ['/dist/'], 6 | globals: { 7 | 'ts-jest': { 8 | tsconfig: 'tsconfig.spec.json', 9 | }, 10 | }, 11 | } -------------------------------------------------------------------------------- /packages/contract-core/migration-guide.md: -------------------------------------------------------------------------------- 1 | ## Migration guide 1.1.0 2 | 3 | ### Key Changes 4 | 5 | **Typed State**: from @wavesenterprise/contract-core@1.1.0 use _@Var_ decorator for entries management 6 | 7 | Before: 8 | ```typescript 9 | import {State, ContractState, Action, Param} from "@wavesenterprise/contract-core"; 10 | 11 | class Example{ 12 | @State() state: ContractState; 13 | 14 | @Action() 15 | setCoordinator( 16 | @Param('coordinator') coordinator: string 17 | ) { 18 | this.state.set('coordinator', coordinator) 19 | } 20 | } 21 | ``` 22 | 23 | After: 24 | ```typescript 25 | import {TVar, Var, Action} from "@wavesenterprise/contract-core"; 26 | 27 | class Example { 28 | @Var() coordinator: TVar; 29 | 30 | @Action() 31 | setCoordinator( 32 | @Param('coordinator') coordinator: string 33 | ) { 34 | this.coordinator.set(coordinator) 35 | } 36 | } 37 | ``` 38 | 39 | **API changes**: 40 | 41 | Replace `start(contractPath)` with `initContract(config)` 42 | 43 | Contract Config 44 | ``` 45 | export type ContractConfig = { 46 | contractPath: string, 47 | concurrencyLevel?: number, // cpu's count - 1 by default 48 | } 49 | ``` 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /packages/contract-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wavesenterprise/contract-core", 3 | "version": "1.2.3", 4 | "description": "Implements JS Contract SDK core functionality, rpc services, tools and utilities.", 5 | "type": "commonjs", 6 | "main": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "private": false, 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "scripts": { 13 | "build": "tsc -p tsconfig.json", 14 | "build:watch": "tsc -p tsconfig.json --watch", 15 | "start": "node dist/src/index.js", 16 | "typecheck": "npx tsc --noEmit", 17 | "dev": "tsc -w -p tsconfig.json", 18 | "lint": "eslint src/**/*.ts --quiet --fix", 19 | "test": "jest", 20 | "prepublish": "npm run build" 21 | }, 22 | "author": { 23 | "name": "Timofey Semenyuk", 24 | "email": "rustfy@gmail.com", 25 | "url": "https://github.com/stfy" 26 | }, 27 | "license": "MIT", 28 | "dependencies": { 29 | "@grpc/grpc-js": "^1.8.0", 30 | "@wavesenterprise/js-contract-grpc-client": "1.12.3", 31 | "@wavesenterprise/we-node-grpc-api": "1.1.1", 32 | "google-protobuf": "^3.21.0", 33 | "long": "^5.2.1", 34 | "reflect-metadata": "^0.1.13" 35 | }, 36 | "devDependencies": { 37 | "@wavesenterprise/signer": "^1.0.3", 38 | "@wavesenterprise/crypto-utils": "^1.0.1", 39 | "@types/bn.js": "^5.1.1", 40 | "@types/jest": "^28.1.8", 41 | "jest": "^28.1.3", 42 | "lerna": "^4.0.0", 43 | "ts-jest": "^28.0.8", 44 | "typescript": "^4.6.2" 45 | }, 46 | "lint-staged": { 47 | "*.{js,ts}": [ 48 | "eslint --fix" 49 | ] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/contract-core/src/api/assets/asset.ts: -------------------------------------------------------------------------------- 1 | import { AssetsStorage } from './assets-storage' 2 | import { 3 | ContractBurn, 4 | ContractCancelLease, 5 | ContractIssue, 6 | ContractLease, 7 | ContractReissue, 8 | ContractTransferOut, 9 | } from '@wavesenterprise/we-node-grpc-api' 10 | import { Base58 } from '../../utils/base58' 11 | import Long from 'long' 12 | import { IAddressService, IContractService } from '../../grpc/grpc-client' 13 | 14 | export type Balance = { 15 | assetId: string, 16 | amount: Long, 17 | decimals: number, 18 | } 19 | 20 | export type AssetConfig = { 21 | assetId: string | undefined, 22 | nonce?: number, 23 | } 24 | 25 | export type AssetIssue = Omit 26 | export type AssetReissue = Omit 27 | export type AssetBurn = Omit 28 | export type AssetTransfer = Omit 29 | export type Lease = Omit 30 | export type CancelLease = ContractCancelLease 31 | 32 | export class Asset { 33 | constructor( 34 | private readonly config: AssetConfig, 35 | private readonly storage: AssetsStorage, 36 | private readonly addressService: IAddressService, 37 | private readonly contractService: IContractService, 38 | ) { 39 | } 40 | 41 | async getBalanceOf(address?: string): Promise { 42 | if (address) { 43 | const balance = await this.addressService.getAssetBalance({ 44 | assetId: Base58.decode(this.config.assetId), 45 | address: Base58.decode(address), 46 | }) 47 | return { 48 | ...balance, 49 | assetId: Base58.encode(balance.assetId), 50 | } 51 | } else { 52 | const [balance] = await this.contractService.getContractBalances({ 53 | assetsIds: [this.config.assetId ?? ''], 54 | }) 55 | return balance 56 | } 57 | } 58 | 59 | getId() { 60 | return this.config.assetId 61 | } 62 | 63 | issue(config: AssetIssue) { 64 | this.storage.addIssue({ 65 | ...config, 66 | assetId: this.config.assetId, 67 | nonce: this.config.nonce!, 68 | }) 69 | } 70 | 71 | reissue(config: AssetReissue) { 72 | this.storage.addReissue({ 73 | ...config, 74 | assetId: this.config.assetId, 75 | }) 76 | } 77 | 78 | burn(config: AssetBurn) { 79 | this.storage.addBurn({ 80 | ...config, 81 | assetId: this.config.assetId, 82 | }) 83 | } 84 | 85 | transfer(config: AssetTransfer) { 86 | this.storage.addTransfer({ 87 | ...config, 88 | assetId: this.config.assetId, 89 | }) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /packages/contract-core/src/api/assets/assets-service.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext } from '../../execution' 2 | import { 3 | Asset, 4 | AssetBurn, 5 | AssetConfig, 6 | AssetIssue, 7 | AssetReissue, 8 | AssetTransfer, 9 | Balance, 10 | CancelLease, 11 | Lease, 12 | } from './asset' 13 | 14 | export class AssetsService { 15 | private leaseNonce = 1 16 | private issueNonce = 1 17 | 18 | constructor(private readonly context: ExecutionContext) { 19 | } 20 | 21 | calculateAssetId() { 22 | return this.context.grpcClient.contractService.calculateAssetId({ 23 | nonce: this.issueNonce, 24 | }) 25 | } 26 | 27 | async calculateLeaseId(): Promise<{ leaseId: string, nonce: number }> { 28 | const leaseId = await this.context.grpcClient.contractService.calculateAssetId({ 29 | nonce: this.leaseNonce, 30 | }) 31 | 32 | const nonce = this.leaseNonce 33 | this.leaseNonce++ 34 | 35 | return { leaseId, nonce } 36 | } 37 | 38 | private makeAsset(config: AssetConfig) { 39 | return new Asset( 40 | config, 41 | this.context.assets, 42 | this.context.grpcClient.contractAddressService, 43 | this.context.grpcClient.contractService, 44 | ) 45 | } 46 | 47 | getAsset(assetId?: string | undefined) { 48 | return this.makeAsset({ 49 | assetId, 50 | }) 51 | } 52 | 53 | async issueAsset(config: AssetIssue) { 54 | const assetId = await this.calculateAssetId() 55 | const asset = this.makeAsset({ assetId, nonce: this.issueNonce }) 56 | this.issueNonce++ 57 | asset.issue(config) 58 | return asset 59 | } 60 | 61 | reissueAsset(assetId: string | undefined, config: AssetReissue) { 62 | return this.getAsset(assetId).reissue(config) 63 | } 64 | 65 | burnAsset(assetId: string | undefined, config: AssetBurn) { 66 | return this.getAsset(assetId).burn(config) 67 | } 68 | 69 | transferAsset(assetId: string | undefined, config: AssetTransfer) { 70 | return this.getAsset(assetId).transfer(config) 71 | } 72 | 73 | getAssetBalance(assetId: string | undefined, address?: string) { 74 | return this.getAsset(assetId).getBalanceOf(address) 75 | } 76 | 77 | getBatchAssetBalances(assetsIds: string[]): Promise { 78 | return this.context.grpcClient.contractService.getContractBalances({ 79 | assetsIds, 80 | }) 81 | } 82 | 83 | async lease(lease: Lease): Promise { 84 | const { leaseId, nonce } = await this.calculateLeaseId() 85 | 86 | this.context.assets.addLease({ leaseId, nonce, ...lease }) 87 | 88 | return leaseId 89 | } 90 | 91 | leaseCancel(lease: CancelLease) { 92 | return this.context.assets.addCancelLease(lease) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /packages/contract-core/src/api/assets/assets-storage.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ContractAssetOperation, 3 | ContractBurn, 4 | ContractCancelLease, 5 | ContractIssue, 6 | ContractLease, 7 | ContractReissue, 8 | ContractTransferOut, 9 | } from '@wavesenterprise/we-node-grpc-api' 10 | 11 | export class AssetsStorage { 12 | 13 | private readonly operations: ContractAssetOperation[] = [] 14 | 15 | addIssue(operation: ContractIssue) { 16 | this.operations.push({ 17 | contractIssue: operation, 18 | }) 19 | } 20 | 21 | addReissue(operation: ContractReissue) { 22 | this.operations.push({ 23 | contractReissue: operation, 24 | }) 25 | } 26 | 27 | addBurn(operation: ContractBurn) { 28 | this.operations.push({ 29 | contractBurn: operation, 30 | }) 31 | } 32 | 33 | addTransfer(operation: ContractTransferOut) { 34 | this.operations.push({ 35 | contractTransferOut: operation, 36 | }) 37 | } 38 | 39 | addLease(operation: ContractLease) { 40 | this.operations.push({ 41 | contractLease: operation, 42 | }) 43 | } 44 | 45 | addCancelLease(operation: ContractCancelLease) { 46 | this.operations.push({ 47 | contractCancelLease: operation, 48 | }) 49 | } 50 | 51 | getOperations() { 52 | return this.operations 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/contract-core/src/api/assets/index.ts: -------------------------------------------------------------------------------- 1 | export * from './asset' 2 | export * from './assets-service' 3 | export * from './assets-storage' 4 | -------------------------------------------------------------------------------- /packages/contract-core/src/api/container.ts: -------------------------------------------------------------------------------- 1 | import { Constructable } from '../intefaces/helpers' 2 | 3 | export class InjectionToken { 4 | constructor(public tokenString: string) { 5 | } 6 | } 7 | 8 | const PROVIDER = 'sc:provided' 9 | 10 | let counter = 0 11 | 12 | export class Container { 13 | private static container = new Map() 14 | 15 | static set(token: InjectionToken, instance: unknown): void 16 | static set(instance: unknown): void 17 | static set(...args: any[]) { 18 | if (args.length === 2) { 19 | const [token, instance] = args 20 | 21 | this.container.set((token as InjectionToken).tokenString, instance) 22 | } else { 23 | const instance = args[0] 24 | 25 | const instanceKey = 'provided-' + counter 26 | 27 | counter++ 28 | 29 | Reflect.defineMetadata(PROVIDER, instanceKey, instance.constructor) 30 | 31 | this.container.set(instanceKey, instance) 32 | } 33 | } 34 | 35 | static get

(token: InjectionToken): P 36 | static get(token: Constructable): T 37 | static get(token: unknown): unknown { 38 | if (token instanceof InjectionToken) { 39 | return this.container.get(token.tokenString) as unknown 40 | } 41 | 42 | const constructor = token as Constructable 43 | 44 | const instanceKey = Reflect.getMetadata(PROVIDER, constructor) 45 | 46 | return this.container.get(instanceKey) as unknown 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/contract-core/src/api/contants.ts: -------------------------------------------------------------------------------- 1 | export const CONTRACT_PRELOADED_ENTRIES = 'we:preloaded_entries' 2 | export const VAR_META = 'we:var:meta' 3 | export const CONTRACT_VARS = 'we:contract:vars' 4 | 5 | 6 | export const ACTION_METADATA = 'we:contract:actions' 7 | export const ARGS_METADATA = 'we:contract:args' 8 | export const ALL_PARAMS_KEY = '__all' 9 | 10 | export enum ReservedParamNames { 11 | action = 'action', 12 | } 13 | 14 | export enum TxId { 15 | call = 104, 16 | create = 103, 17 | } 18 | -------------------------------------------------------------------------------- /packages/contract-core/src/api/decorators/action.ts: -------------------------------------------------------------------------------- 1 | import { ACTION_METADATA } from '../contants' 2 | import { Constructable } from '../../intefaces/helpers' 3 | import { TContractActionMetadata, TContractActionsMetadata } from '../meta' 4 | 5 | type TContractActionOptions = { 6 | name?: string, 7 | preload?: string[], 8 | onInit?: boolean, 9 | } 10 | 11 | const defaultContractOptions: TContractActionOptions = { 12 | onInit: false, 13 | } 14 | 15 | export function Action(target: object, propertyName: string | symbol, descriptor): void 16 | export function Action(options?: TContractActionOptions): MethodDecorator 17 | 18 | export function Action(...args): MethodDecorator | void { 19 | if (arguments.length > 1) { 20 | decorateMethod(args[0], args[1], args[2]) 21 | 22 | return 23 | } 24 | 25 | const config = args[0] 26 | 27 | return function (target: Constructable, propertyName: string | symbol, descriptor): void { 28 | decorateMethod(target, propertyName, descriptor, config) 29 | } 30 | } 31 | 32 | const decorateMethod = ( 33 | target: Constructable, 34 | propertyName: string | symbol, 35 | descriptor, 36 | options: TContractActionOptions = defaultContractOptions, 37 | ) => { 38 | let actionsMetadata: TContractActionsMetadata = Reflect.getMetadata(ACTION_METADATA, target.constructor) 39 | 40 | if (!actionsMetadata) { 41 | actionsMetadata = { actions: {} as Record } as TContractActionsMetadata 42 | } 43 | 44 | const actionName = options?.name ?? (propertyName as string) 45 | const actionMetadata = { 46 | name: options?.name ? options.name : (propertyName as string), 47 | propertyName: propertyName as string, 48 | params: Reflect.getMetadata('design:paramtypes', target, propertyName as string), 49 | preload: options.preload, 50 | } 51 | 52 | if (options.onInit) { 53 | actionsMetadata.initializer = actionMetadata 54 | } else { 55 | actionsMetadata.actions[actionName] = actionMetadata 56 | } 57 | 58 | 59 | Reflect.defineMetadata(ACTION_METADATA, actionsMetadata, target.constructor) 60 | } 61 | -------------------------------------------------------------------------------- /packages/contract-core/src/api/decorators/common.ts: -------------------------------------------------------------------------------- 1 | import { ContractState } from '../state' 2 | import { Container } from '../container' 3 | import { BlockInfo, ExecutionContext, IncomingTx, TransferIn } from '../../execution' 4 | 5 | export function getExecutionContext(): ExecutionContext { 6 | return Container.get(ExecutionContext) 7 | } 8 | 9 | export function getTx(): IncomingTx { 10 | return getExecutionContext().tx 11 | } 12 | 13 | export function getPayments(): TransferIn[] { 14 | return getTx().payments 15 | } 16 | 17 | export function getState(): ContractState { 18 | return getExecutionContext().state 19 | } 20 | 21 | export function getSender(): string { 22 | return getTx().sender 23 | } 24 | 25 | export function getBlock(): BlockInfo { 26 | return getExecutionContext().blockInfo 27 | } 28 | 29 | export function getTime(): number { 30 | return getBlock().timestamp 31 | } 32 | -------------------------------------------------------------------------------- /packages/contract-core/src/api/decorators/contract.ts: -------------------------------------------------------------------------------- 1 | import { CONTRACT_PRELOADED_ENTRIES } from '../contants' 2 | 3 | type ContractOptions = { 4 | component: string, 5 | } 6 | 7 | export function Contract(_: ContractOptions = { component: 'default' }): ClassDecorator { 8 | return (target: unknown) => { 9 | 10 | Reflect.defineMetadata(CONTRACT_PRELOADED_ENTRIES, new Map(), target as unknown as object) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/contract-core/src/api/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export { Contract } from './contract' 2 | export { Action } from './action' 3 | export { Param, Params, Payments, Ctx, Tx } from './param' 4 | export { State } from './state' 5 | export { Var, JsonVar } from './var' 6 | export { Assets } from './services' 7 | -------------------------------------------------------------------------------- /packages/contract-core/src/api/decorators/param.ts: -------------------------------------------------------------------------------- 1 | import { ALL_PARAMS_KEY, ARGS_METADATA } from '../contants' 2 | import { TArgs } from '../meta' 3 | import { getBlock, getExecutionContext, getPayments, getSender, getTime, getTx } from './common' 4 | 5 | function assignMetadata(args: TArgs, index: number, paramKeyOrGetter: string | (() => void)): TArgs { 6 | const key = `arg:${index}` 7 | 8 | return { 9 | ...args, 10 | [key]: 11 | typeof paramKeyOrGetter === 'string' 12 | ? { 13 | index, 14 | paramKey: paramKeyOrGetter, 15 | } 16 | : { 17 | index, 18 | getter: paramKeyOrGetter, 19 | }, 20 | } 21 | } 22 | 23 | const createParamsDecorator = 24 | (paramKeyOrGetter: string | (() => void)): ParameterDecorator => 25 | (target, propertyKey, parameterIndex) => { 26 | const args: TArgs = Reflect.getMetadata(ARGS_METADATA, target.constructor, propertyKey) || {} 27 | Reflect.defineMetadata( 28 | ARGS_METADATA, 29 | assignMetadata(args, parameterIndex, paramKeyOrGetter), 30 | target.constructor, 31 | propertyKey, 32 | ) 33 | } 34 | 35 | export function Param(paramName: string) { 36 | return createParamsDecorator(paramName) 37 | } 38 | 39 | export function Params() { 40 | return createParamsDecorator(ALL_PARAMS_KEY) 41 | } 42 | 43 | export function Payments() { 44 | return createParamsDecorator(getPayments) 45 | } 46 | 47 | export function Ctx() { 48 | return createParamsDecorator(getExecutionContext) 49 | } 50 | 51 | export function Tx() { 52 | return createParamsDecorator(getTx) 53 | } 54 | 55 | export function Sender() { 56 | return createParamsDecorator(getSender) 57 | } 58 | 59 | export function Block() { 60 | return createParamsDecorator(getBlock) 61 | } 62 | 63 | export function Time() { 64 | return createParamsDecorator(getTime) 65 | } 66 | -------------------------------------------------------------------------------- /packages/contract-core/src/api/decorators/services.ts: -------------------------------------------------------------------------------- 1 | import { Constructable } from '../../intefaces/helpers' 2 | import { AssetsService } from '../assets/assets-service' 3 | import { getExecutionContext } from './common' 4 | 5 | export function Assets() { 6 | return (...args: any[]) => decorateService.call(undefined, ...args) as void 7 | } 8 | 9 | export function decorateService(target: Constructable, propertyKey: string, _): void { 10 | const designType = Reflect.getMetadata('design:type', target, propertyKey) 11 | 12 | Object.defineProperty(target, propertyKey, { 13 | set: () => { 14 | throw new Error('cannot reassign typed property') 15 | }, 16 | get: () => { 17 | switch (designType) { 18 | case AssetsService: { 19 | return new AssetsService(getExecutionContext()) 20 | } 21 | default: 22 | throw Error('Unknown service') 23 | } 24 | }, 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /packages/contract-core/src/api/decorators/state.ts: -------------------------------------------------------------------------------- 1 | import { Constructable } from '../../intefaces/helpers' 2 | import { getState } from './common' 3 | 4 | 5 | export function State(): PropertyDecorator 6 | export function State(target: object, propertyKey: string): void 7 | export function State(...args: any[]) { 8 | if (args.length > 1) { 9 | return decorateState(args[0], args[1]) 10 | } 11 | 12 | return (target: Constructable, propertyKey: string) => decorateState(target, propertyKey) 13 | } 14 | 15 | const decorateState = (target: Constructable, propertyKey: string) => { 16 | Object.defineProperty(target, propertyKey, { 17 | get(): unknown { 18 | return getState() 19 | }, 20 | set(_: unknown) { 21 | throw new Error('Contract state is initialized') 22 | }, 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /packages/contract-core/src/api/decorators/var.ts: -------------------------------------------------------------------------------- 1 | import { Constructable } from '../../intefaces/helpers' 2 | import { getState } from './common' 3 | import { ContractError } from '../../execution' 4 | import { CONTRACT_VARS } from '../contants' 5 | import { TContractVarsMeta } from '../meta' 6 | import { TValue } from '../../intefaces/contract' 7 | import { ContractMapping } from '../state' 8 | import { ContractValue } from '../state/types/value' 9 | 10 | 11 | export type TVarConfig = { 12 | key: string, 13 | readonly?: boolean, 14 | contractId?: string, 15 | serialize?: (value: unknown) => TValue, 16 | deserialize?: (value: TValue) => unknown, 17 | } 18 | 19 | export function Var(target: object, propertyName: string | symbol, descriptor): void 20 | export function Var(props?: TVarConfig): PropertyDecorator 21 | export function Var(...args: any[]) { 22 | if (args.length > 1) { 23 | decorateProperty.call(undefined, ...args, { 24 | key: args[1], 25 | }) 26 | return 27 | } 28 | 29 | return (...args_: any[]): void => decorateProperty.call(undefined, ...args_, { 30 | key: args_[1], 31 | ...args[0], 32 | }) as void 33 | } 34 | 35 | export function JsonVar(target: object, propertyName: string | symbol, descriptor): void 36 | export function JsonVar(props?: Omit): PropertyDecorator 37 | export function JsonVar(...args: any[]) { 38 | if (args.length > 1) { 39 | decorateProperty.call(undefined, ...args, { 40 | key: args[1], 41 | serialize: JSON.stringify, 42 | deserialize: JSON.parse, 43 | }) 44 | return 45 | } 46 | 47 | return (...args_: any[]): void => decorateProperty.call(undefined, ...args_, { 48 | key: args_[1], 49 | serialize: JSON.stringify, 50 | deserialize: JSON.parse, 51 | ...args[0], 52 | }) as void 53 | } 54 | 55 | export function decorateProperty(target: Constructable, propertyKey: string, _, config: TVarConfig): void { 56 | const contractKey = config.key 57 | 58 | const meta: TContractVarsMeta = Reflect.getMetadata(CONTRACT_VARS, target.constructor) || {} 59 | const designType = Reflect.getMetadata('design:type', target, propertyKey) 60 | const isMapping = designType === ContractMapping 61 | 62 | if (!!meta[contractKey]) { 63 | throw new ContractError('Variable names need to be unique') 64 | } 65 | 66 | meta[contractKey] = { 67 | propertyKey, 68 | meta: config, 69 | } 70 | 71 | Reflect.defineMetadata( 72 | CONTRACT_VARS, 73 | meta, 74 | target.constructor, 75 | ) 76 | 77 | Object.defineProperty(target, propertyKey, { 78 | set: () => { 79 | throw new Error('cannot reassign typed property') 80 | }, 81 | get: () => { 82 | if (isMapping) { 83 | return new ContractMapping(getState(), config) 84 | } 85 | return new ContractValue(getState(), config) 86 | }, 87 | }) 88 | } 89 | -------------------------------------------------------------------------------- /packages/contract-core/src/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from './decorators' 2 | export * from './state' 3 | export * from './container' 4 | export * from './logger' 5 | export * from './assets' 6 | -------------------------------------------------------------------------------- /packages/contract-core/src/api/logger.ts: -------------------------------------------------------------------------------- 1 | import http from 'http' 2 | 3 | type LogLevel = 'verbose' | 'info' | 'error' 4 | 5 | const { DEBUG, HOST_NETWORK, HOSTNAME, VERBOSE_LOG, REMOTE_LOG } = process.env 6 | 7 | const request = (url: string, data: unknown) => { 8 | const body = JSON.stringify(data, (key, value) => { 9 | if (typeof value === 'object' && 'toNumber' in value) { 10 | return value.toNumber() as number 11 | } 12 | return value as unknown 13 | }) 14 | 15 | const options = { 16 | method: 'POST', 17 | headers: { 18 | 'Content-Type': 'application/json', 19 | 'Content-Length': body.length, 20 | }, 21 | } 22 | 23 | const req = http.request(url, options) 24 | 25 | req.write(body) 26 | req.end() 27 | } 28 | 29 | export const writeLog = (...message: [LogLevel, ...unknown[]]) => { 30 | if (Number(DEBUG)) { 31 | const [type, ...data] = message 32 | const prefix = HOSTNAME ? `[${(new Date()).toISOString()}] Host: ${HOSTNAME}` : `[${(new Date()).toISOString()}]` 33 | // eslint-disable-next-line no-console 34 | console[type](prefix, ...data) 35 | if (Number(REMOTE_LOG)) { 36 | const loggerAddress = `http://${HOST_NETWORK}:5050` 37 | request(loggerAddress, [type, prefix, ...data]) 38 | } 39 | } 40 | } 41 | 42 | export class Logger { 43 | 44 | component: string 45 | 46 | private static lastTimeStamp = Date.now() 47 | 48 | static workerIdx: number | string = 'Main Thread' 49 | 50 | setComponent(name: string) { 51 | this.component = name 52 | } 53 | 54 | info(...args: unknown[]) { 55 | this.printMessage('info', ...this.prefixes, ...args) 56 | } 57 | 58 | verbose(...args: unknown[]) { 59 | if (VERBOSE_LOG) { 60 | this.printMessage('info', ...this.prefixes, ...args) 61 | } 62 | } 63 | 64 | error(...args: unknown[]) { 65 | this.printMessage('info', ...this.prefixes, ...args) 66 | } 67 | 68 | private get prefixes() { 69 | return [ 70 | this.component && `[${this.component}]`, 71 | Logger.workerPrefix, 72 | Logger.timestampDiff, 73 | ] 74 | } 75 | 76 | private printMessage(logLevel: LogLevel, ...args: unknown[]) { 77 | writeLog(logLevel, ...args) 78 | } 79 | 80 | private static get timestampDiff(): string { 81 | const result = `+${Date.now() - Logger.lastTimeStamp}ms` 82 | 83 | Logger.lastTimeStamp = Date.now() 84 | 85 | return result 86 | } 87 | 88 | private static get workerPrefix() { 89 | if (typeof Logger.workerIdx === 'number') { 90 | return `Worker#${Logger.workerIdx}` 91 | } 92 | return Logger.workerIdx 93 | } 94 | 95 | } 96 | 97 | export const CommonLogger = new Logger() 98 | CommonLogger.setComponent('Common log') 99 | 100 | export function logger(c: { constructor: { name: string } }): Logger { 101 | const loggerInstance = new Logger() 102 | loggerInstance.setComponent(c.constructor.name) 103 | 104 | return loggerInstance 105 | } 106 | -------------------------------------------------------------------------------- /packages/contract-core/src/api/meta.ts: -------------------------------------------------------------------------------- 1 | export type TVarMeta = { 2 | name: string, 3 | 4 | readonly?: boolean, 5 | contractId: string, 6 | } 7 | 8 | export type TContractVarsMeta = Record }> 9 | 10 | export type TContractActionMetadata = { 11 | name: string, 12 | propertyName: string, 13 | params: unknown[], 14 | preload?: string[], 15 | } 16 | 17 | export type TContractActionsMetadata = { 18 | initializer: TContractActionMetadata, 19 | actions: Record, 20 | } 21 | 22 | export type TArgs = { 23 | [key: string]: { 24 | index: number, 25 | paramKey?: string, 26 | getter?: () => unknown, 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /packages/contract-core/src/api/state/contract-state.ts: -------------------------------------------------------------------------------- 1 | import { DataEntry } from '@wavesenterprise/js-contract-grpc-client/data_entry' 2 | import { TVal, TValue } from '../../intefaces/contract' 3 | import { Storage } from './storage' 4 | import { ExecutionContext } from '../../execution' 5 | import { Optional } from '../../intefaces/helpers' 6 | import { getValueStateKey } from '../../utils' 7 | import { logger } from '../logger' 8 | 9 | export class ContractState { 10 | 11 | private log = logger(this) 12 | 13 | readonly storage: Storage 14 | 15 | constructor(context: ExecutionContext) { 16 | this.storage = new Storage(context.contractId, context.grpcClient.contractService) 17 | } 18 | 19 | get(key: string, contractId?: string): Promise { 20 | return this.storage.read(key, contractId) as Promise 21 | } 22 | 23 | async tryGet(key: string, contractId?: string): Promise> { 24 | try { 25 | return await this.get(key, contractId) as T 26 | } catch (e) { 27 | if ('metadata' in e) { 28 | const [errorCode] = e.metadata.get('error-code') 29 | if (Number(errorCode) === 304) { 30 | return undefined 31 | } 32 | } 33 | throw e 34 | } 35 | } 36 | 37 | getAll(contractId?: string) { 38 | return this.storage.readAll(contractId) 39 | } 40 | 41 | getBatch(keys: string[], contractId?: string) { 42 | return this.storage.readBatch(keys, contractId) 43 | } 44 | 45 | set(key: string, value: TVal) { 46 | this.storage.set(key, value) 47 | } 48 | 49 | getUpdatedEntries(): DataEntry[] { 50 | const entries: DataEntry[] = [] 51 | const updated = Object.entries(this.storage.getUpdates()) 52 | 53 | for (const [key, value] of updated) { 54 | const valueStateKey = getValueStateKey(value) 55 | 56 | entries.push(DataEntry.fromPartial({ key: String(key), [valueStateKey]: value })) 57 | } 58 | 59 | return entries 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /packages/contract-core/src/api/state/index.ts: -------------------------------------------------------------------------------- 1 | export { Storage } from './storage' 2 | export { ContractState } from './contract-state' 3 | export { ContractMapping } from './types/mapping' 4 | export { ContractValue } from './types/value' 5 | export { preload } from './preload' 6 | -------------------------------------------------------------------------------- /packages/contract-core/src/api/state/preload.ts: -------------------------------------------------------------------------------- 1 | import { getContractVarsMetadata } from '../../execution/reflect' 2 | import { getState } from '../decorators/common' 3 | 4 | function getPreloadKeys(contract: object, keys: Array) { 5 | const contractVars = getContractVarsMetadata(contract.constructor) 6 | const keysMap = new Map() 7 | for (const contractVarsKey in contractVars) { 8 | const { propertyKey, meta } = contractVars[contractVarsKey] 9 | keysMap.set(propertyKey, { 10 | contractKey: contractVarsKey, 11 | contractId: meta.contractId, 12 | }) 13 | } 14 | return keys 15 | .filter((key) => keysMap.has(Array.isArray(key) ? key[0] : key)) 16 | .map((key) => { 17 | if (Array.isArray(key)) { 18 | const keyData = keysMap.get(key[0])! 19 | return { 20 | contractKey: keyData.contractKey + '_' + key[1], 21 | contractId: keyData.contractId, 22 | } 23 | } 24 | return keysMap.get(key)! 25 | }) 26 | } 27 | 28 | export async function preload(contract: T, keys: Array): Promise { 29 | const preloadVars = getPreloadKeys(contract, keys as string[]) 30 | if (preloadVars.length > 0) { 31 | const contractGroups = new Map() 32 | for (const keyData of preloadVars) { 33 | const { contractId, contractKey } = keyData 34 | if (!contractGroups.has(contractId)) { 35 | contractGroups.set(contractId, []) 36 | } 37 | contractGroups.get(contractId)!.push(contractKey) 38 | } 39 | await Promise.all(Array.from(contractGroups.entries()).map(async ([contractId, keys]) => { 40 | await getState().getBatch(keys, contractId) 41 | })) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/contract-core/src/api/state/storage.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '../' 2 | import { TVal } from '../../intefaces/contract' 3 | import { _parseDataEntry } from '../../utils' 4 | import { IContractService } from '../../grpc/grpc-client' 5 | 6 | export class Storage { 7 | 8 | log = logger(this) 9 | 10 | private readonly cache = new Map() 11 | 12 | private readonly changedKeys = new Set() 13 | 14 | constructor( 15 | private readonly contractId: string, 16 | private readonly client: IContractService, 17 | ) { 18 | } 19 | 20 | private composeCacheKey = (contractId: string, key: string) => { 21 | return `${contractId}:${key}` 22 | } 23 | 24 | async read(key: string, contractId?: string) { 25 | const actualContractId = contractId ?? this.contractId 26 | const cacheKey = this.composeCacheKey(actualContractId, key) 27 | this.log.verbose(`Requested key ${cacheKey}, in cache`, this.cache.has(cacheKey)) 28 | if (!this.cache.has(cacheKey)) { 29 | const res = await this.client.getContractKey({ 30 | contractId: actualContractId, 31 | key, 32 | }) 33 | 34 | if (res) { 35 | this.cache.set(cacheKey, _parseDataEntry(res)) 36 | } 37 | 38 | } 39 | return this.cache.get(cacheKey)! 40 | } 41 | 42 | async readAll(contractId?: string): Promise> { 43 | const actualContractId = contractId ?? this.contractId 44 | const res = await this.client.getContractKeys({ 45 | contractId: actualContractId, 46 | }) 47 | const loaded: Record = {} 48 | res.forEach((entry) => { 49 | const parsedValue = _parseDataEntry(entry) 50 | loaded[entry.key] = parsedValue 51 | }) 52 | return loaded 53 | } 54 | 55 | async readBatch(keys: string[], contractId?: string): Promise> { 56 | const actualContractId = contractId ?? this.contractId 57 | const cacheKeys = keys.map((key) => this.composeCacheKey(actualContractId, key)) 58 | this.log.verbose(`Requested keys ${JSON.stringify(cacheKeys)}`) 59 | const cached: Record = {} 60 | const missingKeys = keys.filter((key) => { 61 | const cacheKey = this.composeCacheKey(actualContractId, key) 62 | if (this.cache.has(cacheKey)) { 63 | cached[cacheKey] = this.cache.get(cacheKey)! 64 | return false 65 | } 66 | return true 67 | }) 68 | this.log.verbose(`Cached keys ${JSON.stringify(Object.keys(cached))}`) 69 | 70 | const loaded: Record = {} 71 | this.log.verbose(`Missing keys are ${JSON.stringify(missingKeys)}`) 72 | if (missingKeys.length > 0) { 73 | const res = await this.client.getContractKeys({ 74 | contractId: actualContractId, 75 | keysFilter: { 76 | keys: missingKeys, 77 | }, 78 | }) 79 | res.forEach((entry) => { 80 | const parsedValue = _parseDataEntry(entry) 81 | this.cache.set(this.composeCacheKey(actualContractId, entry.key), parsedValue) 82 | loaded[entry.key] = parsedValue 83 | }) 84 | this.log.verbose(`Loaded keys ${JSON.stringify(Object.keys(loaded))}`) 85 | } 86 | 87 | return { 88 | ...cached, 89 | ...loaded, 90 | } 91 | } 92 | 93 | set(key: string, value: TVal): void { 94 | // modification of external contracts state is prohibited 95 | this.changedKeys.add(key) 96 | this.cache.set(this.composeCacheKey(this.contractId, key), value) 97 | } 98 | 99 | delete(key: string) { 100 | this.cache.delete(key) 101 | } 102 | 103 | getUpdates(): Record { 104 | return Object.fromEntries( 105 | Array.from(this.changedKeys.values()) 106 | .map((changedKey) => { 107 | return [changedKey, this.cache.get(this.composeCacheKey(this.contractId, changedKey))!] 108 | }), 109 | ) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /packages/contract-core/src/api/state/types/mapping.ts: -------------------------------------------------------------------------------- 1 | import { TValue } from '../../../intefaces/contract' 2 | import { Optional } from '../../../intefaces/helpers' 3 | import { ContractState } from '../contract-state' 4 | import { TVarConfig } from '../../decorators/var' 5 | import { ContractError } from '../../../execution' 6 | 7 | export class ContractMapping { 8 | 9 | constructor( 10 | protected readonly state: ContractState, 11 | protected readonly config: TVarConfig, 12 | ) { 13 | } 14 | 15 | protected deserialize(value: TValue): Deserialized { 16 | if (this.config.deserialize) { 17 | return this.config.deserialize(value) as Deserialized 18 | } 19 | return value as unknown as Deserialized 20 | } 21 | 22 | protected serialize(value: Deserialized): TValue { 23 | if (this.config.serialize) { 24 | return this.config.serialize(value) as TValue 25 | } 26 | return value as unknown as TValue 27 | } 28 | 29 | protected composeKey(key: string) { 30 | if (this.config.key) { 31 | return `${this.config.key}_${key}` 32 | } 33 | return key 34 | } 35 | 36 | tryGet(key: string): Promise> { 37 | return this.state.tryGet(this.composeKey(key)) 38 | .then((value) => value === undefined ? value : this.deserialize(value)) 39 | } 40 | 41 | get(key: string): Promise { 42 | return this.state.get(this.composeKey(key)) 43 | .then((value) => this.deserialize(value)) 44 | } 45 | 46 | set(key: string, value: Deserialized) { 47 | const composedKey = this.composeKey(key) 48 | if (this.config.contractId) { 49 | throw new ContractError(`Attempt to set external contract value of ${composedKey}`) 50 | } 51 | if (this.config.readonly) { 52 | throw new ContractError(`Attempt to set readonly value of key ${composedKey}`) 53 | } 54 | this.state.set(composedKey, this.serialize(value)) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/contract-core/src/api/state/types/value.ts: -------------------------------------------------------------------------------- 1 | import { ContractState } from '../contract-state' 2 | import { TVarConfig } from '../../decorators/var' 3 | import { TValue } from '../../../intefaces/contract' 4 | import { ContractError } from '../../../execution' 5 | 6 | export class ContractValue { 7 | 8 | constructor( 9 | private readonly state: ContractState, 10 | private readonly config: TVarConfig, 11 | ) { 12 | } 13 | 14 | protected deserialize(value: TValue): Deserialized { 15 | if (this.config.deserialize) { 16 | return this.config.deserialize(value) as Deserialized 17 | } 18 | return value as unknown as Deserialized 19 | } 20 | 21 | protected serialize(value: Deserialized): TValue { 22 | if (this.config.serialize) { 23 | return this.config.serialize(value) as TValue 24 | } 25 | return value as unknown as TValue 26 | } 27 | 28 | get(): Promise { 29 | return this.state.get(this.config.key, this.config.contractId) 30 | .then((value) => this.deserialize(value)) 31 | } 32 | 33 | set(value: Deserialized) { 34 | if (this.config.contractId) { 35 | throw new ContractError(`Attempt to set external contract value of ${this.config.key}`) 36 | } 37 | if (this.config.readonly) { 38 | throw new ContractError(`Attempt to set readonly value of key ${this.config.key}`) 39 | } 40 | this.state.set(this.config.key, this.serialize(value)) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/contract-core/src/entrypoint.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { ContractConfig, ContractService } from './execution/contract-service' 3 | 4 | function bindProcessHandlers() { 5 | process.on('SIGINT', () => { 6 | try { 7 | console.log('Graceful shutdown') 8 | process.exit(0) 9 | } catch (err) { 10 | console.log(`Graceful shutdown failure: ${err.message}`) 11 | process.exit(1) 12 | } 13 | }) 14 | } 15 | 16 | export function initContract(cfg: ContractConfig) { 17 | bindProcessHandlers() 18 | 19 | const contractService = new ContractService(cfg) 20 | 21 | try { 22 | contractService.start() 23 | } catch (err) { 24 | console.error(err) 25 | process.exit(1) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/contract-core/src/execution/constants.ts: -------------------------------------------------------------------------------- 1 | export enum ApiErrors { 2 | DataKeyNotExists = 304, 3 | } 4 | 5 | export const ERROR_CODE = { 6 | FATAL: 0, 7 | RECOVERABLE: 1, 8 | } 9 | 10 | export const ACTION_METADATA = 'we:contract:actions' 11 | export const ARGS_METADATA = 'we:contract:args' 12 | 13 | export enum ReservedParamNames { 14 | action = 'action', 15 | } 16 | 17 | export enum TxId { 18 | call = 104, 19 | create = 103, 20 | } 21 | -------------------------------------------------------------------------------- /packages/contract-core/src/execution/contract-processor.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext } from './execution-context' 2 | import { ERROR_CODE } from './constants' 3 | import { Container, logger, preload } from '../api' 4 | import { ParamsExtractor } from './params-extractor' 5 | import { setContractEntries } from './reflect' 6 | import { ContractError } from './exceptions' 7 | import { IGrpcClient } from '../grpc/grpc-client' 8 | import { ContractTransactionResponse } from '@wavesenterprise/we-node-grpc-api' 9 | import { ContractAssetOperation } from '@wavesenterprise/js-contract-grpc-client/contract_asset_operation' 10 | 11 | export function clearPreloadedEntries(contract: object): void { 12 | return setContractEntries(contract, new Map()) 13 | } 14 | 15 | export class ContractProcessor { 16 | logger = logger(this) 17 | 18 | private readonly paramsExtractor = new ParamsExtractor() 19 | 20 | constructor( 21 | private readonly contract: unknown, 22 | protected readonly grpcClient: IGrpcClient, 23 | ) { 24 | } 25 | 26 | async handleIncomingTx(resp: ContractTransactionResponse): Promise { 27 | this.grpcClient.setMetadata({ 28 | authorization: resp.authToken, 29 | }) 30 | const executionContext = new ExecutionContext(resp, this.grpcClient) 31 | 32 | 33 | Container.set(executionContext) 34 | 35 | try { 36 | const { args, actionMetadata } = this.paramsExtractor 37 | .extract(this.contract as ObjectConstructor, executionContext) 38 | 39 | const c = this.contract as ObjectConstructor 40 | const contractInstance = new c() 41 | clearPreloadedEntries(contractInstance) 42 | 43 | if (actionMetadata.preload) { 44 | await preload(contractInstance, actionMetadata.preload as keyof object) 45 | } 46 | await contractInstance[actionMetadata.propertyName](...args) 47 | 48 | return this.tryCommitSuccess(executionContext) 49 | } catch (e) { 50 | return this.tryCommitError(executionContext, e) 51 | } 52 | } 53 | 54 | async tryCommitSuccess(executionContext: ExecutionContext) { 55 | const results = executionContext.state.getUpdatedEntries() 56 | const assetOperations = executionContext.assets.getOperations() 57 | const result = { 58 | txId: executionContext.txId, 59 | results, 60 | assetOperations: assetOperations.map(ContractAssetOperation.fromPartial), 61 | } 62 | this.logger.verbose('Commiting success with params', result) 63 | try { 64 | await this.grpcClient.contractService.commitExecutionSuccess(result) 65 | } catch (e) { 66 | await this.tryCommitError(executionContext, e) 67 | } 68 | } 69 | 70 | tryCommitError(executionContext: ExecutionContext, e: ContractError) { 71 | this.logger.error('Committing Error ' + (e.code || ERROR_CODE.FATAL), e.message) 72 | this.logger.verbose(e) 73 | 74 | return this.grpcClient.contractService.commitExecutionError({ 75 | txId: executionContext.txId, 76 | message: e.message || 'Unhandled error', 77 | code: e.code || ERROR_CODE.FATAL, 78 | }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /packages/contract-core/src/execution/contract-service.ts: -------------------------------------------------------------------------------- 1 | import { WorkerPool } from '../utils/workers/static-pool' 2 | import path from 'path' 3 | import { logger } from '../api' 4 | import { getCpusCount } from '../utils' 5 | import { GrpcClient } from '../grpc/grpc-client' 6 | import { CONNECTION_ID, CONNECTION_TOKEN, NODE_ADDRESS } from '../grpc/config' 7 | import { ClientReadableStream } from '@grpc/grpc-js' 8 | import { ContractTransaction, ContractTransactionResponse, CurrentBlockInfo } from '@wavesenterprise/we-node-grpc-api' 9 | import { 10 | BlockInfo as RawBlockInfo, 11 | ContractTransaction as RawContractTransaction, 12 | } from '@wavesenterprise/js-contract-grpc-client/contract/contract_contract_service' 13 | 14 | export type ContractConfig = { 15 | contractPath: string, 16 | concurrencyLevel?: number, 17 | } 18 | 19 | export class ContractService { 20 | log = logger(this) 21 | 22 | private readonly workerPool: WorkerPool 23 | private readonly grpcClient: GrpcClient 24 | private connection: ClientReadableStream 25 | 26 | constructor(private config: ContractConfig) { 27 | this.workerPool = new WorkerPool({ 28 | filename: path.join(__dirname, './worker.js'), 29 | size: config.concurrencyLevel ?? getCpusCount() - 1, 30 | contractPath: config.contractPath, 31 | }) 32 | this.grpcClient = new GrpcClient({ 33 | connectionToken: CONNECTION_TOKEN, 34 | nodeAddress: NODE_ADDRESS, 35 | }) 36 | } 37 | 38 | start() { 39 | this.connection = this.grpcClient.contractService.connect({ 40 | connectionId: CONNECTION_ID, 41 | }) 42 | this.connection.on('close', () => { 43 | this.log.verbose('Connection stream closed') 44 | }) 45 | this.connection.on('end', () => { 46 | this.log.verbose('Connection stream ended') 47 | }) 48 | this.connection.on('error', (error) => { 49 | this.log.verbose('Connection stream error: ', error) 50 | }) 51 | this.connection.on('readable', () => { 52 | this.log.verbose('Connection stream readable') 53 | this.connection.read() 54 | }) 55 | this.connection.on('pause', () => { 56 | this.log.verbose('Connection stream paused') 57 | }) 58 | this.connection.on('resume', () => { 59 | this.log.verbose('Connection stream resume') 60 | }) 61 | this.connection.on('data', this.handle) 62 | 63 | this.log.verbose('Contract client connected') 64 | } 65 | 66 | handle = async (resp: ContractTransactionResponse) => { 67 | this.log.verbose('Handling tx') 68 | 69 | if (!resp.transaction) { 70 | throw new Error('Transaction not provided') 71 | } 72 | 73 | try { 74 | this.log.verbose('Sending task to worker pool', resp.transaction.id) 75 | await this.workerPool.execute({ 76 | authToken: resp.authToken, 77 | transaction: RawContractTransaction.toJSON(resp.transaction as RawContractTransaction) as ContractTransaction, 78 | currentBlockInfo: RawBlockInfo.toJSON(resp.currentBlockInfo as RawBlockInfo) as CurrentBlockInfo, 79 | }) 80 | this.log.verbose('Worker processed task', resp.transaction.id) 81 | } catch (e) { 82 | this.log.error('Worker execution error', e.message) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /packages/contract-core/src/execution/converter.ts: -------------------------------------------------------------------------------- 1 | import { BlockInfo, IncomingTx, TParam, TransferIn } from './types' 2 | import { _parseDataEntry } from '../utils' 3 | import { ContractTransaction, ContractTransferIn, CurrentBlockInfo, DataEntry } from '@wavesenterprise/we-node-grpc-api' 4 | import { TVal } from '../intefaces/contract' 5 | 6 | export class Param implements TParam { 7 | constructor(public key: string, public value: TVal) { 8 | } 9 | 10 | get type(): 'string' | 'integer' | 'binary' | 'boolean' { 11 | // TODO based on value type 12 | return 'string' 13 | } 14 | } 15 | 16 | export function convertTransferIn(transferIn: ContractTransferIn): TransferIn { 17 | return { 18 | assetId: transferIn.assetId, 19 | amount: transferIn.amount, 20 | } 21 | } 22 | 23 | export function convertDataEntryToParam(entry: DataEntry): TParam { 24 | return new Param(entry.key, _parseDataEntry(entry)) 25 | } 26 | 27 | export function convertContractTransaction(tx: ContractTransaction): IncomingTx { 28 | return { 29 | id: tx.id, 30 | type: tx.type, 31 | sender: tx.sender, 32 | senderPublicKey: tx.senderPublicKey, 33 | contractId: tx.contractId, 34 | version: tx.version, 35 | fee: tx.fee, 36 | feeAssetId: tx.feeAssetId?.value, 37 | timestamp: tx.timestamp.toNumber(), 38 | proofs: tx.proofs, 39 | payments: tx.payments.map(convertTransferIn), 40 | params: tx.params.map(convertDataEntryToParam), 41 | } 42 | } 43 | 44 | export function convertBlockInfo(block: CurrentBlockInfo): BlockInfo { 45 | return { 46 | height: block.height.toNumber(), 47 | timestamp: block.timestamp.toNumber(), 48 | minerAddress: block.minerAddress, 49 | reference: block.reference, 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/contract-core/src/execution/exceptions.ts: -------------------------------------------------------------------------------- 1 | import { ERROR_CODE } from './constants' 2 | 3 | export class ContractError extends Error { 4 | constructor(public message: string = 'Unhandled internal error', public code: number = ERROR_CODE.FATAL) { 5 | super(message) 6 | } 7 | } 8 | 9 | export class RetryableError extends ContractError { 10 | constructor(public message: string = 'Unhandled internal error') { 11 | super(message, ERROR_CODE.RECOVERABLE) 12 | } 13 | } 14 | 15 | 16 | export class UnexpectedParamTypeException extends ContractError { 17 | constructor(key: string) { 18 | super(`Required call param with name "${key}" has unexpected type`) 19 | } 20 | } 21 | 22 | export class UnavailableContractParamException extends Error { 23 | constructor(key: string) { 24 | super(`Required call param with name "${key}" not founded`) 25 | } 26 | } 27 | 28 | export class UnavailableContractActionException extends ContractError { 29 | constructor(key: string) { 30 | super(`Contract Action with name "${key}" not founded`) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/contract-core/src/execution/execution-context.ts: -------------------------------------------------------------------------------- 1 | import { ContractState } from '../api' 2 | import { BlockInfo, IncomingTx } from './types' 3 | import { ContractTransactionResponse } from '@wavesenterprise/we-node-grpc-api' 4 | import { convertBlockInfo, convertContractTransaction } from './converter' 5 | import { IGrpcClient } from '../grpc/grpc-client' 6 | import { AssetsStorage } from '../api/assets/assets-storage' 7 | 8 | export class ExecutionContext { 9 | readonly tx: IncomingTx 10 | readonly state: ContractState 11 | readonly assets: AssetsStorage 12 | readonly blockInfo: BlockInfo 13 | 14 | constructor( 15 | private incomingTxResp: ContractTransactionResponse, 16 | readonly grpcClient: IGrpcClient, 17 | ) { 18 | 19 | this.tx = convertContractTransaction(incomingTxResp.transaction) 20 | this.blockInfo = convertBlockInfo(incomingTxResp.currentBlockInfo) 21 | this.state = new ContractState(this) 22 | this.assets = new AssetsStorage() 23 | } 24 | 25 | get txId(): string { 26 | return this.tx.id 27 | } 28 | 29 | get contractId(): string { 30 | return this.tx.contractId 31 | } 32 | 33 | get params(): Map { 34 | const paramsMap = new Map() 35 | 36 | for (const p of this.tx.params) { 37 | paramsMap.set(p.key, p.value) 38 | } 39 | 40 | return paramsMap 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/contract-core/src/execution/index.ts: -------------------------------------------------------------------------------- 1 | export * from './exceptions' 2 | export * from './execution-context' 3 | export * from './types' -------------------------------------------------------------------------------- /packages/contract-core/src/execution/params-extractor.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext } from './execution-context' 2 | import { getArgsMetadata, getContractMetadata } from './reflect' 3 | import { TArgs, TContractActionMetadata } from '../api/meta' 4 | import { isPrimitive, isWrappedType } from '../utils' 5 | import { Constructable } from '../intefaces/helpers' 6 | import { ReservedParamNames, TxId } from './constants' 7 | import { 8 | ContractError, 9 | UnavailableContractActionException, 10 | UnavailableContractParamException, 11 | UnexpectedParamTypeException, 12 | } from './exceptions' 13 | import { ALL_PARAMS_KEY } from '../api/contants' 14 | import Long from 'long' 15 | 16 | function getArgKey(idx: number) { 17 | return `arg:${idx}` 18 | } 19 | 20 | export class ParamsExtractor { 21 | extractAction(contract: Constructable, executionContext: ExecutionContext) { 22 | const metadata = getContractMetadata(contract) 23 | 24 | switch (executionContext.tx.type as TxId) { 25 | case TxId.create: 26 | return metadata.initializer 27 | case TxId.call: 28 | const actionName = executionContext.params.get(ReservedParamNames.action) 29 | 30 | if (!actionName) { 31 | throw new UnavailableContractParamException(ReservedParamNames.action) 32 | } 33 | 34 | const actionMetadata = metadata.actions[actionName] 35 | 36 | if (!actionMetadata) { 37 | throw new UnavailableContractActionException(actionName) 38 | } 39 | 40 | return actionMetadata 41 | } 42 | } 43 | 44 | extract( 45 | contract: Constructable, 46 | executionContext: ExecutionContext, 47 | ): { actionMetadata: TContractActionMetadata, args: unknown[] } { 48 | const actionMetadata = this.extractAction(contract, executionContext) 49 | 50 | const argsMetadata: TArgs = getArgsMetadata(contract, actionMetadata.propertyName) 51 | 52 | const paramTypes = Reflect.getMetadata( 53 | 'design:paramtypes', 54 | contract.prototype, 55 | actionMetadata.propertyName, 56 | ) as ObjectConstructor[] 57 | 58 | const actionArgs = new Array(paramTypes.length) 59 | 60 | for (const [paramIndex, param] of paramTypes.entries()) { 61 | 62 | const argFromParams = argsMetadata[getArgKey(paramIndex)] 63 | 64 | if (!argFromParams) { 65 | throw new ContractError(`Argument at index ${paramIndex} should be annotated with @Param decorator`) 66 | } else { 67 | if (argFromParams.getter) { 68 | actionArgs[paramIndex] = argFromParams.getter() 69 | 70 | continue 71 | } 72 | 73 | if (argFromParams.paramKey === ALL_PARAMS_KEY) { 74 | actionArgs[paramIndex] = Object.fromEntries(executionContext.params.entries()) 75 | 76 | continue 77 | } 78 | 79 | const paramValue = executionContext.params.get(argFromParams.paramKey!) 80 | 81 | if (paramValue === null || paramValue === undefined) { 82 | throw new UnavailableContractParamException(argFromParams.paramKey || `#${paramIndex.toString()}`) 83 | } 84 | if (isPrimitive(param)) { 85 | actionArgs[paramIndex] = paramValue 86 | } else if (isWrappedType(param)) { 87 | try { 88 | // TODO marshaler functions 89 | actionArgs[paramIndex] = Long.fromString(String(paramValue)) 90 | } catch (e) { 91 | throw new ContractError(e.message) 92 | } 93 | } else { 94 | throw new UnexpectedParamTypeException(argFromParams.paramKey || `#${paramIndex.toString()}`) 95 | } 96 | } 97 | } 98 | 99 | return { 100 | actionMetadata, 101 | args: actionArgs, 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /packages/contract-core/src/execution/reflect.ts: -------------------------------------------------------------------------------- 1 | import { ACTION_METADATA, ARGS_METADATA } from './constants' 2 | import { TArgs, TContractActionsMetadata, TContractVarsMeta } from '../api/meta' 3 | import { Constructable, TContract } from '../intefaces/helpers' 4 | import { CONTRACT_PRELOADED_ENTRIES, CONTRACT_VARS } from '../api/contants' 5 | import { TVal } from '../intefaces/contract' 6 | 7 | export function getArgsMetadata(contract: Constructable, property: string): TArgs { 8 | return (Reflect.getMetadata(ARGS_METADATA, contract, property) || {}) as TArgs 9 | } 10 | 11 | export function getContractMetadata(contract: Constructable): TContractActionsMetadata { 12 | return Reflect.getMetadata(ACTION_METADATA, contract) as TContractActionsMetadata 13 | } 14 | 15 | export function getContractVarsMetadata(contract: unknown): TContractVarsMeta { 16 | return Reflect.getMetadata(CONTRACT_VARS, contract as object) as TContractVarsMeta 17 | } 18 | 19 | 20 | export function setContractEntry(contract: TContract, key: string, value: TVal): void { 21 | const entries = getContractEntries(contract) 22 | 23 | entries.set(key, value) 24 | 25 | return Reflect.defineMetadata(CONTRACT_PRELOADED_ENTRIES, entries, contract) 26 | } 27 | 28 | export function setContractEntries(contract: TContract, keys: Map): void { 29 | return Reflect.defineMetadata(CONTRACT_PRELOADED_ENTRIES, keys, contract.constructor) 30 | } 31 | 32 | export function getContractEntries(contract: TContract): Map { 33 | return Reflect.getMetadata(CONTRACT_PRELOADED_ENTRIES, contract.constructor) as Map 34 | } 35 | 36 | 37 | -------------------------------------------------------------------------------- /packages/contract-core/src/execution/types.ts: -------------------------------------------------------------------------------- 1 | import { TVal } from '../intefaces/contract' 2 | import Long from 'long' 3 | 4 | export type TParam = { 5 | key: string, 6 | value: TVal, 7 | type: string, 8 | } 9 | 10 | export type TransferIn = { 11 | assetId?: string, 12 | amount: Long, 13 | } 14 | 15 | export type AttachedPayments = TransferIn[] 16 | 17 | export type IncomingTx = { 18 | id: string, 19 | type: number, 20 | sender: string, 21 | senderPublicKey: string, 22 | contractId: string, 23 | version: number, 24 | fee: Long, 25 | proofs: Uint8Array, 26 | timestamp: number, 27 | feeAssetId?: string, 28 | payments: TransferIn[], 29 | params: TParam[], 30 | } 31 | 32 | export type BlockInfo = { 33 | height: number, 34 | timestamp: number, 35 | minerAddress: string, 36 | reference: string, 37 | } 38 | -------------------------------------------------------------------------------- /packages/contract-core/src/execution/worker.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata' 2 | import { isMainThread, parentPort, workerData } from 'node:worker_threads' 3 | import { ContractProcessor } from './contract-processor' 4 | import { CommonLogger, Logger } from '../api' 5 | import { GrpcClient } from '../grpc/grpc-client' 6 | import { CONNECTION_TOKEN, NODE_ADDRESS } from '../grpc/config' 7 | import { ContractTransaction, ContractTransactionResponse, CurrentBlockInfo } from '@wavesenterprise/we-node-grpc-api' 8 | import { 9 | ContractTransaction as RawContractTransaction, 10 | BlockInfo as RawBlockInfo, 11 | } from '@wavesenterprise/js-contract-grpc-client/contract/contract_contract_service' 12 | 13 | Logger.workerIdx = workerData.index 14 | 15 | if (isMainThread) { 16 | throw new Error('Main thread error') 17 | } 18 | 19 | (async function () { 20 | if (parentPort === null) { 21 | throw new Error('parent port not found') 22 | } 23 | const ContractClassConstructor = await import(workerData.contractPath) 24 | .then(({ default: ContractClass }) => ContractClass as unknown) 25 | 26 | const grpcClient = new GrpcClient({ 27 | nodeAddress: NODE_ADDRESS, 28 | connectionToken: CONNECTION_TOKEN, 29 | }) 30 | 31 | const processor = new ContractProcessor(ContractClassConstructor, grpcClient) 32 | 33 | parentPort.on('message', async (incoming: ContractTransactionResponse) => { 34 | const start = Date.now() 35 | const { authToken, transaction: serializedTx, currentBlockInfo: serializedBlockInfo } = incoming 36 | const transaction = RawContractTransaction.fromJSON(serializedTx) as ContractTransaction 37 | const currentBlockInfo = RawBlockInfo.fromJSON(serializedBlockInfo) as CurrentBlockInfo 38 | const txId = transaction.id 39 | CommonLogger.verbose(`Worker received tx ${txId}`) 40 | try { 41 | await processor.handleIncomingTx({ 42 | authToken, 43 | transaction, 44 | currentBlockInfo, 45 | }) 46 | CommonLogger.info(`Worker handled tx ${txId} in ${Date.now() - start}ms`) 47 | } catch (e) { 48 | CommonLogger.error( 49 | `Uncaught error "${e.message}" tx ${transaction?.id} may not be committed`, 50 | e.stack, 51 | ) 52 | } finally { 53 | parentPort!.postMessage('done') 54 | } 55 | }) 56 | })() 57 | -------------------------------------------------------------------------------- /packages/contract-core/src/grpc/config.ts: -------------------------------------------------------------------------------- 1 | export const CONNECTION_ID = process.env.CONNECTION_ID || '' 2 | export const CONNECTION_TOKEN = process.env.CONNECTION_TOKEN || '' 3 | export const NODE = process.env.NODE || '' 4 | export const NODE_PORT = process.env.NODE_PORT || '' 5 | export const NODE_ADDRESS = `${NODE}:${NODE_PORT}` 6 | -------------------------------------------------------------------------------- /packages/contract-core/src/grpc/grpc-client.ts: -------------------------------------------------------------------------------- 1 | import { ContractAddressService, ContractService } from '@wavesenterprise/we-node-grpc-api' 2 | import { ClientReadableStream, MetadataValue } from '@grpc/grpc-js' 3 | import { 4 | CalculateAssetIdRequest, 5 | ConnectionRequest, 6 | ContractBalancesRequest, 7 | ContractKeyRequest, 8 | ContractKeysRequest, 9 | ExecutionErrorRequest, 10 | ExecutionSuccessRequest, 11 | } from '@wavesenterprise/js-contract-grpc-client/contract/contract_contract_service' 12 | import { 13 | AssetBalanceResponse, 14 | ContractBalanceResponse, 15 | ContractTransactionResponse, 16 | DataEntry, 17 | } from '@wavesenterprise/we-node-grpc-api/src/types' 18 | import { 19 | AddressDataRequest, 20 | AssetBalanceRequest, 21 | } from '@wavesenterprise/js-contract-grpc-client/contract/contract_address_service' 22 | 23 | export type GrpcClientProps = { 24 | connectionToken: string, 25 | nodeAddress: string, 26 | } 27 | 28 | 29 | export type IContractService = { 30 | connect(request: ConnectionRequest): ClientReadableStream, 31 | 32 | commitExecutionSuccess(request: ExecutionSuccessRequest): Promise, 33 | 34 | commitExecutionError(request: ExecutionErrorRequest): Promise, 35 | 36 | getContractKeys(request: ContractKeysRequest): Promise, 37 | 38 | getContractKey(request: ContractKeyRequest): Promise, 39 | 40 | getContractBalances(request: ContractBalancesRequest): Promise, 41 | 42 | calculateAssetId(request: CalculateAssetIdRequest): Promise, 43 | 44 | setMetadata(metadata: Record), 45 | } 46 | 47 | export type IAddressService = { 48 | getAddresses(): Promise, 49 | 50 | getAddressData(request: AddressDataRequest): Promise, 51 | 52 | getAssetBalance(request: AssetBalanceRequest): Promise, 53 | 54 | setMetadata(metadata: Record), 55 | } 56 | 57 | export type IGrpcClient = { 58 | contractService: IContractService, 59 | contractAddressService: IAddressService, 60 | 61 | setMetadata(metadata: Record): void, 62 | } 63 | 64 | 65 | export class GrpcClient implements IGrpcClient { 66 | readonly contractService: IContractService 67 | readonly contractAddressService: IAddressService 68 | // readonly contractUtilService: ContractUtilService 69 | // readonly contractPermissionService: ContractPermissionService 70 | // readonly contractPKIService: ContractPKIService 71 | // readonly contractPrivacyService: ContractPrivacyService 72 | // readonly contractTransactionService: ContractTransactionService 73 | 74 | constructor(private readonly props: GrpcClientProps) { 75 | this.contractService = new ContractService(this.props.nodeAddress) 76 | this.contractAddressService = new ContractAddressService(this.props.nodeAddress) 77 | // this.contractUtilService = new ContractUtilService(this.props.nodeAddress) 78 | // this.contractPermissionService = new ContractPermissionService(this.props.nodeAddress) 79 | // this.contractPKIService = new ContractPKIService(this.props.nodeAddress) 80 | // this.contractPrivacyService = new ContractPrivacyService(this.props.nodeAddress) 81 | // this.contractTransactionService = new ContractTransactionService(this.props.nodeAddress) 82 | this.setMetadata({ 83 | authorization: this.props.connectionToken, 84 | }) 85 | } 86 | 87 | setMetadata(metadata: Record) { 88 | this.contractService.setMetadata(metadata) 89 | this.contractAddressService.setMetadata(metadata) 90 | // this.contractUtilService.setMetadata(metadata) 91 | // this.contractPermissionService.setMetadata(metadata) 92 | // this.contractPKIService.setMetadata(metadata) 93 | // this.contractPrivacyService.setMetadata(metadata) 94 | // this.contractTransactionService.setMetadata(metadata) 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /packages/contract-core/src/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata' 2 | 3 | export * from './api' 4 | export * from './entrypoint' 5 | export * from './execution/types' 6 | export * from './execution/execution-context' 7 | export * from './execution/exceptions' 8 | export * as testUtils from './test-utils' 9 | -------------------------------------------------------------------------------- /packages/contract-core/src/intefaces/contract.ts: -------------------------------------------------------------------------------- 1 | import Long from 'long' 2 | 3 | export type TValue = string | Long | number | boolean | Buffer 4 | export type TVal = TValue | Uint8Array | undefined 5 | -------------------------------------------------------------------------------- /packages/contract-core/src/intefaces/helpers.ts: -------------------------------------------------------------------------------- 1 | export type Constructable = new (...args: any[]) => T 2 | export type TContract = object 3 | export type Optional = T | undefined 4 | -------------------------------------------------------------------------------- /packages/contract-core/src/test-utils/environment/local-contract-processor.ts: -------------------------------------------------------------------------------- 1 | import { ContractProcessor } from '../../execution/contract-processor' 2 | import { ContractTransactionResponse } from '@wavesenterprise/we-node-grpc-api/src/types' 3 | import { LocalGrpcClient } from './local' 4 | 5 | export class LocalContractProcessor extends ContractProcessor { 6 | get client() { 7 | return this.grpcClient as LocalGrpcClient 8 | } 9 | 10 | handleIncomingTx(resp: ContractTransactionResponse): Promise { 11 | this.validateBalances(resp) 12 | 13 | return super.handleIncomingTx(resp) 14 | } 15 | 16 | validateBalances(resp: ContractTransactionResponse) { 17 | if (resp.transaction.payments.length === 0) { 18 | return 19 | } 20 | 21 | for (const r of resp.transaction.payments) { 22 | const isValid = this.client.assets.canTransfer(resp.transaction.sender, r.amount) 23 | 24 | if (!isValid) { 25 | throw new Error('not valid payment attached') 26 | } 27 | 28 | this.client.assets.transfer(resp.transaction.sender, resp.transaction.contractId, r.amount) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/contract-core/src/test-utils/environment/local-contract-state.ts: -------------------------------------------------------------------------------- 1 | import { TVal } from '../../intefaces/contract' 2 | import Long from 'long' 3 | import { ContractIssue } from '@wavesenterprise/we-node-grpc-api' 4 | import { Account } from '../types' 5 | 6 | const Native = ':native' 7 | 8 | export type IBlockchainState = { 9 | setKey(key: string, value: TVal), 10 | 11 | getKeys(keys: string[]): TVal[], 12 | } 13 | 14 | export class LocalContractState implements IBlockchainState { 15 | private state = new Map() 16 | 17 | setKey(key: string, value: TVal) { 18 | this.state.set(key, value) 19 | } 20 | 21 | getKeys(keys: string[]): TVal[] { 22 | return keys.map((key) => this.state.get(key) as TVal) 23 | } 24 | } 25 | 26 | 27 | export class LocalAssets { 28 | private assets: Map 29 | private balances: Map> 30 | 31 | constructor() { 32 | this.balances = new Map>() 33 | this.assets = new Map() 34 | 35 | this.addBalance(Native, '__root', Long.MAX_VALUE) 36 | } 37 | 38 | addBalance(assetId: string, address: string, amount: Long) { 39 | let balancesMap = this.balances.get(assetId) 40 | 41 | if (!balancesMap) { 42 | balancesMap = new Map() 43 | } 44 | 45 | balancesMap.set(address, amount) 46 | 47 | this.balances.set(assetId, balancesMap) 48 | } 49 | 50 | 51 | getBalance(address: string, assetId = Native) { 52 | if (!this.balances.get(assetId)) { 53 | throw new Error('asset not exists') 54 | } 55 | 56 | const balances = this.balances.get(assetId)! 57 | 58 | return balances.get(address) || Long.fromNumber(0) 59 | } 60 | 61 | transfer(from: string, to: string, amount: Long, assetId = Native) { 62 | const senderBalance = this.getBalance(from, assetId) 63 | const recipientBalance = this.getBalance(to, assetId) 64 | 65 | if (!senderBalance.subtract(amount).gte(0)) { 66 | throw new Error('unsufficient balance') 67 | } 68 | 69 | const asset = this.balances.get(assetId)! 70 | 71 | asset.set(from, senderBalance.subtract(amount)) 72 | asset.set(to, recipientBalance.add(amount)) 73 | 74 | this.balances.set(assetId, asset) 75 | } 76 | 77 | canTransfer(address: string, value: Long) { 78 | return this.getBalance(address).gte(value) 79 | } 80 | 81 | issue(res: ContractIssue, _: string) { 82 | if (this.assets.get(res.assetId!)) { 83 | throw new Error('asset already issued') 84 | } 85 | 86 | this.assets.set(res.assetId!, res) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /packages/contract-core/src/test-utils/environment/local.ts: -------------------------------------------------------------------------------- 1 | import { IAddressService, IContractService, IGrpcClient } from '../../grpc/grpc-client' 2 | import { 3 | AssetBalanceResponse, 4 | ContractBalanceResponse, 5 | ContractTransactionResponse, 6 | DataEntry, 7 | } from '@wavesenterprise/we-node-grpc-api/src/types' 8 | import { 9 | CalculateAssetIdRequest, 10 | ConnectionRequest, 11 | ContractBalancesRequest, 12 | ContractKeyRequest, 13 | ContractKeysRequest, 14 | ExecutionErrorRequest, 15 | ExecutionSuccessRequest, 16 | } from '@wavesenterprise/js-contract-grpc-client/contract/contract_contract_service' 17 | import { ClientReadableStream, MetadataValue } from '@grpc/grpc-js' 18 | import { AddressDataRequest } from '@wavesenterprise/js-contract-grpc-client/address/address_public_service' 19 | import { AssetBalanceRequest } from '@wavesenterprise/js-contract-grpc-client/contract/contract_address_service' 20 | import { IBlockchainState, LocalAssets } from './local-contract-state' 21 | import { _parseDataEntry, getValueStateKey, isUndefined } from '../../utils' 22 | import { TVal } from '../../intefaces/contract' 23 | import Long from 'long' 24 | import { ContractIssue } from '@wavesenterprise/we-node-grpc-api' 25 | 26 | function makeid(length) { 27 | let result = '' 28 | const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' 29 | const charactersLength = characters.length 30 | let counter = 0 31 | while (counter < length) { 32 | result += characters.charAt(Math.floor(Math.random() * charactersLength)) 33 | counter += 1 34 | } 35 | return result 36 | } 37 | 38 | export class LocalContractService implements IContractService { 39 | protected metadata?: Record 40 | 41 | constructor(private client: LocalGrpcClient) { 42 | } 43 | 44 | getContractBalances(_: ContractBalancesRequest): Promise { 45 | throw new Error('not implemented') 46 | } 47 | 48 | getContractKey(request: ContractKeyRequest): Promise { 49 | const resp = this.client.state.getKeys([request.key as string]) 50 | const value = resp[0] 51 | 52 | if (isUndefined(value)) { 53 | return Promise.resolve(undefined as any as DataEntry) 54 | } 55 | 56 | const valueKey = getValueStateKey(value) 57 | 58 | return Promise.resolve( 59 | { 60 | key: request.key as string, 61 | [valueKey]: value, 62 | } as DataEntry, 63 | ) 64 | } 65 | 66 | getContractKeys(request: ContractKeysRequest): Promise { 67 | return Promise.all(request.keysFilter!.keys!.map( 68 | (k) => this.getContractKey({ key: k as string }), 69 | )) 70 | } 71 | 72 | connect(_: ConnectionRequest): ClientReadableStream { 73 | throw new Error('not implemented') 74 | } 75 | 76 | commitExecutionSuccess(request: ExecutionSuccessRequest): Promise { 77 | if (!request.results || !request.assetOperations) { 78 | return Promise.reject('keys not changed') 79 | } 80 | 81 | for (const res of request.results) { 82 | const value = _parseDataEntry(res as DataEntry) 83 | 84 | this.client.state.setKey(res.key as string, value as TVal) 85 | } 86 | 87 | for (const res of request.assetOperations) { 88 | if (res.contractTransferOut) { 89 | this.client.assets.transfer( 90 | this.client.contractId, 91 | res.contractTransferOut.recipient!, 92 | res.contractTransferOut.amount as Long, 93 | res.contractTransferOut.assetId, 94 | ) 95 | } 96 | if (res.contractIssue) { 97 | this.client.assets.issue(res.contractIssue as ContractIssue, this.client.contractId) 98 | } 99 | } 100 | 101 | 102 | // TODO asset operations 103 | 104 | return Promise.resolve() 105 | } 106 | 107 | 108 | commitExecutionError(request: ExecutionErrorRequest): Promise { 109 | 110 | return Promise.reject(request.message) 111 | } 112 | 113 | calculateAssetId(_: CalculateAssetIdRequest): Promise { 114 | return Promise.resolve(makeid(32)) 115 | } 116 | 117 | setMetadata(metadata: Record) { 118 | this.metadata = metadata 119 | } 120 | } 121 | 122 | export class LocalAddressService implements IAddressService { 123 | constructor(private client: IGrpcClient) { 124 | } 125 | 126 | protected metadata?: Record 127 | 128 | getAddresses(): Promise { 129 | throw new Error('not implemented') 130 | } 131 | 132 | getAssetBalance(_: AssetBalanceRequest): Promise { 133 | throw new Error('not implemented') 134 | } 135 | 136 | getAddressData(_: AddressDataRequest): Promise { 137 | throw new Error('not implemented') 138 | } 139 | 140 | setMetadata(metadata: Record) { 141 | this.metadata = metadata 142 | } 143 | } 144 | 145 | export class LocalGrpcClient implements IGrpcClient { 146 | contractService = new LocalContractService(this) 147 | contractAddressService = new LocalAddressService(this) 148 | 149 | constructor( 150 | public state: IBlockchainState, 151 | public assets: LocalAssets, 152 | public contractId: string, 153 | ) { 154 | } 155 | 156 | setMetadata(metadata: Record) { 157 | this.contractService.setMetadata(metadata) 158 | this.contractAddressService.setMetadata(metadata) 159 | } 160 | } 161 | 162 | -------------------------------------------------------------------------------- /packages/contract-core/src/test-utils/environment/response-factory.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ContractTransactionResponse as ContractTransactionResponseRaw, 3 | } from '@wavesenterprise/js-contract-grpc-client/contract/contract_contract_service' 4 | import { ContractTransferIn } from '@wavesenterprise/js-contract-grpc-client/contract_transfer_in' 5 | import { ContractTransactionResponse, DataEntry } from '@wavesenterprise/we-node-grpc-api' 6 | 7 | import { DataEntry as DE } from '@wavesenterprise/js-contract-grpc-client/data_entry' 8 | 9 | import { Keypair } from '@wavesenterprise/signer' 10 | import { randomUint8Array, toBase58 } from '@wavesenterprise/crypto-utils' 11 | 12 | export async function txFactory({ action, sender, params = [], payments, contractId }: { 13 | action: string, 14 | params?: DataEntry[], 15 | payments?: Array<{ assetId?: string, amount: number }>, 16 | contractId: string, 17 | sender: Keypair, 18 | }) { 19 | return ContractTransactionResponseRaw.fromPartial({ 20 | authToken: toBase58(randomUint8Array(32)), 21 | transaction: { 22 | id: toBase58(randomUint8Array(32)), 23 | type: 104, 24 | sender: await sender.address(), 25 | senderPublicKey: await sender.publicKey(), 26 | contractId, 27 | params: [ 28 | { 29 | stringValue: action, 30 | key: 'action', 31 | }, 32 | 33 | 34 | ...(params.map(DE.fromPartial)), 35 | ], 36 | version: 5, 37 | payments: payments?.map((p) => { 38 | return ContractTransferIn.fromPartial({ 39 | assetId: p.assetId, 40 | amount: p.amount, 41 | }) 42 | }), 43 | proofs: randomUint8Array(32), 44 | timestamp: new Date().getTime(), 45 | }, 46 | currentBlockInfo: { 47 | height: 1, 48 | timestamp: Date.now(), 49 | minerAddress: '', 50 | reference: '', 51 | }, 52 | }) as ContractTransactionResponse 53 | } 54 | -------------------------------------------------------------------------------- /packages/contract-core/src/test-utils/example/example-test.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata' 2 | 3 | import {Sandbox} from "../sandbox"; 4 | import {Action, Assets, AssetsService, ContractMapping, ContractState, JsonVar, Param, State, Tx, Var} from "../../api"; 5 | import {Test} from "./test-wrapper"; 6 | import {OpenedContract} from "../types"; 7 | import {IncomingTx} from "../../execution"; 8 | import Long from "long"; 9 | import {getKeypair} from "../utils/address"; 10 | import {Keypair} from "@wavesenterprise/signer"; 11 | 12 | type DepositRequest = { 13 | id: string; 14 | status: 'pending' | 'done' | 'rejected'; 15 | 16 | targetAddress: string; 17 | 18 | doneTxId?: string; 19 | rejectTxId?: string; 20 | 21 | amount: string; 22 | assetId: string; 23 | } 24 | 25 | class Transborder { 26 | @State state: ContractState 27 | @Assets() assets: AssetsService; 28 | 29 | @JsonVar({key: 'REQUEST'}) 30 | depositRequests: ContractMapping 31 | 32 | @Var({key: "ASSET_NAME"}) 33 | assetNameToId: ContractMapping 34 | 35 | @Var({key: "ASSET"}) 36 | assetIdToName: ContractMapping 37 | 38 | @Action 39 | async mintBankCheques( 40 | @Param('amount') amount: Long, 41 | @Param('assetName') assetName: string, 42 | ) { 43 | const asset = await this.assets.issueAsset({ 44 | decimals: 8, 45 | description: `BANK_${assetName}`, 46 | isReissuable: true, 47 | name: assetName.toUpperCase(), 48 | quantity: Long.fromNumber(1000 * 10e7), 49 | }) 50 | 51 | this.assetIdToName.set(asset.getId()!, assetName) 52 | this.assetNameToId.set(assetName, asset.getId()!) 53 | } 54 | 55 | @Action 56 | async requestDeposit( 57 | @Tx() tx: IncomingTx, 58 | @Param('amount') amount: Long, 59 | @Param('assetId') assetId: string, 60 | @Param('targetAddress') address: string, 61 | ) { 62 | const request: DepositRequest = { 63 | id: tx.id, 64 | status: 'pending', 65 | assetId, 66 | targetAddress: address, 67 | amount: amount.toString() 68 | } 69 | 70 | this.depositRequests.set(tx.id, request) 71 | } 72 | 73 | @Action 74 | async confirmDeposit( 75 | @Tx() tx: IncomingTx, 76 | @Param('requestId') requestId: string, 77 | ) { 78 | const request = await this.depositRequests.get(requestId) 79 | 80 | if (request.status !== 'pending') { 81 | throw new Error('request should be pending') 82 | } 83 | 84 | this.assets.reissueAsset(request.assetId, { 85 | isReissuable: true, 86 | quantity: Long.fromString(request.amount) 87 | }); 88 | 89 | this.assets.transferAsset(request.assetId, { 90 | recipient: request.targetAddress, 91 | amount: Long.fromString(request.amount) 92 | }); 93 | 94 | request.status = 'done' 95 | request.doneTxId = tx.id; 96 | 97 | this.depositRequests.set(request.id, request) 98 | } 99 | 100 | 101 | @Action() 102 | rejectDeposit() { 103 | throw new Error('not implemented') 104 | } 105 | } 106 | 107 | describe('Transborder', () => { 108 | let blockchain: Sandbox; 109 | let contract: OpenedContract 110 | 111 | let bank: Keypair, client: Keypair 112 | let WRUB: string, WUSDT: string 113 | 114 | beforeAll(async () => { 115 | [bank, client] = await getKeypair(2) 116 | 117 | blockchain = new Sandbox(); 118 | 119 | const contractId = blockchain.deployContract(Transborder); 120 | contract = blockchain.connect(Test.createFromAddress(contractId, Transborder)); 121 | 122 | blockchain.fundAccount(await bank.address(), 10000); 123 | blockchain.fundAccount(await client.address(), 10000); 124 | 125 | WUSDT = blockchain.issueAsset({ 126 | name: "WUSDT", 127 | description: "WUSDT", 128 | decimals: 8, 129 | isReissuable: true, 130 | quantity: Long.fromNumber(2_000_000 * 10e7) 131 | }) 132 | 133 | WRUB = blockchain.issueAsset({ 134 | name: "WRUB", 135 | description: "WRUB", 136 | decimals: 8, 137 | isReissuable: true, 138 | quantity: Long.fromNumber(2_000_000 * 10e7) 139 | }) 140 | 141 | 142 | blockchain.fundAccount(contractId, 1_000_000 * 10e7, WUSDT) 143 | blockchain.fundAccount(contractId, 1_000_000 * 10e7, WRUB) 144 | }) 145 | 146 | it('should create deposit request', async function () { 147 | const depositAmount = 100 * 10e7 148 | 149 | const requestTxId = await contract.as(bank).invokeRequestDeposit(depositAmount, WUSDT, await client.address()) 150 | const requestString = await contract.getKey(`REQUEST_${requestTxId}`) 151 | 152 | expect(requestString).not.toBeUndefined() 153 | 154 | const res = JSON.parse(requestString as string) as DepositRequest 155 | 156 | expect(res.status).toBe('pending') 157 | expect(res.id).toBe(requestTxId) 158 | expect(res.amount).toBe(String(depositAmount)) 159 | 160 | const confirmTxId = await contract.as(bank).invokeConfirmDeposit(requestTxId) 161 | const requestAfterConfirmString = await contract.getKey(`REQUEST_${requestTxId}`) 162 | 163 | expect(requestAfterConfirmString).not.toBeUndefined() 164 | 165 | const resAfterConfirm = JSON.parse(requestAfterConfirmString as string) as DepositRequest 166 | 167 | expect(resAfterConfirm.status).toBe('done') 168 | expect(resAfterConfirm.doneTxId).toBe(confirmTxId) 169 | 170 | const balanceAfter = blockchain.getBalance(await client.address(), WUSDT) 171 | const contractBalanceAfter = blockchain.getBalance(contract.address, WUSDT) 172 | 173 | expect(balanceAfter.toString()).toBe(String(depositAmount)) 174 | expect(contractBalanceAfter.toString()).toBe(String((1_000_000 * 10e7) - depositAmount)) 175 | }); 176 | }) 177 | -------------------------------------------------------------------------------- /packages/contract-core/src/test-utils/example/test-wrapper.ts: -------------------------------------------------------------------------------- 1 | import { Constructable } from '../../intefaces/helpers' 2 | import { ContractExecutable } from '../types' 3 | import { Executor } from '../executor' 4 | 5 | export type Contract = { 6 | address: string, 7 | targetExecutable: ContractExecutable, 8 | } 9 | 10 | export class Test implements Contract { 11 | constructor( 12 | public address: string, 13 | public targetExecutable: ContractExecutable, 14 | ) { 15 | } 16 | 17 | static createFromAddress(address: string, executable: Constructable) { 18 | return new Test(address, executable) 19 | } 20 | 21 | invokeRequestDeposit( 22 | executor: Executor, 23 | amount: number, 24 | assetId: string, 25 | targetAddress: string, 26 | ) { 27 | return executor.invoke({ 28 | call: 'requestDeposit', 29 | params: { 30 | amount, 31 | assetId, 32 | targetAddress, 33 | }, 34 | }) 35 | } 36 | 37 | invokeConfirmDeposit( 38 | executor: Executor, 39 | requestId: string, 40 | ) { 41 | return executor.invoke({ 42 | call: 'confirmDeposit', 43 | params: { 44 | requestId, 45 | }, 46 | }) 47 | } 48 | 49 | invokeMintBankCheques( 50 | executor: Executor, 51 | amount: number, 52 | assetName: string, 53 | ) { 54 | return executor.invoke({ 55 | call: 'mintBankCheques', 56 | payments: [], 57 | params: { 58 | amount, 59 | assetName, 60 | }, 61 | }) 62 | } 63 | 64 | invokeCreateCheque( 65 | executor: Executor, 66 | amount: number, 67 | assetId: string, 68 | targetAddress: string, 69 | ) { 70 | return executor.invoke({ 71 | call: 'createCheque', 72 | payments: [], 73 | params: { 74 | amount, 75 | assetId, 76 | targetAddress, 77 | }, 78 | }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /packages/contract-core/src/test-utils/executor.ts: -------------------------------------------------------------------------------- 1 | import { ContractProcessor } from '../execution/contract-processor' 2 | import { txFactory } from './environment/response-factory' 3 | import { ContractExecutable } from './types' 4 | import { Keypair } from '@wavesenterprise/signer' 5 | import * as marshal from '../utils/marshal' 6 | 7 | export class Executor { 8 | processor: ContractProcessor 9 | sender: Keypair 10 | 11 | constructor(private executable: ContractExecutable) { 12 | } 13 | 14 | setProcessor(cp: ContractProcessor) { 15 | this.processor = cp 16 | } 17 | 18 | setSender(kp: Keypair) { 19 | this.sender = kp 20 | } 21 | 22 | async invoke(params: { 23 | call: string, 24 | params?: Record, 25 | payments?: Array<{ assetId?: string, amount: number }>, 26 | 27 | }): Promise { 28 | const { 29 | call, 30 | params: txParams, 31 | ...tx 32 | } = params 33 | 34 | const response = await txFactory({ 35 | ...tx, 36 | action: call, 37 | sender: this.sender, 38 | contractId: this.executable.name, 39 | params: txParams ? marshal.params(txParams) : undefined, 40 | }) 41 | 42 | await this.processor.handleIncomingTx(response) 43 | 44 | return response.transaction.id 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/contract-core/src/test-utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sandbox' 2 | export * from './environment/local' 3 | export * from './utils/address' 4 | export * from './types' 5 | -------------------------------------------------------------------------------- /packages/contract-core/src/test-utils/sandbox.ts: -------------------------------------------------------------------------------- 1 | import { Constructable } from '../intefaces/helpers' 2 | import { Contract } from './example/test-wrapper' 3 | import { Account, ContractExecutable, OpenedContract } from './types' 4 | import { LocalGrpcClient } from './environment/local' 5 | import { Executor } from './executor' 6 | import { IBlockchainState, LocalAssets, LocalContractState } from './environment/local-contract-state' 7 | import { LocalContractProcessor } from './environment/local-contract-processor' 8 | import { Keypair } from '@wavesenterprise/signer' 9 | import { ContractIssue } from '@wavesenterprise/we-node-grpc-api' 10 | import { randomUint8Array, toBase58 } from '@wavesenterprise/crypto-utils' 11 | import Long from 'long' 12 | 13 | type PartialBy = Omit & Partial> 14 | 15 | export class Sandbox { 16 | contractStates = new Map() 17 | contractExecutables = new Map>() 18 | 19 | contracts = new WeakMap, IBlockchainState>() 20 | 21 | balances = new LocalAssets() 22 | 23 | issueAsset({ assetId, nonce = 0, ...request }: PartialBy): string { 24 | if (!assetId) { 25 | 26 | assetId = toBase58(randomUint8Array(32)) 27 | } 28 | 29 | this.balances.issue({ assetId, nonce, ...request }, '__root') 30 | this.balances.addBalance(assetId, '__root', request.quantity) 31 | 32 | return assetId 33 | } 34 | 35 | fundAccount(recipient: Account, value: number, assetId?: string) { 36 | this.balances.transfer('__root', recipient, Long.fromNumber(value), assetId) 37 | } 38 | 39 | deployContract(c: Constructable, initState?: IBlockchainState): string { 40 | const state = initState ?? new LocalContractState() 41 | 42 | // TODO emulate init transaction 43 | const txId = toBase58(randomUint8Array(32)) 44 | 45 | this.contractExecutables.set(txId, c) 46 | this.contractStates.set(txId, state) 47 | 48 | return txId 49 | } 50 | 51 | connect(t: T): OpenedContract { 52 | const executor = new Executor(t.targetExecutable) 53 | const contractState = this.contractStates.get(t.address) 54 | 55 | if (!contractState) { 56 | throw new Error('contract not deployed') 57 | } 58 | 59 | const processor = new LocalContractProcessor( 60 | t.targetExecutable, 61 | new LocalGrpcClient(contractState, this.balances, t.address), 62 | ) 63 | 64 | executor.setProcessor(processor) 65 | 66 | return new Proxy(t as any, { 67 | get: (target, prop, receiver) => { 68 | const value: unknown = target[prop] 69 | 70 | switch (prop) { 71 | case 'getKey': 72 | return (...args: any[]) => contractState.getKeys(args)[0] 73 | case 'as': 74 | return (kp: Keypair) => { 75 | executor.setSender(kp) 76 | 77 | return receiver 78 | } 79 | default: 80 | if (typeof prop === 'string' && (prop.startsWith('get') || prop.startsWith('invoke'))) { 81 | if (typeof value === 'function') { 82 | return (...args: any[]) => value.apply(target, [executor, ...args]) 83 | } 84 | } 85 | 86 | return value 87 | } 88 | }, 89 | }) as OpenedContract 90 | } 91 | 92 | getBalance(address: string, assetId?: string) { 93 | return this.balances.getBalance(address, assetId) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /packages/contract-core/src/test-utils/types.ts: -------------------------------------------------------------------------------- 1 | import { TVal } from '../intefaces/contract' 2 | import { Executor } from './executor' 3 | import { Keypair } from '@wavesenterprise/signer' 4 | 5 | export type ContractExecutable = any 6 | export type Account = string 7 | 8 | export type Invokable = { 9 | [P in keyof F]: P extends `${'invoke'}${string}` 10 | ? (F[P] extends (x: Executor, ...args: infer Arguments) => infer R ? (...args: Arguments) => R : never) 11 | : F[P]; 12 | } 13 | 14 | export type BaseMethods = { 15 | getKey(key: string): TVal, 16 | as(t: Keypair): OpenedContract, 17 | } 18 | 19 | export type BaseMethodName = keyof BaseMethods 20 | export type OpenedContract = BaseMethods & Invokable 21 | -------------------------------------------------------------------------------- /packages/contract-core/src/test-utils/utils/address.ts: -------------------------------------------------------------------------------- 1 | import { Keypair } from '@wavesenterprise/signer' 2 | 3 | export async function getKeypair(num: number): Promise { 4 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 5 | return Array(num).fill(await Keypair.generate()) 6 | } 7 | -------------------------------------------------------------------------------- /packages/contract-core/src/utils/base58.ts: -------------------------------------------------------------------------------- 1 | const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' 2 | const ALPHABET_MAP = ALPHABET.split('').reduce((res: Record, c, i) => { 3 | res[c] = i 4 | return res 5 | }, {}) 6 | 7 | 8 | export class Base58 { 9 | static encode(buffer: Uint8Array | number[]): string { 10 | if (!buffer.length) {return ''} 11 | 12 | const digits = [0] 13 | 14 | for (const buf of buffer) { 15 | for (let j = 0; j < digits.length; j++) { 16 | digits[j] <<= 8 17 | } 18 | 19 | digits[0] += buf 20 | let carry = 0 21 | 22 | for (let k = 0; k < digits.length; k++) { 23 | digits[k] += carry 24 | carry = (digits[k] / 58) | 0 25 | digits[k] %= 58 26 | } 27 | 28 | while (carry) { 29 | digits.push(carry % 58) 30 | carry = (carry / 58) | 0 31 | } 32 | 33 | } 34 | 35 | for (let i = 0; buffer[i] === 0 && i < buffer.length - 1; i++) { 36 | digits.push(0) 37 | } 38 | 39 | return digits.reverse().map(function (digit) { 40 | return ALPHABET[digit] 41 | }).join('') 42 | 43 | } 44 | 45 | static decode(str = ''): Uint8Array { 46 | if (!str.length) {return new Uint8Array(0)} 47 | 48 | const bytes = [0] 49 | 50 | for (const letter of str) { 51 | if (!(letter in ALPHABET_MAP)) { 52 | throw new Error(`There is no character "${letter}" in the Base58 sequence!`) 53 | } 54 | 55 | for (let j = 0; j < bytes.length; j++) { 56 | bytes[j] *= 58 57 | } 58 | 59 | bytes[0] += ALPHABET_MAP[letter] 60 | let carry = 0 61 | 62 | for (let j = 0; j < bytes.length; j++) { 63 | bytes[j] += carry 64 | carry = bytes[j] >> 8 65 | bytes[j] &= 0xff 66 | } 67 | 68 | while (carry) { 69 | bytes.push(carry & 0xff) 70 | carry >>= 8 71 | } 72 | 73 | } 74 | 75 | for (let i = 0; str[i] === '1' && i < str.length - 1; i++) { 76 | bytes.push(0) 77 | } 78 | 79 | return new Uint8Array(bytes.reverse()) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /packages/contract-core/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import * as os from 'os' 2 | import { DataEntry } from '@wavesenterprise/we-node-grpc-api' 3 | import { TVal } from '../intefaces/contract' 4 | import Long from 'long' 5 | 6 | export const isUndefined = (v: unknown): v is undefined => { 7 | return v === undefined 8 | } 9 | 10 | export const isString = (v: unknown): v is string => { 11 | return typeof v === 'string' 12 | } 13 | 14 | export const isBool = (v: unknown): v is boolean => { 15 | return typeof v === 'boolean' 16 | } 17 | 18 | export const isNum = (v: unknown): v is number => { 19 | return typeof v === 'number' 20 | } 21 | 22 | export function nil() { 23 | return undefined 24 | } 25 | 26 | export function getValueStateKey(v: unknown): 'boolValue' | 'intValue' | 'binaryValue' | 'stringValue' { 27 | if (isBool(v)) { 28 | return 'boolValue' 29 | } 30 | 31 | if (isNum(v)) { 32 | return 'intValue' 33 | } 34 | 35 | if (v instanceof Uint8Array) { 36 | return 'binaryValue' 37 | } 38 | 39 | return 'stringValue' 40 | } 41 | 42 | export function _parseDataEntry(d: DataEntry): TVal { 43 | if ('stringValue' in d && !isUndefined(d.stringValue)) { 44 | return d.stringValue 45 | } 46 | 47 | if ('intValue' in d && !isUndefined(d.intValue)) { 48 | return Number(d.intValue) 49 | } 50 | 51 | if ('boolValue' in d && !isUndefined(d.boolValue)) { 52 | return d.boolValue 53 | } 54 | 55 | if ('binaryValue' in d && !isUndefined(d.binaryValue)) { 56 | return Buffer.from(d.binaryValue) 57 | } 58 | } 59 | 60 | 61 | export function isPrimitive(v: ObjectConstructor) { 62 | return ( 63 | v.prototype === String.prototype || 64 | v.prototype === Number.prototype || 65 | v.prototype === Boolean.prototype || 66 | v.prototype === Buffer.prototype 67 | ) 68 | } 69 | 70 | export function isWrappedType(v: unknown) { 71 | return Long === v 72 | } 73 | 74 | export const getCpusCount = () => os.cpus().length 75 | -------------------------------------------------------------------------------- /packages/contract-core/src/utils/marshal.ts: -------------------------------------------------------------------------------- 1 | import { DataEntry } from '@wavesenterprise/we-node-grpc-api' 2 | import { getValueStateKey } from './index' 3 | 4 | export function params(value: Record): DataEntry[] { 5 | const result: DataEntry[] = [] 6 | for (const [key, val] of Object.entries(value)) { 7 | const stateKey = getValueStateKey(val) 8 | 9 | // typescript 10 | switch (stateKey) { 11 | case 'boolValue': 12 | result.push({ 13 | key, 14 | [stateKey]: val as any, 15 | }) 16 | break 17 | case 'intValue': 18 | result.push({ 19 | key, 20 | [stateKey]: val as any, 21 | }) 22 | break 23 | 24 | case 'stringValue': 25 | result.push({ 26 | key, 27 | [stateKey]: val as any, 28 | }) 29 | break 30 | case 'binaryValue': 31 | result.push({ 32 | key, 33 | [stateKey]: val as any, 34 | }) 35 | 36 | break 37 | } 38 | } 39 | 40 | return result 41 | } 42 | -------------------------------------------------------------------------------- /packages/contract-core/src/utils/workers/static-pool.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-redeclare */ 2 | import { SHARE_ENV, Worker, WorkerOptions } from 'worker_threads' 3 | import { URL } from 'node:url' 4 | import { EventEmitter } from 'events' 5 | import { CommonLogger } from '../../api' 6 | 7 | type WorkerPoolProps = { 8 | size: number, 9 | filename: string, 10 | contractPath: string, 11 | } 12 | 13 | export class WorkerPoolWorker extends Worker { 14 | 15 | initialized = false 16 | 17 | idle = true 18 | 19 | constructor(filename: string | URL, options?: WorkerOptions) { 20 | super(filename, { 21 | ...options, 22 | // stderr: true, 23 | // stdin: true, 24 | // stdout: true, 25 | }) 26 | } 27 | 28 | init() { 29 | if (this.initialized) { 30 | return 31 | } 32 | this.once('online', () => { 33 | CommonLogger.verbose('Worker is online') 34 | this.initialized = true 35 | this.emit('ready') 36 | }) 37 | } 38 | 39 | execute(value: Request): Promise { 40 | if (!this.initialized || !this.idle) { 41 | return Promise.reject('Worker is not ready') 42 | } 43 | this.idle = false 44 | return new Promise((resolve, reject) => { 45 | this.postMessage(value) 46 | const messageHandle = (value: Response) => { 47 | this.idle = true 48 | this.emit('ready') 49 | resolve(value) 50 | this.off('error', errorHandle) 51 | } 52 | const errorHandle = (error: unknown) => { 53 | this.idle = true 54 | this.emit('ready') 55 | reject(error) 56 | this.off('message', messageHandle) 57 | } 58 | this.once('message', messageHandle) 59 | this.once('error', errorHandle) 60 | }) 61 | } 62 | 63 | } 64 | 65 | export class WorkerPool extends EventEmitter { 66 | 67 | workers: Set> = new Set() 68 | 69 | waitingTasks: Array<(worker: WorkerPoolWorker) => void> = [] 70 | 71 | private terminated = false 72 | 73 | constructor(props: WorkerPoolProps) { 74 | super() 75 | const { size, filename, contractPath } = props 76 | this.on('worker-ready', (worker: WorkerPoolWorker) => { 77 | if (this.waitingTasks.length > 0) { 78 | this.waitingTasks.shift()!(worker) 79 | } 80 | }) 81 | for (let i = 0; i < size; i++) { 82 | this.createWorker(filename, contractPath, i) 83 | } 84 | } 85 | 86 | private createWorker(filename: string, contractPath: string, idx: number) { 87 | const worker = new WorkerPoolWorker(filename, { 88 | env: SHARE_ENV, 89 | workerData: { contractPath, index: idx }, 90 | }) 91 | worker.init() 92 | worker.on('exit', () => { 93 | this.workers.delete(worker) 94 | if (!this.terminated) { 95 | this.createWorker(filename, contractPath, idx) 96 | } 97 | }) 98 | worker.on('ready', () => { 99 | this.emit('worker-ready', worker) 100 | }) 101 | this.workers.add(worker) 102 | } 103 | 104 | private getIdleWorker(): Promise> | WorkerPoolWorker { 105 | for (const worker of this.workers) { 106 | if (worker.idle && worker.initialized) { 107 | return worker 108 | } 109 | } 110 | return new Promise((resolve) => { 111 | this.waitingTasks.push(resolve) 112 | }) 113 | } 114 | 115 | async execute(value: Request): Promise { 116 | if (this.terminated) { 117 | throw new Error('Pool is terminated') 118 | } 119 | CommonLogger.verbose('Waiting for idle worker') 120 | const worker = await this.getIdleWorker() 121 | CommonLogger.verbose('Worker found, executing') 122 | return worker.execute(value) 123 | } 124 | 125 | terminate() { 126 | const terminated: Array> = [] 127 | this.terminated = true 128 | this.waitingTasks = [] 129 | for (const worker of this.workers) { 130 | terminated.push(worker.terminate()) 131 | } 132 | return Promise.all(terminated) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /packages/contract-core/test/assets.spec.ts: -------------------------------------------------------------------------------- 1 | import { mockAction } from './mocks/contract-transaction-response' 2 | import { Action, Assets, AssetsService, AttachedPayments, Contract, ExecutionContext, Payments } from '../src' 3 | import Long from 'long' 4 | import { createContractProcessor } from './mocks/contract-processor' 5 | import { ContractAddressService, ContractService } from '@wavesenterprise/we-node-grpc-api' 6 | 7 | 8 | describe('Asset Operations', () => { 9 | 10 | @Contract() 11 | class TestContract { 12 | 13 | @Assets() 14 | assets!: AssetsService 15 | 16 | @Action() 17 | async operations() { 18 | 19 | const TestAsset = await this.assets.issueAsset({ 20 | name: 'Habr/Rbah-LP', 21 | description: 'HabrAMM Habr/Rbah LP Shares', 22 | decimals: 8, 23 | quantity: new Long(1000000), 24 | isReissuable: true, 25 | }) 26 | 27 | TestAsset.transfer({ 28 | amount: new Long(1000), 29 | recipient: 'me', 30 | }) 31 | 32 | TestAsset.burn({ 33 | amount: new Long(2000), 34 | }) 35 | 36 | TestAsset.reissue({ 37 | isReissuable: true, 38 | quantity: new Long(3000), 39 | }) 40 | } 41 | 42 | @Action() 43 | async transfers(@Payments() payments: AttachedPayments) { 44 | 45 | } 46 | 47 | @Action() 48 | async balances() { 49 | const TestAsset = this.assets.getAsset('test') 50 | await TestAsset.getBalanceOf('me') 51 | await TestAsset.getBalanceOf() 52 | } 53 | } 54 | 55 | it('must perform asset manipulations', async () => { 56 | const tx = mockAction('operations') 57 | const { processor, success } = createContractProcessor(TestContract) 58 | const assetId = '123' 59 | jest.spyOn(AssetsService.prototype, 'calculateAssetId').mockImplementation(() => Promise.resolve(assetId)) 60 | await processor.handleIncomingTx(tx) 61 | const resultContext = success.mock.calls[0][0] as ExecutionContext 62 | expect(resultContext.assets.getOperations()).toEqual([ 63 | { 64 | contractIssue: { 65 | assetId, 66 | nonce: 1, 67 | name: 'Habr/Rbah-LP', 68 | description: 'HabrAMM Habr/Rbah LP Shares', 69 | decimals: 8, 70 | quantity: new Long(1000000), 71 | isReissuable: true, 72 | }, 73 | }, 74 | { 75 | contractTransferOut: { 76 | assetId, 77 | recipient: 'me', 78 | amount: new Long(1000), 79 | }, 80 | }, 81 | { 82 | contractBurn: { 83 | assetId, 84 | amount: new Long(2000), 85 | }, 86 | }, 87 | { 88 | contractReissue: { 89 | assetId, 90 | isReissuable: true, 91 | quantity: new Long(3000), 92 | }, 93 | }, 94 | ]) 95 | }) 96 | 97 | it('must receive incoming transfers', async () => { 98 | const tx = mockAction('transfers') 99 | const { processor } = createContractProcessor(TestContract) 100 | const action = jest.spyOn(TestContract.prototype, 'transfers') 101 | await processor.handleIncomingTx(tx) 102 | expect(action).toHaveBeenCalledWith([{ 103 | assetId: 'test', 104 | amount: new Long(10000), 105 | }]) 106 | }) 107 | 108 | it('must request balances', async () => { 109 | const tx = mockAction('balances') 110 | const { processor } = createContractProcessor(TestContract) 111 | const addressService = jest.spyOn(ContractAddressService.prototype, 'getAssetBalance') 112 | .mockImplementation(() => Promise.resolve({ 113 | assetId: new Uint8Array(), 114 | amount: new Long(1000), 115 | decimals: 8, 116 | })) 117 | const contractService = jest.spyOn(ContractService.prototype, 'getContractBalances') 118 | .mockImplementation(() => Promise.resolve([{ 119 | assetId: '', 120 | amount: new Long(1000), 121 | decimals: 8, 122 | }])) 123 | await processor.handleIncomingTx(tx) 124 | expect(contractService).toHaveBeenCalledTimes(1) 125 | expect(addressService).toHaveBeenCalledTimes(1) 126 | }) 127 | 128 | }) 129 | -------------------------------------------------------------------------------- /packages/contract-core/test/contract-processor.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata' 2 | import { ContractProcessor } from '../src/execution/contract-processor' 3 | import { Action, AttachedPayments, Contract, Ctx, Payments } from '../src' 4 | import { 5 | ContractError, 6 | ExecutionContext, 7 | RetryableError, 8 | UnavailableContractActionException, 9 | UnavailableContractParamException, 10 | } from '../src/execution' 11 | import { createContractProcessor } from './mocks/contract-processor' 12 | import { mockAction } from './mocks/contract-transaction-response' 13 | import { ReservedParamNames } from '../src/execution/constants' 14 | 15 | @Contract() 16 | class TestContract { 17 | @Action() 18 | test() { 19 | 20 | } 21 | 22 | @Action() 23 | fatal() { 24 | throw new ContractError('Somethin went wrong') 25 | } 26 | 27 | @Action() 28 | retryable() { 29 | throw new RetryableError('Somethin went wrong') 30 | } 31 | 32 | 33 | @Action() 34 | params( 35 | @Payments() payments: AttachedPayments, 36 | @Ctx() ctx: ExecutionContext, 37 | ) { 38 | 39 | console.log(ctx, payments) 40 | } 41 | } 42 | 43 | 44 | describe('ContractProcessor', () => { 45 | 46 | it('should execute test action successfully', async function () { 47 | const { processor, success, error } = createContractProcessor(TestContract) 48 | await processor.handleIncomingTx(mockAction('test')) 49 | 50 | expect(success).toBeCalled() 51 | expect(error).not.toBeCalled() 52 | }) 53 | 54 | 55 | it('should reject execution with not found action', async function () { 56 | const { processor, error } = createContractProcessor(TestContract) 57 | await processor.handleIncomingTx(mockAction('unknown')) 58 | 59 | expect(error).toHaveBeenCalledWith(expect.anything(), new UnavailableContractActionException('unknown')) 60 | }) 61 | 62 | it('should reject execution with not found param action in tx', async function () { 63 | const { processor, error } = createContractProcessor(TestContract) 64 | const action = mockAction('unknown') 65 | action.transaction.params = [] 66 | console.log(action) 67 | await processor.handleIncomingTx(action) 68 | 69 | expect(error).toHaveBeenCalledWith(expect.anything(), new UnavailableContractParamException(ReservedParamNames.action)) 70 | }) 71 | 72 | it('should reject execution by fatal error in contract action', async function () { 73 | const { processor, error } = createContractProcessor(TestContract) 74 | await processor.handleIncomingTx(mockAction('fatal')) 75 | 76 | expect(error).toHaveBeenCalledWith(expect.anything(), new ContractError('Somethin went wrong')) 77 | }) 78 | 79 | it('should reject execution by retryable error in contract action', async function () { 80 | const { processor, error } = createContractProcessor(TestContract) 81 | await processor.handleIncomingTx(mockAction('fatal')) 82 | 83 | expect(error).toHaveBeenCalledWith(expect.anything(), new RetryableError('Somethin went wrong')) 84 | }) 85 | }) 86 | -------------------------------------------------------------------------------- /packages/contract-core/test/converter.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata' 2 | import { 3 | ContractTransactionResponse as RawContractTransactionResponse, 4 | } from '@wavesenterprise/js-contract-grpc-client/contract/contract_contract_service' 5 | 6 | import { DataEntry } from '@wavesenterprise/js-contract-grpc-client/data_entry' 7 | import { ContractTransferIn } from '@wavesenterprise/js-contract-grpc-client/contract_transfer_in' 8 | import { IncomingTx } from '../src/execution/types' 9 | import { convertContractTransaction } from '../src/execution/converter' 10 | import { ContractTransaction } from '@wavesenterprise/we-node-grpc-api' 11 | 12 | 13 | describe('TransactionConverter', () => { 14 | let mockTx = RawContractTransactionResponse.fromPartial({ 15 | authToken: 'test-token', 16 | transaction: { 17 | id: 'some-tx-id', 18 | type: 104, 19 | sender: 'iam', 20 | senderPublicKey: 'mypc', 21 | contractId: 'test-contract', 22 | params: [ 23 | DataEntry.fromPartial({ 24 | intValue: 10000, 25 | key: 'integer-key', 26 | }), 27 | DataEntry.fromPartial({ 28 | stringValue: 'test', 29 | key: 'string-key', 30 | }), 31 | ], 32 | version: 5, 33 | proofs: new Uint8Array(), 34 | timestamp: new Date().getTime(), 35 | feeAssetId: { 36 | value: 'WAVES', 37 | }, 38 | payments: [ 39 | ContractTransferIn.fromPartial({ 40 | amount: 1000000, 41 | }), 42 | ContractTransferIn.fromPartial({ 43 | amount: 1000000, 44 | assetId: 'assetId', 45 | }), 46 | ], 47 | }, 48 | }) 49 | 50 | describe('parce incoming tx', () => { 51 | let tx: IncomingTx 52 | 53 | beforeAll(() => { 54 | tx = convertContractTransaction(mockTx.transaction as ContractTransaction) 55 | }) 56 | 57 | it('should parse transaction response to incoming', async function () { 58 | expect(tx.id).toEqual('some-tx-id') 59 | 60 | expect(tx.payments[0].amount.toNumber()).toEqual(1000000) 61 | expect(tx.payments[1].amount.toNumber()).toEqual(1000000) 62 | }) 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /packages/contract-core/test/metadata.spec.ts: -------------------------------------------------------------------------------- 1 | import { Action, Param } from '../src' 2 | import { getArgsMetadata, getContractMetadata } from '../src/execution/reflect' 3 | 4 | describe('Decorators', () => { 5 | // describe('@Contract', () => { 6 | // it('should apply decorator to class ', () => { 7 | // const target = class { 8 | // } 9 | // 10 | // Contract()(target) 11 | // 12 | // expect(ContractRegistry.getDefault()).toEqual(target) 13 | // }) 14 | // }) 15 | 16 | describe('@Action', () => { 17 | it('should add action with propertyName equals method name', () => { 18 | class TestClass { 19 | @Action() 20 | test() { 21 | } 22 | } 23 | 24 | const meta = getContractMetadata(TestClass) 25 | 26 | expect(meta.actions.test.propertyName).toEqual('test') 27 | expect(meta.actions.test.params.length).toEqual(0) 28 | expect(meta.actions.test.name).toEqual('test') 29 | }) 30 | 31 | it('should add action with propertyName from param decorator', () => { 32 | class TestClass { 33 | @Action({ name: 'specificName' }) 34 | test() { 35 | } 36 | } 37 | 38 | const meta = getContractMetadata(TestClass) 39 | 40 | expect(meta.actions.specificName.propertyName).toEqual('test') 41 | expect(meta.actions.specificName.params.length).toEqual(0) 42 | expect(meta.actions.specificName.name).toEqual('specificName') 43 | }) 44 | 45 | it('should add initializer action', () => { 46 | class TestClass { 47 | @Action({ name: 'specificName', onInit: true }) 48 | test() { 49 | } 50 | } 51 | 52 | const meta = getContractMetadata(TestClass) 53 | 54 | expect(meta.initializer.propertyName).toEqual('test') 55 | expect(meta.initializer.params.length).toEqual(0) 56 | expect(meta.initializer.name).toEqual('specificName') 57 | }) 58 | }) 59 | 60 | describe('@Param', () => { 61 | it('should add param with type string', () => { 62 | class TestClass { 63 | @Action() 64 | test( 65 | @Param('name') stringValue: string, 66 | ) { 67 | } 68 | } 69 | 70 | const meta = getArgsMetadata(TestClass, 'test') 71 | 72 | expect(meta['arg:0'].paramKey).toEqual('name') 73 | expect(meta['arg:0'].index).toEqual(0) 74 | }) 75 | 76 | it('should add param with type number', () => { 77 | class TestClass { 78 | @Action() 79 | test( 80 | @Param('numberParam') numberParam: number, 81 | ) { 82 | } 83 | } 84 | 85 | const meta = getArgsMetadata(TestClass, 'test') 86 | 87 | expect(meta['arg:0'].paramKey).toEqual('numberParam') 88 | expect(meta['arg:0'].index).toEqual(0) 89 | }) 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /packages/contract-core/test/mocks/contract-processor.ts: -------------------------------------------------------------------------------- 1 | import { ContractProcessor } from '../../src/execution/contract-processor' 2 | import { createGrpcClient } from './grpc-client' 3 | 4 | export const createContractProcessor = (contract: unknown) => { 5 | const processor = new ContractProcessor(contract, createGrpcClient()) 6 | const success = jest.spyOn(processor, 'tryCommitSuccess') 7 | const error = jest.spyOn(processor, 'tryCommitError') 8 | success.mockImplementation(() => Promise.resolve()) 9 | error.mockImplementation((...args) => { 10 | // eslint-disable-next-line no-console 11 | console.log('error', args) 12 | return Promise.resolve() 13 | }) 14 | return { processor, success, error } 15 | } 16 | -------------------------------------------------------------------------------- /packages/contract-core/test/mocks/contract-transaction-response.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ContractTransactionResponse as ContractTransactionResponseRaw, 3 | } from '@wavesenterprise/js-contract-grpc-client/contract/contract_contract_service' 4 | import { ContractTransferIn } from '@wavesenterprise/js-contract-grpc-client/contract_transfer_in' 5 | import { ContractTransactionResponse } from '@wavesenterprise/we-node-grpc-api' 6 | 7 | export function mockAction(action: string) { 8 | return ContractTransactionResponseRaw.fromPartial({ 9 | authToken: 'test-token', 10 | transaction: { 11 | id: 'some-tx-id', 12 | type: 104, 13 | sender: 'iam', 14 | senderPublicKey: 'mypc', 15 | contractId: 'test-contract', 16 | params: [ 17 | { 18 | stringValue: action, 19 | key: 'action', 20 | }, 21 | ], 22 | version: 5, 23 | payments: [ 24 | ContractTransferIn.fromPartial({ 25 | assetId: 'test', 26 | amount: 10000, 27 | }), 28 | ], 29 | proofs: new Uint8Array(), 30 | timestamp: new Date().getTime(), 31 | }, 32 | currentBlockInfo: { 33 | height: 1, 34 | timestamp: Date.now(), 35 | minerAddress: '', 36 | reference: '', 37 | }, 38 | }) as ContractTransactionResponse 39 | } 40 | -------------------------------------------------------------------------------- /packages/contract-core/test/mocks/grpc-client.ts: -------------------------------------------------------------------------------- 1 | import { GrpcClient } from '../../src/grpc/grpc-client' 2 | 3 | export const createGrpcClient = () => { 4 | return new GrpcClient({ 5 | nodeAddress: 'localhost:3000', 6 | connectionToken: '', 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /packages/contract-core/test/params-extractor.spec.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext } from '../src/execution' 2 | import { mockAction } from './mocks/contract-transaction-response' 3 | import { Action, AttachedPayments, Container, Contract, Ctx, Param, Payments } from '../src' 4 | import { ParamsExtractor } from '../src/execution/params-extractor' 5 | import Long from 'long' 6 | import { createGrpcClient } from './mocks/grpc-client' 7 | 8 | 9 | describe('Param Extractors', () => { 10 | let extractor: ParamsExtractor 11 | 12 | beforeAll(() => { 13 | extractor = new ParamsExtractor() 14 | }) 15 | 16 | 17 | it('should apply @Payments decorator to action', function () { 18 | @Contract() 19 | class TestContract { 20 | @Action 21 | test( 22 | @Payments() attachedPayments: AttachedPayments, 23 | ) { 24 | console.log(attachedPayments) 25 | } 26 | } 27 | 28 | const ec = new ExecutionContext(mockAction('test'), createGrpcClient()) 29 | 30 | Container.set(ec) 31 | 32 | const { args } = extractor.extract(TestContract, ec) 33 | 34 | 35 | expect((args[0] as AttachedPayments)[0].assetId).toEqual('test') 36 | expect((args[0] as AttachedPayments)[0].amount.toNumber()).toEqual(10000) 37 | }) 38 | 39 | it('should apply @Ctx decorator to action', function () { 40 | @Contract() 41 | class TestContract { 42 | @Action 43 | test( 44 | @Ctx() ctx: ExecutionContext, 45 | @Payments() payments: AttachedPayments, 46 | ) { 47 | console.log(ctx, payments) 48 | } 49 | } 50 | 51 | const ec = new ExecutionContext(mockAction('test'), createGrpcClient()) 52 | 53 | Container.set(ec) 54 | 55 | const { args } = extractor.extract(TestContract, ec) 56 | expect(args[0]).toEqual(ec) 57 | }) 58 | 59 | it('should apply @Param decorator to action ', () => { 60 | @Contract() 61 | class TestContract { 62 | @Action 63 | test( 64 | @Param('key') value: string, 65 | @Param('next') big: Long, 66 | @Param('binary') buf: Buffer, 67 | ) { 68 | 69 | } 70 | } 71 | 72 | const tx = mockAction('test') 73 | 74 | tx.transaction.params.push( 75 | { 76 | stringValue: 'testValue', 77 | key: 'key', 78 | }, 79 | { 80 | intValue: new Long(2), 81 | key: 'next', 82 | }, 83 | { 84 | binaryValue: new Uint8Array([2, 3, 4, 1]), 85 | key: 'binary', 86 | }, 87 | ) 88 | 89 | const ec = new ExecutionContext(tx, createGrpcClient()) 90 | 91 | const { args } = extractor.extract(TestContract, ec) 92 | 93 | expect(args[0]).toEqual('testValue') 94 | expect((args[1] as Long).toNumber()).toEqual(2) 95 | }) 96 | }) 97 | -------------------------------------------------------------------------------- /packages/contract-core/test/state.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Action, 3 | Contract, 4 | ContractMapping, 5 | ContractState, 6 | ContractValue, 7 | ExecutionContext, 8 | preload, 9 | State, 10 | Var, 11 | } from '../src' 12 | import { mockAction } from './mocks/contract-transaction-response' 13 | import { ContractKeysRequest } from '@wavesenterprise/js-contract-grpc-client/contract/contract_contract_service' 14 | import { createContractProcessor } from './mocks/contract-processor' 15 | import Long from 'long' 16 | import { ContractService } from '@wavesenterprise/we-node-grpc-api' 17 | 18 | 19 | describe('State', () => { 20 | 21 | beforeEach(() => { 22 | jest.restoreAllMocks(); 23 | }) 24 | 25 | describe('ContractState', () => { 26 | 27 | it('should set string value', async () => { 28 | class TestContract { 29 | @State state: ContractState 30 | 31 | @Action 32 | test() { 33 | this.state.set('str', 'str1') 34 | this.state.set('int', 100) 35 | this.state.set('bool', false) 36 | this.state.set('bin', new Uint8Array([0, 1, 2])) 37 | } 38 | } 39 | 40 | const resp = mockAction('test') 41 | 42 | const { processor, success } = createContractProcessor(TestContract) 43 | 44 | await processor.handleIncomingTx(resp) 45 | 46 | const context = success.mock.calls[0][0] as ExecutionContext 47 | 48 | expect(context.state.getUpdatedEntries()).toEqual([ 49 | { 50 | key: 'str', 51 | stringValue: 'str1', 52 | }, 53 | { 54 | key: 'int', 55 | intValue: new Long(100), 56 | }, 57 | { 58 | key: 'bool', 59 | boolValue: false, 60 | }, 61 | { 62 | key: 'bin', 63 | binaryValue: new Uint8Array([0, 1, 2]), 64 | }, 65 | ]) 66 | }) 67 | }) 68 | 69 | describe('Properties', () => { 70 | @Contract() 71 | class TestContract { 72 | @Var() intVar: ContractValue 73 | @Var() decimalVar: ContractValue 74 | @Var() myVar: ContractValue 75 | @Var() user: ContractMapping 76 | 77 | @Action 78 | test() { 79 | this.myVar.set('test') 80 | this.user.set('user1', 'value1') 81 | } 82 | 83 | @Action 84 | async getterTest() { 85 | await this.myVar.get() 86 | await this.user.get('user1') 87 | } 88 | 89 | @Action 90 | async preloadInActionVars() { 91 | await preload(this, ['intVar', 'decimalVar', ['user', 'user1']]) 92 | 93 | await this.intVar.get() 94 | await this.decimalVar.get() 95 | await this.user.get('user1') 96 | } 97 | } 98 | 99 | it('should get value by propertyKey', async function () { 100 | const resp = mockAction('getterTest') 101 | 102 | const { processor } = createContractProcessor(TestContract) 103 | const getKey = jest.spyOn(ContractService.prototype, 'getContractKey') 104 | .mockImplementationOnce(() => Promise.resolve({ 105 | key: 'myVar', 106 | stringValue: 'test', 107 | })) 108 | .mockImplementationOnce(() => Promise.resolve({ 109 | key: 'user_user1', 110 | stringValue: 'nice', 111 | })) 112 | await processor.handleIncomingTx(resp) 113 | expect(getKey).toHaveBeenCalledTimes(2) 114 | expect(getKey).toHaveBeenNthCalledWith(2, 115 | { 116 | contractId: 'test-contract', 117 | key: 'user_user1', 118 | }, 119 | ) 120 | expect(getKey).toHaveBeenNthCalledWith(1, 121 | { 122 | contractId: 'test-contract', 123 | key: 'myVar', 124 | }, 125 | ) 126 | }) 127 | 128 | it('should initialize proxy state value', async function () { 129 | const resp = mockAction('test') 130 | 131 | const { processor, success } = createContractProcessor(TestContract) 132 | 133 | await processor.handleIncomingTx(resp) 134 | 135 | const context = success.mock.calls[0][0] as ExecutionContext 136 | 137 | expect(context.state.getUpdatedEntries()).toEqual([ 138 | { 139 | key: 'myVar', 140 | stringValue: 'test', 141 | }, 142 | { 143 | key: 'user_user1', 144 | stringValue: 'value1', 145 | }, 146 | ]) 147 | }) 148 | 149 | it('should preload keys in a batch request', async function () { 150 | const resp = mockAction('preloadInActionVars') 151 | 152 | const { processor } = createContractProcessor(TestContract) 153 | const getKeys = jest.spyOn(ContractService.prototype, 'getContractKeys') 154 | .mockImplementation(() => Promise.resolve([ 155 | { 156 | key: 'intVar', 157 | intValue: new Long(10), 158 | }, 159 | { 160 | key: 'decimalVar', 161 | intValue: new Long(20), 162 | }, 163 | { 164 | key: 'user_user1', 165 | stringValue: 'nice', 166 | }, 167 | ])) 168 | const getKey = jest.spyOn(ContractService.prototype, 'getContractKey') 169 | .mockImplementation(() => Promise.resolve({ 170 | key: '', 171 | intValue: new Long(0), 172 | })) 173 | await processor.handleIncomingTx(resp) 174 | 175 | expect(getKeys).toBeCalledWith( 176 | ContractKeysRequest.fromPartial({ 177 | contractId: 'test-contract', 178 | keysFilter: { 179 | keys: ['intVar', 'decimalVar', 'user_user1'], 180 | }, 181 | }), 182 | ) 183 | expect(getKey).not.toHaveBeenCalled() 184 | }) 185 | }) 186 | }) 187 | -------------------------------------------------------------------------------- /packages/contract-core/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["node", "jest"], 5 | "module": "commonjs", 6 | "preserveSymlinks": true, 7 | "declaration": true, 8 | "removeComments": true, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "esModuleInterop": true, 12 | "declarationMap": true, 13 | "allowSyntheticDefaultImports": true, 14 | "moduleResolution": "node", 15 | "target": "es2020", 16 | "sourceMap": true, 17 | "sourceRoot": "./src", 18 | "outDir": "./dist", 19 | "baseUrl": "./src", 20 | "incremental": true, 21 | "skipLibCheck": true, 22 | "strictNullChecks": true, 23 | "noImplicitAny": false, 24 | "strictBindCallApply": false, 25 | "forceConsistentCasingInFileNames": false, 26 | "noFallthroughCasesInSwitch": false, 27 | "resolveJsonModule": true 28 | }, 29 | "exclude": ["node_modules", "dist", "test"] 30 | } 31 | -------------------------------------------------------------------------------- /packages/contract-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["node", "jest"], 5 | "module": "commonjs", 6 | "preserveSymlinks": true, 7 | "declaration": true, 8 | "removeComments": true, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "esModuleInterop": true, 12 | "declarationMap": true, 13 | "allowSyntheticDefaultImports": true, 14 | "moduleResolution": "node", 15 | "target": "es2020", 16 | "sourceMap": true, 17 | "sourceRoot": "./src", 18 | "outDir": "./dist", 19 | "baseUrl": "./src", 20 | "incremental": true, 21 | "skipLibCheck": true, 22 | "strictNullChecks": true, 23 | "noImplicitAny": false, 24 | "strictBindCallApply": false, 25 | "forceConsistentCasingInFileNames": false, 26 | "noFallthroughCasesInSwitch": false, 27 | "resolveJsonModule": true 28 | }, 29 | "exclude": ["node_modules", "dist", "test"] 30 | } 31 | -------------------------------------------------------------------------------- /packages/contract-core/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | 4 | "compilerOptions": { 5 | "experimentalDecorators": true, 6 | "types": [ 7 | "jest", 8 | "node" 9 | ] 10 | } 11 | } -------------------------------------------------------------------------------- /packages/create-we-contract/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [1.2.2](https://gitlab.wvservices.com/waves-enterprise/js-contract-sdk/compare/create-we-contract@1.2.1...create-we-contract@1.2.2) (2022-11-22) 7 | 8 | 9 | ### Bug Fixes 10 | 11 | * **create-we-contract:** fix publish ([1d92350](https://gitlab.wvservices.com/waves-enterprise/js-contract-sdk/commit/1d92350b142219f39160957c358f5a6b64505d95)) 12 | * **create-we-contract:** fix templates folder missing ([d27193d](https://gitlab.wvservices.com/waves-enterprise/js-contract-sdk/commit/d27193d7119b7ca785189b13805859c6fc7d1f8c)) 13 | 14 | 15 | 16 | 17 | 18 | ## [1.2.1](https://gitlab.wvservices.com/waves-enterprise/js-contract-sdk/compare/create-we-contract@1.2.0...create-we-contract@1.2.1) (2022-11-22) 19 | 20 | 21 | ### Bug Fixes 22 | 23 | * **create-we-contract:** fix dirname ([ae948e7](https://gitlab.wvservices.com/waves-enterprise/js-contract-sdk/commit/ae948e7682231358bdb8d04dd7be2ff05aa65217)) 24 | 25 | 26 | 27 | 28 | 29 | # [1.2.0](https://github.com/waves-enterprise/js-contract-sdk/compare/create-we-contract@1.1.0...create-we-contract@1.2.0) (2022-11-08) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * **create-we-contract:** fix npm create compatibility ([fd2a736](https://github.com/waves-enterprise/js-contract-sdk/commit/fd2a736bea3c178ee4b12e2dc168712c3bd8eb2c)) 35 | 36 | 37 | 38 | 39 | 40 | ## [1.0.7](https://github.com/waves-enterprise/js-contract-sdk/compare/create-we-contract@1.0.6...create-we-contract@1.0.7) (2022-07-26) 41 | 42 | **Note:** Version bump only for package create-we-contract 43 | 44 | 45 | 46 | 47 | 48 | ## [1.0.6](https://github.com/waves-enterprise/js-contract-sdk/compare/create-we-contract@1.0.5...create-we-contract@1.0.6) (2022-07-26) 49 | 50 | **Note:** Version bump only for package create-we-contract 51 | 52 | 53 | 54 | 55 | 56 | ## [1.0.2](https://github.com/waves-enterprise/js-contract-sdk/compare/create-we-contract@1.0.1...create-we-contract@1.0.2) (2022-05-18) 57 | 58 | 59 | ### Bug Fixes 60 | 61 | * **create-we-contract:** bump versions, add we-cli to dependencies ([e4f574a](https://github.com/waves-enterprise/js-contract-sdk/commit/e4f574ada1c57d845799e499209cee2ee233abb0)) 62 | -------------------------------------------------------------------------------- /packages/create-we-contract/LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2022 Waves Enterprise 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /packages/create-we-contract/README.md: -------------------------------------------------------------------------------- 1 | # create-we-contract 2 | 3 | Quickstart with Waves Enterprise platform contracts 4 | 5 | ## Prerequisites 6 | 7 | * Node.js 8 | 9 | ## Getting Started 10 | 11 | Run following command for create first project with default configuration 12 | 13 | ```sh 14 | npm create we-contract MyContract 15 | ``` 16 | 17 | Options: 18 | 19 | * `-t path-to-folder` - specify folder to scaffold project, default we-contract-starter 20 | * `-n package-name` - specify name in package.json of your project 21 | 22 | ## License 23 | 24 | This project is licensed under the [MIT License](LICENSE). 25 | 26 | -------------------------------------------------------------------------------- /packages/create-we-contract/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-we-contract", 3 | "version": "1.2.3", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "create-we-contract", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "commander": "^9.1.0" 13 | }, 14 | "bin": { 15 | "create-contract": "index.js" 16 | } 17 | }, 18 | "node_modules/commander": { 19 | "version": "9.1.0", 20 | "resolved": "https://registry.npmjs.org/commander/-/commander-9.1.0.tgz", 21 | "integrity": "sha512-i0/MaqBtdbnJ4XQs4Pmyb+oFQl+q0lsAmokVUH92SlSw4fkeAcG3bVon+Qt7hmtF+u3Het6o4VgrcY3qAoEB6w==", 22 | "engines": { 23 | "node": "^12.20.0 || >=14" 24 | } 25 | } 26 | }, 27 | "dependencies": { 28 | "commander": { 29 | "version": "9.1.0", 30 | "resolved": "https://registry.npmjs.org/commander/-/commander-9.1.0.tgz", 31 | "integrity": "sha512-i0/MaqBtdbnJ4XQs4Pmyb+oFQl+q0lsAmokVUH92SlSw4fkeAcG3bVon+Qt7hmtF+u3Het6o4VgrcY3qAoEB6w==" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/create-we-contract/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-we-contract", 3 | "version": "1.2.3", 4 | "description": "Scaffold Waves Enterprise JS Contract project", 5 | "private": false, 6 | "type": "module", 7 | "module": "./dist/index.mjs", 8 | "types": "./dist/index.d.ts", 9 | "files": [ 10 | "dist", 11 | "template" 12 | ], 13 | "exports": { 14 | ".": { 15 | "import": "./dist/index.mjs" 16 | } 17 | }, 18 | "publishConfig": { 19 | "access": "public" 20 | }, 21 | "bin": { 22 | "create-contract": "./dist/index.mjs" 23 | }, 24 | "author": { 25 | "name": "Timofey Semenyuk", 26 | "email": "rustfy@gmail.com", 27 | "url": "https://github.com/stfy" 28 | }, 29 | "scripts": { 30 | "build": "unbuild", 31 | "typecheck": "npx tsc --noEmit", 32 | "prepublish": "npm run build" 33 | }, 34 | "engines": { 35 | "node": ">=14.16" 36 | }, 37 | "license": "MIT", 38 | "dependencies": { 39 | "chalk": "^5.0.1", 40 | "commander": "^9.1.0", 41 | "execa": "^6.1.0", 42 | "fs-extra": "^10.1.0", 43 | "ora": "^6.1.2", 44 | "pascal-case": "^3.1.2", 45 | "sade": "^1.8.1" 46 | }, 47 | "devDependencies": { 48 | "@types/fs-extra": "^9.0.13", 49 | "@types/node": "^18.8.0", 50 | "ts-node": "^10.9.1", 51 | "typescript": "^4.8.4", 52 | "unbuild": "^0.8.11" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/create-we-contract/src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint-disable max-len,no-console */ 3 | 4 | import sade from 'sade' 5 | import ora from 'ora' 6 | import chalk from 'chalk' 7 | import * as process from 'process' 8 | import fs from 'fs-extra' 9 | import path from 'path' 10 | import { pascalCase } from 'pascal-case' 11 | import { getInstallCmd } from './install-cmd' 12 | import { execa } from 'execa' 13 | import pkg from '../package.json' 14 | import { fileURLToPath } from 'url' 15 | 16 | const { realpath } = fs 17 | const prog = sade('we-create-contract ', true) 18 | 19 | prog 20 | .version(pkg.version) 21 | .option('--path', 'Folder to create contract', 'we-contract-starter') 22 | .describe('Create a new contract') 23 | .example('create mypackage') 24 | .action(async (contractName: string, opts: { path: string }) => { 25 | console.log( 26 | chalk.green(` 27 | | | /| / / _ | | / / __/ __/ / __/ |/ /_ __/ __/ _ \\/ _ \\/ _/ __/ __/ 28 | | |/ |/ / __ | |/ / _/_\\ \\ / _// / / / / _// , _/ ____/ /_\\ \\/ _/ 29 | |__/|__/_/ |_|___/___/___/ /___/_/|_/ /_/ /___/_/|_/_/ /___/___/___/ 30 | `), 31 | ) 32 | 33 | const bootSpinner = ora(`Scaffolding Waves Enterpise JS Contract in ${chalk.bold.green(opts.path)}`) 34 | bootSpinner.start() 35 | 36 | if (!contractName) { 37 | bootSpinner.fail() 38 | 39 | console.log(` 40 | Contract name is not specified. 41 | Try ${chalk.yellowBright('we-create-contract MyContract')} 42 | `) 43 | 44 | process.exit(0) 45 | } 46 | 47 | bootSpinner.info() 48 | 49 | const realPath = await realpath(process.cwd()) 50 | const projectPath = path.join(realPath, opts.path) 51 | const relContractPath = `./src/${contractName}.ts` 52 | const contractPath = path.resolve(projectPath, relContractPath) 53 | 54 | const __filename = fileURLToPath(import.meta.url) 55 | const __dirname = path.dirname(__filename) 56 | 57 | try { 58 | bootSpinner.start('Copy template files ...') 59 | 60 | await fs.copy( 61 | path.resolve(__dirname, '../template'), 62 | projectPath, 63 | { 64 | overwrite: true, 65 | }, 66 | ) 67 | 68 | await fs.move( 69 | path.resolve(projectPath, './_gitignore'), 70 | path.resolve(projectPath, './.gitignore'), 71 | { overwrite: true }, 72 | ) 73 | 74 | await fs.move( 75 | path.resolve(projectPath, './src/contract'), 76 | contractPath, 77 | { overwrite: true }, 78 | ) 79 | 80 | await fs.move( 81 | path.resolve(projectPath, './index'), 82 | path.resolve(projectPath, './index.ts'), 83 | { overwrite: true }, 84 | ) 85 | 86 | let contractTpl: string = await fs.readFile(contractPath, { encoding: 'utf-8' }) 87 | 88 | contractTpl = contractTpl.replace('#{contractName}', pascalCase(contractName)) 89 | 90 | await fs.writeFile(contractPath, contractTpl, { 91 | encoding: 'utf-8', 92 | }) 93 | 94 | // TODO deprecate index.ts 95 | let entrypoint: string = await fs.readFile(path.resolve(projectPath, './index.ts'), { encoding: 'utf-8' }) 96 | entrypoint = entrypoint.replace('./src/contract', relContractPath) 97 | await fs.writeFile( 98 | path.resolve(projectPath, './index.ts'), 99 | entrypoint, 100 | { 101 | encoding: 'utf-8', 102 | }, 103 | ) 104 | 105 | bootSpinner.succeed() 106 | } catch (e) { 107 | 108 | console.log(e) 109 | process.exit(0) 110 | } 111 | 112 | process.chdir(projectPath) 113 | try { 114 | const cmd = await getInstallCmd() 115 | bootSpinner.start('Installing dependencies') 116 | await execa(cmd, ['install']) 117 | bootSpinner.succeed() 118 | } catch (e) { 119 | 120 | console.log(e) 121 | process.exit(0) 122 | } 123 | 124 | console.log(` 125 | Successfully scaffolded project in ${chalk.blue(projectPath)} 126 | Start build contract by change ${chalk.greenBright(relContractPath)} 127 | `) 128 | }) 129 | .parse(process.argv) 130 | 131 | 132 | -------------------------------------------------------------------------------- /packages/create-we-contract/src/install-cmd.ts: -------------------------------------------------------------------------------- 1 | import { execa } from 'execa' 2 | 3 | let cmd: InstallCommand 4 | 5 | export type InstallCommand = 'yarn' | 'npm' 6 | 7 | export async function getInstallCmd(): Promise { 8 | if (cmd) { 9 | return cmd 10 | } 11 | 12 | try { 13 | await execa('yarnpkg', ['--version']) 14 | cmd = 'yarn' 15 | } catch (e) { 16 | cmd = 'npm' 17 | } 18 | 19 | return cmd 20 | } -------------------------------------------------------------------------------- /packages/create-we-contract/template/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | packages -------------------------------------------------------------------------------- /packages/create-we-contract/template/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14-alpine 2 | 3 | ARG HOST_NETWORK 4 | ENV HOST_NETWORK ${HOST_NETWORK} 5 | 6 | WORKDIR /usr/app 7 | 8 | ADD src src 9 | ADD tsconfig.json tsconfig.json 10 | ADD package.json package.json 11 | ADD index.ts index.ts 12 | ADD package-lock.json package-lock.json 13 | 14 | RUN npm ci 15 | RUN npm run build 16 | 17 | ADD entrypoint.sh entrypoint.sh 18 | RUN chmod +x entrypoint.sh 19 | 20 | RUN ls -l 21 | 22 | ENTRYPOINT ["/usr/app/entrypoint.sh"] 23 | -------------------------------------------------------------------------------- /packages/create-we-contract/template/_gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /packages/create-we-contract/template/contract.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | image: "contract-image", 3 | name: 'Your Contract Name', 4 | version: '0.0.1', // default=latest 5 | networks: { 6 | testnet: { 7 | seed: '#paste your seed phrase here', 8 | }, 9 | 10 | mainnet: { 11 | seed: '#paste your seed phrase here' 12 | }, 13 | sandbox: { 14 | registry: 'localhost:5000', 15 | nodeAddress: 'http://localhost:6862', 16 | seed: '#paste your seed phrase here', 17 | params: { 18 | init: () => ({ 19 | param: '${value}' 20 | }) 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /packages/create-we-contract/template/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | eval $SET_ENV_CMD 4 | node dist/index.js 5 | -------------------------------------------------------------------------------- /packages/create-we-contract/template/index: -------------------------------------------------------------------------------- 1 | import { initContract } from '@wavesenterprise/contract-core' 2 | 3 | initContract({ 4 | contractPath: __dirname + '/src/contract.js', 5 | }) 6 | -------------------------------------------------------------------------------- /packages/create-we-contract/template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-contract-sdk", 3 | "version": "1.0.1", 4 | "type": "commonjs", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "tsc -p tsconfig.json", 8 | "deploy": "we-cli deploy -n mainnet", 9 | "deploy:testnet": "we-cli deploy -n testnet", 10 | "deploy:sandbox": "we-cli deploy -n sandbox" 11 | }, 12 | "dependencies": { 13 | "@grpc/grpc-js": "^1.7.1", 14 | "@wavesenterprise/contract-core": "^1.1.5", 15 | "@wavesenterprise/crypto-utils": "^1.0.0-rc.1" 16 | }, 17 | "devDependencies": { 18 | "typescript": "^4.6.2", 19 | "@wavesenterprise/contract-cli": "^0.0.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/create-we-contract/template/src/contract: -------------------------------------------------------------------------------- 1 | import { 2 | Action, 3 | Contract, 4 | ContractMapping, 5 | ContractValue, 6 | IncomingTx, 7 | JsonVar, 8 | logger, 9 | Param, 10 | Params, 11 | Tx, 12 | Var, 13 | } from '@wavesenterprise/contract-core' 14 | import BN from 'bn.js' 15 | 16 | @Contract() 17 | export default class #{contractName} { 18 | 19 | log = logger(this) 20 | 21 | @Var() 22 | counter!: ContractValue 23 | 24 | @JsonVar() 25 | participants!: ContractMapping 26 | 27 | @Action({ onInit: true }) 28 | init(@Params() params: Record) { 29 | this.counter.set(0) 30 | this.log.info('all params', params) 31 | } 32 | 33 | @Action({ preload: ['counter'] }) 34 | async increment(@Tx tx: IncomingTx, @Param('by') by: BN) { 35 | const { senderPublicKey, sender } = tx 36 | const counter = await this.counter.get() 37 | let participant = await this.participants.tryGet(senderPublicKey) 38 | if (!participant) { 39 | participant = { 40 | publicKey: senderPublicKey, 41 | address: sender, 42 | amount: 0, 43 | } 44 | } 45 | participant.amount += by.toNumber() 46 | this.counter.set(counter + by.toNumber()) 47 | this.participants.set(senderPublicKey, participant) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/create-we-contract/template/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "esModuleInterop": true, 11 | "sourceMap": true, 12 | "outDir": "./dist", 13 | "baseUrl": ".", 14 | "incremental": true, 15 | "skipLibCheck": true, 16 | "strictNullChecks": false, 17 | "noImplicitAny": false, 18 | "strictBindCallApply": false, 19 | "forceConsistentCasingInFileNames": false, 20 | "noFallthroughCasesInSwitch": false, 21 | "resolveJsonModule": true, 22 | }, 23 | "include": ["src", "index.ts"] 24 | } -------------------------------------------------------------------------------- /packages/create-we-contract/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "esModuleInterop": true, 7 | "resolveJsonModule": true, 8 | "strict": true 9 | }, 10 | "include": [ 11 | "src" 12 | ] 13 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "preserveSymlinks": true, 5 | "declaration": true, 6 | "removeComments": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "allowSyntheticDefaultImports": true, 10 | "target": "es2017", 11 | "sourceMap": true, 12 | "outDir": "./dist", 13 | "baseUrl": "./src", 14 | "incremental": true, 15 | "skipLibCheck": true, 16 | "strictNullChecks": true, 17 | "noImplicitAny": false, 18 | "strictBindCallApply": false, 19 | "forceConsistentCasingInFileNames": false, 20 | "noFallthroughCasesInSwitch": false, 21 | "resolveJsonModule": true 22 | } 23 | } 24 | --------------------------------------------------------------------------------