├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ ├── DEV-tests.yml │ ├── PROD-publish.yml │ └── PROD-upgrade-example.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── jest.config.js ├── package.json ├── src ├── api │ ├── api.ts │ ├── client.ts │ ├── data.ts │ ├── index.ts │ ├── info │ │ ├── api.ts │ │ ├── data.ts │ │ └── index.ts │ ├── jevm │ │ ├── api.ts │ │ ├── data.ts │ │ └── index.ts │ ├── jvm │ │ ├── api.ts │ │ ├── data.ts │ │ └── index.ts │ └── platform │ │ ├── api.ts │ │ ├── data.ts │ │ └── index.ts ├── asset │ ├── asset.ts │ ├── evm.ts │ ├── index.ts │ ├── jevm.ts │ └── jvm.ts ├── chain │ ├── chain.ts │ ├── constants.ts │ ├── index.ts │ ├── jevm.ts │ ├── jvm.ts │ ├── platform.ts │ ├── solidity │ │ ├── abi.ts │ │ ├── contract.ts │ │ └── index.ts │ └── vm.ts ├── index.ts ├── juneo.ts ├── network │ ├── banana.ts │ ├── bluebyte.ts │ ├── fusion.ts │ ├── genesis.ts │ ├── index.ts │ ├── local.ts │ ├── mainnet.ts │ ├── network.ts │ └── socotra.ts ├── transaction │ ├── builder.ts │ ├── constants.ts │ ├── genesis │ │ ├── evm.ts │ │ └── index.ts │ ├── index.ts │ ├── input.ts │ ├── jevm │ │ ├── builder.ts │ │ ├── index.ts │ │ ├── status.ts │ │ └── transaction.ts │ ├── jvm │ │ ├── builder.ts │ │ ├── index.ts │ │ ├── operation.ts │ │ ├── status.ts │ │ └── transaction.ts │ ├── output.ts │ ├── platform │ │ ├── builder.ts │ │ ├── index.ts │ │ ├── status.ts │ │ ├── supernet.ts │ │ └── transaction.ts │ ├── signature.ts │ ├── transaction.ts │ └── types.ts ├── utils │ ├── asset.ts │ ├── bytes.ts │ ├── chain.ts │ ├── crypto.ts │ ├── encoding.ts │ ├── errors.ts │ ├── index.ts │ ├── reward.ts │ ├── time.ts │ ├── transaction.ts │ ├── utxo.ts │ └── wallet.ts └── wallet │ ├── account │ ├── account.ts │ ├── balance.ts │ ├── evm.ts │ ├── index.ts │ ├── jvm.ts │ ├── mcn.ts │ └── platform.ts │ ├── index.ts │ ├── operation │ ├── executable.ts │ ├── index.ts │ ├── operation.ts │ └── summary.ts │ ├── transaction │ ├── base.ts │ ├── constants.ts │ ├── cross │ │ ├── cross.ts │ │ ├── evm.ts │ │ ├── index.ts │ │ ├── jvm.ts │ │ └── platform.ts │ ├── evm.ts │ ├── fee.ts │ ├── index.ts │ ├── platform.ts │ └── transaction.ts │ ├── vault.ts │ └── wallet.ts ├── tests ├── e2e │ ├── api │ │ ├── info.test.ts │ │ ├── jevm.test.ts │ │ ├── jvm.test.ts │ │ └── platform.test.ts │ ├── constants.ts │ └── wallet │ │ ├── cross.test.ts │ │ ├── send.test.ts │ │ ├── stake.test.ts │ │ └── wrap.test.ts └── unit │ ├── chain │ └── asset.test.ts │ ├── transaction │ ├── input.test.ts │ ├── jevm │ │ └── transaction.test.ts │ ├── jvm │ │ └── transaction.test.ts │ ├── output.test.ts │ ├── platform │ │ └── transaction.test.ts │ ├── signature.test.ts │ └── type.test.ts │ ├── utils │ ├── bytes.test.ts │ └── encoding.test.ts │ └── wallet │ └── wallet.test.ts ├── tsconfig.eslint.json └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | /tests_dev/* 2 | /dist/* 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es2021": true 5 | }, 6 | "extends": "standard-with-typescript", 7 | "overrides": [], 8 | "parser": "@typescript-eslint/parser", 9 | "parserOptions": { 10 | "project": "tsconfig.eslint.json", 11 | "ecmaVersion": "latest", 12 | "sourceType": "module" 13 | }, 14 | "rules": { 15 | "@typescript-eslint/no-non-null-assertion": "off", 16 | "@typescript-eslint/no-unsafe-argument": "warn" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/DEV-tests.yml: -------------------------------------------------------------------------------- 1 | name: DEV build and tests 2 | on: 3 | pull_request: 4 | types: [closed] 5 | branches: 6 | - dev 7 | 8 | jobs: 9 | dev_build: 10 | runs-on: [self-hosted] 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | - name: Setup Node 15 | uses: actions/setup-node@v3 16 | with: 17 | node-version: '18.x' 18 | registry-url: 'https://registry.npmjs.org' 19 | 20 | - name: Install dependencies and build 🔧 21 | run: npm install && npm run build 22 | 23 | dev_test: 24 | runs-on: [self-hosted] 25 | env: 26 | MNEMONIC: ${{ secrets.MNEMONIC }} 27 | needs: dev_build 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v3 31 | - name: Setup Node 32 | uses: actions/setup-node@v3 33 | with: 34 | node-version: '18.x' 35 | registry-url: 'https://registry.npmjs.org' 36 | 37 | - name: Install dependencies🔧 38 | run: npm install 39 | 40 | - name: Run the unit tests 🧪 41 | run: npm run test-unit 42 | 43 | - name: Run the e2e tests 🧪 44 | run: npm run test-e2e 45 | -------------------------------------------------------------------------------- /.github/workflows/PROD-publish.yml: -------------------------------------------------------------------------------- 1 | name: PROD Publish Package to npm 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | prod_test: 9 | runs-on: [self-hosted] 10 | env: 11 | MNEMONIC: ${{ secrets.MNEMONIC }} 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | - name: Setup Node 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: '18.x' 19 | registry-url: 'https://registry.npmjs.org' 20 | 21 | - name: Install dependencies🔧 22 | run: npm install 23 | 24 | - name: Run the tests 🧪 25 | run: npm run test-unit 26 | 27 | publish: 28 | needs: prod_test 29 | runs-on: [self-hosted] 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v3 33 | - name: Setup Node 34 | uses: actions/setup-node@v3 35 | with: 36 | node-version: '18.x' 37 | registry-url: 'https://registry.npmjs.org' 38 | 39 | - name: Install dependencies and build 🔧 40 | run: npm install && npm run build 41 | 42 | - name: Check if version has changed 43 | id: version_check 44 | run: | 45 | CURRENT_VERSION=$(node -p "require('./package.json').version") 46 | NPM_VERSION=$(npm show juneojs version) 47 | if [[ "$CURRENT_VERSION" != "$NPM_VERSION" ]]; then 48 | echo "New version detected: $CURRENT_VERSION" 49 | echo "should_publish=true" >> $GITHUB_ENV 50 | else 51 | echo "No new version detected." 52 | echo "should_publish=false" >> $GITHUB_ENV 53 | fi 54 | 55 | - name: Publish package on NPM 📦 56 | if: env.should_publish == 'true' 57 | run: npm publish 58 | env: 59 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 60 | 61 | - name: Trigger juneojs-examples upgrade workflow 62 | if: env.should_publish == 'true' 63 | run: | 64 | curl -X POST https://api.github.com/repos/Juneo-io/juneojs-examples/dispatches \ 65 | -H "Accept: application/vnd.github.everest-preview+json" \ 66 | -H "Authorization: token ${{ secrets.ACTIONS_KEY }}" \ 67 | --data '{"event_type": "Trigger Workflow", "client_payload": { "repository": "'"$GITHUB_REPOSITORY"'" }}' 68 | -------------------------------------------------------------------------------- /.github/workflows/PROD-upgrade-example.yml: -------------------------------------------------------------------------------- 1 | name: PROD upgrade juneojs-examples 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | PROD_dispatch: 6 | description: 'ًPROD_dispatch which will be triggered' 7 | required: true 8 | default: 'PROD_dispatch' 9 | 10 | workflow2_github_account: 11 | description: 'GitHub Account Owner' 12 | required: true 13 | default: 'Juneo-io' 14 | 15 | workflow2_repo_github: 16 | description: 'repo-name' 17 | required: true 18 | default: 'juneojs-examples' 19 | 20 | jobs: 21 | build: 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - name: Trigger juneojs-examples upgrade workflow 26 | run: | 27 | curl -X POST https://api.github.com/repos/Juneo-io/juneojs-examples/dispatches \ 28 | -H 'Accept: application/vnd.github.everest-preview+json' \ 29 | -u ${{ secrets.ACTIONS_KEY }} \ 30 | --data '{"event_type": "Trigger Workflow", "client_payload": { "repository": "'"$GITHUB_REPOSITORY"'" }}' 31 | - uses: actions/checkout@v3 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/node 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=node 3 | 4 | # Compiled files 5 | dist/ 6 | 7 | # Temporarily ignore unreleased tests 8 | tests_dev/ 9 | 10 | ### Node ### 11 | # Logs 12 | logs 13 | *.log 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | lerna-debug.log* 18 | .pnpm-debug.log* 19 | package-lock.json 20 | 21 | # Diagnostic reports (https://nodejs.org/api/report.html) 22 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 23 | 24 | # Runtime data 25 | pids 26 | *.pid 27 | *.seed 28 | *.pid.lock 29 | 30 | # Directory for instrumented libs generated by jscoverage/JSCover 31 | lib-cov 32 | 33 | # Coverage directory used by tools like istanbul 34 | coverage 35 | *.lcov 36 | 37 | # nyc test coverage 38 | .nyc_output 39 | 40 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 41 | .grunt 42 | 43 | # Bower dependency directory (https://bower.io/) 44 | bower_components 45 | 46 | # node-waf configuration 47 | .lock-wscript 48 | 49 | # Compiled binary addons (https://nodejs.org/api/addons.html) 50 | build/Release 51 | 52 | # Dependency directories 53 | node_modules/ 54 | jspm_packages/ 55 | 56 | # Snowpack dependency directory (https://snowpack.dev/) 57 | web_modules/ 58 | 59 | # TypeScript cache 60 | *.tsbuildinfo 61 | 62 | # Optional npm cache directory 63 | .npm 64 | 65 | # Optional eslint cache 66 | .eslintcache 67 | 68 | # Optional stylelint cache 69 | .stylelintcache 70 | 71 | # Microbundle cache 72 | .rpt2_cache/ 73 | .rts2_cache_cjs/ 74 | .rts2_cache_es/ 75 | .rts2_cache_umd/ 76 | 77 | # Optional REPL history 78 | .node_repl_history 79 | 80 | # Output of 'npm pack' 81 | *.tgz 82 | 83 | # Yarn Integrity file 84 | .yarn-integrity 85 | 86 | # dotenv environment variable files 87 | .env 88 | .env.development.local 89 | .env.test.local 90 | .env.production.local 91 | .env.local 92 | 93 | # parcel-bundler cache (https://parceljs.org/) 94 | .cache 95 | .parcel-cache 96 | 97 | # Next.js build output 98 | .next 99 | out 100 | 101 | # Nuxt.js build / generate output 102 | .nuxt 103 | dist 104 | 105 | # Gatsby files 106 | .cache/ 107 | # Comment in the line in if your project uses Gatsby and not Next.js 108 | # https://nextjs.org/blog/next-9-1#public-directory-support 109 | # public 110 | 111 | # vuepress build output 112 | .vuepress/dist 113 | 114 | # vuepress v2.x temp and cache directory 115 | .temp 116 | 117 | # Docusaurus cache and generated files 118 | .docusaurus 119 | 120 | # Serverless directories 121 | .serverless/ 122 | 123 | # FuseBox cache 124 | .fusebox/ 125 | 126 | # DynamoDB Local files 127 | .dynamodb/ 128 | 129 | # TernJS port file 130 | .tern-port 131 | 132 | # Stores VSCode versions used for testing VSCode extensions 133 | .vscode-test 134 | 135 | # yarn v2 136 | .yarn/cache 137 | .yarn/unplugged 138 | .yarn/build-state.yml 139 | .yarn/install-state.gz 140 | .pnp.* 141 | 142 | ### Node Patch ### 143 | # Serverless Webpack directories 144 | .webpack/ 145 | 146 | # Optional stylelint cache 147 | 148 | # SvelteKit build / generate output 149 | .svelte-kit 150 | 151 | # End of https://www.toptal.com/developers/gitignore/api/node 152 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *.md 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "semi": false 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2023, JUNEO AG 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **DISCLAIMER:** The library is currently in its early development stage. Already available functionalities may still be incomplete, and some could be broken and/or removed. Breaking changes may be introduced from one update to the other without notice until first release. 2 | 3 | # JuneoJS - JuneoMCN JavaScript Library 4 | 5 | ## Overview 6 | 7 | JuneoJS is a JavaScript Library to help building on the JuneoMCN. Interfaces are provided to interact with nodes that have enabled their APIs endpoints to retrieve informations about the current state of the network. Types, helpers and builders are also provided to create, verify, sign and send messages to it. 8 | 9 | That allows developers to use transactions to easily transfer funds, stake funds, create assets, create supernets, create blockchains and even more. You can use this library to learn about the JuneoMCN, create a simple project or even build a complete DApp. To support you in such task, the library provides both low and high level functions to have the choice between speed and customization when building a project. 10 | 11 | ## Installation 12 | 13 | JuneoJS can be installed via npm: 14 | 15 | `npm i juneojs` 16 | 17 | ## Usage 18 | 19 | For usage you can currently see the examples repository. It is available at: https://github.com/Juneo-io/juneojs-examples 20 | 21 | Documentation for the library is planned to be released later. 22 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | verbose: true, 4 | testEnvironment: 'node', 5 | roots: ['/tests'], 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "juneojs", 3 | "version": "0.0.151", 4 | "description": "Juneo JS Library", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "build": "tsc", 9 | "clean": "rm -rf ./dist", 10 | "fix": "npm run prettier && npm run lint", 11 | "lint": "eslint . --ext .ts --fix", 12 | "prep": "npm run build && npm run test-unit && npm run fix", 13 | "prettier": "npx prettier . --write", 14 | "test": "jest", 15 | "test-e2e": "jest --roots=./tests/e2e", 16 | "test-unit": "jest --roots=./tests/unit" 17 | }, 18 | "keywords": [ 19 | "Juneo", 20 | "blockchain", 21 | "defi" 22 | ], 23 | "author": "Aleksander Waslet", 24 | "license": "BSD-3-Clause", 25 | "devDependencies": { 26 | "@jest/globals": "29.7.0", 27 | "@types/jest": "29.5.12", 28 | "@types/node": "20.14.9", 29 | "@typescript-eslint/eslint-plugin": "6.21.0", 30 | "dotenv": "16.4.5", 31 | "eslint": "8.57.0", 32 | "eslint-config-prettier": "9.1.0", 33 | "eslint-config-standard-with-typescript": "43.0.1", 34 | "eslint-plugin-import": "2.29.1", 35 | "eslint-plugin-n": "16.6.2", 36 | "eslint-plugin-prettier": "5.1.3", 37 | "eslint-plugin-promise": "6.2.0", 38 | "jest": "29.7.0", 39 | "prettier": "3.3.2", 40 | "ts-jest": "29.2.4", 41 | "typescript": "5.3.3" 42 | }, 43 | "dependencies": { 44 | "@noble/curves": "1.5.0", 45 | "@noble/hashes": "1.4.0", 46 | "axios": "1.8.4", 47 | "bech32": "2.0.0", 48 | "bs58": "6.0.0", 49 | "ethers": "6.13.2" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/api/api.ts: -------------------------------------------------------------------------------- 1 | import { JsonRpcRequest, type JsonRpcResponse, type JuneoClient } from './client' 2 | import { type GetTxResponse, type IssueTxResponse, type GetUTXOsResponse, type UTXOIndex } from './data' 3 | 4 | export abstract class AbstractAPI { 5 | protected readonly client: JuneoClient 6 | private readonly endpoint: string 7 | private readonly service: string 8 | 9 | constructor (client: JuneoClient, endpoint: string, service: string) { 10 | this.client = client 11 | this.endpoint = endpoint 12 | this.service = service 13 | } 14 | 15 | protected async callServiceAt ( 16 | service: string, 17 | endpoint: string, 18 | method: string, 19 | params?: object[] | string[] 20 | ): Promise { 21 | return await this.client.rpcCall( 22 | `${endpoint}`, 23 | new JsonRpcRequest(`${service}${service.length > 0 ? '.' : ''}${method}`, params) 24 | ) 25 | } 26 | 27 | protected async callAt (endpoint: string, method: string, params?: object[] | string[]): Promise { 28 | return await this.callServiceAt(this.service, endpoint, method, params) 29 | } 30 | 31 | protected async call (method: string, params?: object[] | string[]): Promise { 32 | return await this.callAt(this.endpoint, method, params) 33 | } 34 | } 35 | 36 | export abstract class AbstractJuneoChainAPI extends AbstractAPI { 37 | async getTx (txID: string, encoding?: string): Promise { 38 | const response: JsonRpcResponse = await this.call('getTx', [{ txID, encoding }]) 39 | return response.result 40 | } 41 | 42 | async issueTx (tx: string, encoding?: string): Promise { 43 | const response: JsonRpcResponse = await this.call('issueTx', [{ tx, encoding }]) 44 | return response.result 45 | } 46 | } 47 | 48 | export abstract class AbstractUtxoAPI extends AbstractJuneoChainAPI { 49 | async getUTXOs ( 50 | addresses: string[], 51 | limit?: number, 52 | startIndex?: UTXOIndex, 53 | encoding?: string 54 | ): Promise { 55 | const response: JsonRpcResponse = await this.call('getUTXOs', [{ addresses, limit, startIndex, encoding }]) 56 | return response.result 57 | } 58 | 59 | async getUTXOsFrom ( 60 | addresses: string[], 61 | sourceChain?: string, 62 | limit?: number, 63 | startIndex?: UTXOIndex, 64 | encoding?: string 65 | ): Promise { 66 | const response: JsonRpcResponse = await this.call('getUTXOs', [ 67 | { addresses, sourceChain, limit, startIndex, encoding } 68 | ]) 69 | return response.result 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/api/client.ts: -------------------------------------------------------------------------------- 1 | import axios, { type AxiosResponse } from 'axios' 2 | import { HttpError, JsonRpcError, NetworkError, ProtocolError } from '../utils' 3 | 4 | const JsonRpcVersion = '2.0' 5 | const HttpHeaders = { 6 | 'Content-Type': 'application/json' 7 | } 8 | export const HttpProtocol = 'http' 9 | export const HttpsProtocol = 'https' 10 | const DefaultProtocol = HttpsProtocol 11 | const Protocols: string[] = [HttpProtocol, HttpsProtocol] 12 | 13 | export class JuneoClient { 14 | private nextRequestId = 1 15 | private protocol = DefaultProtocol 16 | private host = '' 17 | private url = '' 18 | 19 | private constructor () {} 20 | 21 | static parse (address: string): JuneoClient { 22 | const client = new JuneoClient() 23 | client.parseAddress(address) 24 | return client 25 | } 26 | 27 | getUrl (): string { 28 | return this.url 29 | } 30 | 31 | parseAddress (address: string): void { 32 | this.url = address 33 | const protocolSplit: string[] = address.split('://') 34 | const protocol = protocolSplit.length > 1 ? protocolSplit[0] : DefaultProtocol 35 | const host = protocolSplit.length > 1 ? protocolSplit[1] : protocolSplit[0] 36 | this.setProtocol(protocol) 37 | this.host = host 38 | } 39 | 40 | setProtocol (protocol: string): void { 41 | if (!Protocols.includes(protocol)) { 42 | throw new ProtocolError(`invalid protocol "${protocol}"`) 43 | } 44 | this.protocol = protocol 45 | } 46 | 47 | getProtocol (): string { 48 | return this.protocol 49 | } 50 | 51 | getNextRequestId (): number { 52 | return this.nextRequestId 53 | } 54 | 55 | async rpcCall (endpoint: string, request: JsonRpcRequest): Promise { 56 | const jsonRpcObject = request.getJsonRpcObject(this.nextRequestId++) 57 | const response = await this.post(endpoint, JSON.stringify(jsonRpcObject)) 58 | const status = response.status 59 | if (status < 200 || status >= 300) { 60 | throw new HttpError(`request status is not accepted "${status}"`) 61 | } 62 | let data = response.data 63 | if (typeof data === 'string') { 64 | data = JSON.parse(data) 65 | } 66 | if (data === null || data.error !== undefined) { 67 | throw new JsonRpcError(data.error.message) 68 | } 69 | return new JsonRpcResponse(data.jsonrpc, data.id, data.result) 70 | } 71 | 72 | async post (endpoint: string, data: any): Promise { 73 | return await axios 74 | .post(endpoint, data, { 75 | method: 'post', 76 | baseURL: `${this.protocol}://${this.host}`, 77 | headers: HttpHeaders, 78 | responseType: 'json', 79 | responseEncoding: 'utf8' 80 | }) 81 | .catch((error) => { 82 | throw new NetworkError(error.message) 83 | }) 84 | } 85 | 86 | async get (endpoint: string): Promise { 87 | return await axios 88 | .get(endpoint, { 89 | method: 'get', 90 | baseURL: `${this.protocol}://${this.host}`, 91 | headers: HttpHeaders, 92 | responseType: 'json', 93 | responseEncoding: 'utf8' 94 | }) 95 | .catch((error) => { 96 | throw new NetworkError(error.message) 97 | }) 98 | } 99 | } 100 | 101 | export class JsonRpcRequest { 102 | method: string 103 | params: object[] | string[] 104 | 105 | constructor (method: string, params: object[] | string[] = []) { 106 | this.method = method 107 | this.params = params 108 | } 109 | 110 | getJsonRpcObject (id: number): any { 111 | return { 112 | jsonrpc: JsonRpcVersion, 113 | id, 114 | method: this.method, 115 | params: this.params 116 | } 117 | } 118 | } 119 | 120 | export class JsonRpcResponse { 121 | jsonrpc: string 122 | id: number 123 | result: any 124 | 125 | constructor (jsonrpc: string, id: number, result: string) { 126 | this.jsonrpc = jsonrpc 127 | this.id = id 128 | this.result = result 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/api/data.ts: -------------------------------------------------------------------------------- 1 | import { TimeUtils } from '../utils' 2 | import { type JsonRpcRequest, type JsonRpcResponse, type JuneoClient } from './client' 3 | 4 | export class CachedResponse { 5 | private response: T | undefined 6 | private lastUpdate: bigint = BigInt(0) 7 | duration: bigint | undefined 8 | 9 | constructor (duration?: bigint) { 10 | this.duration = duration 11 | } 12 | 13 | async rpcCall ( 14 | client: JuneoClient, 15 | endpoint: string, 16 | request: JsonRpcRequest, 17 | forceUpdate: boolean = false 18 | ): Promise { 19 | const update: boolean = this.shouldUpdate() 20 | if (!update && !forceUpdate && this.response !== undefined) { 21 | return this.response 22 | } 23 | const response: JsonRpcResponse = await client.rpcCall(endpoint, request) 24 | this.response = response.result 25 | return response.result 26 | } 27 | 28 | private shouldUpdate (): boolean { 29 | // no duration set never update 30 | if (this.duration === undefined) { 31 | return false 32 | } 33 | const currentTime: bigint = TimeUtils.now() 34 | const update: boolean = currentTime >= this.lastUpdate + this.duration 35 | if (update) { 36 | this.lastUpdate = currentTime 37 | } 38 | return update 39 | } 40 | } 41 | 42 | export interface GetBlockResponse { 43 | block: string 44 | encoding: string 45 | } 46 | 47 | export interface GetHeightResponse { 48 | height: number 49 | } 50 | 51 | export interface GetUTXOsResponse { 52 | numFetched: string 53 | utxos: string[] 54 | endIndex: UTXOIndex 55 | encoding: string 56 | } 57 | 58 | export interface UTXOIndex { 59 | address: string 60 | utxo: string 61 | } 62 | 63 | export interface GetTxResponse { 64 | tx: string 65 | encoding: string 66 | } 67 | 68 | export interface GetTxStatusResponse { 69 | status: string 70 | } 71 | 72 | export interface IssueTxResponse { 73 | txID: string 74 | } 75 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api' 2 | export * from './client' 3 | export * from './data' 4 | export * from './info' 5 | export * from './jevm' 6 | export * from './jvm' 7 | export * from './platform' 8 | -------------------------------------------------------------------------------- /src/api/info/api.ts: -------------------------------------------------------------------------------- 1 | import { AbstractAPI } from '../api' 2 | import { JsonRpcRequest, type JsonRpcResponse, type JuneoClient } from '../client' 3 | import { CachedResponse } from '../data' 4 | import { 5 | type GetBlockchainIDResponse, 6 | type GetNetworkIDResponse, 7 | type GetNetworkNameResponse, 8 | type GetNodeIDResponse, 9 | type GetNodeIPResponse, 10 | type GetNodeVersionResponse, 11 | type GetTxFeeResponse, 12 | type GetVMsResponse, 13 | type IsBootstrappedResponse, 14 | type PeersResponse, 15 | type UptimeResponse 16 | } from './data' 17 | 18 | const Service: string = 'info' 19 | const Endpoint = '/ext/info' 20 | 21 | /** 22 | * The InfoAPI provides information about the network and/or a specific node connected to it. 23 | * The specific node related informations is unique, another InfoAPI should be used if such data must be retrieved from a different one. 24 | * The node which is used for this API depends on the client that is used to instantiate this API. 25 | */ 26 | export class InfoAPI extends AbstractAPI { 27 | private readonly feesCache = new CachedResponse() 28 | 29 | /** 30 | * Creates a new InfoAPI with its corresponding info service and endpoint. 31 | * @param client The client to use to send network requests for this API. 32 | */ 33 | constructor (client: JuneoClient) { 34 | super(client, Endpoint, Service) 35 | } 36 | 37 | /** 38 | * Checks if a chain is done bootstrapping on the node. 39 | * @param chain The id or alias of an existing chain. 40 | * @returns Promise of IsBootstrappedResponse. 41 | */ 42 | async isBootstrapped (chain: string): Promise { 43 | const response: JsonRpcResponse = await this.call('isBootstrapped', [{ chain }]) 44 | return response.result 45 | } 46 | 47 | /** 48 | * Fetches the id of the chain corresponding to an alias on the node. 49 | * Each node can have different aliases defined. 50 | * @param alias The alias of the chain id to retrieve. 51 | * @returns Promise of GetBlockchainIDResponse. 52 | */ 53 | async getBlockchainID (alias: string): Promise { 54 | const response: JsonRpcResponse = await this.call('getBlockchainID', [{ alias }]) 55 | return response.result 56 | } 57 | 58 | /** 59 | * Gets the id of the network this node is connected to. 60 | * @returns Promise of GetNetworkIDResponse. 61 | */ 62 | async getNetworkID (): Promise { 63 | const response: JsonRpcResponse = await this.call('getNetworkID') 64 | return response.result 65 | } 66 | 67 | /** 68 | * Gets the name of the network this node is connected to. 69 | * @returns Promise of GetNetworkNameResponse. 70 | */ 71 | async getNetworkName (): Promise { 72 | const response: JsonRpcResponse = await this.call('getNetworkName') 73 | return response.result 74 | } 75 | 76 | /** 77 | * Gets the id data of the node. 78 | * @returns Promise of GetNodeIDResponse. 79 | */ 80 | async getNodeID (): Promise { 81 | const response: JsonRpcResponse = await this.call('getNodeID') 82 | return response.result 83 | } 84 | 85 | /** 86 | * Gets the ip of the node. 87 | * @returns Promise of GetNodeIPResponse. 88 | */ 89 | async getNodeIP (): Promise { 90 | const response: JsonRpcResponse = await this.call('getNodeIP') 91 | return response.result 92 | } 93 | 94 | /** 95 | * Gets the informations related to the version of the node. 96 | * @returns Promise of GetNodeVersionResponse. 97 | */ 98 | async getNodeVersion (): Promise { 99 | const response: JsonRpcResponse = await this.call('getNodeVersion') 100 | return response.result 101 | } 102 | 103 | /** 104 | * Gets the values of the fee from different transactions types of the network this node is connected to. 105 | * @param forceUpdate **Optional**. Force the retrieval of the value from the node and update the fee cache. 106 | * @returns Promise of GetTxFeeResponse. 107 | */ 108 | async getTxFee (forceUpdate: boolean = false): Promise { 109 | return await this.feesCache.rpcCall( 110 | this.client, 111 | Endpoint, 112 | new JsonRpcRequest(`${Service}.getTxFee`, []), 113 | forceUpdate 114 | ) 115 | } 116 | 117 | /** 118 | * Gets the VMs that are installed on the node. 119 | * @returns Promise of GetVMsResponse. 120 | */ 121 | async getVMs (): Promise { 122 | const response: JsonRpcResponse = await this.call('getVMs') 123 | return response.result 124 | } 125 | 126 | /** 127 | * Fetches the number of peers connected to the node and a description of them. 128 | * @param nodeIDs **Optional**. A list of the nodeIDs that should return a description of them. 129 | * If not used, all peers connected to the node will provide a description. 130 | * @returns Promise of PeersResponse. 131 | */ 132 | async peers (nodeIDs?: string[]): Promise { 133 | const response: JsonRpcResponse = await this.call('peers', [{ nodeIDs }]) 134 | return response.result 135 | } 136 | 137 | /** 138 | * Checks the uptime of the node on a supernet. 139 | * @param supernetID **Optional**. The supernet id to check the uptime of. 140 | * If not used, it will use the primary supernet id. 141 | * @returns Promise of UptimeResponse. 142 | */ 143 | async uptime (supernetID?: string): Promise { 144 | const response: JsonRpcResponse = await this.call('uptime', [{ supernetID }]) 145 | return response.result 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/api/info/data.ts: -------------------------------------------------------------------------------- 1 | export interface IsBootstrappedResponse { 2 | isBootstrapped: boolean 3 | } 4 | 5 | export interface GetBlockchainIDResponse { 6 | blockchainID: string 7 | } 8 | 9 | export interface GetNetworkIDResponse { 10 | networkID: number 11 | } 12 | 13 | export interface GetNetworkNameResponse { 14 | networkName: string 15 | } 16 | 17 | export interface GetNodeIDResponse { 18 | nodeID: string 19 | nodePOP: NodePOP 20 | } 21 | 22 | export interface NodePOP { 23 | publicKey: string 24 | proofOfPossession: string 25 | } 26 | 27 | export interface GetNodeIPResponse { 28 | ip: string 29 | } 30 | 31 | export interface GetNodeVersionResponse { 32 | version: string 33 | databaseVersion: string 34 | gitCommit: string 35 | vmVersions: Record 36 | rpcProtocolVersion: string 37 | } 38 | 39 | export interface GetTxFeeResponse { 40 | txFee: number 41 | createAssetTxFee: number 42 | createSupernetTxFee: number 43 | transformSupernetTxFee: number 44 | createBlockchainTxFee: number 45 | addPrimaryNetworkValidatorFee: number 46 | addPrimaryNetworkDelegatorFee: number 47 | addSupernetValidatorFee: number 48 | addSupernetDelegatorFee: number 49 | } 50 | 51 | export interface GetVMsResponse { 52 | vms: Record 53 | } 54 | 55 | export interface PeersResponse { 56 | numPeers: number 57 | peers: Peer[] 58 | } 59 | 60 | export interface Peer { 61 | ip: string 62 | publicIP: string 63 | nodeID: string 64 | version: string 65 | lastSent: string 66 | lastReceived: string 67 | benched: string[] 68 | observedUptime: number 69 | observedSupernetUptime: Record 70 | } 71 | 72 | export interface UptimeResponse { 73 | rewardingStakePercentage: number 74 | weightedAveragePercentage: number 75 | } 76 | -------------------------------------------------------------------------------- /src/api/info/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api' 2 | export * from './data' 3 | -------------------------------------------------------------------------------- /src/api/jevm/api.ts: -------------------------------------------------------------------------------- 1 | import { type JEVMBlockchain } from '../../chain' 2 | import { AbstractUtxoAPI } from '../api' 3 | import { JsonRpcRequest, type JsonRpcResponse, type JuneoClient } from '../client' 4 | import { CachedResponse } from '../data' 5 | import { type GetAtomicTxResponse, type GetAtomicTxStatusResponse } from './data' 6 | 7 | const Service: string = 'june' 8 | 9 | export class JEVMAPI extends AbstractUtxoAPI { 10 | chain: JEVMBlockchain 11 | private readonly rpcEndpoint: string 12 | private readonly baseFeeCache = new CachedResponse(BigInt(60)) 13 | 14 | constructor (client: JuneoClient, chain: JEVMBlockchain) { 15 | super(client, `/ext/bc/${chain.id}/june`, Service) 16 | this.chain = chain 17 | this.rpcEndpoint = `/ext/bc/${chain.id}/rpc` 18 | } 19 | 20 | override async getTx (txID: string, encoding?: string): Promise { 21 | const response: JsonRpcResponse = await this.call('getAtomicTx', [{ txID, encoding }]) 22 | return response.result 23 | } 24 | 25 | async getTxStatus (txID: string): Promise { 26 | const response: JsonRpcResponse = await this.call('getAtomicTxStatus', [{ txID }]) 27 | return response.result 28 | } 29 | 30 | async eth_getAssetBalance (address: string, block: string, assetID: string): Promise { 31 | const response: JsonRpcResponse = await this.callServiceAt('', this.rpcEndpoint, 'eth_getAssetBalance', [ 32 | address, 33 | block, 34 | assetID 35 | ]) 36 | return BigInt.asUintN(256, response.result) 37 | } 38 | 39 | async eth_baseFee (forceUpdate: boolean = false): Promise { 40 | return BigInt.asUintN( 41 | 256, 42 | await this.baseFeeCache.rpcCall( 43 | this.client, 44 | this.rpcEndpoint, 45 | new JsonRpcRequest('eth_baseFee', []), 46 | forceUpdate 47 | ) 48 | ) 49 | } 50 | 51 | async eth_maxPriorityFeePerGas (): Promise { 52 | const response: JsonRpcResponse = await this.callServiceAt('', this.rpcEndpoint, 'eth_maxPriorityFeePerGas') 53 | return BigInt.asUintN(256, response.result) 54 | } 55 | 56 | async eth_getChainConfig (): Promise { 57 | const response: JsonRpcResponse = await this.callServiceAt('', this.rpcEndpoint, 'eth_getChainConfig') 58 | return response.result 59 | } 60 | 61 | async eth_getBalance (address: string, block: string): Promise { 62 | const response: JsonRpcResponse = await this.callServiceAt('', this.rpcEndpoint, 'eth_getBalance', [address, block]) 63 | return BigInt.asUintN(256, response.result) 64 | } 65 | 66 | async eth_getTransactionCount (address: string, block: string): Promise { 67 | const response: JsonRpcResponse = await this.callServiceAt('', this.rpcEndpoint, 'eth_getTransactionCount', [ 68 | address, 69 | block 70 | ]) 71 | return BigInt.asUintN(256, response.result) 72 | } 73 | 74 | async eth_sendRawTransaction (transaction: string): Promise { 75 | const response: JsonRpcResponse = await this.callServiceAt('', this.rpcEndpoint, 'eth_sendRawTransaction', [ 76 | transaction 77 | ]) 78 | return response.result 79 | } 80 | 81 | async eth_getTransactionReceipt (hash: string): Promise { 82 | const response: JsonRpcResponse = await this.callServiceAt('', this.rpcEndpoint, 'eth_getTransactionReceipt', [ 83 | hash 84 | ]) 85 | return response.result 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/api/jevm/data.ts: -------------------------------------------------------------------------------- 1 | import { type GetTxResponse, type GetTxStatusResponse } from '../data' 2 | 3 | export interface GetAtomicTxResponse extends GetTxResponse { 4 | blockHeight: string 5 | } 6 | 7 | export interface GetAtomicTxStatusResponse extends GetTxStatusResponse { 8 | blockHeight: string 9 | } 10 | -------------------------------------------------------------------------------- /src/api/jevm/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api' 2 | export * from './data' 3 | -------------------------------------------------------------------------------- /src/api/jvm/api.ts: -------------------------------------------------------------------------------- 1 | import { type JVMBlockchain } from '../../chain' 2 | import { AbstractUtxoAPI } from '../api' 3 | import { type JsonRpcResponse, type JuneoClient } from '../client' 4 | import { type GetBlockResponse, type GetHeightResponse } from '../data' 5 | import { type BuildGenesisResponse, type GetAssetDescriptionResponse } from './data' 6 | 7 | const Service: string = 'jvm' 8 | const VMEndpoint = '/vm/jvm' 9 | 10 | export class JVMAPI extends AbstractUtxoAPI { 11 | chain: JVMBlockchain 12 | 13 | constructor (client: JuneoClient, chain: JVMBlockchain) { 14 | super(client, `/ext/bc/${chain.id}`, Service) 15 | this.chain = chain 16 | } 17 | 18 | async buildGenesis (networkID: number, genesisData: JSON, encoding?: string): Promise { 19 | const response: JsonRpcResponse = await this.callAt(VMEndpoint, 'buildGenesis', [ 20 | { networkID, genesisData, encoding } 21 | ]) 22 | return response.result 23 | } 24 | 25 | async getAssetDescription (assetID: string): Promise { 26 | const response: JsonRpcResponse = await this.call('getAssetDescription', [{ assetID }]) 27 | return response.result 28 | } 29 | 30 | async getBlock (blockID: string, encoding?: string): Promise { 31 | const response: JsonRpcResponse = await this.call('getBlock', [{ blockID, encoding }]) 32 | return response.result 33 | } 34 | 35 | async getBlockByHeight (height: number, encoding?: string): Promise { 36 | const response: JsonRpcResponse = await this.call('getBlockByHeight', [{ height, encoding }]) 37 | return response.result 38 | } 39 | 40 | async getHeight (): Promise { 41 | const response: JsonRpcResponse = await this.call('getHeight') 42 | return response.result 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/api/jvm/data.ts: -------------------------------------------------------------------------------- 1 | export interface BuildGenesisResponse { 2 | bytes: string 3 | encoding: string 4 | } 5 | 6 | export interface GetAssetDescriptionResponse { 7 | assetID: string 8 | name: string 9 | symbol: string 10 | denomination: number 11 | } 12 | -------------------------------------------------------------------------------- /src/api/jvm/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api' 2 | export * from './data' 3 | -------------------------------------------------------------------------------- /src/api/platform/api.ts: -------------------------------------------------------------------------------- 1 | import { type PlatformBlockchain } from '../../chain' 2 | import { AbstractUtxoAPI } from '../api' 3 | import { type JsonRpcResponse, type JuneoClient } from '../client' 4 | import { type GetBlockResponse, type GetHeightResponse, type GetTxStatusResponse } from '../data' 5 | import { 6 | type GetBlockchainsResponse, 7 | type GetBlockchainStatusResponse, 8 | type GetCurrentSupplyResponse, 9 | type GetCurrentValidatorsResponse, 10 | type GetMinStakeResponse, 11 | type GetRewardPoolSupplyResponse, 12 | type GetStakingAssetIDResponse, 13 | type GetSupernetsResponse, 14 | type GetTimestampResponse, 15 | type GetTotalStakeResponse, 16 | type GetValidatorsAtResponse, 17 | type SampleValidatorsResponse, 18 | type ValidatedByResponse, 19 | type ValidatesResponse 20 | } from './data' 21 | 22 | const Service: string = 'platform' 23 | 24 | export class PlatformAPI extends AbstractUtxoAPI { 25 | chain: PlatformBlockchain 26 | 27 | constructor (client: JuneoClient, chain: PlatformBlockchain) { 28 | super(client, `/ext/bc/${chain.id}`, Service) 29 | this.chain = chain 30 | } 31 | 32 | async getBlock (blockID: string, encoding?: string): Promise { 33 | const response: JsonRpcResponse = await this.call('getBlock', [{ blockID, encoding }]) 34 | return response.result 35 | } 36 | 37 | async getBlockByHeight (height: number, encoding?: string): Promise { 38 | const response: JsonRpcResponse = await this.call('getBlockByHeight', [{ height, encoding }]) 39 | return response.result 40 | } 41 | 42 | /** 43 | * @deprecated 44 | */ 45 | async getBlockchains (): Promise { 46 | const response: JsonRpcResponse = await this.call('getBlockchains') 47 | return response.result 48 | } 49 | 50 | async getBlockchainStatus (blockchainID: string): Promise { 51 | const response: JsonRpcResponse = await this.call('getBlockchainStatus', [{ blockchainID }]) 52 | return response.result 53 | } 54 | 55 | async getCurrentSupply (supernetID?: string): Promise { 56 | const response: JsonRpcResponse = await this.call('getCurrentSupply', [{ supernetID }]) 57 | return response.result 58 | } 59 | 60 | async getRewardPoolSupply (supernetID?: string): Promise { 61 | const response: JsonRpcResponse = await this.call('getRewardPoolSupply', [{ supernetID }]) 62 | return response.result 63 | } 64 | 65 | async getCurrentValidators (supernetID?: string, nodeIDs?: string[]): Promise { 66 | const response: JsonRpcResponse = await this.call('getCurrentValidators', [{ supernetID, nodeIDs }]) 67 | return response.result 68 | } 69 | 70 | async getHeight (): Promise { 71 | const response: JsonRpcResponse = await this.call('getHeight') 72 | return response.result 73 | } 74 | 75 | async getMinStake (supernetID?: string): Promise { 76 | const response: JsonRpcResponse = await this.call('getMinStake', [{ supernetID }]) 77 | return response.result 78 | } 79 | 80 | async getStakingAssetID (supernetID?: string): Promise { 81 | const response: JsonRpcResponse = await this.call('getStakingAssetID', [{ supernetID }]) 82 | return response.result 83 | } 84 | 85 | /** 86 | * @deprecated 87 | * // TODO must be replace with getSupernet 88 | */ 89 | async getSupernets (ids: string[]): Promise { 90 | const response: JsonRpcResponse = await this.call('getSupernets', [{ ids }]) 91 | return response.result 92 | } 93 | 94 | async getTimestamp (): Promise { 95 | const response: JsonRpcResponse = await this.call('getTimestamp') 96 | return response.result 97 | } 98 | 99 | async getTotalStake (supernetID: string): Promise { 100 | const response: JsonRpcResponse = await this.call('getTotalStake', [{ supernetID }]) 101 | return response.result 102 | } 103 | 104 | async getTxStatus (txID: string): Promise { 105 | const response: JsonRpcResponse = await this.call('getTxStatus', [{ txID }]) 106 | return response.result 107 | } 108 | 109 | async getValidatorsAt (height: number, supernetID?: string): Promise { 110 | const response: JsonRpcResponse = await this.call('getValidatorsAt', [{ height, supernetID }]) 111 | return response.result 112 | } 113 | 114 | async sampleValidators (size: number, supernetID?: string): Promise { 115 | const response: JsonRpcResponse = await this.call('sampleValidators', [{ size, supernetID }]) 116 | return response.result 117 | } 118 | 119 | async validatedBy (blockchainID: string): Promise { 120 | const response: JsonRpcResponse = await this.call('validatedBy', [{ blockchainID }]) 121 | return response.result 122 | } 123 | 124 | async validates (supernetID: string): Promise { 125 | const response: JsonRpcResponse = await this.call('validates', [{ supernetID }]) 126 | return response.result 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/api/platform/data.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @deprecated 3 | */ 4 | export interface GetBlockchainsResponse { 5 | blockchains: BlockchainData[] 6 | } 7 | 8 | /** 9 | * @deprecated 10 | */ 11 | export interface BlockchainData { 12 | id: string 13 | name: string 14 | supernetID: string 15 | vmID: string 16 | chainAssetID: string 17 | } 18 | 19 | export interface GetBlockchainStatusResponse { 20 | status: string 21 | } 22 | 23 | export interface GetCurrentSupplyResponse { 24 | supply: bigint 25 | } 26 | 27 | export interface GetRewardPoolSupplyResponse { 28 | rewardPoolSupply: bigint 29 | } 30 | 31 | export interface GetCurrentValidatorsResponse { 32 | validators: ValidatorData[] 33 | } 34 | 35 | export interface ValidatorData { 36 | txID: string 37 | startTime: bigint 38 | endTime: bigint 39 | weight: bigint 40 | nodeID: string 41 | stakeAmount: bigint 42 | rewardOwner: RewardOwner 43 | validationRewardOwner: RewardOwner 44 | delegationRewardOwner: RewardOwner 45 | potentialReward: bigint 46 | accruedDelegateeReward: bigint 47 | delegationFee: number 48 | uptime: number 49 | connected: boolean 50 | signer: ValidatorSigner 51 | delegatorCount: number 52 | delegatorWeight: bigint 53 | delegators: DelegatorData[] 54 | } 55 | 56 | export interface RewardOwner { 57 | locktime: bigint 58 | threshold: number 59 | addresses: string[] 60 | } 61 | 62 | export interface ValidatorSigner { 63 | publicKey: string 64 | proofOfPossession: string 65 | } 66 | 67 | export interface DelegatorData { 68 | txID: string 69 | startTime: bigint 70 | endTime: bigint 71 | weight: bigint 72 | nodeID: string 73 | stakeAmount: bigint 74 | rewardOwner: RewardOwner 75 | potentialReward: bigint 76 | } 77 | 78 | export interface GetMinStakeResponse { 79 | minValidatorStake: bigint 80 | minDelegatorStake: bigint 81 | } 82 | 83 | export interface GetStakingAssetIDResponse { 84 | assetID: string 85 | } 86 | 87 | /** 88 | * @deprecated 89 | */ 90 | export interface GetSupernetsResponse { 91 | supernets: SupernetData[] 92 | } 93 | 94 | /** 95 | * @deprecated 96 | */ 97 | export interface SupernetData { 98 | id: string 99 | controlKeys: string[] 100 | threshold: number 101 | } 102 | 103 | export interface GetTimestampResponse { 104 | timestamp: string 105 | } 106 | 107 | export interface GetTotalStakeResponse { 108 | stake: bigint 109 | weight: bigint 110 | } 111 | 112 | export interface GetValidatorsAtResponse { 113 | validators: Record 114 | } 115 | 116 | export interface ValidatorAtData { 117 | publicKey: string 118 | weight: bigint 119 | } 120 | 121 | export interface SampleValidatorsResponse { 122 | validators: string[] 123 | } 124 | 125 | export interface ValidatedByResponse { 126 | supernetID: string 127 | } 128 | 129 | export interface ValidatesResponse { 130 | blockchainIDs: string[] 131 | } 132 | -------------------------------------------------------------------------------- /src/api/platform/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api' 2 | export * from './data' 3 | -------------------------------------------------------------------------------- /src/asset/asset.ts: -------------------------------------------------------------------------------- 1 | import { AssetValue } from '../utils' 2 | 3 | export enum TokenType { 4 | Generic = 'generic', 5 | Gas = 'gas', 6 | ERC20 = 'erc20', 7 | Wrapped = 'wrapped', 8 | JRC20 = 'jrc20', 9 | JNT = 'jnt', 10 | } 11 | 12 | /** 13 | * Representation of an asset on a chain with its common values 14 | * such as an id, a name, a symbol and decimals. 15 | */ 16 | export class TokenAsset { 17 | readonly type: TokenType = TokenType.Generic 18 | readonly assetId: string 19 | readonly name: string 20 | readonly symbol: string 21 | readonly decimals: number 22 | 23 | constructor (assetId: string, name: string, symbol: string, decimals: number) { 24 | this.assetId = assetId 25 | this.name = name 26 | this.symbol = symbol 27 | this.decimals = decimals 28 | } 29 | 30 | /** 31 | * Get an AssetValue helper of this token set to a value. 32 | * @param value The value, or amount of this asset to set. 33 | * @returns A new AssetValue which can be used to display the provided value with the decimals of this token. 34 | */ 35 | getAssetValue (value: bigint): AssetValue { 36 | return new AssetValue(value, this.decimals) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/asset/evm.ts: -------------------------------------------------------------------------------- 1 | import { WrappedContractAdapter } from '../chain' 2 | import { TokenAsset, TokenType } from './asset' 3 | 4 | const EVMGasTokenDecimals = 18 5 | 6 | /** 7 | * Representation of the gas token used to pay fees in an EVM. 8 | * It is also known as the native asset of an EVM. 9 | */ 10 | export class EVMGasToken extends TokenAsset { 11 | override readonly type: TokenType = TokenType.Gas 12 | 13 | constructor (assetId: string, name: string, symbol: string) { 14 | super(assetId, name, symbol, EVMGasTokenDecimals) 15 | } 16 | } 17 | 18 | export interface EVMContract { 19 | getAddress: () => string 20 | } 21 | 22 | /** 23 | * Representation of an ERC20 smart contract. 24 | */ 25 | export class ERC20Asset extends TokenAsset implements EVMContract { 26 | override readonly type: TokenType = TokenType.ERC20 27 | readonly address 28 | 29 | constructor (address: string, name: string, symbol: string, decimals: number) { 30 | super(address, name, symbol, decimals) 31 | this.address = address 32 | } 33 | 34 | getAddress (): string { 35 | return this.address 36 | } 37 | } 38 | 39 | /** 40 | * Representation of a wrapped gas token smart contract. 41 | * Also known as wrapped native. In the Juneo network it is deployed as the wJUNE. 42 | */ 43 | export class WrappedAsset extends ERC20Asset { 44 | override readonly type: TokenType = TokenType.Wrapped 45 | readonly adapter: WrappedContractAdapter 46 | 47 | constructor (address: string, name: string, symbol: string, decimals: number) { 48 | super(address, name, symbol, decimals) 49 | this.adapter = new WrappedContractAdapter(address) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/asset/index.ts: -------------------------------------------------------------------------------- 1 | export * from './asset' 2 | export * from './evm' 3 | export * from './jevm' 4 | export * from './jvm' 5 | -------------------------------------------------------------------------------- /src/asset/jevm.ts: -------------------------------------------------------------------------------- 1 | import { JRC20ContractAdapter } from '../chain' 2 | import { TokenType } from './asset' 3 | import { ERC20Asset, EVMGasToken } from './evm' 4 | import { type JNTAsset } from './jvm' 5 | 6 | /** 7 | * Representation of the gas token used to pay fees in an JEVM. 8 | * JEVM gas token also has a reference to its native JNT asset. 9 | * It is also known as the native asset of a JEVM. 10 | */ 11 | export class JEVMGasToken extends EVMGasToken { 12 | nativeAsset: JNTAsset 13 | 14 | constructor (nativeAsset: JNTAsset) { 15 | super(nativeAsset.assetId, nativeAsset.name, nativeAsset.symbol) 16 | this.nativeAsset = nativeAsset 17 | } 18 | } 19 | 20 | /** 21 | * Representation of a JRC20 smart contract. 22 | */ 23 | export class JRC20Asset extends ERC20Asset { 24 | override readonly type: TokenType = TokenType.JRC20 25 | readonly nativeAssetId: string 26 | readonly adapter: JRC20ContractAdapter 27 | 28 | constructor (address: string, name: string, symbol: string, decimals: number, nativeAssetId: string) { 29 | super(address, name, symbol, decimals) 30 | this.nativeAssetId = nativeAssetId 31 | this.adapter = new JRC20ContractAdapter(address) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/asset/jvm.ts: -------------------------------------------------------------------------------- 1 | import { TokenAsset, TokenType } from './asset' 2 | 3 | /** 4 | * Representation of a Juneo native asset. 5 | */ 6 | export class JNTAsset extends TokenAsset { 7 | override readonly type: TokenType = TokenType.JNT 8 | readonly mintable: boolean 9 | 10 | constructor (assetId: string, name: string, symbol: string, decimals: number, mintable: boolean) { 11 | super(assetId, name, symbol, decimals) 12 | this.mintable = mintable 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/chain/chain.ts: -------------------------------------------------------------------------------- 1 | import { type TokenAsset } from '../asset' 2 | import { type ChainVM, type MCNProvider } from '../juneo' 3 | 4 | export interface Blockchain { 5 | name: string 6 | id: string 7 | vm: ChainVM 8 | asset: TokenAsset 9 | assetId: string 10 | aliases: string[] 11 | 12 | getRegisteredAssets: () => IterableIterator 13 | 14 | addRegisteredAsset: (asset: TokenAsset) => void 15 | 16 | getAsset: (provider: MCNProvider, assetId: string) => Promise 17 | 18 | validateAddress: (address: string, hrp?: string) => boolean 19 | } 20 | 21 | export abstract class AbstractBlockchain implements Blockchain { 22 | name: string 23 | id: string 24 | vm: ChainVM 25 | asset: TokenAsset 26 | assetId: string 27 | aliases: string[] 28 | registeredAssets = new Map() 29 | 30 | constructor ( 31 | name: string, 32 | id: string, 33 | vm: ChainVM, 34 | asset: TokenAsset, 35 | aliases: string[] = [], 36 | registeredAssets: TokenAsset[] = [] 37 | ) { 38 | this.name = name 39 | this.id = id 40 | this.vm = vm 41 | this.asset = asset 42 | this.assetId = asset.assetId 43 | this.aliases = aliases 44 | for (const asset of registeredAssets) { 45 | this.addRegisteredAsset(asset) 46 | } 47 | this.addRegisteredAsset(asset) 48 | } 49 | 50 | getRegisteredAssets (): IterableIterator { 51 | return this.registeredAssets.values() 52 | } 53 | 54 | addRegisteredAsset (asset: TokenAsset): void { 55 | this.registeredAssets.set(asset.assetId, asset) 56 | } 57 | 58 | async getAsset (provider: MCNProvider, assetId: string): Promise { 59 | if (this.registeredAssets.has(assetId)) { 60 | return this.registeredAssets.get(assetId)! 61 | } 62 | const asset = await this.fetchAsset(provider, assetId) 63 | this.addRegisteredAsset(asset) 64 | return asset 65 | } 66 | 67 | protected abstract fetchAsset (provider: MCNProvider, assetId: string): Promise 68 | 69 | abstract validateAddress (address: string, hrp?: string): boolean 70 | } 71 | -------------------------------------------------------------------------------- /src/chain/constants.ts: -------------------------------------------------------------------------------- 1 | import { TokenType } from '../asset' 2 | 3 | export const PLATFORMVM_ID: string = '11111111111111111111111111111111LpoYY' 4 | export const JVM_ID: string = 'otSmSxFRBqdRX7kestRW732n3WS2MrLAoWwHZxHnmMGMuLYX8' 5 | export const JEVM_ID: string = 'orkbbNQVf27TiBe6GqN5dm8d8Lo3rutEov8DUWZaKNUjckwSk' 6 | export const EVM_ID: string = 'mgj786NP7uDwBCcq6YwThhaN8FLyybkCa4zBWTQbNgmK6k9A6' 7 | 8 | export const JVM_HD_PATH = 9000 9 | export const EVM_HD_PATH = 60 10 | 11 | export const BaseShare: number = 100_0000 // 100% 12 | 13 | export const NativeAssetBalanceContract: string = '0x0100000000000000000000000000000000000001' 14 | export const NativeAssetCallContract: string = '0x0100000000000000000000000000000000000002' 15 | 16 | export const SendEtherGasLimit = BigInt(21_000) 17 | export const EmptyCallData = '0x' 18 | export const EVMTransferables: TokenType[] = [TokenType.ERC20, TokenType.JRC20, TokenType.Wrapped] 19 | -------------------------------------------------------------------------------- /src/chain/index.ts: -------------------------------------------------------------------------------- 1 | export * from './chain' 2 | export * from './constants' 3 | export * from './jevm' 4 | export * from './jvm' 5 | export * from './platform' 6 | export * from './solidity' 7 | export * from './vm' 8 | -------------------------------------------------------------------------------- /src/chain/jevm.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers' 2 | import { type JEVMAPI } from '../api' 3 | import { type JEVMGasToken, type JRC20Asset, type TokenAsset, type WrappedAsset } from '../asset' 4 | import { type MCNProvider } from '../juneo' 5 | import { AssetId } from '../transaction' 6 | import { ChainError, fetchJNT, isContractAddress } from '../utils' 7 | import { AbstractBlockchain } from './chain' 8 | import { EmptyCallData, EVM_HD_PATH, EVMTransferables, JEVM_ID } from './constants' 9 | import { ContractManager, ERC20TokenHandler } from './solidity' 10 | import { ChainVM, VMType, VMWalletType } from './vm' 11 | 12 | export class JEVMBlockchain extends AbstractBlockchain { 13 | override asset: JEVMGasToken 14 | chainId: bigint 15 | baseFee: bigint 16 | ethProvider: ethers.JsonRpcProvider 17 | jrc20Assets: JRC20Asset[] 18 | wrappedAsset: WrappedAsset | undefined 19 | private readonly contractManager: ContractManager = new ContractManager() 20 | 21 | constructor ( 22 | name: string, 23 | id: string, 24 | asset: JEVMGasToken, 25 | chainId: bigint, 26 | baseFee: bigint, 27 | nodeAddress: string, 28 | aliases?: string[], 29 | registeredAssets: TokenAsset[] = [], 30 | jrc20Assets: JRC20Asset[] = [], 31 | wrappedAsset?: WrappedAsset | undefined 32 | ) { 33 | super(name, id, new ChainVM(JEVM_ID, VMType.EVM, VMWalletType.Nonce, EVM_HD_PATH), asset, aliases, registeredAssets) 34 | this.asset = asset 35 | this.chainId = chainId 36 | this.baseFee = baseFee 37 | this.ethProvider = new ethers.JsonRpcProvider(`${nodeAddress}/ext/bc/${id}/rpc`) 38 | this.jrc20Assets = jrc20Assets 39 | this.wrappedAsset = wrappedAsset 40 | if (typeof wrappedAsset !== 'undefined') { 41 | this.addRegisteredAsset(wrappedAsset) 42 | } 43 | this.contractManager.registerHandler(new ERC20TokenHandler(this.ethProvider)) 44 | } 45 | 46 | setProvider (nodeAddress: string): void { 47 | this.ethProvider = new ethers.JsonRpcProvider(`${nodeAddress}/ext/bc/${this.id}/rpc`) 48 | this.contractManager.resetHandlers() 49 | this.contractManager.registerHandler(new ERC20TokenHandler(this.ethProvider)) 50 | } 51 | 52 | async getContractTransactionData ( 53 | provider: MCNProvider, 54 | assetId: string, 55 | to: string, 56 | amount: bigint 57 | ): Promise { 58 | // could rather use contract manager but it would require one extra network call 59 | // we avoid it if the asset is already registered 60 | const asset = await this.getAsset(provider, assetId) 61 | if (EVMTransferables.includes(asset.type)) { 62 | return this.contractManager.getTransferData(this.ethProvider, assetId, to, amount) 63 | } 64 | return EmptyCallData 65 | } 66 | 67 | protected async fetchAsset (provider: MCNProvider, assetId: string): Promise { 68 | if (isContractAddress(assetId)) { 69 | const handler = await this.contractManager.getHandler(assetId) 70 | if (handler === null) { 71 | throw new ChainError(`contract address ${assetId} does not implement a compatible interface`) 72 | } 73 | return await handler.queryTokenData(assetId) 74 | } 75 | return await fetchJNT(provider, assetId) 76 | } 77 | 78 | validateAddress (address: string): boolean { 79 | return ethers.isAddress(address) 80 | } 81 | 82 | async queryBalance (api: JEVMAPI, address: string, assetId: string): Promise { 83 | // native asset 84 | if (assetId === this.assetId) { 85 | return await api.eth_getBalance(address, 'pending') 86 | } 87 | // jnt asset 88 | if (AssetId.validate(assetId)) { 89 | return await api.eth_getAssetBalance(address, 'pending', assetId) 90 | } 91 | // from here should only be solidity smart contract 92 | if (!isContractAddress(assetId)) { 93 | throw new ChainError(`cannot query balance of invalid asset id ${assetId}`) 94 | } 95 | return await this.contractManager.balanceOf(this.ethProvider, assetId, address) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/chain/jvm.ts: -------------------------------------------------------------------------------- 1 | import { type JNTAsset, type TokenAsset } from '../asset' 2 | import { type MCNProvider } from '../juneo' 3 | import { fetchJNT, validateBech32 } from '../utils' 4 | import { AbstractBlockchain } from './chain' 5 | import { JVM_HD_PATH, JVM_ID } from './constants' 6 | import { ChainVM, VMType, VMWalletType } from './vm' 7 | 8 | export class JVMBlockchain extends AbstractBlockchain { 9 | constructor (name: string, id: string, asset: JNTAsset, aliases?: string[], registeredAssets: TokenAsset[] = []) { 10 | super(name, id, new ChainVM(JVM_ID, VMType.JVM, VMWalletType.Utxo, JVM_HD_PATH), asset, aliases, registeredAssets) 11 | } 12 | 13 | protected async fetchAsset (provider: MCNProvider, assetId: string): Promise { 14 | return await fetchJNT(provider, assetId) 15 | } 16 | 17 | validateAddress (address: string, hrp?: string): boolean { 18 | return validateBech32(address, hrp, this.aliases.concat(this.id)) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/chain/platform.ts: -------------------------------------------------------------------------------- 1 | import { type JNTAsset, type TokenAsset } from '../asset' 2 | import { type MCNProvider } from '../juneo' 3 | import { fetchJNT, RewardCalculator, TimeUtils, validateBech32 } from '../utils' 4 | import { AbstractBlockchain } from './chain' 5 | import { BaseShare, JVM_HD_PATH, PLATFORMVM_ID } from './constants' 6 | import { ChainVM, VMType, VMWalletType } from './vm' 7 | 8 | export class PlatformBlockchain extends AbstractBlockchain { 9 | stakeConfig: StakeConfig 10 | rewardConfig: RewardConfig 11 | rewardCalculator: RewardCalculator 12 | 13 | constructor ( 14 | name: string, 15 | id: string, 16 | asset: JNTAsset, 17 | stakeConfig: StakeConfig, 18 | rewardConfig: RewardConfig, 19 | aliases?: string[], 20 | registeredAssets: TokenAsset[] = [] 21 | ) { 22 | super( 23 | name, 24 | id, 25 | new ChainVM(PLATFORMVM_ID, VMType.JVM, VMWalletType.Utxo, JVM_HD_PATH), 26 | asset, 27 | aliases, 28 | registeredAssets 29 | ) 30 | this.stakeConfig = stakeConfig 31 | this.rewardConfig = rewardConfig 32 | this.rewardCalculator = new RewardCalculator(rewardConfig) 33 | } 34 | 35 | protected async fetchAsset (provider: MCNProvider, assetId: string): Promise { 36 | return await fetchJNT(provider, assetId) 37 | } 38 | 39 | validateAddress (address: string, hrp?: string): boolean { 40 | return validateBech32(address, hrp, this.aliases.concat(this.id)) 41 | } 42 | 43 | estimatePrimaryValidationReward (stakePeriod: bigint, stakeAmount: bigint): bigint { 44 | return this.rewardCalculator.calculate(stakePeriod, TimeUtils.now(), stakeAmount) 45 | } 46 | 47 | estimatePrimaryDelegationReward (stakePeriod: bigint, stakeAmount: bigint): bigint { 48 | const rewards = this.rewardCalculator.calculate(stakePeriod, TimeUtils.now(), stakeAmount) 49 | return (rewards * BigInt(BaseShare - this.stakeConfig.minDelegationFee)) / BigInt(BaseShare) 50 | } 51 | } 52 | 53 | export class StakeConfig { 54 | uptimeRequirement: number 55 | minValidatorStake: bigint 56 | maxValidatorStake: bigint 57 | minDelegatorStake: bigint 58 | minDelegationFee: number 59 | maxDelegationFee: number 60 | minStakeDuration: bigint 61 | maxStakeDuration: bigint 62 | 63 | constructor ( 64 | uptimeRequirement: number, 65 | minValidatorStake: bigint, 66 | maxValidatorStake: bigint, 67 | minDelegatorStake: bigint, 68 | minDelegationFee: number, 69 | maxDelegationFee: number, 70 | minStakeDuration: bigint, 71 | maxStakeDuration: bigint 72 | ) { 73 | this.uptimeRequirement = uptimeRequirement 74 | this.minValidatorStake = minValidatorStake 75 | this.maxValidatorStake = maxValidatorStake 76 | this.minDelegatorStake = minDelegatorStake 77 | this.minDelegationFee = minDelegationFee 78 | this.maxDelegationFee = maxDelegationFee 79 | this.minStakeDuration = minStakeDuration 80 | this.maxStakeDuration = maxStakeDuration 81 | } 82 | } 83 | 84 | export class RewardConfig { 85 | minStakePeriod: bigint 86 | maxStakePeriod: bigint 87 | stakePeriodRewardShare: bigint 88 | startRewardTime: bigint 89 | startRewardShare: bigint 90 | diminishingRewardTime: bigint 91 | diminishingRewardShare: bigint 92 | targetRewardTime: bigint 93 | targetRewardShare: bigint 94 | 95 | constructor ( 96 | minStakePeriod: bigint, 97 | maxStakePeriod: bigint, 98 | stakePeriodRewardShare: bigint, 99 | startRewardTime: bigint, 100 | startRewardShare: bigint, 101 | diminishingRewardTime: bigint, 102 | diminishingRewardShare: bigint, 103 | targetRewardTime: bigint, 104 | targetRewardShare: bigint 105 | ) { 106 | this.minStakePeriod = minStakePeriod 107 | this.maxStakePeriod = maxStakePeriod 108 | this.stakePeriodRewardShare = stakePeriodRewardShare 109 | this.startRewardTime = startRewardTime 110 | this.startRewardShare = startRewardShare 111 | this.diminishingRewardTime = diminishingRewardTime 112 | this.diminishingRewardShare = diminishingRewardShare 113 | this.targetRewardTime = targetRewardTime 114 | this.targetRewardShare = targetRewardShare 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/chain/solidity/abi.ts: -------------------------------------------------------------------------------- 1 | export const TransferABI: string[] = ['function transfer(address to, uint amount) returns (bool)'] 2 | 3 | export const BalanceOfABI: string[] = ['function balanceOf(address owner) view returns (uint256)'] 4 | 5 | export const ERC20ABI: string[] = [ 6 | 'function name() view returns (string)', 7 | 'function decimals() view returns (uint8)', 8 | 'function symbol() view returns (string)', 9 | 'function totalSupply() view returns (uint256)', 10 | 'function balanceOf(address owner) view returns (uint256)', 11 | 'function allowance(address owner, address spender) view returns (uint256)', 12 | 'function transfer(address to, uint amount) returns (bool)', 13 | 'function approve(address spender, uint256 amount) returns (bool)', 14 | 'function transferFrom(address from, address to, uint256 amount) returns (bool)', 15 | 'event Transfer(address indexed from, address indexed to, uint256 value)', 16 | 'event Approval(address indexed owner, address indexed spender, uint256 value)' 17 | ] 18 | 19 | export const JRC20ABI: string[] = ERC20ABI.concat([ 20 | 'function deposit()', 21 | 'function withdraw(uint256 value)', 22 | 'function totalNativeSupply() view returns (uint256)', 23 | 'function nativeAssetId() view returns (uint256)' 24 | ]) 25 | 26 | export const WrappedABI: string[] = ERC20ABI.concat([ 27 | 'function deposit() payable', 28 | 'function withdraw(uint wad)', 29 | 'event Deposit(address indexed dst, uint wad)', 30 | 'event Withdrawal(address indexed src, uint wad)' 31 | ]) 32 | -------------------------------------------------------------------------------- /src/chain/solidity/contract.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers' 2 | import { ERC20Asset, type TokenAsset } from '../../asset' 3 | import { AssetId } from '../../transaction' 4 | import * as abi from './abi' 5 | 6 | export class ContractManager { 7 | private handlers: SolidityTokenHandler[] = [] 8 | 9 | async getHandler (contractAddress: string): Promise { 10 | for (const handler of this.handlers) { 11 | if (await handler.instanceOf(contractAddress)) { 12 | return handler 13 | } 14 | } 15 | return null 16 | } 17 | 18 | resetHandlers (): void { 19 | this.handlers = [] 20 | } 21 | 22 | registerHandler (handler: SolidityTokenHandler): void { 23 | // register at first position because of getHandler iteration implementation 24 | // since specific handler may have common functions with default ones 25 | // it is preferable to check if it is part of those first. 26 | // Should do a better getHandler function in the future 27 | this.handlers.unshift(handler) 28 | } 29 | 30 | async balanceOf (provider: ethers.JsonRpcProvider, contractAddress: string, address: string): Promise { 31 | const contract = new ethers.Contract(contractAddress, abi.BalanceOfABI, provider) 32 | return BigInt.asUintN(256, BigInt(await contract.balanceOf(address))) 33 | } 34 | 35 | getTransferData (provider: ethers.JsonRpcProvider, contractAddress: string, to: string, amount: bigint): string { 36 | const contract = new ethers.Contract(contractAddress, abi.TransferABI, provider) 37 | return contract.interface.encodeFunctionData('transfer', [to, amount]) 38 | } 39 | } 40 | 41 | export interface SolidityTokenHandler { 42 | instanceOf: (contractAddress: string) => Promise 43 | 44 | queryTokenData: (contractAddress: string) => Promise 45 | } 46 | 47 | export class ERC20TokenHandler implements SolidityTokenHandler { 48 | protected readonly provider: ethers.JsonRpcProvider 49 | 50 | constructor (provider: ethers.JsonRpcProvider) { 51 | this.provider = provider 52 | } 53 | 54 | async instanceOf (contractAddress: string): Promise { 55 | const contract = this.getContract(contractAddress) 56 | // checking if is ERC20 by calling decimals read only function 57 | // other main tokens interfaces should not be using decimals 58 | // IERC165 is not widespread enough to be used by ERC20 tokens 59 | await contract.decimals().catch(() => { 60 | return false 61 | }) 62 | return true 63 | } 64 | 65 | async queryTokenData (contractAddress: string): Promise { 66 | const contract = this.getContract(contractAddress) 67 | const name: string = await contract.name() 68 | const symbol: string = await contract.symbol() 69 | const decimals: number = await contract.decimals() 70 | return new ERC20Asset(contractAddress, name, symbol, decimals) 71 | } 72 | 73 | protected getContract (contractAddress: string): ethers.Contract { 74 | return new ethers.Contract(contractAddress, abi.ERC20ABI, this.provider) 75 | } 76 | } 77 | 78 | export class EVMCallAdapter { 79 | private readonly contract: ethers.Contract 80 | 81 | constructor (contractAddress: string, abi: string[]) { 82 | this.contract = new ethers.Contract(contractAddress, abi) 83 | } 84 | 85 | getFunctionData (name: string, parameters: any[] = []): string { 86 | return this.contract.interface.encodeFunctionData(name, parameters) 87 | } 88 | } 89 | 90 | export class JRC20ContractAdapter extends EVMCallAdapter { 91 | private readonly contractAddress: string 92 | 93 | constructor (contractAddress: string) { 94 | super(contractAddress, abi.JRC20ABI) 95 | this.contractAddress = contractAddress 96 | } 97 | 98 | getWithdrawData (value: bigint): string { 99 | return this.getFunctionData('withdraw', [value]) 100 | } 101 | 102 | getDepositData (assetId: string, amount: bigint): string { 103 | // native asset call data 104 | let data = ethers.solidityPacked( 105 | ['address', 'uint256', 'uint256'], 106 | [this.contractAddress, `0x${new AssetId(assetId).serialize().toHex()}`, amount] 107 | ) 108 | // add deposit function removed hex prefix 109 | data += this.getFunctionData('deposit').substring(2) 110 | return data 111 | } 112 | } 113 | 114 | export class WrappedContractAdapter extends EVMCallAdapter { 115 | constructor (contractAddress: string) { 116 | super(contractAddress, abi.WrappedABI) 117 | } 118 | 119 | getWithdrawData (value: bigint): string { 120 | return this.getFunctionData('withdraw', [value]) 121 | } 122 | 123 | getDepositData (): string { 124 | return this.getFunctionData('deposit') 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/chain/solidity/index.ts: -------------------------------------------------------------------------------- 1 | export * from './contract' 2 | -------------------------------------------------------------------------------- /src/chain/vm.ts: -------------------------------------------------------------------------------- 1 | export enum VMType { 2 | EVM = 'EVM', 3 | JVM = 'JVM', 4 | Custom = 'Custom', 5 | } 6 | 7 | export enum VMWalletType { 8 | Utxo = 'Utxo', 9 | Nonce = 'Nonce', 10 | Custom = 'Custom', 11 | } 12 | 13 | export class ChainVM { 14 | id: string 15 | type: VMType 16 | walletType: VMWalletType 17 | hdPath: number 18 | 19 | constructor (id: string, type: VMType, walletType: VMWalletType, hdPath: number) { 20 | this.id = id 21 | this.type = type 22 | this.walletType = walletType 23 | this.hdPath = hdPath 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as juneojs from './juneo' 2 | 3 | export { juneojs } 4 | 5 | export * from './juneo' 6 | -------------------------------------------------------------------------------- /src/juneo.ts: -------------------------------------------------------------------------------- 1 | import { InfoAPI, JEVMAPI, JuneoClient, JVMAPI, PlatformAPI } from './api' 2 | import { JEVM_ID, type JEVMBlockchain, type JVMBlockchain, type PlatformBlockchain } from './chain' 3 | import { 4 | BananaNetwork, 5 | BluebyteNetwork, 6 | FusionNetwork, 7 | LocalNetwork, 8 | MainNetwork, 9 | SocotraNetwork, 10 | type MCN 11 | } from './network' 12 | import { MCNAccount } from './wallet' 13 | 14 | export class MCNProvider { 15 | mcn: MCN 16 | client: JuneoClient 17 | info: InfoAPI 18 | platformChain: PlatformBlockchain 19 | platformApi: PlatformAPI 20 | jvmChain: JVMBlockchain 21 | jvmApi: JVMAPI 22 | juneChain: JEVMBlockchain 23 | juneApi: JEVMAPI 24 | jevmApi: Record = {} 25 | juneAssetId: string 26 | 27 | constructor (mcn: MCN = MainNetwork, client: JuneoClient = JuneoClient.parse(mcn.url)) { 28 | this.mcn = mcn 29 | this.mcn.url = client.getUrl() 30 | this.client = client 31 | this.info = new InfoAPI(client) 32 | this.platformChain = this.mcn.primary.platform 33 | this.platformApi = new PlatformAPI(client, this.platformChain) 34 | this.jvmChain = this.mcn.primary.jvm 35 | this.jvmApi = new JVMAPI(client, this.jvmChain) 36 | this.juneChain = this.mcn.primary.june 37 | this.juneApi = new JEVMAPI(client, this.juneChain) 38 | for (const supernet of this.mcn.supernets) { 39 | for (const chain of supernet.chains) { 40 | if (chain.vm.id === JEVM_ID) { 41 | const jevm = chain as JEVMBlockchain 42 | this.jevmApi[chain.id] = new JEVMAPI(client, jevm) 43 | jevm.setProvider(this.mcn.url) 44 | } 45 | } 46 | } 47 | this.juneAssetId = this.platformChain.assetId 48 | } 49 | 50 | recoverAccount (recoveryData: string): MCNAccount { 51 | return new MCNAccount(this, this.mcn.recoverWallet(recoveryData)) 52 | } 53 | 54 | generateAccount (wordsCount?: number): MCNAccount { 55 | return new MCNAccount(this, this.mcn.generateWallet(wordsCount)) 56 | } 57 | 58 | async getStaticProvider (): Promise { 59 | return this 60 | } 61 | 62 | static fromId (id: number): MCNProvider { 63 | switch (id) { 64 | case MainNetwork.id: { 65 | return new MCNProvider(MainNetwork) 66 | } 67 | case SocotraNetwork.id: { 68 | return new MCNProvider(SocotraNetwork) 69 | } 70 | case BananaNetwork.id: { 71 | return new MCNProvider(BananaNetwork) 72 | } 73 | case BluebyteNetwork.id: { 74 | return new MCNProvider(BluebyteNetwork) 75 | } 76 | case FusionNetwork.id: { 77 | return new MCNProvider(FusionNetwork) 78 | } 79 | case LocalNetwork.id: { 80 | return new MCNProvider(LocalNetwork) 81 | } 82 | default: { 83 | throw new Error(`unsupported network id: ${id}`) 84 | } 85 | } 86 | } 87 | 88 | static fromHrp (hrp: string): MCNProvider { 89 | switch (hrp) { 90 | case MainNetwork.hrp: { 91 | return new MCNProvider(MainNetwork) 92 | } 93 | case SocotraNetwork.hrp: { 94 | return new MCNProvider(SocotraNetwork) 95 | } 96 | case BananaNetwork.hrp: { 97 | return new MCNProvider(BananaNetwork) 98 | } 99 | case BluebyteNetwork.hrp: { 100 | return new MCNProvider(BluebyteNetwork) 101 | } 102 | case FusionNetwork.hrp: { 103 | return new MCNProvider(FusionNetwork) 104 | } 105 | case LocalNetwork.hrp: { 106 | return new MCNProvider(LocalNetwork) 107 | } 108 | default: { 109 | throw new Error(`unsupported network hrp: ${hrp}`) 110 | } 111 | } 112 | } 113 | } 114 | 115 | export * from './api' 116 | export * from './asset' 117 | export * from './chain' 118 | export * from './network' 119 | export * from './transaction' 120 | export * from './utils' 121 | export * from './wallet' 122 | -------------------------------------------------------------------------------- /src/network/index.ts: -------------------------------------------------------------------------------- 1 | export * from './banana' 2 | export * from './bluebyte' 3 | export * from './fusion' 4 | export * from './genesis' 5 | export * from './local' 6 | export * from './mainnet' 7 | export * from './network' 8 | export * from './socotra' 9 | -------------------------------------------------------------------------------- /src/network/network.ts: -------------------------------------------------------------------------------- 1 | import { type Blockchain, type JEVMBlockchain, type JVMBlockchain, type PlatformBlockchain } from '../chain' 2 | import { MCNWallet } from '../wallet' 3 | 4 | const PrimarySupernetName = 'Primary Supernet' 5 | 6 | export class MCN { 7 | name: string 8 | url: string 9 | id: number 10 | hrp: string 11 | primary: PrimarySupernet 12 | supernets: Supernet[] 13 | 14 | constructor ( 15 | name: string, 16 | url: string, 17 | id: number, 18 | hrp: string, 19 | primary: PrimarySupernet, 20 | supernets: Supernet[] = [primary] 21 | ) { 22 | this.name = name 23 | this.url = url 24 | this.id = id 25 | this.hrp = hrp 26 | this.primary = primary 27 | this.supernets = supernets 28 | } 29 | 30 | recoverWallet (recoveryData: string): MCNWallet { 31 | return new MCNWallet(this.hrp, recoveryData) 32 | } 33 | 34 | generateWallet (wordsCount: number = 12): MCNWallet { 35 | return new MCNWallet(this.hrp, wordsCount) 36 | } 37 | 38 | getChain (chainId: string): Blockchain { 39 | for (const supernet of this.supernets) { 40 | const chain = supernet.getSupernetChain(chainId) 41 | if (chain !== undefined) { 42 | return chain 43 | } 44 | } 45 | throw new Error(`no registered chain with id ${chainId} in ${this.name}`) 46 | } 47 | } 48 | 49 | export class Supernet { 50 | name: string 51 | id: string 52 | chains: Blockchain[] 53 | 54 | constructor (name: string, id: string, chains: Blockchain[]) { 55 | this.name = name 56 | this.id = id 57 | this.chains = chains 58 | } 59 | 60 | getSupernetChain (chainId: string): Blockchain { 61 | for (const chain of this.chains) { 62 | if (chain.id === chainId) { 63 | return chain 64 | } 65 | } 66 | throw new Error(`no registered chain with id ${chainId} in supernet ${this.name}`) 67 | } 68 | } 69 | 70 | export class PrimarySupernet extends Supernet { 71 | platform: PlatformBlockchain 72 | jvm: JVMBlockchain 73 | june: JEVMBlockchain 74 | 75 | constructor ( 76 | id: string, 77 | chains: Blockchain[], 78 | platform: PlatformBlockchain, 79 | jvm: JVMBlockchain, 80 | june: JEVMBlockchain 81 | ) { 82 | super(PrimarySupernetName, id, chains) 83 | this.platform = platform 84 | this.jvm = jvm 85 | this.june = june 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/transaction/constants.ts: -------------------------------------------------------------------------------- 1 | export const AddressSize = 20 2 | export const AssetIdSize = 32 3 | export const TransactionIdSize = 32 4 | export const BlockchainIdSize = 32 5 | export const SupernetIdSize = 32 6 | export const DynamicIdSize = 32 7 | export const SignatureSize = 65 8 | export const NodeIdSize = 20 9 | export const BLSPublicKeySize = 48 10 | export const BLSSignatureSize = 96 11 | export const ProofOfPossessionSize = BLSPublicKeySize + BLSSignatureSize 12 | export const PrimarySignerSize = 4 + ProofOfPossessionSize 13 | export const EmptySignerSize = 4 14 | export const ValidatorSize = NodeIdSize + 3 * 8 15 | export const EVMOutputSize = AddressSize + 8 + AssetIdSize 16 | export const EVMInputSize = AddressSize + 8 + AssetIdSize + 8 17 | 18 | export const CodecId = 0 19 | 20 | export const InvalidTypeId = -1 21 | 22 | export const Secp256k1InitialStateFxId = 0x00000000 23 | 24 | export const JVMBaseTransactionTypeId = 0x00000000 25 | export const CreateAssetTransactionTypeId = 0x00000001 26 | export const JVMImportTransactionTypeId = 0x00000003 27 | export const JVMExportTransactionTypeId = 0x00000004 28 | export const Secp256k1InputTypeId = 0x00000005 29 | export const Secp256k1OutputTypeId = 0x00000007 30 | export const Secp256k1CredentialsTypeId = 0x00000009 31 | export const SupernetAuthTypeId = 0x0000000a 32 | export const Secp256k1OutputOwnersTypeId = 0x0000000b 33 | export const AddSupernetValidatorTransactionType = 0x0000000d 34 | export const CreateChainTransactionTypeId = 0x0000000f 35 | export const CreateSupernetTransactionTypeId = 0x00000010 36 | export const PlatformImportTransactionTypeId = 0x00000011 37 | export const PlatformExportTransactionTypeId = 0x00000012 38 | export const StakeableLockedInputTypeId = 0x00000015 39 | export const StakeableLockedOutputTypeId = 0x00000016 40 | export const RemoveSupernetValidatorTransactionTypeId = 0x00000017 41 | export const TransformSupernetTransactionTypeId = 0x00000018 42 | export const AddPermissionlessValidatorTransactionTypeId = 0x00000019 43 | export const AddPermissionlessDelegatorTransactionTypeId = 0x0000001a 44 | export const EmptySignerTypeId = 0x0000001b 45 | export const PrimarySignerTypeId = 0x0000001c 46 | export const TransferSupernetOwnershipTransactionTypeId = 0x00000021 47 | export const PlatformBaseTransactionTypeId = 0x00000022 48 | 49 | export const EVMImportTransactionTypeId = 0 50 | export const EVMExportTransactionTypeId = 1 51 | 52 | export const EVMFeeConfigDefaultGasLimit: number = 15_000_000 53 | export const EVMFeeConfigDefaultTargetBlockRate: number = 2 54 | export const EVMFeeConfigDefaultMinBaseFee: number = 25_000_000_000 55 | export const EVMFeeConfigDefaultTargetGas: number = 15_000_000 56 | export const EVMFeeConfigDefaultBaseFeeChangeDenominator: number = 36 57 | export const EVMFeeConfigDefaultMinBlockGasCost: number = 0 58 | export const EVMFeeConfigDefaultMaxBlockGasCost: number = 1_000_000 59 | export const EVMFeeConfigDefaultBlockGasCostStep: number = 200_000 60 | -------------------------------------------------------------------------------- /src/transaction/genesis/evm.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers' 2 | import { GenesisError } from '../../utils' 3 | import { 4 | EVMFeeConfigDefaultBaseFeeChangeDenominator, 5 | EVMFeeConfigDefaultBlockGasCostStep, 6 | EVMFeeConfigDefaultGasLimit, 7 | EVMFeeConfigDefaultMaxBlockGasCost, 8 | EVMFeeConfigDefaultMinBaseFee, 9 | EVMFeeConfigDefaultMinBlockGasCost, 10 | EVMFeeConfigDefaultTargetBlockRate, 11 | EVMFeeConfigDefaultTargetGas 12 | } from '../constants' 13 | 14 | class EVMGenesis { 15 | chainId: number 16 | allocations: EVMAllocation[] 17 | 18 | constructor (chainId: number, allocations: EVMAllocation[] = []) { 19 | this.chainId = chainId 20 | this.allocations = allocations 21 | } 22 | 23 | generate (): string { 24 | const alloc: any = {} 25 | for (const allocation of this.allocations) { 26 | let address: string = allocation.address 27 | try { 28 | address = ethers.getAddress(address).substring(2) 29 | } catch { 30 | throw new GenesisError(`invalid address: ${allocation.address}`) 31 | } 32 | alloc[address] = { 33 | balance: `0x${allocation.balance.toString(16).toUpperCase()}` 34 | } 35 | } 36 | const json: any = JSON.parse(`{ 37 | "config": { 38 | "chainId": ${this.chainId}, 39 | "homesteadBlock": 0, 40 | "eip150Block": 0, 41 | "eip150Hash": "0x2086799aeebeae135c246c65021c82b4e15a2c451340993aacfd2751886514f0", 42 | "eip155Block": 0, 43 | "eip158Block": 0, 44 | "byzantiumBlock": 0, 45 | "constantinopleBlock": 0, 46 | "petersburgBlock": 0, 47 | "istanbulBlock": 0, 48 | "muirGlacierBlock": 0 49 | }, 50 | "alloc": ${JSON.stringify(alloc)}, 51 | "nonce": "0x0", 52 | "timestamp": "0x0", 53 | "extraData": "0x00", 54 | "gasLimit": "0x7A1200", 55 | "difficulty": "0x0", 56 | "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", 57 | "coinbase": "0x0000000000000000000000000000000000000000", 58 | "number": "0x0", 59 | "gasUsed": "0x0", 60 | "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000" 61 | }`) 62 | return JSON.stringify(json) 63 | } 64 | } 65 | 66 | export class EVMAllocation { 67 | address: string 68 | balance: bigint 69 | 70 | constructor (address: string, balance: number | bigint) { 71 | this.address = address 72 | this.balance = typeof balance === 'number' ? BigInt(balance) : balance 73 | } 74 | } 75 | 76 | export class SupernetEVMGenesis extends EVMGenesis { 77 | allowFeeRecipients: boolean 78 | feeConfig: SupernetEVMFeeConfig 79 | 80 | constructor ( 81 | chainId: number, 82 | allocations: EVMAllocation[] = [], 83 | allowFeeRecipients: boolean = true, 84 | feeConfig: SupernetEVMFeeConfig = new SupernetEVMFeeConfig() 85 | ) { 86 | super(chainId, allocations) 87 | this.allowFeeRecipients = allowFeeRecipients 88 | this.feeConfig = feeConfig 89 | } 90 | 91 | override generate (): string { 92 | const genesis: string = super.generate() 93 | const json: any = JSON.parse(genesis) 94 | json.config.supernetEVMTimestamp = 0 95 | json.gasLimit = `0x${BigInt(this.feeConfig.gasLimit).toString(16).toUpperCase()}` 96 | json.config.feeConfig = this.feeConfig 97 | json.config.allowFeeRecipients = this.allowFeeRecipients 98 | return JSON.stringify(json) 99 | } 100 | } 101 | 102 | export class SupernetEVMFeeConfig { 103 | gasLimit: number 104 | targetBlockRate: number 105 | minBaseFee: number 106 | targetGas: number 107 | baseFeeChangeDenominator: number 108 | minBlockGasCost: number 109 | maxBlockGasCost: number 110 | blockGasCostStep: number 111 | 112 | constructor ( 113 | gasLimit: number = EVMFeeConfigDefaultGasLimit, 114 | targetBlockRate: number = EVMFeeConfigDefaultTargetBlockRate, 115 | minBaseFee: number = EVMFeeConfigDefaultMinBaseFee, 116 | targetGas: number = EVMFeeConfigDefaultTargetGas, 117 | baseFeeChangeDenominator: number = EVMFeeConfigDefaultBaseFeeChangeDenominator, 118 | minBlockGasCost: number = EVMFeeConfigDefaultMinBlockGasCost, 119 | maxBlockGasCost: number = EVMFeeConfigDefaultMaxBlockGasCost, 120 | blockGasCostStep: number = EVMFeeConfigDefaultBlockGasCostStep 121 | ) { 122 | this.gasLimit = gasLimit 123 | this.targetBlockRate = targetBlockRate 124 | this.minBaseFee = minBaseFee 125 | this.targetGas = targetGas 126 | this.baseFeeChangeDenominator = baseFeeChangeDenominator 127 | this.minBlockGasCost = minBlockGasCost 128 | this.maxBlockGasCost = maxBlockGasCost 129 | this.blockGasCostStep = blockGasCostStep 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/transaction/genesis/index.ts: -------------------------------------------------------------------------------- 1 | export * from './evm' 2 | -------------------------------------------------------------------------------- /src/transaction/index.ts: -------------------------------------------------------------------------------- 1 | export * from './builder' 2 | export * from './constants' 3 | export * from './genesis' 4 | export * from './input' 5 | export * from './jevm' 6 | export * from './jvm' 7 | export * from './output' 8 | export * from './platform' 9 | export * from './signature' 10 | export * from './transaction' 11 | export * from './types' 12 | -------------------------------------------------------------------------------- /src/transaction/jevm/index.ts: -------------------------------------------------------------------------------- 1 | export * from './builder' 2 | export * from './status' 3 | export * from './transaction' 4 | -------------------------------------------------------------------------------- /src/transaction/jevm/status.ts: -------------------------------------------------------------------------------- 1 | import { type JEVMAPI } from '../../api' 2 | import { TimeUtils } from '../../utils' 3 | import { type TransactionStatusFetcher } from '../transaction' 4 | 5 | export enum JEVMTransactionStatus { 6 | Accepted = 'Accepted', 7 | Processing = 'Processing', 8 | Dropped = 'Dropped', 9 | Unknown = 'Unknown', 10 | } 11 | 12 | export class JEVMTransactionStatusFetcher implements TransactionStatusFetcher { 13 | jevmApi: JEVMAPI 14 | private attempts: number = 0 15 | transactionId: string 16 | currentStatus: string = JEVMTransactionStatus.Unknown 17 | 18 | constructor (jevmApi: JEVMAPI, transactionId: string) { 19 | this.jevmApi = jevmApi 20 | this.transactionId = transactionId 21 | } 22 | 23 | async fetch (timeout: number, delay: number): Promise { 24 | const maxAttempts: number = timeout / delay 25 | while (this.attempts < maxAttempts && !this.isCurrentStatusSettled()) { 26 | await TimeUtils.sleep(delay) 27 | await this.jevmApi.getTxStatus(this.transactionId).then( 28 | (value) => { 29 | this.currentStatus = value.status 30 | }, 31 | (error) => { 32 | if (error.message !== 'not found') { 33 | this.currentStatus = JEVMTransactionStatus.Unknown 34 | } 35 | } 36 | ) 37 | this.attempts += 1 38 | } 39 | return this.currentStatus 40 | } 41 | 42 | private isCurrentStatusSettled (): boolean { 43 | return ( 44 | this.currentStatus !== JEVMTransactionStatus.Unknown && this.currentStatus !== JEVMTransactionStatus.Processing 45 | ) 46 | } 47 | } 48 | 49 | export enum EVMTransactionStatus { 50 | Success = 'Success', 51 | Failure = 'Failure', 52 | Pending = 'Pending', 53 | Unknown = 'Unknown', 54 | } 55 | 56 | export class EVMTransactionStatusFetcher implements TransactionStatusFetcher { 57 | jevmApi: JEVMAPI 58 | private attempts: number = 0 59 | transactionHash: string 60 | currentStatus: string = EVMTransactionStatus.Unknown 61 | 62 | constructor (jevmApi: JEVMAPI, transactionHash: string) { 63 | this.jevmApi = jevmApi 64 | this.transactionHash = transactionHash 65 | } 66 | 67 | async fetch (timeout: number, delay: number): Promise { 68 | const maxAttempts: number = timeout / delay 69 | this.currentStatus = EVMTransactionStatus.Pending 70 | while (this.attempts < maxAttempts && !this.isCurrentStatusSettled()) { 71 | await TimeUtils.sleep(delay) 72 | const receipt: any = await this.jevmApi.eth_getTransactionReceipt(this.transactionHash).catch(() => { 73 | return null 74 | }) 75 | if (receipt === null) { 76 | this.attempts += 1 77 | continue 78 | } 79 | const status: number = Number(receipt.status) 80 | this.currentStatus = status === 1 ? EVMTransactionStatus.Success : EVMTransactionStatus.Failure 81 | } 82 | return this.currentStatus 83 | } 84 | 85 | private isCurrentStatusSettled (): boolean { 86 | return this.currentStatus !== EVMTransactionStatus.Unknown && this.currentStatus !== EVMTransactionStatus.Pending 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/transaction/jvm/index.ts: -------------------------------------------------------------------------------- 1 | export * from './builder' 2 | export * from './operation' 3 | export * from './status' 4 | export * from './transaction' 5 | -------------------------------------------------------------------------------- /src/transaction/jvm/operation.ts: -------------------------------------------------------------------------------- 1 | import { JuneoBuffer, type Serializable } from '../../utils' 2 | import { Secp256k1InitialStateFxId } from '../constants' 3 | import { type TransactionOutput } from '../output' 4 | import { type AssetId, type TransactionId } from '../types' 5 | 6 | export class TransferableOp implements Serializable { 7 | assetId: AssetId 8 | utxoIds: Array<[TransactionId, number]> 9 | op: TransferOp 10 | 11 | constructor (assetId: AssetId, utxoIds: Array<[TransactionId, number]>, op: TransferOp) { 12 | this.assetId = assetId 13 | this.utxoIds = utxoIds 14 | this.op = op 15 | } 16 | 17 | serialize (): JuneoBuffer { 18 | throw new Error('not implemented') 19 | } 20 | 21 | static comparator = (a: TransferableOp, b: TransferableOp): number => { 22 | return JuneoBuffer.comparator(a.serialize(), b.serialize()) 23 | } 24 | } 25 | 26 | export interface TransferOp extends Serializable { 27 | typeId: number 28 | addressIndices: number[] 29 | } 30 | 31 | export interface InitialState extends Serializable { 32 | fxId: number 33 | outputs: TransactionOutput[] 34 | } 35 | 36 | export class Secp256k1InitialState implements InitialState { 37 | fxId: number = Secp256k1InitialStateFxId 38 | outputs: TransactionOutput[] 39 | 40 | constructor (outputs: TransactionOutput[]) { 41 | this.outputs = outputs 42 | } 43 | 44 | serialize (): JuneoBuffer { 45 | const outputsBytes: JuneoBuffer[] = [] 46 | let outputsSize = 0 47 | for (const output of this.outputs) { 48 | const bytes: JuneoBuffer = output.serialize() 49 | outputsSize += bytes.length 50 | outputsBytes.push(bytes) 51 | } 52 | const buffer = JuneoBuffer.alloc(4 + 4 + outputsSize) 53 | buffer.writeUInt32(this.fxId) 54 | buffer.writeUInt32(this.outputs.length) 55 | for (const bytes of outputsBytes) { 56 | buffer.write(bytes) 57 | } 58 | return buffer 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/transaction/jvm/status.ts: -------------------------------------------------------------------------------- 1 | import { type JVMAPI } from '../../api' 2 | import { TimeUtils } from '../../utils' 3 | import { type TransactionStatusFetcher } from '../transaction' 4 | 5 | export enum JVMTransactionStatus { 6 | Accepted = 'Accepted', 7 | Processing = 'Processing', 8 | Unknown = 'Unknown', 9 | } 10 | 11 | export class JVMTransactionStatusFetcher implements TransactionStatusFetcher { 12 | jvmApi: JVMAPI 13 | private attempts: number = 0 14 | transactionId: string 15 | currentStatus: string = JVMTransactionStatus.Unknown 16 | 17 | constructor (jvmApi: JVMAPI, transactionId: string) { 18 | this.jvmApi = jvmApi 19 | this.transactionId = transactionId 20 | } 21 | 22 | async fetch (timeout: number, delay: number): Promise { 23 | const maxAttempts: number = timeout / delay 24 | this.currentStatus = JVMTransactionStatus.Processing 25 | while (this.attempts < maxAttempts && !this.isCurrentStatusSettled()) { 26 | await TimeUtils.sleep(delay) 27 | await this.jvmApi.getTx(this.transactionId).then( 28 | () => { 29 | this.currentStatus = JVMTransactionStatus.Accepted 30 | }, 31 | (error) => { 32 | if (error.message !== 'not found') { 33 | this.currentStatus = JVMTransactionStatus.Unknown 34 | } 35 | } 36 | ) 37 | this.attempts += 1 38 | } 39 | return this.currentStatus 40 | } 41 | 42 | private isCurrentStatusSettled (): boolean { 43 | return this.currentStatus !== JVMTransactionStatus.Unknown && this.currentStatus !== JVMTransactionStatus.Processing 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/transaction/jvm/transaction.ts: -------------------------------------------------------------------------------- 1 | import { JuneoBuffer } from '../../utils' 2 | import { 3 | CreateAssetTransactionTypeId, 4 | JVMBaseTransactionTypeId, 5 | JVMExportTransactionTypeId, 6 | JVMImportTransactionTypeId 7 | } from '../constants' 8 | import { type TransferableInput } from '../input' 9 | import { type TransferableOutput } from '../output' 10 | import { BaseTransaction, ExportTransaction, ImportTransaction } from '../transaction' 11 | import { type BlockchainId } from '../types' 12 | import { type InitialState } from './operation' 13 | 14 | export class JVMBaseTransaction extends BaseTransaction { 15 | constructor ( 16 | networkId: number, 17 | blockchainId: BlockchainId, 18 | outputs: TransferableOutput[], 19 | inputs: TransferableInput[], 20 | memo: string 21 | ) { 22 | super(JVMBaseTransactionTypeId, networkId, blockchainId, outputs, inputs, memo) 23 | } 24 | 25 | static parse (data: string | JuneoBuffer): JVMBaseTransaction { 26 | return BaseTransaction.parse(data, JVMBaseTransactionTypeId) 27 | } 28 | } 29 | 30 | export class JVMExportTransaction extends ExportTransaction { 31 | constructor ( 32 | networkId: number, 33 | blockchainId: BlockchainId, 34 | outputs: TransferableOutput[], 35 | inputs: TransferableInput[], 36 | memo: string, 37 | destinationChain: BlockchainId, 38 | exportedOutputs: TransferableOutput[] 39 | ) { 40 | super(JVMExportTransactionTypeId, networkId, blockchainId, outputs, inputs, memo, destinationChain, exportedOutputs) 41 | } 42 | 43 | static parse (data: string | JuneoBuffer): JVMExportTransaction { 44 | return ExportTransaction.parse(data, JVMExportTransactionTypeId) 45 | } 46 | } 47 | 48 | export class JVMImportTransaction extends ImportTransaction { 49 | constructor ( 50 | networkId: number, 51 | blockchainId: BlockchainId, 52 | outputs: TransferableOutput[], 53 | inputs: TransferableInput[], 54 | memo: string, 55 | sourceChain: BlockchainId, 56 | importedInputs: TransferableInput[] 57 | ) { 58 | super(JVMImportTransactionTypeId, networkId, blockchainId, outputs, inputs, memo, sourceChain, importedInputs) 59 | } 60 | 61 | static parse (data: string | JuneoBuffer): JVMImportTransaction { 62 | return ImportTransaction.parse(data, JVMImportTransactionTypeId) 63 | } 64 | } 65 | 66 | export class CreateAssetTransaction extends BaseTransaction { 67 | name: string 68 | symbol: string 69 | denomination: number 70 | initialStates: InitialState[] 71 | 72 | constructor ( 73 | networkId: number, 74 | blockchainId: BlockchainId, 75 | outputs: TransferableOutput[], 76 | inputs: TransferableInput[], 77 | memo: string, 78 | name: string, 79 | symbol: string, 80 | denomination: number, 81 | initialStates: InitialState[] 82 | ) { 83 | super(CreateAssetTransactionTypeId, networkId, blockchainId, outputs, inputs, memo) 84 | this.name = name 85 | this.symbol = symbol 86 | this.denomination = denomination 87 | this.initialStates = initialStates 88 | } 89 | 90 | serialize (): JuneoBuffer { 91 | const baseTransaction = super.serialize() 92 | const initialStatesBytes: JuneoBuffer[] = [] 93 | let initialStatesBytesSize = 0 94 | for (const state of this.initialStates) { 95 | const bytes = state.serialize() 96 | initialStatesBytesSize += bytes.length 97 | initialStatesBytes.push(bytes) 98 | } 99 | const buffer = JuneoBuffer.alloc( 100 | baseTransaction.length + 2 + this.name.length + 2 + this.symbol.length + 1 + 4 + initialStatesBytesSize 101 | ) 102 | buffer.write(baseTransaction) 103 | buffer.writeUInt16(this.name.length) 104 | buffer.writeString(this.name) 105 | buffer.writeUInt16(this.symbol.length) 106 | buffer.writeString(this.symbol) 107 | buffer.writeUInt8(this.denomination) 108 | buffer.writeUInt32(this.initialStates.length) 109 | for (const bytes of initialStatesBytes) { 110 | buffer.write(bytes) 111 | } 112 | return buffer 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/transaction/platform/index.ts: -------------------------------------------------------------------------------- 1 | export * from './builder' 2 | export * from './status' 3 | export * from './supernet' 4 | export * from './transaction' 5 | -------------------------------------------------------------------------------- /src/transaction/platform/status.ts: -------------------------------------------------------------------------------- 1 | import { type PlatformAPI } from '../../api' 2 | import { TimeUtils } from '../../utils' 3 | import { type TransactionStatusFetcher } from '../transaction' 4 | 5 | export enum PlatformTransactionStatus { 6 | Committed = 'Committed', 7 | Aborted = 'Aborted', 8 | Processing = 'Processing', 9 | Dropped = 'Dropped', 10 | Unknown = 'Unknown', 11 | } 12 | 13 | export class PlatformTransactionStatusFetcher implements TransactionStatusFetcher { 14 | platformApi: PlatformAPI 15 | private attempts: number = 0 16 | transactionId: string 17 | currentStatus: string = PlatformTransactionStatus.Unknown 18 | 19 | constructor (platformApi: PlatformAPI, transactionId: string) { 20 | this.platformApi = platformApi 21 | this.transactionId = transactionId 22 | } 23 | 24 | async fetch (timeout: number, delay: number): Promise { 25 | const maxAttempts: number = timeout / delay 26 | this.currentStatus = PlatformTransactionStatus.Processing 27 | while (this.attempts < maxAttempts && !this.isCurrentStatusSettled()) { 28 | await TimeUtils.sleep(delay) 29 | await this.platformApi.getTxStatus(this.transactionId).then( 30 | (value) => { 31 | this.currentStatus = value.status 32 | }, 33 | (error) => { 34 | if (error.message !== 'not found') { 35 | this.currentStatus = PlatformTransactionStatus.Unknown 36 | } 37 | } 38 | ) 39 | this.attempts += 1 40 | } 41 | return this.currentStatus 42 | } 43 | 44 | private isCurrentStatusSettled (): boolean { 45 | return ( 46 | this.currentStatus !== PlatformTransactionStatus.Unknown && 47 | this.currentStatus !== PlatformTransactionStatus.Processing 48 | ) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/transaction/platform/supernet.ts: -------------------------------------------------------------------------------- 1 | import { JuneoBuffer, SignatureError, TimeUtils, type Serializable } from '../../utils' 2 | import { getSignersIndices } from '../builder' 3 | import { 4 | BLSPublicKeySize, 5 | BLSSignatureSize, 6 | EmptySignerTypeId, 7 | NodeIdSize, 8 | PrimarySignerTypeId, 9 | ProofOfPossessionSize, 10 | SupernetAuthTypeId, 11 | ValidatorSize 12 | } from '../constants' 13 | import { type Secp256k1OutputOwners } from '../output' 14 | import { AbstractSignable } from '../signature' 15 | import { BLSPublicKey, BLSSignature, NodeId, type Address } from '../types' 16 | 17 | export class Validator implements Serializable { 18 | nodeId: NodeId 19 | startTime: bigint 20 | endTime: bigint 21 | weight: bigint 22 | 23 | constructor (nodeId: NodeId, stakePeriod: bigint, weight: bigint) { 24 | this.nodeId = nodeId 25 | // Since Durango upgrade only end time value is used to calculate stake period, 26 | // we use a fixed start time of now() as another would not have any effect anyways. 27 | this.startTime = TimeUtils.now() 28 | this.endTime = this.startTime + stakePeriod 29 | this.weight = weight 30 | } 31 | 32 | getStakePeriod (): bigint { 33 | return this.endTime - this.startTime 34 | } 35 | 36 | serialize (): JuneoBuffer { 37 | const buffer = JuneoBuffer.alloc(ValidatorSize) 38 | buffer.write(this.nodeId) 39 | buffer.writeUInt64(this.startTime) 40 | buffer.writeUInt64(this.endTime) 41 | buffer.writeUInt64(this.weight) 42 | return buffer 43 | } 44 | 45 | static parse (data: string | JuneoBuffer): Validator { 46 | const reader = JuneoBuffer.from(data).createReader() 47 | const nodeId = new NodeId(reader.read(NodeIdSize).toCB58()) 48 | const startTime = reader.readUInt64() 49 | const endTime = reader.readUInt64() 50 | const validator = new Validator(nodeId, endTime - startTime, reader.readUInt64()) 51 | validator.startTime = startTime 52 | validator.endTime = endTime 53 | return validator 54 | } 55 | } 56 | 57 | export class SupernetAuth extends AbstractSignable implements Serializable { 58 | readonly typeId: number = SupernetAuthTypeId 59 | addressIndices: number[] 60 | rewardsOwner: Secp256k1OutputOwners | undefined 61 | 62 | constructor (addresses: Address[], rewardsOwner?: Secp256k1OutputOwners) { 63 | super() 64 | this.addressIndices = [] 65 | if (typeof rewardsOwner !== 'undefined') { 66 | this.addressIndices = getSignersIndices(addresses, rewardsOwner.addresses) 67 | this.addressIndices.sort((a: number, b: number) => { 68 | return a - b 69 | }) 70 | } 71 | this.rewardsOwner = rewardsOwner 72 | } 73 | 74 | getAddresses (): Address[] { 75 | if (typeof this.rewardsOwner === 'undefined') { 76 | throw new SignatureError('cannot get addresses of read only supernet auth') 77 | } 78 | const addresses: Address[] = [] 79 | for (let i = 0; i < this.addressIndices.length; i++) { 80 | addresses.push(this.rewardsOwner.addresses[i]) 81 | } 82 | return addresses 83 | } 84 | 85 | getThreshold (): number { 86 | if (typeof this.rewardsOwner === 'undefined') { 87 | throw new SignatureError('cannot get threshold of read only supernet auth') 88 | } 89 | return this.rewardsOwner.threshold 90 | } 91 | 92 | serialize (): JuneoBuffer { 93 | const buffer: JuneoBuffer = JuneoBuffer.alloc(4 + 4 + this.addressIndices.length * 4) 94 | buffer.writeUInt32(this.typeId) 95 | buffer.writeUInt32(this.addressIndices.length) 96 | for (const indice of this.addressIndices) { 97 | buffer.writeUInt32(indice) 98 | } 99 | return buffer 100 | } 101 | 102 | static parse (data: string | JuneoBuffer): SupernetAuth { 103 | const reader = JuneoBuffer.from(data).createReader() 104 | reader.readAndVerifyTypeId(SupernetAuthTypeId) 105 | const addressIndicesCount = reader.readUInt32() 106 | const indices: number[] = [] 107 | for (let i = 0; i < addressIndicesCount; i++) { 108 | const indice = reader.readUInt32() 109 | indices.push(indice) 110 | } 111 | const supernetAuth = new SupernetAuth([]) 112 | supernetAuth.addressIndices = indices 113 | return supernetAuth 114 | } 115 | } 116 | 117 | export class ProofOfPossession implements Serializable { 118 | publicKey: BLSPublicKey 119 | signature: BLSSignature 120 | 121 | constructor (publicKey: BLSPublicKey, signature: BLSSignature) { 122 | this.publicKey = publicKey 123 | this.signature = signature 124 | } 125 | 126 | serialize (): JuneoBuffer { 127 | const publicKeyBytes = this.publicKey.serialize() 128 | const signatureBytes = this.signature.serialize() 129 | const buffer: JuneoBuffer = JuneoBuffer.alloc(publicKeyBytes.length + signatureBytes.length) 130 | buffer.write(publicKeyBytes) 131 | buffer.write(signatureBytes) 132 | return buffer 133 | } 134 | 135 | static parse (data: string | JuneoBuffer): ProofOfPossession { 136 | const reader = JuneoBuffer.from(data).createReader() 137 | return new ProofOfPossession( 138 | new BLSPublicKey(reader.read(BLSPublicKeySize).toHex()), 139 | new BLSSignature(reader.read(BLSSignatureSize).toHex()) 140 | ) 141 | } 142 | } 143 | 144 | export class BLSSigner implements Serializable { 145 | typeId: number 146 | 147 | constructor (typeId: number) { 148 | this.typeId = typeId 149 | } 150 | 151 | serialize (): JuneoBuffer { 152 | const buffer = JuneoBuffer.alloc(4) 153 | buffer.writeUInt32(this.typeId) 154 | return buffer 155 | } 156 | } 157 | 158 | export class PrimarySigner extends BLSSigner { 159 | signer: ProofOfPossession 160 | 161 | constructor (signer: ProofOfPossession) { 162 | super(PrimarySignerTypeId) 163 | this.signer = signer 164 | } 165 | 166 | serialize (): JuneoBuffer { 167 | const baseBytes = super.serialize() 168 | const signerBytes = this.signer.serialize() 169 | const buffer = JuneoBuffer.alloc(baseBytes.length + signerBytes.length) 170 | buffer.write(baseBytes) 171 | buffer.write(signerBytes) 172 | return buffer 173 | } 174 | 175 | static parse (data: string | JuneoBuffer): PrimarySigner { 176 | const reader = JuneoBuffer.from(data).createReader() 177 | reader.readAndVerifyTypeId(PrimarySignerTypeId) 178 | return new PrimarySigner(ProofOfPossession.parse(reader.read(ProofOfPossessionSize))) 179 | } 180 | } 181 | 182 | export class EmptySigner extends BLSSigner { 183 | constructor () { 184 | super(EmptySignerTypeId) 185 | } 186 | 187 | static parse (data: string | JuneoBuffer): EmptySigner { 188 | const reader = JuneoBuffer.from(data).createReader() 189 | reader.readAndVerifyTypeId(EmptySignerTypeId) 190 | return new EmptySigner() 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/utils/asset.ts: -------------------------------------------------------------------------------- 1 | const RoundedValueDefaultDecimals = 2 2 | 3 | /** 4 | * Representation of the value of an asset. It uses the decimals of the asset to provide 5 | * helper functions that are useful to display the properly formatted value to a user. 6 | */ 7 | export class AssetValue { 8 | value: bigint 9 | readonly decimals: number 10 | 11 | constructor (value: bigint, decimals: number) { 12 | this.value = value 13 | this.decimals = decimals 14 | } 15 | 16 | /** 17 | * Get an AssetValue from a string and the expected decimals. 18 | * The string can already contains some of the decimals if they are separated by a dot. 19 | * If there are more decimals in the provided string than in the decimals value, they will be truncated to decimals value. 20 | * @param value A string that contains the value to use. 21 | * @param decimals The number of decimals to format the value with. 22 | * @returns A new AssetValue with a value formatted from the provided string and decimals. 23 | */ 24 | static fromString (value: string, decimals: number): AssetValue { 25 | const split: string[] = value.split('.') 26 | let tail: string = '' 27 | if (split.length > 1) { 28 | tail = split[1].length > decimals ? split[1].substring(0, decimals) : split[1] 29 | } 30 | return new AssetValue(BigInt(split[0] + tail.padEnd(decimals, '0')), decimals) 31 | } 32 | 33 | /** 34 | * Get a human friendly representation of this value. 35 | * @returns The complete value with its decimals separated by a dot. 36 | */ 37 | getReadableValue (): string { 38 | let value: string = this.getReadableValuePadded() 39 | if (value.indexOf('.') < 1) { 40 | return value 41 | } 42 | while (value.lastIndexOf('0') === value.length - 1) { 43 | value = value.substring(0, value.length - 1) 44 | } 45 | if (value.charAt(value.length - 1) === '.') { 46 | return value.substring(0, value.length - 1) 47 | } 48 | return value 49 | } 50 | 51 | /** 52 | * Get a human friendly representation of this value padded with zeros. 53 | * @returns The complete value with its decimals separated by a dot and zero padded. 54 | */ 55 | getReadableValuePadded (): string { 56 | let stringValue: string = this.value.toString() 57 | if (stringValue.charAt(0) === '-') { 58 | stringValue = stringValue.substring(1, stringValue.length) 59 | } 60 | const length: number = stringValue.length 61 | const prefix: string = this.value < 0 ? '-' : '' 62 | if (length <= this.decimals) { 63 | return `${prefix}0.${stringValue.padStart(this.decimals, '0')}` 64 | } else { 65 | let readableValue: string = stringValue.substring(0, length - this.decimals) 66 | if (this.decimals > 0) { 67 | readableValue += '.' 68 | readableValue += stringValue.substring(length - this.decimals, length).padEnd(this.decimals, '0') 69 | } 70 | return `${prefix}${readableValue}` 71 | } 72 | } 73 | 74 | /** 75 | * Get a human friendly representation of this value rounded to n decimals. 76 | * @param decimals **Optional**. The amount of decimals to display. If none are provided defaults to a hardcoded value. 77 | * @returns The value with its decimals separated by a dot. Decimals are rounded 78 | * by the provided number. 79 | */ 80 | getReadableValueRounded (decimals: number = RoundedValueDefaultDecimals): string { 81 | const readableValue: string = this.getReadableValuePadded() 82 | if (this.decimals < 1) { 83 | return readableValue 84 | } 85 | let index: number = readableValue.indexOf('.') 86 | index += decimals + 1 87 | let value: string = readableValue.substring(0, index) 88 | if (Number(value) === 0 && value.charAt(0) === '-') { 89 | value = value.substring(1, value.length) 90 | } 91 | if (value.charAt(value.length - 1) === '.') { 92 | return `${value.substring(0, value.length - 1)}` 93 | } 94 | return `${value}` 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/utils/chain.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers' 2 | import { type GetAssetDescriptionResponse, type JVMAPI } from '../api' 3 | import { TokenAsset } from '../asset' 4 | import { type Blockchain } from '../chain' 5 | import { type MCNProvider } from '../juneo' 6 | import { AssetId, type BlockchainId, JEVMExportTransaction, JEVMImportTransaction } from '../transaction' 7 | import { isHex } from './encoding' 8 | import { ChainError } from './errors' 9 | 10 | export const AtomicDenomination: bigint = BigInt(1_000_000_000) 11 | const AtomicSignatureCost: bigint = BigInt(1_000) 12 | const AtomicBaseCost: bigint = BigInt(10_000) 13 | 14 | function calculateAtomicGas (size: bigint, signaturesCount: bigint): bigint { 15 | return size + AtomicSignatureCost * signaturesCount + AtomicBaseCost 16 | } 17 | 18 | export function estimateAtomicExportGas ( 19 | chainAssetId: string, 20 | exportedAssets: string[], 21 | importFeeAssetId: string 22 | ): bigint { 23 | const signaturesCount: number = JEVMExportTransaction.estimateSignaturesCount( 24 | exportedAssets, 25 | chainAssetId, 26 | importFeeAssetId 27 | ) 28 | const size: number = JEVMExportTransaction.estimateSize(signaturesCount) 29 | return calculateAtomicGas(BigInt(size), BigInt(signaturesCount)) 30 | } 31 | 32 | export function estimateAtomicImportGas (inputsCount: number, outputsCount: number): bigint { 33 | const size: number = JEVMImportTransaction.estimateSize(inputsCount, outputsCount) 34 | return calculateAtomicGas(BigInt(size), BigInt(inputsCount)) 35 | } 36 | 37 | export function calculateAtomicCost (gas: bigint, baseFee: bigint): bigint { 38 | return (gas * baseFee) / AtomicDenomination 39 | } 40 | 41 | export function isContractAddress (assetId: string): boolean { 42 | // ethers.isAddress supports also ICAP addresses so check if it is hex too 43 | return isHex(assetId) && ethers.isAddress(assetId) 44 | } 45 | 46 | export async function fetchJNT (provider: MCNProvider, assetId: string): Promise { 47 | if (AssetId.validate(assetId)) { 48 | throw new ChainError(`cannot fetch invalid asset id ${assetId}`) 49 | } 50 | const jvm: JVMAPI = provider.jvmApi 51 | if (jvm.chain.registeredAssets.has(assetId)) { 52 | return jvm.chain.registeredAssets.get(assetId)! 53 | } 54 | const response: GetAssetDescriptionResponse = await jvm.getAssetDescription(assetId).catch((error) => { 55 | throw new ChainError(`could not fetch asset id ${assetId}: ${error.message}`) 56 | }) 57 | const asset: TokenAsset = new TokenAsset(response.assetID, response.name, response.symbol, response.denomination) 58 | // use the jvm registered assets as a cache 59 | jvm.chain.addRegisteredAsset(asset) 60 | return asset 61 | } 62 | 63 | export function getBlockchain (provider: MCNProvider, blockchainId: BlockchainId): Blockchain { 64 | const chain = provider.mcn.getChain(blockchainId.value) 65 | if (typeof chain === 'undefined') { 66 | throw new ChainError(`unregistered chain id: ${blockchainId.value}`) 67 | } 68 | return chain 69 | } 70 | -------------------------------------------------------------------------------- /src/utils/crypto.ts: -------------------------------------------------------------------------------- 1 | import { type RecoveredSignatureType } from '@noble/curves/abstract/weierstrass' 2 | import { secp256k1 } from '@noble/curves/secp256k1' 3 | import { ripemd160 } from '@noble/hashes/ripemd160' 4 | import { sha256 as nobleSha256 } from '@noble/hashes/sha256' 5 | import { type Signature } from '../transaction' 6 | import { JuneoBuffer } from './bytes' 7 | 8 | export function rmd160 (data: string | JuneoBuffer): JuneoBuffer { 9 | const buffer = typeof data === 'string' ? JuneoBuffer.fromString(data, 'hex') : data 10 | return JuneoBuffer.fromBytes(Buffer.from(ripemd160(buffer.getBytes()))) 11 | } 12 | 13 | export function sha256 (data: string | JuneoBuffer): JuneoBuffer { 14 | const buffer = typeof data === 'string' ? JuneoBuffer.fromString(data, 'hex') : data 15 | return JuneoBuffer.fromBytes(Buffer.from(nobleSha256(buffer.getBytes()))) 16 | } 17 | 18 | export function recoverPubKey (signature: Signature, message: JuneoBuffer): string { 19 | const sig = parseSignature(signature) 20 | const bytes = Buffer.from(sig.recoverPublicKey(nobleSha256(message.getBytes())).toRawBytes(true)) 21 | return JuneoBuffer.fromBytes(bytes).toHex() 22 | } 23 | 24 | export function verifySignature (signature: Signature, message: JuneoBuffer, publicKey: string): boolean { 25 | return secp256k1.verify(parseSignature(signature), nobleSha256(message.getBytes()), publicKey) 26 | } 27 | 28 | function parseSignature (signature: Signature): RecoveredSignatureType { 29 | const bytes = signature.serialize().getBytes() 30 | const sig = secp256k1.Signature.fromCompact(bytes.subarray(0, 64)) 31 | return sig.addRecoveryBit(signature.v) 32 | } 33 | 34 | export class ECKeyPair { 35 | readonly privateKey: string 36 | readonly publicKey: string 37 | 38 | constructor (privateKey: string) { 39 | this.privateKey = privateKey 40 | this.publicKey = JuneoBuffer.fromBytes(Buffer.from(secp256k1.getPublicKey(privateKey, true))).toHex() 41 | } 42 | 43 | sign (message: JuneoBuffer): JuneoBuffer { 44 | const signature = secp256k1.sign(nobleSha256(message.getBytes()), this.privateKey) 45 | return JuneoBuffer.fromString(`${signature.toCompactHex()}${signature.recovery.toString(16).padStart(2, '0')}`) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/utils/encoding.ts: -------------------------------------------------------------------------------- 1 | import { DecodingError } from './errors' 2 | import * as bech32 from 'bech32' 3 | import bs58 from 'bs58' 4 | import { sha256 } from './crypto' 5 | import { JuneoBuffer } from './bytes' 6 | 7 | const HexPrefix: string = '0x' 8 | 9 | function concatChecksum (buffer: JuneoBuffer): JuneoBuffer { 10 | const hashBuffer: JuneoBuffer = sha256(buffer).copyOf(28, 32) 11 | return JuneoBuffer.concat([buffer, hashBuffer]) 12 | } 13 | 14 | export function verifyChecksum (value: JuneoBuffer): boolean { 15 | if (value.length < 5) { 16 | return false 17 | } 18 | const buffer: JuneoBuffer = value.copyOf(0, value.length - 4) 19 | return value.toHex() === concatChecksum(buffer).toHex() 20 | } 21 | 22 | export function encodeBech32 (hrp: string, buffer: JuneoBuffer): string { 23 | return bech32.bech32.encode(hrp, bech32.bech32.toWords(buffer.getBytes())) 24 | } 25 | 26 | export function decodeBech32 (value: string): JuneoBuffer { 27 | const split: string[] = value.split('-') 28 | const part: string = split.length > 1 ? split[1] : split[0] 29 | const startIndex: number = part.lastIndexOf('1') 30 | if (startIndex < 0) { 31 | throw new DecodingError('bech32 format must include "1" separator') 32 | } 33 | const hrp: string = part.slice(0, startIndex) 34 | if (hrp.length < 1) { 35 | throw new DecodingError('bech32 hrp missing') 36 | } 37 | return JuneoBuffer.fromBytes(Buffer.from(bech32.bech32.fromWords(bech32.bech32.decode(part).words))) 38 | } 39 | 40 | export function validateBech32 (value: string, expectedHrp?: string, expectedPrefix: string[] = []): boolean { 41 | const parts: string[] = value.split('-') 42 | if (parts.length < 1) { 43 | return false 44 | } 45 | if (!expectedPrefix.includes(parts[0])) { 46 | return false 47 | } 48 | const bech32: string = parts[1] 49 | try { 50 | decodeBech32(bech32) 51 | } catch (error) { 52 | return false 53 | } 54 | if (expectedHrp === undefined) { 55 | return true 56 | } 57 | const hrp: string = bech32.slice(0, bech32.lastIndexOf('1')) 58 | return hrp === expectedHrp 59 | } 60 | 61 | export function encodeCB58 (buffer: JuneoBuffer): string { 62 | return bs58.encode(concatChecksum(buffer).getBytes()) 63 | } 64 | 65 | export function isBase58 (value: string): boolean { 66 | if (value === '') { 67 | return false 68 | } 69 | return /^[A-HJ-NP-Za-km-z1-9]*$/.test(value) 70 | } 71 | 72 | export function decodeCB58 (value: string): JuneoBuffer { 73 | if (!isBase58(value)) { 74 | throw new DecodingError('value is not base58') 75 | } 76 | const buffer: JuneoBuffer = JuneoBuffer.fromBytes(Buffer.from(bs58.decode(value))) 77 | if (!verifyChecksum(buffer)) { 78 | throw new DecodingError('value checksum is not valid') 79 | } 80 | return buffer.copyOf(0, buffer.length - 4) 81 | } 82 | 83 | export function encodeCHex (buffer: JuneoBuffer): string { 84 | return `${HexPrefix}${concatChecksum(buffer).toHex()}` 85 | } 86 | 87 | export function isHex (value: string): boolean { 88 | if (value === '') { 89 | return false 90 | } 91 | const hex: string = hasHexPrefix(value) ? value.substring(2) : value 92 | return /^[0-9A-Fa-f]*$/.test(hex) 93 | } 94 | 95 | export function hasHexPrefix (value: string): boolean { 96 | return value.startsWith(HexPrefix) 97 | } 98 | 99 | export function decodeHex (value: string): JuneoBuffer { 100 | if (!isHex(value)) { 101 | throw new DecodingError('value is not hexadecimal') 102 | } 103 | let hex: string = value 104 | if (hasHexPrefix(hex)) { 105 | hex = hex.substring(2) 106 | } 107 | return JuneoBuffer.fromString(hex, 'hex') 108 | } 109 | 110 | export function decodeCHex (value: string): JuneoBuffer { 111 | const buffer: JuneoBuffer = decodeHex(value) 112 | if (!verifyChecksum(buffer)) { 113 | throw new DecodingError('value checksum is not valid') 114 | } 115 | return buffer.copyOf(0, buffer.length - 4) 116 | } 117 | -------------------------------------------------------------------------------- /src/utils/errors.ts: -------------------------------------------------------------------------------- 1 | const NETWORK_CODE = 'Network error' 2 | const HTTP_CODE = 'HTTP error' 3 | const JSON_RPC_CODE = 'JsonRPC error' 4 | const NOT_IMPLEMENTED_CODE = 'Not implemented' 5 | const PROTOCOL_CODE = 'Protocol error' 6 | const DECODING_CODE = 'Decoding error' 7 | const WALLET_CODE = 'Wallet error' 8 | const JUNEO_TYPE_CODE = 'Juneo type error' 9 | const PARSING_CODE = 'Parsing error' 10 | const INPUT_CODE = 'Input error' 11 | const OUTPUT_CODE = 'Output error' 12 | const SIGNATURE_CODE = 'Signature error' 13 | const GENESIS_CODE = 'Genesis error' 14 | const ACCOUNT_CODE = 'Account error' 15 | const CHAIN_CODE = 'Chain error' 16 | const CROSS_CODE = 'Cross error' 17 | const STAKE_CODE = 'Stake error' 18 | const AMOUNT_CODE = 'Amount error' 19 | const CAPACITY_CODE = 'Capacity error' 20 | const TRANSACTION_CODE = 'Transaction error' 21 | const VAULT_CODE = 'Vault error' 22 | const TIME_CODE = 'Time error' 23 | 24 | export class JuneoError extends Error { 25 | private readonly code: string 26 | 27 | protected constructor (message: string, code: string) { 28 | super(message) 29 | Object.setPrototypeOf(this, JuneoError.prototype) 30 | this.code = code 31 | } 32 | 33 | getCode (): string { 34 | return this.code 35 | } 36 | } 37 | 38 | export class NetworkError extends JuneoError { 39 | constructor (message: string) { 40 | super(message, NETWORK_CODE) 41 | Object.setPrototypeOf(this, NetworkError.prototype) 42 | } 43 | } 44 | 45 | export class HttpError extends JuneoError { 46 | constructor (message: string) { 47 | super(message, HTTP_CODE) 48 | Object.setPrototypeOf(this, HttpError.prototype) 49 | } 50 | } 51 | 52 | export class JsonRpcError extends JuneoError { 53 | constructor (message: string) { 54 | super(message, JSON_RPC_CODE) 55 | Object.setPrototypeOf(this, JsonRpcError.prototype) 56 | } 57 | } 58 | 59 | export class NotImplementedError extends JuneoError { 60 | constructor (message: string) { 61 | super(message, NOT_IMPLEMENTED_CODE) 62 | Object.setPrototypeOf(this, NotImplementedError.prototype) 63 | } 64 | } 65 | 66 | export class ProtocolError extends JuneoError { 67 | constructor (message: string) { 68 | super(message, PROTOCOL_CODE) 69 | Object.setPrototypeOf(this, ProtocolError.prototype) 70 | } 71 | } 72 | 73 | export class DecodingError extends JuneoError { 74 | constructor (message: string) { 75 | super(message, DECODING_CODE) 76 | Object.setPrototypeOf(this, DecodingError.prototype) 77 | } 78 | } 79 | 80 | export class WalletError extends JuneoError { 81 | constructor (message: string) { 82 | super(message, WALLET_CODE) 83 | Object.setPrototypeOf(this, WalletError.prototype) 84 | } 85 | } 86 | 87 | export class JuneoTypeError extends JuneoError { 88 | constructor (message: string) { 89 | super(message, JUNEO_TYPE_CODE) 90 | Object.setPrototypeOf(this, JuneoTypeError.prototype) 91 | } 92 | } 93 | 94 | export class ParsingError extends JuneoError { 95 | constructor (message: string) { 96 | super(message, PARSING_CODE) 97 | Object.setPrototypeOf(this, ParsingError.prototype) 98 | } 99 | } 100 | 101 | export class InputError extends JuneoError { 102 | constructor (message: string) { 103 | super(message, INPUT_CODE) 104 | Object.setPrototypeOf(this, InputError.prototype) 105 | } 106 | } 107 | 108 | export class OutputError extends JuneoError { 109 | constructor (message: string) { 110 | super(message, OUTPUT_CODE) 111 | Object.setPrototypeOf(this, OutputError.prototype) 112 | } 113 | } 114 | 115 | export class SignatureError extends JuneoError { 116 | constructor (message: string) { 117 | super(message, SIGNATURE_CODE) 118 | Object.setPrototypeOf(this, SignatureError.prototype) 119 | } 120 | } 121 | 122 | export class GenesisError extends JuneoError { 123 | constructor (message: string) { 124 | super(message, GENESIS_CODE) 125 | Object.setPrototypeOf(this, GenesisError.prototype) 126 | } 127 | } 128 | 129 | export class AccountError extends JuneoError { 130 | constructor (message: string) { 131 | super(message, ACCOUNT_CODE) 132 | Object.setPrototypeOf(this, AccountError.prototype) 133 | } 134 | } 135 | 136 | export class ChainError extends JuneoError { 137 | constructor (message: string) { 138 | super(message, CHAIN_CODE) 139 | Object.setPrototypeOf(this, ChainError.prototype) 140 | } 141 | } 142 | 143 | export class CrossError extends JuneoError { 144 | constructor (message: string) { 145 | super(message, CROSS_CODE) 146 | Object.setPrototypeOf(this, CrossError.prototype) 147 | } 148 | } 149 | 150 | export class StakeError extends JuneoError { 151 | constructor (message: string) { 152 | super(message, STAKE_CODE) 153 | Object.setPrototypeOf(this, StakeError.prototype) 154 | } 155 | } 156 | 157 | export class AmountError extends JuneoError { 158 | constructor (message: string) { 159 | super(message, AMOUNT_CODE) 160 | Object.setPrototypeOf(this, AmountError.prototype) 161 | } 162 | } 163 | 164 | export class CapacityError extends JuneoError { 165 | constructor (message: string) { 166 | super(message, CAPACITY_CODE) 167 | Object.setPrototypeOf(this, CapacityError.prototype) 168 | } 169 | } 170 | 171 | export class TransactionError extends JuneoError { 172 | constructor (message: string) { 173 | super(message, TRANSACTION_CODE) 174 | Object.setPrototypeOf(this, TransactionError.prototype) 175 | } 176 | } 177 | 178 | export class VaultError extends JuneoError { 179 | constructor (message: string) { 180 | super(message, VAULT_CODE) 181 | Object.setPrototypeOf(this, VaultError.prototype) 182 | } 183 | } 184 | 185 | export class TimeError extends JuneoError { 186 | constructor (message: string) { 187 | super(message, TIME_CODE) 188 | Object.setPrototypeOf(this, TimeError.prototype) 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './asset' 2 | export * from './bytes' 3 | export * from './chain' 4 | export * from './crypto' 5 | export * from './encoding' 6 | export * from './errors' 7 | export * from './reward' 8 | export * from './time' 9 | export * from './transaction' 10 | export * from './utxo' 11 | export * from './wallet' 12 | -------------------------------------------------------------------------------- /src/utils/reward.ts: -------------------------------------------------------------------------------- 1 | import { type RewardConfig } from '../chain' 2 | 3 | const PrecisionConstant: bigint = BigInt(1_000_000) 4 | 5 | export class RewardCalculator { 6 | config: RewardConfig 7 | 8 | constructor (config: RewardConfig) { 9 | this.config = config 10 | } 11 | 12 | calculate (stakePeriod: bigint, currentTime: bigint, stakeAmount: bigint): bigint { 13 | let reward = this.getCurrentReward(currentTime) 14 | reward += this.getStakePeriodReward(stakePeriod) 15 | if (this.config.maxStakePeriod === BigInt(0)) { 16 | return (stakeAmount * reward) / PrecisionConstant 17 | } 18 | const stakePeriodRatio = (stakePeriod * PrecisionConstant) / this.config.maxStakePeriod 19 | const effectiveReward = (reward * stakePeriodRatio) / PrecisionConstant 20 | return (stakeAmount * effectiveReward) / PrecisionConstant 21 | } 22 | 23 | getStakePeriodReward (stakePeriod: bigint): bigint { 24 | if (this.config.maxStakePeriod === this.config.minStakePeriod) { 25 | return this.config.stakePeriodRewardShare 26 | } 27 | const adjustedStakePeriod = (stakePeriod - this.config.minStakePeriod) * PrecisionConstant 28 | const adjustedMaxStakePeriod = this.config.maxStakePeriod - this.config.minStakePeriod 29 | const adjustedStakePeriodRatio = adjustedStakePeriod / adjustedMaxStakePeriod 30 | return (adjustedStakePeriodRatio * this.config.stakePeriodRewardShare) / PrecisionConstant 31 | } 32 | 33 | getCurrentReward (currentTime: bigint): bigint { 34 | if (currentTime >= this.config.targetRewardTime) { 35 | return this.config.targetRewardShare 36 | } 37 | if (currentTime >= this.config.diminishingRewardTime) { 38 | return this.getReward( 39 | this.config.targetRewardShare, 40 | this.config.diminishingRewardShare, 41 | this.getRemainingTimeBoundsPercentage( 42 | this.config.diminishingRewardTime, 43 | this.config.targetRewardTime, 44 | currentTime 45 | ) 46 | ) 47 | } 48 | if (currentTime >= this.config.startRewardTime) { 49 | return this.getReward( 50 | this.config.diminishingRewardShare, 51 | this.config.startRewardShare, 52 | this.getRemainingTimeBoundsPercentage( 53 | this.config.startRewardTime, 54 | this.config.diminishingRewardTime, 55 | currentTime 56 | ) 57 | ) 58 | } 59 | // Start period or before 60 | return this.config.startRewardShare 61 | } 62 | 63 | getReward (lowerReward: bigint, upperReward: bigint, remainingTimeBoundsPercentage: bigint): bigint { 64 | const diminishingReward = upperReward - lowerReward 65 | const remainingReward = (diminishingReward * remainingTimeBoundsPercentage) / PrecisionConstant 66 | return remainingReward + lowerReward 67 | } 68 | 69 | /** 70 | * Get the remaining percentage of time between two bounds with the current time. 71 | * @param lowerTimeBound The lowest time value of the bounds. Must be lower than upper bound. 72 | * @param upperTimeBound The highest time value of the bounds. Must be higher than lower bound. 73 | * @param currentTime The current time value. 74 | * @returns The remaining percentage between lower and upper bounds calculated against current time. 75 | * Returned value is [PrecisionConstant, 0]. If currentTime is out of upper bound then 76 | * 0 is returned. If currentTime is out of lower bound then PrecisionConstant (100%) is returned. 77 | */ 78 | getRemainingTimeBoundsPercentage (lowerTimeBound: bigint, upperTimeBound: bigint, currentTime: bigint): bigint { 79 | // Current time is before or at lower bound 80 | if (currentTime <= lowerTimeBound) { 81 | return PrecisionConstant 82 | } 83 | const maxElapsedTime = upperTimeBound - lowerTimeBound 84 | const elapsedTime = currentTime - lowerTimeBound 85 | // Current time is after or at upper bound 86 | if (elapsedTime >= maxElapsedTime) { 87 | return BigInt(0) 88 | } 89 | return PrecisionConstant - (elapsedTime * PrecisionConstant) / maxElapsedTime 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/utils/time.ts: -------------------------------------------------------------------------------- 1 | import { TimeError } from './errors' 2 | 3 | export class TimeUtils { 4 | private static INSTANCE: TimeUtils | undefined 5 | private readonly provider: TimeProvider 6 | 7 | private constructor (provider: TimeProvider) { 8 | this.provider = provider 9 | } 10 | 11 | static getSingleton (): TimeUtils { 12 | if (TimeUtils.INSTANCE === undefined) { 13 | TimeUtils.INSTANCE = new TimeUtils(new TimeProvider()) 14 | } 15 | return TimeUtils.INSTANCE 16 | } 17 | 18 | // TODO get time from an external service to avoid using local machine timestamp 19 | static now (): bigint { 20 | return TimeUtils.getSingleton().provider.getCurrentClientTimeSeconds() 21 | } 22 | 23 | static minute (): bigint { 24 | return BigInt(60) 25 | } 26 | 27 | static hour (): bigint { 28 | return TimeUtils.minute() * BigInt(60) 29 | } 30 | 31 | static day (): bigint { 32 | return TimeUtils.hour() * BigInt(24) 33 | } 34 | 35 | static week (): bigint { 36 | return TimeUtils.day() * BigInt(7) 37 | } 38 | 39 | static year (): bigint { 40 | return TimeUtils.day() * BigInt(365) 41 | } 42 | 43 | static async sleep (milliseconds: number): Promise { 44 | await new Promise((resolve) => setTimeout(resolve, milliseconds)) 45 | } 46 | 47 | static verifyLocktime (locktime: bigint): void { 48 | const now = TimeUtils.now() 49 | const minValue = BigInt(60) 50 | const maxValue = TimeUtils.year() * BigInt(3) 51 | if (locktime > now && locktime < now + minValue) { 52 | throw new TimeError(`locktime value ${locktime} below minimum of ${minValue}`) 53 | } 54 | if (locktime > now + maxValue) { 55 | throw new TimeError(`locktime value ${locktime} above maximum of ${maxValue}`) 56 | } 57 | } 58 | } 59 | 60 | class TimeProvider { 61 | getCurrentClientTimeSeconds (): bigint { 62 | return BigInt(Math.round(new Date().getTime() / 1000)) 63 | } 64 | 65 | // TODO add and use if available getCurrentServerTimeSeconds() 66 | } 67 | -------------------------------------------------------------------------------- /src/utils/wallet.ts: -------------------------------------------------------------------------------- 1 | import { JEVM_ID, JVM_ID, PLATFORMVM_ID, type Blockchain } from '../chain' 2 | import { Address, UserInput, type Signature } from '../transaction' 3 | import { BaseSpending, type ExecutableOperation, type MCNAccount, type Spending, type TransactionType } from '../wallet' 4 | import { type JuneoBuffer } from './bytes' 5 | import { recoverPubKey, rmd160, sha256 } from './crypto' 6 | import { decodeCB58, encodeBech32, hasHexPrefix, isBase58, isHex } from './encoding' 7 | 8 | export const JVMPrivateKeyPrefix = 'PrivateKey-' 9 | const PrivateKeyLength: number = 64 10 | 11 | export function sortSpendings (spendings: Spending[]): Map { 12 | const values = new Map() 13 | for (const spending of spendings) { 14 | const key: string = `${spending.chain.id}_${spending.assetId}` 15 | if (!values.has(key)) { 16 | values.set(key, new BaseSpending(spending.chain, spending.amount, spending.assetId)) 17 | } else { 18 | values.get(key)!.amount += spending.amount 19 | } 20 | } 21 | return values 22 | } 23 | 24 | export function getImportUserInputs ( 25 | values: Map, 26 | feeAssetId: string, 27 | feeAmount: bigint, 28 | source: Blockchain, 29 | destination: Blockchain, 30 | address: string 31 | ): UserInput[] { 32 | const inputs: UserInput[] = [] 33 | for (const [key, value] of values) { 34 | const amount: bigint = key === feeAssetId ? value - feeAmount : value 35 | if (amount > BigInt(0)) { 36 | inputs.push(new UserInput(key, source, amount, [address], 1, destination)) 37 | } 38 | } 39 | return inputs 40 | } 41 | 42 | export async function trackJuneoTransaction ( 43 | chain: Blockchain, 44 | executable: ExecutableOperation, 45 | transactionId: string, 46 | transactionType: TransactionType 47 | ): Promise { 48 | let success: boolean = false 49 | const vmId: string = chain.vm.id 50 | if (vmId === JVM_ID) { 51 | success = await executable.trackJVMTransaction(transactionId, transactionType) 52 | } else if (vmId === PLATFORMVM_ID) { 53 | success = await executable.trackPlatformTransaction(transactionId, transactionType) 54 | } else if (vmId === JEVM_ID) { 55 | success = await executable.trackJEVMTransaction(chain.id, transactionId, transactionType) 56 | } 57 | return success 58 | } 59 | 60 | export function validatePrivateKey (data: string): boolean { 61 | if (isHex(data)) { 62 | const hasPrefix: boolean = hasHexPrefix(data) 63 | const length = hasPrefix ? data.substring(2).length : data.length 64 | return length === PrivateKeyLength 65 | } 66 | if (data.includes(JVMPrivateKeyPrefix)) { 67 | const split: string[] = data.split('-') 68 | const base58: boolean = split.length > 1 && isBase58(split[1]) 69 | return base58 && decodeCB58(split[1]).length === PrivateKeyLength / 2 70 | } 71 | return false 72 | } 73 | 74 | export function recoverAddress (signature: Signature, message: JuneoBuffer): Address { 75 | return new Address(publicKeyToAddress(recoverPubKey(signature, message))) 76 | } 77 | 78 | function publicKeyToAddress (publicKey: string): JuneoBuffer { 79 | return rmd160(sha256(publicKey)) 80 | } 81 | 82 | export function encodeJuneoAddress (publicKey: string, hrp: string): string { 83 | return encodeBech32(hrp, publicKeyToAddress(publicKey)) 84 | } 85 | 86 | /** 87 | * Fetch the balances of all the registered assets of the chains of the accounts. 88 | */ 89 | export async function fetchAllChainsBalances (account: MCNAccount): Promise { 90 | const promises: Array> = [] 91 | for (const chainAccount of account.chainAccounts.values()) { 92 | promises.push(chainAccount.fetchBalances(chainAccount.chain.getRegisteredAssets())) 93 | } 94 | await Promise.all(promises) 95 | } 96 | -------------------------------------------------------------------------------- /src/wallet/account/balance.ts: -------------------------------------------------------------------------------- 1 | import { TimeUtils } from '../../utils' 2 | 3 | export enum BalanceStatus { 4 | Updating = 'Updating', 5 | Done = 'Done', 6 | } 7 | 8 | export class BalanceUpdateEvent { 9 | previousValue: bigint 10 | value: bigint = BigInt(0) 11 | 12 | constructor (previousValue: bigint) { 13 | this.previousValue = previousValue 14 | } 15 | } 16 | 17 | export interface BalanceListener { 18 | onBalanceUpdateEvent: (event: BalanceUpdateEvent) => void 19 | } 20 | 21 | // 30 seconds between updates 22 | const UpdateTimeValidity: bigint = BigInt(30) 23 | 24 | export class Balance { 25 | private readonly listeners: BalanceListener[] = [] 26 | private lastUpdate: bigint = BigInt(0) 27 | private status: BalanceStatus = BalanceStatus.Done 28 | private value: bigint = BigInt(0) 29 | 30 | registerEvents (listener: BalanceListener): void { 31 | this.listeners.push(listener) 32 | } 33 | 34 | update (value: bigint): void { 35 | if (this.status === BalanceStatus.Updating) { 36 | return 37 | } 38 | this.status = BalanceStatus.Updating 39 | const event: BalanceUpdateEvent = new BalanceUpdateEvent(this.value) 40 | this.value = value 41 | this.status = BalanceStatus.Done 42 | event.value = value 43 | this.callBalanceUpdateEvent(event) 44 | } 45 | 46 | async updateAsync (fetch: Promise): Promise { 47 | if (this.status === BalanceStatus.Updating) { 48 | return 49 | } 50 | this.status = BalanceStatus.Updating 51 | const event: BalanceUpdateEvent = new BalanceUpdateEvent(this.value) 52 | this.value = await fetch 53 | this.status = BalanceStatus.Done 54 | event.value = this.value 55 | this.callBalanceUpdateEvent(event) 56 | } 57 | 58 | shouldUpdate (): boolean { 59 | return TimeUtils.now() < this.lastUpdate + UpdateTimeValidity 60 | } 61 | 62 | getStatus (): BalanceStatus { 63 | return this.status 64 | } 65 | 66 | getValue (): bigint { 67 | return this.value 68 | } 69 | 70 | spend (value: bigint): void { 71 | const event: BalanceUpdateEvent = new BalanceUpdateEvent(this.value) 72 | this.value -= value 73 | event.value = this.value 74 | this.callBalanceUpdateEvent(event) 75 | } 76 | 77 | private callBalanceUpdateEvent (event: BalanceUpdateEvent): void { 78 | this.lastUpdate = TimeUtils.now() 79 | this.listeners.forEach((listener) => { 80 | listener.onBalanceUpdateEvent(event) 81 | }) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/wallet/account/evm.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers' 2 | import { EmptyCallData, type JEVMBlockchain } from '../../chain' 3 | import { type MCNProvider } from '../../juneo' 4 | import { AccountError, isContractAddress } from '../../utils' 5 | import { 6 | type ChainNetworkOperation, 7 | type ChainOperationSummary, 8 | type EthCallOperation, 9 | type ExecutableOperation, 10 | NetworkOperationType, 11 | type SendOperation, 12 | type UnwrapOperation, 13 | type WrapOperation 14 | } from '../operation' 15 | import { 16 | BaseSpending, 17 | type EVMFeeData, 18 | TransactionType, 19 | estimateEVMOperation, 20 | executeEVMTransaction 21 | } from '../transaction' 22 | import { type JEVMWallet, type MCNWallet } from '../wallet' 23 | import { AbstractChainAccount, AccountType } from './account' 24 | import { Balance } from './balance' 25 | 26 | export class EVMAccount extends AbstractChainAccount { 27 | override chain: JEVMBlockchain 28 | override chainWallet: JEVMWallet 29 | private readonly provider: MCNProvider 30 | 31 | constructor (provider: MCNProvider, chainId: string, wallet: MCNWallet) { 32 | super(AccountType.Nonce, provider.jevmApi[chainId].chain, wallet) 33 | this.chain = provider.jevmApi[chainId].chain 34 | this.chainWallet = wallet.getJEVMWallet(this.chain) 35 | this.provider = provider 36 | } 37 | 38 | async estimate (operation: ChainNetworkOperation): Promise { 39 | const provider = await this.provider.getStaticProvider() 40 | if (operation.type === NetworkOperationType.Send) { 41 | const send = operation as SendOperation 42 | const isContract = isContractAddress(send.assetId) 43 | const data = isContract 44 | ? await this.chain.getContractTransactionData(provider, send.assetId, send.address, send.amount) 45 | : EmptyCallData 46 | return await estimateEVMOperation( 47 | provider, 48 | this.chain, 49 | this.chainWallet.getAddress(), 50 | send, 51 | [new BaseSpending(this.chain, send.amount, send.assetId)], 52 | new Map([[send.assetId, send.amount]]), 53 | isContract ? send.assetId : send.address, 54 | isContract ? BigInt(0) : send.amount, 55 | data, 56 | TransactionType.Base 57 | ) 58 | } else if (operation.type === NetworkOperationType.Wrap) { 59 | const wrap = operation as WrapOperation 60 | return await estimateEVMOperation( 61 | provider, 62 | this.chain, 63 | this.chainWallet.getAddress(), 64 | wrap, 65 | [new BaseSpending(this.chain, wrap.amount, this.chain.assetId)], 66 | new Map([[this.chain.assetId, wrap.amount]]), 67 | wrap.asset.address, 68 | wrap.amount, 69 | wrap.asset.adapter.getDepositData(), 70 | TransactionType.Wrap 71 | ) 72 | } else if (operation.type === NetworkOperationType.Unwrap) { 73 | const unwrap = operation as UnwrapOperation 74 | return await estimateEVMOperation( 75 | provider, 76 | this.chain, 77 | this.chainWallet.getAddress(), 78 | unwrap, 79 | [new BaseSpending(this.chain, unwrap.amount, unwrap.asset.assetId)], 80 | new Map([[unwrap.asset.assetId, unwrap.amount]]), 81 | unwrap.asset.address, 82 | BigInt(0), 83 | unwrap.asset.adapter.getWithdrawData(unwrap.amount), 84 | TransactionType.Unwrap 85 | ) 86 | } else if (operation.type === NetworkOperationType.EthCall) { 87 | const ethCall = operation as EthCallOperation 88 | const contract = new ethers.Contract(ethCall.contract, ethCall.abi, this.chain.ethProvider) 89 | const data = contract.interface.encodeFunctionData(ethCall.functionName, ethCall.values) 90 | const values = new Map() 91 | if (ethCall.amount > 0) { 92 | values.set(this.chain.assetId, ethCall.amount) 93 | } 94 | return await estimateEVMOperation( 95 | provider, 96 | this.chain, 97 | this.chainWallet.getAddress(), 98 | ethCall, 99 | [], 100 | values, 101 | ethCall.contract, 102 | ethCall.amount, 103 | data, 104 | TransactionType.EthCall 105 | ) 106 | } 107 | throw new AccountError(`unsupported operation: ${operation.type} for the chain with id: ${this.chain.id}`) 108 | } 109 | 110 | async execute (summary: ChainOperationSummary): Promise { 111 | super.spend(summary.spendings) 112 | const operation: NetworkOperationType = summary.operation.type 113 | if (operation === NetworkOperationType.Send) { 114 | await this.executeAndTrackTransaction(summary, TransactionType.Send) 115 | } else if (operation === NetworkOperationType.Wrap) { 116 | await this.executeAndTrackTransaction(summary, TransactionType.Wrap) 117 | } else if (operation === NetworkOperationType.Unwrap) { 118 | await this.executeAndTrackTransaction(summary, TransactionType.Unwrap) 119 | } else if (operation === NetworkOperationType.EthCall) { 120 | await this.executeAndTrackTransaction(summary, TransactionType.EthCall) 121 | } 122 | // could be replaced with correct spend and fund but just sync all now for simplicity 123 | // if replaced it should take some extra cases into account e.g. sending to self 124 | await this.fetchBalances(summary.getAssets().values()) 125 | } 126 | 127 | private async executeAndTrackTransaction (summary: ChainOperationSummary, type: TransactionType): Promise { 128 | const executable: ExecutableOperation = summary.getExecutable() 129 | const transactionHash: string = await executeEVMTransaction( 130 | executable.provider, 131 | this.chainWallet, 132 | summary.fee as EVMFeeData 133 | ) 134 | await executable.trackEVMTransaction(this.chain.id, transactionHash, type) 135 | } 136 | 137 | async fetchBalance (assetId: string): Promise { 138 | if (!this.balances.has(assetId)) { 139 | this.balances.set(assetId, new Balance()) 140 | } 141 | const balance: Balance = this.balances.get(assetId)! 142 | const address: string = this.chainWallet.getAddress() 143 | await balance.updateAsync(this.chain.queryBalance(this.provider.jevmApi[this.chain.id], address, assetId)) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/wallet/account/index.ts: -------------------------------------------------------------------------------- 1 | export * from './account' 2 | export * from './balance' 3 | export * from './evm' 4 | export * from './jvm' 5 | export * from './mcn' 6 | export * from './platform' 7 | -------------------------------------------------------------------------------- /src/wallet/account/jvm.ts: -------------------------------------------------------------------------------- 1 | import { type MCNProvider } from '../../juneo' 2 | import { AccountError } from '../../utils' 3 | import { 4 | type ChainNetworkOperation, 5 | type ChainOperationSummary, 6 | type ExecutableOperation, 7 | NetworkOperationType, 8 | type SendOperation, 9 | type SendUtxoOperation 10 | } from '../operation' 11 | import { 12 | TransactionType, 13 | type UtxoFeeData, 14 | type UtxoSpending, 15 | estimateSendOperation, 16 | estimateSendUtxoOperation 17 | } from '../transaction' 18 | import { type MCNWallet } from '../wallet' 19 | import { UtxoAccount } from './account' 20 | 21 | export class JVMAccount extends UtxoAccount { 22 | provider: MCNProvider 23 | 24 | constructor (provider: MCNProvider, wallet: MCNWallet) { 25 | super(provider.jvmChain, provider.jvmApi, wallet) 26 | this.provider = provider 27 | } 28 | 29 | async estimate (operation: ChainNetworkOperation): Promise { 30 | const provider: MCNProvider = await this.provider.getStaticProvider() 31 | if (operation.type === NetworkOperationType.Send) { 32 | return await estimateSendOperation(provider, this.chain, this, operation as SendOperation) 33 | } else if (operation.type === NetworkOperationType.SendUtxo) { 34 | return await estimateSendUtxoOperation(provider, this.chain, this, operation as SendUtxoOperation) 35 | } 36 | throw new AccountError(`unsupported operation: ${operation.type} for the chain with id: ${this.chain.id}`) 37 | } 38 | 39 | async execute (summary: ChainOperationSummary): Promise { 40 | super.spend(summary.spendings as UtxoSpending[]) 41 | const operation: NetworkOperationType = summary.operation.type 42 | if (operation === NetworkOperationType.Send) { 43 | await this.executeAndTrackTransaction(summary, TransactionType.Send) 44 | } else if (operation === NetworkOperationType.SendUtxo) { 45 | await this.executeAndTrackTransaction(summary, TransactionType.Send) 46 | } 47 | // balances fetching is needed to get new utxos created from this operation 48 | await super.refreshBalances() 49 | } 50 | 51 | private async executeAndTrackTransaction (summary: ChainOperationSummary, type: TransactionType): Promise { 52 | const executable: ExecutableOperation = summary.getExecutable() 53 | const signedTx = await (summary.fee as UtxoFeeData).transaction.signTransaction(this.signers) 54 | const transactionHash = (await executable.provider.jvmApi.issueTx(signedTx)).txID 55 | await executable.trackJVMTransaction(transactionHash, type) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/wallet/index.ts: -------------------------------------------------------------------------------- 1 | export * from './account' 2 | export * from './transaction' 3 | export * from './operation' 4 | export * from './vault' 5 | export * from './wallet' 6 | -------------------------------------------------------------------------------- /src/wallet/operation/executable.ts: -------------------------------------------------------------------------------- 1 | import { type JEVMAPI } from '../../api' 2 | import { type MCNProvider } from '../../juneo' 3 | import { 4 | EVMTransactionStatus, 5 | EVMTransactionStatusFetcher, 6 | JEVMTransactionStatus, 7 | JEVMTransactionStatusFetcher, 8 | PlatformTransactionStatus, 9 | PlatformTransactionStatusFetcher, 10 | JVMTransactionStatus, 11 | JVMTransactionStatusFetcher 12 | } from '../../transaction' 13 | import { 14 | TransactionType, 15 | WalletStatusFetcherTimeout, 16 | WalletStatusFetcherDelay, 17 | TransactionReceipt 18 | } from '../transaction' 19 | import { NetworkOperationStatus } from './operation' 20 | 21 | export class ExecutableOperation { 22 | provider: MCNProvider 23 | status: NetworkOperationStatus = NetworkOperationStatus.Initializing 24 | receipts: TransactionReceipt[] = [] 25 | 26 | constructor (provider: MCNProvider) { 27 | this.provider = provider 28 | } 29 | 30 | async trackEVMTransaction ( 31 | chainId: string, 32 | transactionHash: string, 33 | type: TransactionType = TransactionType.Base 34 | ): Promise { 35 | const api: JEVMAPI = this.provider.jevmApi[chainId] 36 | const receipt: TransactionReceipt = new TransactionReceipt( 37 | api.chain.id, 38 | type, 39 | EVMTransactionStatus.Pending, 40 | transactionHash 41 | ) 42 | this.receipts.push(receipt) 43 | const transactionStatus: string = await new EVMTransactionStatusFetcher(api, transactionHash).fetch( 44 | WalletStatusFetcherTimeout, 45 | WalletStatusFetcherDelay 46 | ) 47 | receipt.transactionStatus = transactionStatus 48 | if (transactionStatus === EVMTransactionStatus.Failure) { 49 | this.status = NetworkOperationStatus.Error 50 | } else if (transactionStatus !== EVMTransactionStatus.Success) { 51 | this.status = NetworkOperationStatus.Timeout 52 | } 53 | return transactionStatus === EVMTransactionStatus.Success 54 | } 55 | 56 | async trackJEVMTransaction ( 57 | chainId: string, 58 | transactionId: string, 59 | type: TransactionType = TransactionType.Base 60 | ): Promise { 61 | const api: JEVMAPI = this.provider.jevmApi[chainId] 62 | const receipt: TransactionReceipt = new TransactionReceipt( 63 | api.chain.id, 64 | type, 65 | JEVMTransactionStatus.Processing, 66 | transactionId 67 | ) 68 | this.receipts.push(receipt) 69 | const transactionStatus: string = await new JEVMTransactionStatusFetcher(api, transactionId).fetch( 70 | WalletStatusFetcherTimeout, 71 | WalletStatusFetcherDelay 72 | ) 73 | receipt.transactionStatus = transactionStatus 74 | if (transactionStatus === JEVMTransactionStatus.Dropped) { 75 | this.status = NetworkOperationStatus.Error 76 | } else if (transactionStatus !== JEVMTransactionStatus.Accepted) { 77 | this.status = NetworkOperationStatus.Timeout 78 | } 79 | return transactionStatus === JEVMTransactionStatus.Accepted 80 | } 81 | 82 | async trackPlatformTransaction ( 83 | transactionId: string, 84 | type: TransactionType = TransactionType.Base 85 | ): Promise { 86 | const receipt: TransactionReceipt = new TransactionReceipt( 87 | this.provider.platformChain.id, 88 | type, 89 | PlatformTransactionStatus.Processing, 90 | transactionId 91 | ) 92 | this.receipts.push(receipt) 93 | const transactionStatus: string = await new PlatformTransactionStatusFetcher( 94 | this.provider.platformApi, 95 | transactionId 96 | ).fetch(WalletStatusFetcherTimeout, WalletStatusFetcherDelay) 97 | receipt.transactionStatus = transactionStatus 98 | if ( 99 | transactionStatus === PlatformTransactionStatus.Dropped || 100 | transactionStatus === PlatformTransactionStatus.Aborted 101 | ) { 102 | this.status = NetworkOperationStatus.Error 103 | } else if (transactionStatus !== PlatformTransactionStatus.Committed) { 104 | this.status = NetworkOperationStatus.Timeout 105 | } 106 | return transactionStatus === PlatformTransactionStatus.Committed 107 | } 108 | 109 | async trackJVMTransaction (transactionId: string, type: TransactionType = TransactionType.Base): Promise { 110 | const receipt: TransactionReceipt = new TransactionReceipt( 111 | this.provider.jvmChain.id, 112 | type, 113 | JVMTransactionStatus.Processing, 114 | transactionId 115 | ) 116 | this.receipts.push(receipt) 117 | const transactionStatus: string = await new JVMTransactionStatusFetcher(this.provider.jvmApi, transactionId).fetch( 118 | WalletStatusFetcherTimeout, 119 | WalletStatusFetcherDelay 120 | ) 121 | receipt.transactionStatus = transactionStatus 122 | if (transactionStatus === JVMTransactionStatus.Unknown) { 123 | this.status = NetworkOperationStatus.Error 124 | } else if (transactionStatus !== JVMTransactionStatus.Accepted) { 125 | this.status = NetworkOperationStatus.Timeout 126 | } 127 | return transactionStatus === JVMTransactionStatus.Accepted 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/wallet/operation/index.ts: -------------------------------------------------------------------------------- 1 | export * from './executable' 2 | export * from './operation' 3 | export * from './summary' 4 | -------------------------------------------------------------------------------- /src/wallet/operation/summary.ts: -------------------------------------------------------------------------------- 1 | import { type Blockchain, type PlatformBlockchain } from '../../chain' 2 | import { type MCNProvider } from '../../juneo' 3 | import { type Utxo } from '../../transaction' 4 | import { type EVMFeeData, type FeeData, type Spending, type UtxoFeeData } from '../transaction' 5 | import { ExecutableOperation } from './executable' 6 | import { 7 | type ChainNetworkOperation, 8 | type CrossOperation, 9 | type CrossResumeOperation, 10 | type DepositResumeOperation, 11 | type NetworkOperation, 12 | type Staking 13 | } from './operation' 14 | 15 | export interface OperationSummary { 16 | operation: NetworkOperation 17 | fees: FeeData[] 18 | spendings: Spending[] 19 | values: Map 20 | 21 | getExecutable: () => ExecutableOperation 22 | 23 | getAssets: () => Set 24 | 25 | getChains: () => Blockchain[] 26 | 27 | getErrors: () => Error[] 28 | } 29 | 30 | abstract class AbstractOperationSummary implements OperationSummary { 31 | operation: NetworkOperation 32 | fees: FeeData[] 33 | spendings: Spending[] 34 | values: Map 35 | errors: Error[] 36 | private readonly executable: ExecutableOperation 37 | 38 | constructor ( 39 | provider: MCNProvider, 40 | operation: NetworkOperation, 41 | fees: FeeData[], 42 | spendings: Spending[], 43 | values: Map, 44 | errors: Error[] 45 | ) { 46 | this.operation = operation 47 | this.fees = fees 48 | this.spendings = spendings 49 | this.values = values 50 | this.executable = new ExecutableOperation(provider) 51 | this.errors = errors 52 | } 53 | 54 | getExecutable (): ExecutableOperation { 55 | return this.executable 56 | } 57 | 58 | getAssets (): Set { 59 | const assets = new Set() 60 | // refresh balances of all sent assets to sync it 61 | for (const asset of this.spendings) { 62 | const assetId: string = asset.assetId 63 | if (!assets.has(assetId)) { 64 | assets.add(assetId) 65 | } 66 | } 67 | // refresh balances of all created values in case it was sent to self 68 | for (const [assetId] of this.values) { 69 | if (!assets.has(assetId)) { 70 | assets.add(assetId) 71 | } 72 | } 73 | return assets 74 | } 75 | 76 | abstract getChains (): Blockchain[] 77 | 78 | getErrors (): Error[] { 79 | return this.errors 80 | } 81 | } 82 | 83 | export class ChainOperationSummary extends AbstractOperationSummary { 84 | override operation: ChainNetworkOperation 85 | chain: Blockchain 86 | fee: FeeData 87 | 88 | constructor ( 89 | provider: MCNProvider, 90 | operation: ChainNetworkOperation, 91 | chain: Blockchain, 92 | fee: FeeData, 93 | spendings: Spending[], 94 | values: Map, 95 | errors: Error[] = [] 96 | ) { 97 | super(provider, operation, [fee], spendings, values, errors) 98 | this.operation = operation 99 | this.chain = chain 100 | this.fee = fee 101 | } 102 | 103 | getChains (): Blockchain[] { 104 | return [this.chain] 105 | } 106 | } 107 | 108 | export class CrossOperationSummary extends AbstractOperationSummary { 109 | override operation: CrossOperation 110 | chains: Blockchain[] 111 | amount: bigint 112 | assetId: string 113 | 114 | constructor ( 115 | provider: MCNProvider, 116 | operation: CrossOperation, 117 | chains: Blockchain[], 118 | amount: bigint, 119 | assetId: string, 120 | fees: FeeData[], 121 | spendings: Spending[], 122 | values: Map, 123 | errors: Error[] = [] 124 | ) { 125 | super(provider, operation, fees, spendings, values, errors) 126 | this.operation = operation 127 | this.chains = chains 128 | this.amount = amount 129 | this.assetId = assetId 130 | } 131 | 132 | getChains (): Blockchain[] { 133 | return this.chains 134 | } 135 | } 136 | 137 | export class StakingOperationSummary extends ChainOperationSummary { 138 | potentialReward: bigint 139 | 140 | constructor ( 141 | provider: MCNProvider, 142 | operation: Staking, 143 | chain: PlatformBlockchain, 144 | fee: UtxoFeeData, 145 | spendings: Spending[], 146 | values: Map, 147 | potentialReward: bigint, 148 | errors: Error[] = [] 149 | ) { 150 | super(provider, operation, chain, fee, spendings, values, errors) 151 | this.potentialReward = potentialReward 152 | } 153 | } 154 | 155 | export class CrossResumeOperationSummary extends ChainOperationSummary { 156 | override operation: CrossResumeOperation 157 | importFee: FeeData 158 | payImportFee: boolean 159 | utxoSet: Utxo[] 160 | 161 | constructor ( 162 | provider: MCNProvider, 163 | operation: CrossResumeOperation, 164 | importFee: FeeData, 165 | spendings: Spending[], 166 | values: Map, 167 | payImportFee: boolean, 168 | utxoSet: Utxo[], 169 | errors: Error[] = [] 170 | ) { 171 | super(provider, operation, operation.destination, importFee, spendings, values, errors) 172 | this.operation = operation 173 | this.importFee = importFee 174 | this.payImportFee = payImportFee 175 | this.utxoSet = utxoSet 176 | } 177 | } 178 | 179 | export class DepositResumeOperationSummary extends ChainOperationSummary { 180 | override operation: DepositResumeOperation 181 | fee: EVMFeeData 182 | 183 | constructor ( 184 | provider: MCNProvider, 185 | operation: DepositResumeOperation, 186 | fee: EVMFeeData, 187 | spendings: Spending[], 188 | values: Map, 189 | errors: Error[] = [] 190 | ) { 191 | super(provider, operation, operation.chain, fee, spendings, values, errors) 192 | this.operation = operation 193 | this.fee = fee 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/wallet/transaction/base.ts: -------------------------------------------------------------------------------- 1 | import { type Blockchain } from '../../chain' 2 | import { type MCNProvider } from '../../juneo' 3 | import { 4 | Address, 5 | buildJVMBaseTransaction, 6 | buildPlatformBaseTransaction, 7 | type UnsignedTransaction, 8 | UserInput, 9 | type Utxo 10 | } from '../../transaction' 11 | import { type UtxoAccount } from '../account' 12 | import { ChainOperationSummary, type SendOperation, type SendUtxoOperation } from '../operation' 13 | import { BaseFeeData, UtxoFeeData } from './fee' 14 | import { BaseSpending, TransactionType, UtxoSpending } from './transaction' 15 | 16 | async function getBaseTxFee (provider: MCNProvider, type: TransactionType, chain: Blockchain): Promise { 17 | return new BaseFeeData(chain, BigInt((await provider.info.getTxFee()).txFee), type) 18 | } 19 | 20 | export async function estimateBaseTransaction ( 21 | provider: MCNProvider, 22 | chain: Blockchain, 23 | utxoSet: Utxo[], 24 | signersAddresses: Address[], 25 | changeAddress: string, 26 | assetId: string, 27 | amount: bigint, 28 | addresses: string[], 29 | threshold: number, 30 | locktime: bigint, 31 | stakeable: boolean 32 | ): Promise { 33 | const fee: BaseFeeData = await getBaseTxFee(provider, TransactionType.Base, chain) 34 | const transaction: UnsignedTransaction = 35 | chain.id === provider.platformChain.id 36 | ? buildPlatformBaseTransaction( 37 | [new UserInput(assetId, chain, amount, addresses, threshold, chain, locktime, stakeable)], 38 | utxoSet, 39 | signersAddresses, 40 | fee.amount, 41 | changeAddress, 42 | provider.mcn.id 43 | ) 44 | : buildJVMBaseTransaction( 45 | [new UserInput(assetId, chain, amount, addresses, threshold, chain, locktime)], 46 | utxoSet, 47 | signersAddresses, 48 | fee.amount, 49 | changeAddress, 50 | provider.mcn.id 51 | ) 52 | return new UtxoFeeData(fee.chain, fee.amount, fee.type, transaction) 53 | } 54 | 55 | export async function estimateSendOperation ( 56 | provider: MCNProvider, 57 | chain: Blockchain, 58 | account: UtxoAccount, 59 | send: SendOperation 60 | ): Promise { 61 | const values = new Map([[send.assetId, send.amount]]) 62 | return await estimateBaseTransaction( 63 | provider, 64 | chain, 65 | account.utxoSet, 66 | Address.toAddresses(account.getSignersAddresses()), 67 | account.address, 68 | send.assetId, 69 | send.amount, 70 | [send.address], 71 | 1, 72 | BigInt(0), 73 | false 74 | ).then( 75 | (fee) => { 76 | const spending: UtxoSpending = new UtxoSpending(chain, send.amount, send.assetId, fee.transaction.getUtxos()) 77 | return new ChainOperationSummary(provider, send, chain, fee, [spending, fee.spending], values) 78 | }, 79 | async () => { 80 | const fee: BaseFeeData = await getBaseTxFee(provider, TransactionType.Base, chain) 81 | return new ChainOperationSummary( 82 | provider, 83 | send, 84 | chain, 85 | fee, 86 | [new BaseSpending(chain, send.amount, send.assetId), fee.spending], 87 | values 88 | ) 89 | } 90 | ) 91 | } 92 | 93 | export async function estimateSendUtxoOperation ( 94 | provider: MCNProvider, 95 | chain: Blockchain, 96 | account: UtxoAccount, 97 | send: SendUtxoOperation 98 | ): Promise { 99 | const values = new Map([[send.assetId, send.amount]]) 100 | const fee = await getBaseTxFee(provider, TransactionType.Base, chain) 101 | // do not add a spending if utxoSet is defined as this means it is a multiSig op 102 | const hasSpending = typeof send.utxoSet === 'undefined' 103 | return await estimateBaseTransaction( 104 | provider, 105 | chain, 106 | send.getPreferredUtxoSet(account, fee.amount), 107 | send.getPreferredSigners(account), 108 | account.address, 109 | send.assetId, 110 | send.amount, 111 | send.addresses, 112 | send.threshold, 113 | send.locktime, 114 | send.stakeable 115 | ).then( 116 | (fee) => { 117 | const spendings = [fee.spending] 118 | if (hasSpending) { 119 | spendings.unshift(new UtxoSpending(chain, send.amount, send.assetId, fee.transaction.getUtxos())) 120 | } 121 | return new ChainOperationSummary(provider, send, chain, fee, spendings, values) 122 | }, 123 | async () => { 124 | const spendings = [fee.spending] 125 | if (hasSpending) { 126 | spendings.unshift(new BaseSpending(chain, send.amount, send.assetId)) 127 | } 128 | return new ChainOperationSummary(provider, send, chain, fee, spendings, values) 129 | } 130 | ) 131 | } 132 | -------------------------------------------------------------------------------- /src/wallet/transaction/constants.ts: -------------------------------------------------------------------------------- 1 | export const WalletStatusFetcherTimeout: number = 60000 2 | // too low delay may not give enough time to some vms to produce the transaction 3 | // and/or the utxos after the transaction is accepted 4 | export const WalletStatusFetcherDelay: number = 100 5 | 6 | export const DefaultWithdrawEstimate: bigint = BigInt(200_000) 7 | export const DefaultDepositEstimate: bigint = BigInt(200_000) 8 | -------------------------------------------------------------------------------- /src/wallet/transaction/cross/evm.ts: -------------------------------------------------------------------------------- 1 | import { type JEVMAPI } from '../../../api' 2 | import { type Blockchain } from '../../../chain' 3 | import { type MCNProvider, TransactionType } from '../../../juneo' 4 | import { 5 | Address, 6 | type JEVMImportTransaction, 7 | UserInput, 8 | type Utxo, 9 | buildJEVMExportTransaction, 10 | buildJEVMImportTransaction 11 | } from '../../../transaction' 12 | import { 13 | AtomicDenomination, 14 | calculateAtomicCost, 15 | estimateAtomicExportGas, 16 | estimateAtomicImportGas, 17 | getImportUserInputs, 18 | getUtxosAmountValues 19 | } from '../../../utils' 20 | import { type JEVMWallet, type MCNWallet, type VMWallet } from '../../wallet' 21 | import { estimateEVMBaseFee, getWalletNonce } from '../evm' 22 | import { BaseFeeData, type FeeData } from '../fee' 23 | 24 | export async function estimateEVMExportTransaction ( 25 | api: JEVMAPI, 26 | assetId: string, 27 | destination: Blockchain 28 | ): Promise { 29 | const gasLimit: bigint = estimateAtomicExportGas(api.chain.assetId, [assetId], destination.assetId) 30 | const baseFee: bigint = await estimateEVMBaseFee(api) 31 | // the evm export fee is paid in gas so it must be multiplied by the atomic denominator 32 | const fee: bigint = calculateAtomicCost(gasLimit, baseFee) * AtomicDenomination 33 | return new BaseFeeData(api.chain, fee, TransactionType.Export) 34 | } 35 | 36 | export async function executeEVMExportTransaction ( 37 | provider: MCNProvider, 38 | api: JEVMAPI, 39 | wallet: MCNWallet, 40 | destination: Blockchain, 41 | assetId: string, 42 | amount: bigint, 43 | address: string, 44 | sendImportFee: boolean, 45 | importFee: bigint, 46 | fee: FeeData 47 | ): Promise { 48 | // exportations of the gas token must be divided by atomic denomination 49 | if (assetId === api.chain.assetId) { 50 | amount /= AtomicDenomination 51 | } 52 | // fee is also gas token 53 | const feeAmount: bigint = fee.amount / AtomicDenomination 54 | const exportAddress: string = wallet.getWallet(destination).getJuneoAddress() 55 | const evmWallet: JEVMWallet = wallet.getJEVMWallet(api.chain) 56 | const nonce = await getWalletNonce(evmWallet, api, false) 57 | const unsignedTransaction = buildJEVMExportTransaction( 58 | [new UserInput(assetId, api.chain, amount, [address], 1, destination)], 59 | wallet.getAddress(api.chain), 60 | nonce, 61 | exportAddress, 62 | feeAmount, 63 | sendImportFee ? importFee : BigInt(0), 64 | provider.mcn.id 65 | ) 66 | const signedTx = await unsignedTransaction.signTransaction([wallet.getWallet(api.chain)]) 67 | return (await api.issueTx(signedTx)).txID 68 | } 69 | 70 | export async function estimateEVMImportTransaction ( 71 | api: JEVMAPI, 72 | inputsCount: number, 73 | outputsCount: number 74 | ): Promise { 75 | const gasLimit: bigint = estimateAtomicImportGas(inputsCount, outputsCount) 76 | const baseFee: bigint = await estimateEVMBaseFee(api) 77 | const fee: BaseFeeData = new BaseFeeData(api.chain, calculateAtomicCost(gasLimit, baseFee), TransactionType.Import) 78 | // import fee is paid with utxos from shared memory so using JNT asset 79 | fee.asset = api.chain.asset.nativeAsset 80 | return fee 81 | } 82 | 83 | export async function executeEVMImportTransaction ( 84 | provider: MCNProvider, 85 | api: JEVMAPI, 86 | wallet: MCNWallet, 87 | source: Blockchain, 88 | fee: FeeData, 89 | utxoSet: Utxo[] 90 | ): Promise { 91 | const chainWallet: VMWallet = wallet.getWallet(api.chain) 92 | const sender: string = chainWallet.getJuneoAddress() 93 | const values = getUtxosAmountValues(utxoSet, source.id) 94 | const inputs: UserInput[] = getImportUserInputs( 95 | values, 96 | fee.assetId, 97 | fee.amount, 98 | source, 99 | api.chain, 100 | wallet.getAddress(api.chain) 101 | ) 102 | const transaction: JEVMImportTransaction = buildJEVMImportTransaction( 103 | inputs, 104 | utxoSet, 105 | [new Address(sender)], 106 | fee.amount, 107 | provider.mcn.id 108 | ) 109 | const signedTx = await transaction.signTransaction([chainWallet]) 110 | return (await api.issueTx(signedTx)).txID 111 | } 112 | -------------------------------------------------------------------------------- /src/wallet/transaction/cross/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cross' 2 | export * from './evm' 3 | export * from './jvm' 4 | export * from './platform' 5 | -------------------------------------------------------------------------------- /src/wallet/transaction/cross/jvm.ts: -------------------------------------------------------------------------------- 1 | import { type JVMAPI } from '../../../api' 2 | import { type Blockchain } from '../../../chain' 3 | import { type MCNProvider, TransactionType } from '../../../juneo' 4 | import { 5 | Address, 6 | type UnsignedTransaction, 7 | UserInput, 8 | type Utxo, 9 | buildJVMExportTransaction, 10 | buildJVMImportTransaction 11 | } from '../../../transaction' 12 | import { getImportUserInputs, getUtxosAmountValues } from '../../../utils' 13 | import { type MCNWallet } from '../../wallet' 14 | import { BaseFeeData, type FeeData } from '../fee' 15 | 16 | export async function estimateJVMExportTransaction (provider: MCNProvider): Promise { 17 | return new BaseFeeData(provider.jvmChain, BigInt((await provider.info.getTxFee()).txFee), TransactionType.Export) 18 | } 19 | 20 | export async function executeJVMExportTransaction ( 21 | provider: MCNProvider, 22 | wallet: MCNWallet, 23 | destination: Blockchain, 24 | assetId: string, 25 | amount: bigint, 26 | address: string, 27 | sendImportFee: boolean, 28 | importFee: bigint, 29 | fee: FeeData, 30 | utxoSet: Utxo[], 31 | extraFeeAmount: bigint = BigInt(0) 32 | ): Promise { 33 | const api: JVMAPI = provider.jvmApi 34 | const sender: string = wallet.getAddress(api.chain) 35 | const inputs: UserInput[] = [new UserInput(assetId, api.chain, amount, [address], 1, destination)] 36 | if (extraFeeAmount > BigInt(0)) { 37 | inputs.push(new UserInput(destination.assetId, api.chain, extraFeeAmount, [address], 1, destination)) 38 | } 39 | const exportAddress: string = wallet.getWallet(destination).getJuneoAddress() 40 | const transaction: UnsignedTransaction = buildJVMExportTransaction( 41 | inputs, 42 | utxoSet, 43 | [new Address(sender)], 44 | exportAddress, 45 | fee.amount, 46 | sendImportFee ? importFee : BigInt(0), 47 | sender, 48 | provider.mcn.id 49 | ) 50 | const signedTx = await transaction.signTransaction([wallet.getWallet(api.chain)]) 51 | return (await api.issueTx(signedTx)).txID 52 | } 53 | 54 | export async function estimateJVMImportTransaction (provider: MCNProvider): Promise { 55 | return new BaseFeeData(provider.jvmChain, BigInt((await provider.info.getTxFee()).txFee), TransactionType.Import) 56 | } 57 | 58 | export async function executeJVMImportTransaction ( 59 | provider: MCNProvider, 60 | wallet: MCNWallet, 61 | source: Blockchain, 62 | fee: FeeData, 63 | utxoSet: Utxo[] 64 | ): Promise { 65 | const api: JVMAPI = provider.jvmApi 66 | const sender: string = wallet.getAddress(api.chain) 67 | const values: Map = getUtxosAmountValues(utxoSet, source.id) 68 | const inputs: UserInput[] = getImportUserInputs(values, fee.assetId, fee.amount, source, api.chain, sender) 69 | const transaction: UnsignedTransaction = buildJVMImportTransaction( 70 | inputs, 71 | utxoSet, 72 | [new Address(sender)], 73 | fee.amount, 74 | sender, 75 | provider.mcn.id 76 | ) 77 | const signedTx = await transaction.signTransaction([wallet.getWallet(api.chain)]) 78 | return (await api.issueTx(signedTx)).txID 79 | } 80 | -------------------------------------------------------------------------------- /src/wallet/transaction/cross/platform.ts: -------------------------------------------------------------------------------- 1 | import { type PlatformAPI } from '../../../api' 2 | import { type Blockchain } from '../../../chain' 3 | import { type MCNProvider, TransactionType } from '../../../juneo' 4 | import { 5 | Address, 6 | type UnsignedTransaction, 7 | UserInput, 8 | type Utxo, 9 | buildPlatformExportTransaction, 10 | buildPlatformImportTransaction 11 | } from '../../../transaction' 12 | import { getImportUserInputs, getUtxosAmountValues } from '../../../utils' 13 | import { type MCNWallet } from '../../wallet' 14 | import { BaseFeeData, type FeeData } from '../fee' 15 | 16 | export async function estimatePlatformExportTransaction (provider: MCNProvider): Promise { 17 | return new BaseFeeData(provider.platformChain, BigInt((await provider.info.getTxFee()).txFee), TransactionType.Export) 18 | } 19 | 20 | export async function executePlatformExportTransaction ( 21 | provider: MCNProvider, 22 | wallet: MCNWallet, 23 | destination: Blockchain, 24 | assetId: string, 25 | amount: bigint, 26 | address: string, 27 | sendImportFee: boolean, 28 | importFee: bigint, 29 | fee: FeeData, 30 | utxoSet: Utxo[] 31 | ): Promise { 32 | const api: PlatformAPI = provider.platformApi 33 | const sender: string = wallet.getAddress(api.chain) 34 | const exportAddress: string = wallet.getWallet(destination).getJuneoAddress() 35 | const transaction: UnsignedTransaction = buildPlatformExportTransaction( 36 | [new UserInput(assetId, api.chain, amount, [address], 1, destination)], 37 | utxoSet, 38 | [new Address(sender)], 39 | exportAddress, 40 | fee.amount, 41 | sendImportFee ? importFee : BigInt(0), 42 | sender, 43 | provider.mcn.id 44 | ) 45 | const signedTx = await transaction.signTransaction([wallet.getWallet(api.chain)]) 46 | return (await api.issueTx(signedTx)).txID 47 | } 48 | 49 | export async function estimatePlatformImportTransaction (provider: MCNProvider): Promise { 50 | return new BaseFeeData(provider.platformChain, BigInt((await provider.info.getTxFee()).txFee), TransactionType.Import) 51 | } 52 | 53 | export async function executePlatformImportTransaction ( 54 | provider: MCNProvider, 55 | wallet: MCNWallet, 56 | source: Blockchain, 57 | fee: FeeData, 58 | utxoSet: Utxo[] 59 | ): Promise { 60 | const api: PlatformAPI = provider.platformApi 61 | const sender: string = wallet.getAddress(api.chain) 62 | const values: Map = getUtxosAmountValues(utxoSet, source.id) 63 | const inputs: UserInput[] = getImportUserInputs(values, fee.assetId, fee.amount, source, api.chain, sender) 64 | const transaction: UnsignedTransaction = buildPlatformImportTransaction( 65 | inputs, 66 | utxoSet, 67 | [new Address(sender)], 68 | fee.amount, 69 | sender, 70 | provider.mcn.id 71 | ) 72 | const signedTx = await transaction.signTransaction([wallet.getWallet(api.chain)]) 73 | return (await api.issueTx(signedTx)).txID 74 | } 75 | -------------------------------------------------------------------------------- /src/wallet/transaction/evm.ts: -------------------------------------------------------------------------------- 1 | import { type ethers } from 'ethers' 2 | import { type JEVMAPI } from '../../api' 3 | import { type JRC20Asset } from '../../asset' 4 | import { EmptyCallData, type JEVMBlockchain, NativeAssetCallContract, SendEtherGasLimit } from '../../chain' 5 | import { type MCNProvider } from '../../juneo' 6 | import { type ChainNetworkOperation, ChainOperationSummary } from '../operation' 7 | import { type JEVMWallet } from '../wallet' 8 | import { DefaultDepositEstimate, DefaultWithdrawEstimate } from './constants' 9 | import { EVMFeeData } from './fee' 10 | import { type BaseSpending, EVMTransactionData, TransactionType } from './transaction' 11 | 12 | export async function getWalletNonce (wallet: JEVMWallet, api: JEVMAPI, synchronize: boolean): Promise { 13 | return await api.eth_getTransactionCount(wallet.getAddress(), 'pending') 14 | // TODO update logic maybe use mutex 15 | // if (synchronize || !wallet.synchronized) { 16 | // wallet.synchronized = true 17 | // wallet.nonce = await api.eth_getTransactionCount(wallet.getAddress(), 'pending') 18 | // } 19 | // return wallet.nonce++ 20 | } 21 | 22 | export async function estimateEVMBaseFee (api: JEVMAPI): Promise { 23 | return await api.eth_baseFee().catch(() => { 24 | return api.chain.baseFee 25 | }) 26 | } 27 | 28 | export async function estimateEVMCall ( 29 | api: JEVMAPI, 30 | from: string, 31 | to: string, 32 | value: bigint, 33 | data: string, 34 | baseFee: bigint 35 | ): Promise { 36 | // in case of native asset transfer return SendEtherLimit 37 | if (value > BigInt(0) && data === EmptyCallData) { 38 | return SendEtherGasLimit 39 | } 40 | const gasLimit: bigint = await api.chain.ethProvider.estimateGas({ 41 | blockTag: 'pending', 42 | from, 43 | to, 44 | value, 45 | // TODO Add it as parameter 46 | // Warning to not use getWalletNonce() as it increments cached nonce. 47 | // nonce: Number(nonce), 48 | chainId: api.chain.chainId, 49 | gasPrice: baseFee, 50 | data 51 | }) 52 | return gasLimit 53 | } 54 | 55 | export async function estimateEVMOperation ( 56 | provider: MCNProvider, 57 | chain: JEVMBlockchain, 58 | from: string, 59 | operation: ChainNetworkOperation, 60 | spendings: BaseSpending[], 61 | values: Map, 62 | address: string, 63 | amount: bigint, 64 | data: string, 65 | feeType: TransactionType 66 | ): Promise { 67 | const api: JEVMAPI = provider.jevmApi[chain.id] 68 | const baseFee: bigint = await estimateEVMBaseFee(api) 69 | const fee = new EVMFeeData( 70 | chain, 71 | baseFee * BigInt(0), 72 | feeType, 73 | baseFee, 74 | BigInt(0), 75 | new EVMTransactionData(from, address, amount, data) 76 | ) 77 | spendings.push(fee.spending) 78 | return await estimateEVMCall(api, from, address, amount, data, baseFee).then( 79 | (gasLimit) => { 80 | fee.setGasLimit(gasLimit) 81 | return new ChainOperationSummary(provider, operation, chain, fee, spendings, values, []) 82 | }, 83 | (error) => { 84 | return new ChainOperationSummary(provider, operation, chain, fee, spendings, values, [error]) 85 | } 86 | ) 87 | } 88 | 89 | export async function executeEVMTransaction ( 90 | provider: MCNProvider, 91 | wallet: JEVMWallet, 92 | feeData: EVMFeeData 93 | ): Promise { 94 | const api: JEVMAPI = provider.jevmApi[wallet.chain.id] 95 | const unsignedTransaction: ethers.TransactionRequest = { 96 | from: wallet.getAddress(), 97 | to: feeData.data.to, 98 | value: feeData.data.value, 99 | nonce: Number(await getWalletNonce(wallet, api, false)), 100 | chainId: api.chain.chainId, 101 | gasLimit: feeData.gasLimit, 102 | gasPrice: feeData.baseFee, 103 | data: feeData.data.data 104 | } 105 | const transaction = await wallet.evmWallet.signTransaction(unsignedTransaction) 106 | return await api.eth_sendRawTransaction(transaction) 107 | } 108 | 109 | export async function estimateEVMWithdrawJRC20 ( 110 | api: JEVMAPI, 111 | sender: string, 112 | jrc20: JRC20Asset, 113 | amount: bigint 114 | ): Promise { 115 | const data = jrc20.adapter.getWithdrawData(amount) 116 | const baseFee = await estimateEVMBaseFee(api) 117 | const fee = new EVMFeeData( 118 | api.chain, 119 | baseFee * DefaultWithdrawEstimate, 120 | TransactionType.Withdraw, 121 | baseFee, 122 | DefaultWithdrawEstimate, 123 | new EVMTransactionData(sender, jrc20.address, BigInt(0), data) 124 | ) 125 | await estimateEVMCall(api, sender, jrc20.address, BigInt(0), data, baseFee).then( 126 | (gasLimit) => { 127 | fee.setGasLimit(gasLimit) 128 | }, 129 | () => {} 130 | ) 131 | return fee 132 | } 133 | 134 | export async function estimateEVMDepositJRC20 ( 135 | api: JEVMAPI, 136 | sender: string, 137 | jrc20: JRC20Asset, 138 | amount: bigint 139 | ): Promise { 140 | const data = jrc20.adapter.getDepositData(jrc20.nativeAssetId, amount) 141 | const baseFee = await estimateEVMBaseFee(api) 142 | const fee = new EVMFeeData( 143 | api.chain, 144 | baseFee * DefaultDepositEstimate, 145 | TransactionType.Deposit, 146 | baseFee, 147 | DefaultDepositEstimate, 148 | new EVMTransactionData(sender, NativeAssetCallContract, BigInt(0), data) 149 | ) 150 | await estimateEVMCall(api, sender, NativeAssetCallContract, BigInt(0), data, baseFee).then( 151 | (gasLimit) => { 152 | fee.setGasLimit(gasLimit) 153 | }, 154 | () => {} 155 | ) 156 | return fee 157 | } 158 | -------------------------------------------------------------------------------- /src/wallet/transaction/fee.ts: -------------------------------------------------------------------------------- 1 | import { type TokenAsset } from '../../asset' 2 | import { type Blockchain, type JEVMBlockchain } from '../../chain' 3 | import { type UnsignedTransaction } from '../../transaction' 4 | import { type AssetValue } from '../../utils' 5 | import { BaseSpending, type TransactionType, UtxoSpending, type EVMTransactionData, type Spending } from './transaction' 6 | 7 | export interface FeeData { 8 | chain: Blockchain 9 | amount: bigint 10 | type: TransactionType 11 | assetId: string 12 | spending: Spending 13 | 14 | getAssetValue: () => AssetValue 15 | } 16 | 17 | export class BaseFeeData implements FeeData { 18 | chain: Blockchain 19 | asset: TokenAsset 20 | amount: bigint 21 | type: TransactionType 22 | assetId: string 23 | spending: BaseSpending 24 | 25 | constructor (chain: Blockchain, amount: bigint, type: TransactionType) { 26 | this.chain = chain 27 | this.asset = chain.asset 28 | this.amount = amount 29 | this.type = type 30 | this.assetId = chain.assetId 31 | this.spending = new BaseSpending(this.chain, this.amount, this.assetId) 32 | } 33 | 34 | getAssetValue (): AssetValue { 35 | return this.asset.getAssetValue(this.amount) 36 | } 37 | } 38 | 39 | export class UtxoFeeData extends BaseFeeData { 40 | transaction: UnsignedTransaction 41 | override spending: UtxoSpending 42 | 43 | constructor (chain: Blockchain, amount: bigint, type: TransactionType, transaction: UnsignedTransaction) { 44 | super(chain, amount, type) 45 | this.transaction = transaction 46 | this.spending = new UtxoSpending(this.chain, this.amount, this.assetId, this.transaction.getUtxos()) 47 | } 48 | 49 | getAssetValue (): AssetValue { 50 | return this.chain.asset.getAssetValue(this.amount) 51 | } 52 | } 53 | 54 | export class EVMFeeData extends BaseFeeData { 55 | baseFee: bigint 56 | gasLimit: bigint 57 | data: EVMTransactionData 58 | 59 | constructor ( 60 | chain: JEVMBlockchain, 61 | amount: bigint, 62 | type: TransactionType, 63 | baseFee: bigint, 64 | gasLimit: bigint, 65 | data: EVMTransactionData 66 | ) { 67 | super(chain, amount, type) 68 | this.baseFee = baseFee 69 | this.gasLimit = gasLimit 70 | this.data = data 71 | } 72 | 73 | setGasLimit (gasLimit: bigint): void { 74 | this.amount = this.baseFee * gasLimit 75 | this.spending.amount = this.amount 76 | this.gasLimit = gasLimit 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/wallet/transaction/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base' 2 | export * from './cross' 3 | export * from './constants' 4 | export * from './evm' 5 | export * from './fee' 6 | export * from './platform' 7 | export * from './transaction' 8 | -------------------------------------------------------------------------------- /src/wallet/transaction/transaction.ts: -------------------------------------------------------------------------------- 1 | import { type Blockchain } from '../../chain' 2 | import { type Utxo } from '../../transaction' 3 | 4 | export enum TransactionType { 5 | Base = 'Base transaction', 6 | Send = 'Send transaction', 7 | Export = 'Export transaction', 8 | Import = 'Import transaction', 9 | Withdraw = 'Withdraw transaction', 10 | Deposit = 'Deposit transaction', 11 | Wrap = 'Wrap transaction', 12 | Unwrap = 'Unwrap transaction', 13 | PrimaryValidation = 'Primary validation transaction', 14 | PrimaryDelegation = 'Primary delegation transaction', 15 | CreateSupernet = 'Create supernet transaction', 16 | ValidateSupernet = 'Supernet validation transaction', 17 | RemoveSupernetValidator = 'Remove supernet validator transaction', 18 | CreateChain = 'Create chain transaction', 19 | EthCall = 'Eth call transaction', 20 | } 21 | 22 | export class TransactionReceipt { 23 | chainId: string 24 | transactionType: TransactionType 25 | transactionStatus: string 26 | transactionId: string 27 | 28 | constructor (chainId: string, transactionType: TransactionType, transactionStatus: string, transactionId: string) { 29 | this.chainId = chainId 30 | this.transactionStatus = transactionStatus 31 | this.transactionType = transactionType 32 | this.transactionId = transactionId 33 | } 34 | } 35 | 36 | export interface Spending { 37 | chain: Blockchain 38 | amount: bigint 39 | assetId: string 40 | } 41 | 42 | export class BaseSpending implements Spending { 43 | chain: Blockchain 44 | amount: bigint 45 | assetId: string 46 | 47 | constructor (chain: Blockchain, amount: bigint, assetId: string) { 48 | this.chain = chain 49 | this.amount = amount 50 | this.assetId = assetId 51 | } 52 | } 53 | 54 | export class UtxoSpending extends BaseSpending { 55 | utxos: Utxo[] 56 | 57 | constructor (chain: Blockchain, amount: bigint, assetId: string, utxos: Utxo[]) { 58 | super(chain, amount, assetId) 59 | this.utxos = utxos 60 | } 61 | } 62 | 63 | export class EVMTransactionData { 64 | from: string 65 | to: string 66 | value: bigint 67 | data: string 68 | 69 | constructor (from: string, to: string, value: bigint, data: string) { 70 | this.from = from 71 | this.to = to 72 | this.value = value 73 | this.data = data 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/wallet/vault.ts: -------------------------------------------------------------------------------- 1 | import { type MCNProvider } from '../juneo' 2 | import { VaultError } from '../utils' 3 | import { AccountType, MCNAccount } from './account' 4 | import { type VMWallet, type MCNWallet } from './wallet' 5 | 6 | function getAccountId (provider: MCNProvider, wallet: MCNWallet): string { 7 | const jvmId: string = wallet.getWallet(provider.jvmChain).getKeyPair().publicKey 8 | const evmId: string = wallet.getWallet(provider.juneChain).getKeyPair().publicKey 9 | return `${jvmId}_${evmId}` 10 | } 11 | 12 | export class VaultWallet { 13 | private readonly provider: MCNProvider 14 | readonly wallet: MCNWallet 15 | 16 | constructor (provider: MCNProvider, wallet: MCNWallet) { 17 | this.provider = provider 18 | this.wallet = wallet 19 | } 20 | 21 | getJVMAddress (): string { 22 | return this.wallet.getAddress(this.provider.jvmChain) 23 | } 24 | 25 | getEVMAddress (): string { 26 | return this.wallet.getAddress(this.provider.juneChain) 27 | } 28 | 29 | getIdentifier (): string { 30 | return getAccountId(this.provider, this.wallet) 31 | } 32 | } 33 | 34 | export class MCNVault { 35 | private readonly provider: MCNProvider 36 | account: MCNAccount 37 | readonly wallets = new Map() 38 | 39 | constructor (provider: MCNProvider, mainWallet: MCNWallet, wallets: MCNWallet[] = []) { 40 | this.provider = provider 41 | this.account = new MCNAccount(provider, mainWallet) 42 | const vaultWallet: VaultWallet = new VaultWallet(provider, mainWallet) 43 | this.wallets.set(vaultWallet.getIdentifier(), vaultWallet) 44 | this.addWallets(wallets) 45 | } 46 | 47 | addWallet (wallet: MCNWallet): void { 48 | this.addVaultWallet(new VaultWallet(this.provider, wallet)) 49 | } 50 | 51 | addWallets (wallets: MCNWallet[]): void { 52 | for (const wallet of wallets) { 53 | this.addWallet(wallet) 54 | } 55 | } 56 | 57 | private addVaultWallet (vaultWallet: VaultWallet): void { 58 | if (this.wallets.has(vaultWallet.getIdentifier())) { 59 | throw new VaultError('vault already contains this wallet') 60 | } 61 | this.wallets.set(vaultWallet.getIdentifier(), vaultWallet) 62 | // adding new wallets as signers 63 | for (const chainAccount of this.account.chainAccounts.values()) { 64 | if (chainAccount.type === AccountType.Utxo) { 65 | const signer: VMWallet = vaultWallet.wallet.getWallet(chainAccount.chain) 66 | chainAccount.signers.push(signer) 67 | } 68 | } 69 | } 70 | 71 | setMainWallet (vaultWallet: VaultWallet): void { 72 | // reset all signers of previous account to avoid errors if switching back to it later 73 | for (const chainAccount of this.account.chainAccounts.values()) { 74 | chainAccount.signers = [chainAccount.chainWallet] 75 | } 76 | // remove self before adding signers to avoid duplicate signers 77 | if (this.wallets.has(vaultWallet.getIdentifier())) { 78 | this.wallets.delete(vaultWallet.getIdentifier()) 79 | } 80 | this.account = new MCNAccount(this.provider, vaultWallet.wallet) 81 | // adding all registered signers to new main account 82 | for (const chainAccount of this.account.chainAccounts.values()) { 83 | if (chainAccount.type === AccountType.Utxo) { 84 | for (const wallet of this.wallets.values()) { 85 | const signer: VMWallet = wallet.wallet.getWallet(chainAccount.chain) 86 | chainAccount.signers.push(signer) 87 | } 88 | } 89 | } 90 | this.wallets.set(vaultWallet.getIdentifier(), vaultWallet) 91 | } 92 | 93 | removeWallet (wallet: MCNWallet): void { 94 | const identifier: string = getAccountId(this.provider, wallet) 95 | if (identifier === getAccountId(this.provider, this.account.wallet)) { 96 | throw new VaultError('cannot remove main wallet') 97 | } 98 | // removing wallet 99 | if (!this.wallets.delete(identifier)) { 100 | throw new VaultError('wallet is not in vault') 101 | } 102 | // removing signers from chain accounts in mainWallet (= account) 103 | for (const chainAccount of this.account.chainAccounts.values()) { 104 | if (chainAccount.type === AccountType.Utxo) { 105 | const signers: VMWallet[] = [] 106 | for (const vaultWallet of this.wallets.values()) { 107 | const signer: VMWallet = vaultWallet.wallet.getWallet(chainAccount.chain) 108 | signers.push(signer) 109 | } 110 | chainAccount.signers = signers 111 | } 112 | } 113 | } 114 | 115 | hasWallet (wallet: MCNWallet): boolean { 116 | return this.wallets.has(getAccountId(this.provider, wallet)) 117 | } 118 | 119 | getWallet (identifier: string): VaultWallet { 120 | const wallet: VaultWallet | undefined = this.wallets.get(identifier) 121 | if (typeof wallet === 'undefined') { 122 | throw new VaultError(`there is no wallet with identifier: ${identifier}`) 123 | } else { 124 | return wallet 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /tests/e2e/api/info.test.ts: -------------------------------------------------------------------------------- 1 | import { PROVIDER } from '../constants' 2 | 3 | describe('InfoAPI', () => { 4 | describe('isBootstrapped', () => { 5 | test.each([{ chainID: PROVIDER.juneChain.id }])('Valid: $chainID', async ({ chainID }) => { 6 | const result = await PROVIDER.info.isBootstrapped(chainID) 7 | expect(result.isBootstrapped).toEqual(true) 8 | }) 9 | test.failing.each([{ chainID: 'WRONG_CHAIN_ID' }])('Invalid: $chainID', async ({ chainID }) => { 10 | await PROVIDER.info.isBootstrapped('WRONG_CHAIN_ID') 11 | }) 12 | }) 13 | 14 | describe('getBlockchainID', () => { 15 | test.each([{ alias: 'JUNE' }])('Valid: $alias', async ({ alias }) => { 16 | const result = await PROVIDER.info.getBlockchainID(alias) 17 | expect(result.blockchainID).toEqual(PROVIDER.juneChain.id) 18 | }) 19 | test.failing.each([{ alias: 'WRONG_ALIAS' }])('Invalid: $alias', async ({ alias }) => { 20 | await PROVIDER.info.getBlockchainID('alias') 21 | }) 22 | }) 23 | 24 | describe('getNetworkID', () => { 25 | test('Returns correct network id', async () => { 26 | const result = await PROVIDER.info.getNetworkID() 27 | expect(result.networkID).toEqual(PROVIDER.mcn.id.toString()) 28 | }) 29 | }) 30 | 31 | describe('getNetworkName', () => { 32 | test('Returns correct network name', async () => { 33 | const result = await PROVIDER.info.getNetworkName() 34 | expect(result.networkName).toEqual(PROVIDER.mcn.hrp) 35 | }) 36 | }) 37 | 38 | describe('getNodeID', () => { 39 | test('Returns node id', async () => { 40 | const result = await PROVIDER.info.getNodeID() 41 | expect(result).toBeDefined() 42 | }) 43 | }) 44 | 45 | describe('getNodeIP', () => { 46 | test('Returns node ip', async () => { 47 | const result = await PROVIDER.info.getNodeIP() 48 | expect(result).toBeDefined() 49 | }) 50 | }) 51 | 52 | describe('getNodeVersion', () => { 53 | test('Returns node version', async () => { 54 | const result = await PROVIDER.info.getNodeVersion() 55 | expect(result).toBeDefined() 56 | }) 57 | }) 58 | 59 | describe('getTxFee', () => { 60 | test('Returns fee config', async () => { 61 | const result = await PROVIDER.info.getTxFee() 62 | expect(result).toBeDefined() 63 | }) 64 | test('Returns cached fee config', async () => { 65 | const result = await PROVIDER.info.getTxFee() 66 | expect(result).toBeDefined() 67 | }) 68 | test('Returns updated fee config', async () => { 69 | const result = await PROVIDER.info.getTxFee(true) 70 | expect(result).toBeDefined() 71 | }) 72 | }) 73 | 74 | describe('getVMs', () => { 75 | test('Returns installed VMs', async () => { 76 | const result = await PROVIDER.info.getVMs() 77 | expect(result).toBeDefined() 78 | }) 79 | }) 80 | 81 | describe('peers', () => { 82 | test('Returns list of peers', async () => { 83 | const result = await PROVIDER.info.peers() 84 | expect(result).toBeDefined() 85 | }) 86 | test.failing.each([ 87 | { nodeIDs: ['WRONG_NODE_ID', 'NodeID-P1ESFUf8tutmR8hszZUWsXAJEKARZ5SPw'] }, 88 | { nodeIDs: ['NodeID-P1ESFUf8tutmR8hszZUWsXAJEKARZ5SPa'] } 89 | ])('Invalid: $nodeIDs', async ({ nodeIDs }) => { 90 | await PROVIDER.info.peers(nodeIDs) 91 | }) 92 | }) 93 | 94 | describe('uptime', () => { 95 | test.failing.each([{ supernetID: 'WRONG_SUPERNET_ID' }])('Invalid: $supernetID', async ({ supernetID }) => { 96 | await PROVIDER.info.uptime(supernetID) 97 | }) 98 | }) 99 | }) 100 | -------------------------------------------------------------------------------- /tests/e2e/api/jevm.test.ts: -------------------------------------------------------------------------------- 1 | import { SocotraEUR1Asset } from '../../../src' 2 | import { PROVIDER } from '../constants' 3 | 4 | describe('JEVMAPI', () => { 5 | describe('getTx', () => { 6 | test.failing.each([ 7 | { txID: '241mEKvJjtzAbVxvSsooEaAYgXkaipSDuxEoXBxBDP8mKHb8Cm' }, 8 | { txID: '0x3c529e9941b6ca0ec34948c7f797e94ff810643ef64896c409ea0df36be9e554' }, 9 | { txID: 'INVALID_TX_ID' } 10 | ])('Invalid: $txID', async ({ txID }) => { 11 | const result = await PROVIDER.jevmApi[PROVIDER.juneChain.id].getTx(txID) 12 | expect(result.tx).toBeDefined() 13 | }) 14 | }) 15 | 16 | describe('eth_getAssetBalance', () => { 17 | test.failing.each([ 18 | { 19 | description: 'Wrong address', 20 | address: '0x9b31d8C5Dd49fCdE96218895f96a6eC894529', 21 | block: '0x4BF7C5', 22 | assetID: SocotraEUR1Asset.assetId 23 | }, 24 | { 25 | description: 'Wrong block', 26 | address: '0x9b31d8C5Dd49fCdE96218895f96a6eC1ea894529', 27 | block: '2000', 28 | assetID: SocotraEUR1Asset.assetId 29 | }, 30 | { 31 | description: 'Wrong assetID', 32 | address: '0x9b31d8C5Dd49fCdE96218895f96a6eC1ea894529', 33 | block: '0x4BF7C5', 34 | assetID: '0x0' 35 | } 36 | ])('$description: $address, $block, $assetID', async ({ address, block, assetID }) => { 37 | await PROVIDER.jevmApi[PROVIDER.juneChain.id].eth_getAssetBalance(address, block, assetID) 38 | }) 39 | }) 40 | 41 | describe('eth_baseFee', () => { 42 | test('Returns base fee', async () => { 43 | const result = await PROVIDER.jevmApi[PROVIDER.juneChain.id].eth_baseFee() 44 | expect(result).toBeDefined() 45 | }) 46 | }) 47 | 48 | describe('eth_maxPriorityFeePerGas', () => { 49 | test('Returns max priority fee per gas', async () => { 50 | const result = await PROVIDER.jevmApi[PROVIDER.juneChain.id].eth_maxPriorityFeePerGas() 51 | expect(result).toBeDefined() 52 | }) 53 | }) 54 | 55 | describe('eth_getChainConfig', () => { 56 | test('Returns chain config', async () => { 57 | const result = await PROVIDER.jevmApi[PROVIDER.juneChain.id].eth_getChainConfig() 58 | expect(result).toBeDefined() 59 | }) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /tests/e2e/api/jvm.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SocotraBCH1Asset, 3 | SocotraEUR1Asset, 4 | type GetAssetDescriptionResponse, 5 | type GetBlockResponse, 6 | type GetHeightResponse, 7 | type GetTxResponse 8 | } from '../../../src' 9 | import { PROVIDER } from '../constants' 10 | 11 | describe('JVMAPI', () => { 12 | describe('buildGenesis', () => { 13 | // TODO later 14 | }) 15 | 16 | describe('getAssetDescription', () => { 17 | test.each([{ asset: PROVIDER.jvmChain.asset }, { asset: SocotraEUR1Asset }, { asset: SocotraBCH1Asset }])( 18 | 'Valid: $asset.assetId ($asset.symbol)', 19 | async ({ asset }) => { 20 | const result: GetAssetDescriptionResponse = await PROVIDER.jvmApi.getAssetDescription(asset.assetId) 21 | // TODO in Socotra2 use asset name 22 | expect(result.name).toBeDefined() 23 | expect(result.symbol).toEqual(asset.symbol) 24 | expect(result.denomination).toEqual(asset.decimals.toString()) 25 | expect(result.assetID).toEqual(asset.assetId) 26 | } 27 | ) 28 | 29 | test.failing.each([ 30 | { assetId: '2RcLCZTsxSnvzeBvtrjRo8PCzLXuecHBoyr8DNp1R8ob8kHkbZ' }, 31 | { assetId: 'INVALID_ASSET_ID' } 32 | ])('Invalid: $assetId', async ({ assetId }) => { 33 | const result: GetAssetDescriptionResponse = await PROVIDER.jvmApi.getAssetDescription(assetId) 34 | expect(result.name).toBeDefined() 35 | }) 36 | }) 37 | 38 | describe('getBlock', () => { 39 | // TODO later, need to have index api enabled 40 | }) 41 | 42 | describe('getBlockByHeight', () => { 43 | test.each([{ height: 1 }, { height: 10 }, { height: 100 }, { height: null }, { height: undefined }])( 44 | 'Valid: $height', 45 | async ({ height }) => { 46 | const result: GetBlockResponse = await PROVIDER.jvmApi.getBlockByHeight(height as any) 47 | expect(result.block).toBeDefined() 48 | expect(result.encoding).toBeDefined() 49 | } 50 | ) 51 | 52 | test.failing.each([ 53 | { description: 'Negative height', height: -1 }, 54 | { description: 'String input', height: 'aString' }, 55 | { description: 'Object input', height: {} }, 56 | { description: 'Array input', height: [] } 57 | ])('$description: $height', async ({ height }) => { 58 | await PROVIDER.jvmApi.getBlockByHeight(height as any) 59 | }) 60 | }) 61 | 62 | describe('getHeight', () => { 63 | test('Returns height', async () => { 64 | const result: GetHeightResponse = await PROVIDER.jvmApi.getHeight() 65 | expect(result.height).toBeDefined() 66 | }) 67 | }) 68 | 69 | describe('getTx', () => { 70 | test.each([ 71 | { txID: 'P1ogBpExP9gRM2Pjqr7J32bzACcob4hZtEj62T71feW6Q8KGb' }, 72 | { txID: '2E5DxrCSQkAc1FoAQ7q1QQyaHnHskEkvDjzFG9YYN7dfsjKGjB' } 73 | ])('Valid: $txID', async ({ txID }) => { 74 | const result: GetTxResponse = await PROVIDER.jvmApi.getTx(txID) 75 | expect(result.encoding).toBeDefined() 76 | expect(result.tx).toBeDefined() 77 | }) 78 | 79 | test.failing.each([ 80 | { description: 'Invalid txID', txID: 'INVALID_TX_ID' }, 81 | { description: 'Numerical string', txID: '123' }, 82 | { description: 'Null input', txID: null }, 83 | { description: 'Undefined input', txID: undefined }, 84 | { description: 'Object input', txID: {} }, 85 | { description: 'Array input', txID: [] } 86 | ])('$description: $txID', async ({ txID }) => { 87 | await PROVIDER.jvmApi.getTx(txID as any) 88 | }) 89 | }) 90 | 91 | describe('issueTx', () => { 92 | // TODO later 93 | }) 94 | }) 95 | -------------------------------------------------------------------------------- /tests/e2e/constants.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv' 2 | import { MCNAccount, MCNProvider, SocotraNetwork } from '../../src' 3 | dotenv.config() 4 | 5 | export const PROVIDER = new MCNProvider(SocotraNetwork) 6 | const WALLET = PROVIDER.mcn.recoverWallet(process.env.MNEMONIC!) 7 | export const ACCOUNT = new MCNAccount(PROVIDER, WALLET) 8 | 9 | export const EXCESSIVE_AMOUNT = BigInt('100000000000000000000000000000000000000000000000') 10 | export const DONE_STATUS = 'Done' 11 | export const DEFAULT_TIMEOUT: number = 120_000 12 | -------------------------------------------------------------------------------- /tests/e2e/wallet/send.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AccountError, 3 | SendOperation, 4 | SocotraEUR1Chain, 5 | SocotraJUNEChain, 6 | SocotraJVMChain, 7 | type ExecutableOperation 8 | } from '../../../src' 9 | import { ACCOUNT, DEFAULT_TIMEOUT, DONE_STATUS, EXCESSIVE_AMOUNT } from '../constants' 10 | 11 | describe('Send operations', () => { 12 | const juneChain = SocotraJUNEChain 13 | const euroChain = SocotraEUR1Chain 14 | const jvmChain = SocotraJVMChain 15 | const euroAddress = '0x3000000000000000000000000000000000000000' 16 | 17 | describe('EVM send', () => { 18 | describe('Valid execute', () => { 19 | test.each([ 20 | { 21 | chain: juneChain, 22 | assetId: juneChain.assetId, 23 | symbol: juneChain.asset.symbol, 24 | value: BigInt(1_000), 25 | recipient: '0x3c647d88Bc92766075feA7A965CA599CAAB2FD26' 26 | }, 27 | { 28 | chain: juneChain, 29 | assetId: euroAddress, 30 | symbol: 'ETH.e', 31 | value: BigInt(1), 32 | recipient: '0x3c647d88Bc92766075feA7A965CA599CAAB2FD26' 33 | }, 34 | { 35 | chain: euroChain, 36 | assetId: euroChain.assetId, 37 | symbol: euroChain.asset.symbol, 38 | value: BigInt('10000000000000'), 39 | recipient: '0x3c647d88Bc92766075feA7A965CA599CAAB2FD26' 40 | } 41 | ])( 42 | '$#) $value $symbol in $chain.name to $recipient', 43 | async ({ chain, assetId, value, recipient }) => { 44 | const operation = new SendOperation(chain, assetId, value, recipient) 45 | const summary = await ACCOUNT.estimate(operation) 46 | await ACCOUNT.execute(summary) 47 | const executable: ExecutableOperation = summary.getExecutable() 48 | expect(executable.status).toEqual(DONE_STATUS) 49 | }, 50 | DEFAULT_TIMEOUT 51 | ) 52 | }) 53 | 54 | describe('Invalid execute', () => { 55 | test.each([ 56 | { 57 | description: 'Negative value', 58 | chain: juneChain, 59 | assetId: juneChain.assetId, 60 | symbol: juneChain.asset.symbol, 61 | value: BigInt(-1), 62 | recipient: '0x3c647d88Bc92766075feA7A965CA599CAAB2FD26', 63 | expectedError: RangeError 64 | }, 65 | { 66 | description: 'Excessive amount', 67 | chain: juneChain, 68 | assetId: juneChain.assetId, 69 | symbol: juneChain.asset.symbol, 70 | value: EXCESSIVE_AMOUNT, 71 | recipient: '0x3c647d88Bc92766075feA7A965CA599CAAB2FD26', 72 | expectedError: AccountError 73 | }, 74 | { 75 | description: 'Excessive amount and different assetId', 76 | chain: juneChain, 77 | assetId: euroChain.assetId, 78 | symbol: euroChain.asset.symbol, 79 | value: EXCESSIVE_AMOUNT, 80 | recipient: '0x3c647d88Bc92766075feA7A965CA599CAAB2FD26', 81 | expectedError: AccountError 82 | } 83 | ])( 84 | '$#) $description $value $symbol in $chain.name to $recipient', 85 | async ({ chain, assetId, value, recipient, expectedError }) => { 86 | const operation = new SendOperation(chain, assetId, value, recipient) 87 | const summary = await ACCOUNT.estimate(operation) 88 | await expect(ACCOUNT.execute(summary)).rejects.toThrow(expectedError) 89 | }, 90 | DEFAULT_TIMEOUT 91 | ) 92 | }) 93 | }) 94 | 95 | describe('JVM send', () => { 96 | describe('Valid execute', () => { 97 | test.each([ 98 | { 99 | chain: jvmChain, 100 | assetId: jvmChain.assetId, 101 | symbol: jvmChain.asset.symbol, 102 | value: BigInt(10_000_000), 103 | recipient: 'JVM-socotra167w40pwvlrf5eg0d9t48zj6kwkaqz2xan50pal' 104 | } 105 | ])( 106 | '$#) $value $symbol in $chain.name to $recipient', 107 | async ({ chain, assetId, value, recipient }) => { 108 | const operation = new SendOperation(chain, assetId, value, recipient) 109 | const summary = await ACCOUNT.estimate(operation) 110 | await ACCOUNT.execute(summary) 111 | const executable: ExecutableOperation = summary.getExecutable() 112 | expect(executable.status).toEqual(DONE_STATUS) 113 | }, 114 | DEFAULT_TIMEOUT 115 | ) 116 | }) 117 | 118 | describe('Invalid execute', () => { 119 | test.each([ 120 | { 121 | description: 'Excessive amount', 122 | chain: jvmChain, 123 | assetId: jvmChain.assetId, 124 | symbol: jvmChain.asset.symbol, 125 | value: EXCESSIVE_AMOUNT, 126 | recipient: 'JVM-socotra167w40pwvlrf5eg0d9t48zj6kwkaqz2xan50pal', 127 | expectedError: AccountError 128 | }, 129 | { 130 | description: 'Zero value', 131 | chain: jvmChain, 132 | assetId: jvmChain.assetId, 133 | symbol: jvmChain.asset.symbol, 134 | value: BigInt(0), 135 | recipient: 'JVM-socotra167w40pwvlrf5eg0d9t48zj6kwkaqz2xan50pal', 136 | expectedError: TypeError 137 | } 138 | ])( 139 | '$#) $description $value $symbol in $chain.name to $recipient', 140 | async ({ chain, assetId, value, recipient, expectedError }) => { 141 | const operation = new SendOperation(chain, assetId, value, recipient) 142 | const summary = await ACCOUNT.estimate(operation) 143 | await expect(ACCOUNT.execute(summary)).rejects.toThrow(expectedError) 144 | }, 145 | DEFAULT_TIMEOUT 146 | ) 147 | }) 148 | }) 149 | }) 150 | -------------------------------------------------------------------------------- /tests/e2e/wallet/stake.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AccountError, 3 | DecodingError, 4 | DelegatePrimaryOperation, 5 | TimeUtils, 6 | type ChainAccount, 7 | type ExecutableOperation 8 | } from '../../../src' 9 | import { ACCOUNT, DEFAULT_TIMEOUT, DONE_STATUS, EXCESSIVE_AMOUNT, PROVIDER } from '../constants' 10 | 11 | const chainAccount: ChainAccount = ACCOUNT.getAccount(PROVIDER.platformChain.id) 12 | // for now we take this nodeID. maybe in the future we can select the node Id with a function 13 | const validNodeId = 'NodeID-4JfgcoMWBpxCQL5VmyQ1f6L36mUbLLBga' 14 | 15 | describe('Staking operations', (): void => { 16 | test.todo('operations instantiations') 17 | 18 | describe('DelegateOperation', () => { 19 | describe('Execute valid', () => { 20 | test.each([ 21 | { 22 | nodeId: validNodeId, 23 | amount: BigInt(100_000_000), // 0.1 JUNE 24 | expectedStatus: DONE_STATUS, 25 | stakePeriod: TimeUtils.week() * BigInt(2) 26 | } 27 | ])( 28 | '$#) $amount tokens to delegate node id: $nodeId stake period of $stakePeriod', 29 | async ({ nodeId, amount, expectedStatus, stakePeriod }) => { 30 | const delegateOperation = new DelegatePrimaryOperation( 31 | PROVIDER.platformChain, 32 | nodeId, 33 | amount, 34 | stakePeriod, 35 | [chainAccount.address], 36 | 1, 37 | [chainAccount.address], 38 | 1 39 | ) 40 | const summary = await ACCOUNT.estimate(delegateOperation) 41 | await ACCOUNT.execute(summary) 42 | const executable: ExecutableOperation = summary.getExecutable() 43 | expect(executable.status).toEqual(expectedStatus) 44 | }, 45 | DEFAULT_TIMEOUT 46 | ) 47 | }) 48 | 49 | describe('Estimate invalid', () => { 50 | test.each([ 51 | { 52 | description: 'Wrong node id', 53 | nodeId: 'WRONG_NODE_ID', 54 | amount: BigInt(10_000_000), 55 | expectedError: DecodingError, 56 | stakePeriod: TimeUtils.day() 57 | } 58 | ])( 59 | '$#) $description $amount tokens to delegate node id: $nodeId stake period of $stakePeriod', 60 | async ({ nodeId, amount, expectedError, stakePeriod }) => { 61 | const delegateOperation = new DelegatePrimaryOperation( 62 | PROVIDER.platformChain, 63 | nodeId, 64 | amount, 65 | stakePeriod, 66 | [chainAccount.address], 67 | 1, 68 | [chainAccount.address], 69 | 1 70 | ) 71 | await expect(ACCOUNT.estimate(delegateOperation)).rejects.toThrow(expectedError) 72 | }, 73 | DEFAULT_TIMEOUT 74 | ) 75 | }) 76 | 77 | describe('Execute invalid', () => { 78 | test.each([ 79 | { 80 | description: 'More than balance', 81 | nodeId: validNodeId, 82 | amount: EXCESSIVE_AMOUNT, 83 | expectedError: AccountError, 84 | stakePeriod: TimeUtils.day() 85 | } 86 | ])( 87 | '$#) $description $amount tokens to delegate node id: $nodeId stake period of $stakePeriod', 88 | async ({ nodeId, amount, expectedError, stakePeriod }) => { 89 | const delegateOperation = new DelegatePrimaryOperation( 90 | PROVIDER.platformChain, 91 | nodeId, 92 | amount, 93 | stakePeriod, 94 | [chainAccount.address], 95 | 1, 96 | [chainAccount.address], 97 | 1 98 | ) 99 | const summary = await ACCOUNT.estimate(delegateOperation) 100 | await expect(ACCOUNT.execute(summary)).rejects.toThrow(expectedError) 101 | }, 102 | DEFAULT_TIMEOUT 103 | ) 104 | }) 105 | }) 106 | }) 107 | -------------------------------------------------------------------------------- /tests/e2e/wallet/wrap.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AccountError, 3 | NetworkOperationRange, 4 | NetworkOperationType, 5 | SocotraJUNEChain, 6 | SocotraWJUNEAsset, 7 | UnwrapOperation, 8 | WrapOperation, 9 | type ExecutableOperation 10 | } from '../../../src' 11 | import { ACCOUNT, DEFAULT_TIMEOUT, DONE_STATUS, EXCESSIVE_AMOUNT } from '../constants' 12 | 13 | describe('Wrapping operations', () => { 14 | const juneChain = SocotraJUNEChain 15 | const wJuneAsset = SocotraWJUNEAsset 16 | 17 | describe('WrapOperation', () => { 18 | describe('Instantiation', () => { 19 | test.each([{ blockchain: juneChain, asset: wJuneAsset, amount: BigInt(1000) }])( 20 | '$#) $amount $asset.name in $blockchain.name', 21 | async ({ blockchain, asset, amount }) => { 22 | const operation = new WrapOperation(blockchain, asset, amount) 23 | expect(operation.chain).toEqual(blockchain) 24 | expect(operation.asset).toEqual(asset) 25 | expect(operation.amount).toEqual(amount) 26 | expect(operation.range).toEqual(NetworkOperationRange.Chain) 27 | expect(operation.type).toEqual(NetworkOperationType.Wrap) 28 | } 29 | ) 30 | }) 31 | 32 | describe('Valid execute', () => { 33 | test.each([{ blockchain: juneChain, asset: wJuneAsset, amount: BigInt(1000), expectedStatus: DONE_STATUS }])( 34 | '$#) $amount $asset.name in $blockchain.name', 35 | async ({ blockchain, asset, amount, expectedStatus }) => { 36 | const operation = new WrapOperation(blockchain, asset, amount) 37 | const summary = await ACCOUNT.estimate(operation) 38 | await ACCOUNT.execute(summary) 39 | const executable: ExecutableOperation = summary.getExecutable() 40 | expect(executable.status).toEqual(expectedStatus) 41 | }, 42 | DEFAULT_TIMEOUT 43 | ) 44 | }) 45 | 46 | describe('Invalid execute', () => { 47 | test.each([ 48 | { 49 | description: 'More than available balance', 50 | blockchain: juneChain, 51 | asset: wJuneAsset, 52 | amount: EXCESSIVE_AMOUNT, 53 | expectedStatus: AccountError 54 | } 55 | ])( 56 | '$#) $description $amount $asset.name in $blockchain.name', 57 | async ({ blockchain, asset, amount, expectedStatus }) => { 58 | const operation = new WrapOperation(blockchain, asset, amount) 59 | const summary = await ACCOUNT.estimate(operation) 60 | await expect(ACCOUNT.execute(summary)).rejects.toThrow(expectedStatus) 61 | }, 62 | DEFAULT_TIMEOUT 63 | ) 64 | }) 65 | }) 66 | 67 | describe('UnwrapOperation', () => { 68 | describe('Instantiation', () => { 69 | test.each([{ blockchain: juneChain, asset: wJuneAsset, amount: BigInt(1000) }])( 70 | '$#) $amount $asset.name in $blockchain.name', 71 | async ({ blockchain, asset, amount }) => { 72 | const operation = new UnwrapOperation(blockchain, asset, amount) 73 | expect(operation.chain).toEqual(blockchain) 74 | expect(operation.asset).toEqual(asset) 75 | expect(operation.amount).toEqual(amount) 76 | expect(operation.range).toEqual(NetworkOperationRange.Chain) 77 | expect(operation.type).toEqual(NetworkOperationType.Unwrap) 78 | } 79 | ) 80 | }) 81 | 82 | describe('Valid execute', () => { 83 | test.each([{ blockchain: juneChain, asset: wJuneAsset, amount: BigInt(1000), expectedStatus: DONE_STATUS }])( 84 | '$#) $amount $asset.name in $blockchain.name', 85 | async ({ blockchain, asset, amount, expectedStatus }) => { 86 | const operation = new UnwrapOperation(blockchain, asset, amount) 87 | const summary = await ACCOUNT.estimate(operation) 88 | await ACCOUNT.execute(summary) 89 | const executable: ExecutableOperation = summary.getExecutable() 90 | expect(executable.status).toEqual(expectedStatus) 91 | }, 92 | DEFAULT_TIMEOUT 93 | ) 94 | }) 95 | 96 | describe('Invalid execute', () => { 97 | test.each([ 98 | { 99 | description: 'More than available wrapped balance', 100 | blockchain: juneChain, 101 | asset: wJuneAsset, 102 | amount: EXCESSIVE_AMOUNT, 103 | expectedError: AccountError 104 | } 105 | ])( 106 | '$#) $description $amount $asset.name in $blockchain.name', 107 | async ({ blockchain, asset, amount, expectedError }) => { 108 | const operation = new UnwrapOperation(blockchain, asset, amount) 109 | const summary = await ACCOUNT.estimate(operation) 110 | await expect(ACCOUNT.execute(summary)).rejects.toThrow(expectedError) 111 | }, 112 | DEFAULT_TIMEOUT 113 | ) 114 | }) 115 | }) 116 | }) 117 | -------------------------------------------------------------------------------- /tests/unit/transaction/jvm/transaction.test.ts: -------------------------------------------------------------------------------- 1 | import { ExportTransaction, ImportTransaction, JVMExportTransaction, JVMImportTransaction } from '../../../../src' 2 | 3 | describe('JVMExportTransaction', () => { 4 | test.each([ 5 | { 6 | description: 'Parsing from JVMAPI hex data', 7 | data: '0x0000000000040000002e8f4b348b4e89d5d614c666bc9d2fed3c2366612efec49853898e6f7505b1768900000001266ecea3035b1435f5cbb17dfc33cc1f4e9b058677915cc52c7ccfae1507b5630000000700005af30eb07c8000000000000000000000000100000001328a3e6647df8b2581ac8e3f538327843c3bb26d00000002d797e2a7994f6cf7daa23cdf1c549c45cc667a576cec91e19e2470a707971ef200000000266ecea3035b1435f5cbb17dfc33cc1f4e9b058677915cc52c7ccfae1507b56300000005002386f26fc100000000000100000000ff16681a218bf9c5a8d6272eaf85079854912c6e4cc728bdd2fc38da2d6c5f9000000000266ecea3035b1435f5cbb17dfc33cc1f4e9b058677915cc52c7ccfae1507b5630000000500005af30fe1a980000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000001266ecea3035b1435f5cbb17dfc33cc1f4e9b058677915cc52c7ccfae1507b56300000007002386f27059968000000000000000000000000100000001328a3e6647df8b2581ac8e3f538327843c3bb26d000000020000000900000001bfc526f263abea7b3cd3f46447a135c2633fbe8579f85fd70336f5ff32e4f3ef51b41b549181ea81819eadb91d45aa3a595d71885e5fa1e430fc6d94f7dc8abf010000000900000001bfc526f263abea7b3cd3f46447a135c2633fbe8579f85fd70336f5ff32e4f3ef51b41b549181ea81819eadb91d45aa3a595d71885e5fa1e430fc6d94f7dc8abf0120c5ce0c' 8 | } 9 | ])('$description', async ({ data }) => { 10 | const parsed = JVMExportTransaction.parse(data) 11 | expect(parsed).toBeInstanceOf(ExportTransaction) 12 | }) 13 | }) 14 | 15 | describe('JVMImportTransaction', () => { 16 | test.each([ 17 | { 18 | description: 'Parsing from JVMAPI hex data', 19 | data: '0x0000000000030000002e8f4b348b4e89d5d614c666bc9d2fed3c2366612efec49853898e6f7505b1768900000001266ecea3035b1435f5cbb17dfc33cc1f4e9b058677915cc52c7ccfae1507b563000000070000000005f5e100000000000000000000000001000000014a68dd4dc1bafa2bbcd2d3ea2e69dff3cb7393850000000000000000000000000000000000000000000000000000000000000000000000000000000000000001d6d29c7c9758865fd862beca3278ec2e4222c10d5ef26742c7cd02e92afb91be00000001266ecea3035b1435f5cbb17dfc33cc1f4e9b058677915cc52c7ccfae1507b5630000000500000000068e778000000001000000000000000100000009000000018d5d5925516e2eb376dd5f78fee7669c022ce507e284e09d119c4e36ebb63e7c6d710e324b139287d8226c799b9b2a1e8e3b22628c4bc51dc43327e0e9de3087013f103c23' 20 | } 21 | ])('$description', async ({ data }) => { 22 | const parsed = JVMImportTransaction.parse(data) 23 | expect(parsed).toBeInstanceOf(ImportTransaction) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /tests/unit/utils/encoding.test.ts: -------------------------------------------------------------------------------- 1 | import { JuneoBuffer } from '../../../src' 2 | import * as encoding from '../../../src/utils/encoding' 3 | 4 | describe('Encoding', (): void => { 5 | test('Verify checksum', (): void => { 6 | // TODO add verification of checksum of other encoding types 7 | // valid 8 | expect(encoding.verifyChecksum(JuneoBuffer.fromString('123abcbcee49f6', 'hex'))).toBe(true) 9 | expect(encoding.verifyChecksum(JuneoBuffer.fromString('7a1b2c3d88ba9d4c', 'hex'))).toBe(true) 10 | expect(encoding.verifyChecksum(JuneoBuffer.fromString('fedcba98765432100fa7e28e', 'hex'))).toBe(true) 11 | expect(encoding.verifyChecksum(JuneoBuffer.fromString('123456789abcdef0668592d1', 'hex'))).toBe(true) 12 | expect(encoding.verifyChecksum(JuneoBuffer.fromString('abcdefd21609a7', 'hex'))).toBe(true) 13 | expect(encoding.verifyChecksum(JuneoBuffer.fromString('deadbeefaa813953', 'hex'))).toBe(true) 14 | expect(encoding.verifyChecksum(JuneoBuffer.fromString('017785459a', 'hex'))).toBe(true) 15 | expect(encoding.verifyChecksum(JuneoBuffer.fromString('0017afa01d', 'hex'))).toBe(true) 16 | // invalid 17 | expect(encoding.verifyChecksum(JuneoBuffer.fromString('123abc', 'hex'))).toBe(false) 18 | expect(encoding.verifyChecksum(JuneoBuffer.fromString('7a1b2c3d', 'hex'))).toBe(false) 19 | expect(encoding.verifyChecksum(JuneoBuffer.fromString('fedcba9876543210', 'hex'))).toBe(false) 20 | expect(encoding.verifyChecksum(JuneoBuffer.fromString('123456789abcdef0', 'hex'))).toBe(false) 21 | expect(encoding.verifyChecksum(JuneoBuffer.fromString('abcdef', 'hex'))).toBe(false) 22 | expect(encoding.verifyChecksum(JuneoBuffer.fromString('deadbeef', 'hex'))).toBe(false) 23 | expect(encoding.verifyChecksum(JuneoBuffer.fromString('01', 'hex'))).toBe(false) 24 | expect(encoding.verifyChecksum(JuneoBuffer.fromString('00', 'hex'))).toBe(false) 25 | }) 26 | test('Has hex prefix', (): void => { 27 | // valid 28 | expect(encoding.hasHexPrefix('0x')).toBe(true) 29 | expect(encoding.hasHexPrefix('0x0')).toBe(true) 30 | expect(encoding.hasHexPrefix('0xa')).toBe(true) 31 | expect(encoding.hasHexPrefix('0xz')).toBe(true) 32 | expect(encoding.hasHexPrefix('0xabc123')).toBe(true) 33 | // invalid 34 | expect(encoding.hasHexPrefix('00x')).toBe(false) 35 | expect(encoding.hasHexPrefix('0')).toBe(false) 36 | expect(encoding.hasHexPrefix('')).toBe(false) 37 | expect(encoding.hasHexPrefix('abc123')).toBe(false) 38 | }) 39 | test('Is hex', (): void => { 40 | // valid 41 | expect(encoding.isHex('0x0')).toBe(true) 42 | expect(encoding.isHex('0x')).toBe(true) 43 | expect(encoding.isHex('0')).toBe(true) 44 | expect(encoding.isHex('0xabcdef0123456789')).toBe(true) 45 | expect(encoding.isHex('abcdef0123456789')).toBe(true) 46 | // invalid 47 | expect(encoding.isHex('00xabcdef0123456789')).toBe(false) 48 | expect(encoding.isHex('0xzabcdef0123456789')).toBe(false) 49 | expect(encoding.isHex('xabcdef0123456789')).toBe(false) 50 | expect(encoding.isHex('g')).toBe(false) 51 | expect(encoding.isHex('')).toBe(false) 52 | }) 53 | test('Decode hex', (): void => { 54 | // valid 55 | expect(encoding.decodeHex('123abc').toHex()).toBe('123abc') 56 | expect(encoding.decodeHex('7a1b2c3d').toHex()).toBe('7a1b2c3d') 57 | expect(encoding.decodeHex('fedcba9876543210').toHex()).toBe('fedcba9876543210') 58 | expect(encoding.decodeHex('123456789abcdef0').toHex()).toBe('123456789abcdef0') 59 | expect(encoding.decodeHex('abcdef').toHex()).toBe('abcdef') 60 | expect(encoding.decodeHex('deadbeef').toHex()).toBe('deadbeef') 61 | expect(encoding.decodeHex('01').toHex()).toBe('01') 62 | expect(encoding.decodeHex('00').toHex()).toBe('00') 63 | }) 64 | test('Encode checksum hex', (): void => { 65 | // valid 66 | expect(encoding.encodeCHex(JuneoBuffer.fromString('123abc', 'hex'))).toBe('0x123abcbcee49f6') 67 | expect(encoding.encodeCHex(JuneoBuffer.fromString('7a1b2c3d', 'hex'))).toBe('0x7a1b2c3d88ba9d4c') 68 | expect(encoding.encodeCHex(JuneoBuffer.fromString('fedcba9876543210', 'hex'))).toBe('0xfedcba98765432100fa7e28e') 69 | expect(encoding.encodeCHex(JuneoBuffer.fromString('123456789abcdef0', 'hex'))).toBe('0x123456789abcdef0668592d1') 70 | expect(encoding.encodeCHex(JuneoBuffer.fromString('abcdef', 'hex'))).toBe('0xabcdefd21609a7') 71 | expect(encoding.encodeCHex(JuneoBuffer.fromString('deadbeef', 'hex'))).toBe('0xdeadbeefaa813953') 72 | expect(encoding.encodeCHex(JuneoBuffer.fromString('01', 'hex'))).toBe('0x017785459a') 73 | expect(encoding.encodeCHex(JuneoBuffer.fromString('00', 'hex'))).toBe('0x0017afa01d') 74 | }) 75 | test('Decode checksum hex', (): void => { 76 | // valid 77 | expect(encoding.decodeCHex('123abcbcee49f6').toHex()).toBe('123abc') 78 | expect(encoding.decodeCHex('7a1b2c3d88ba9d4c').toHex()).toBe('7a1b2c3d') 79 | expect(encoding.decodeCHex('fedcba98765432100fa7e28e').toHex()).toBe('fedcba9876543210') 80 | expect(encoding.decodeCHex('123456789abcdef0668592d1').toHex()).toBe('123456789abcdef0') 81 | expect(encoding.decodeCHex('abcdefd21609a7').toHex()).toBe('abcdef') 82 | expect(encoding.decodeCHex('deadbeefaa813953').toHex()).toBe('deadbeef') 83 | expect(encoding.decodeCHex('017785459a').toHex()).toBe('01') 84 | expect(encoding.decodeCHex('0017afa01d').toHex()).toBe('00') 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /tests/unit/wallet/wallet.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MCNWallet, 3 | MainNetwork, 4 | SocotraJUNEChain, 5 | SocotraJVMChain, 6 | SocotraNetwork, 7 | SocotraPlatformChain, 8 | WalletError, 9 | validatePrivateKey 10 | } from '../../../src' 11 | 12 | describe('MCNWallet', (): void => { 13 | describe('Generate', (): void => { 14 | describe('Valid words count', (): void => { 15 | test.each([[12], [15], [18], [21], [24]])('%i words', (wordsCount) => { 16 | const wallet = new MCNWallet(SocotraNetwork.hrp, wordsCount) 17 | expect(wallet.mnemonic).toBeDefined() 18 | expect(wallet.mnemonic!.split(' ').length).toBe(wordsCount) 19 | }) 20 | }) 21 | 22 | describe('Invalid words count', (): void => { 23 | test.each([[0], [11], [13], [25]])('%i words', (wordsCount) => { 24 | expect(() => { 25 | const wallet = new MCNWallet(SocotraNetwork.hrp, wordsCount) 26 | wallet.setHrp(MainNetwork.hrp) 27 | }).toThrow(WalletError) 28 | }) 29 | }) 30 | }) 31 | 32 | describe('Recover', (): void => { 33 | describe('Valid mnemonic', (): void => { 34 | test.each([['energy correct expire mistake find pair tuna blouse album pig become help']])('%s', (words) => { 35 | const wallet = new MCNWallet(SocotraNetwork.hrp, words) 36 | expect(wallet.mnemonic).toBeDefined() 37 | expect(wallet.mnemonic).toBe(words) 38 | }) 39 | }) 40 | 41 | describe('Invalid mnemonic', (): void => { 42 | test.each([['lounge flush donate journey throw harvest morning brut few juice red rare']])('%s', (words) => { 43 | expect(() => { 44 | const wallet = new MCNWallet(SocotraNetwork.hrp, words) 45 | wallet.setHrp(MainNetwork.hrp) 46 | }).toThrow(WalletError) 47 | }) 48 | }) 49 | }) 50 | 51 | // private key validation should be moved to utils testing 52 | test.each([['06b5fcd14cae2211e884a0914b6f81c0458a90aefae8cf317bf09e9cd057164b']])( 53 | 'Validate hex private key', 54 | (privateKey) => { 55 | expect(validatePrivateKey(privateKey)).toBe(true) 56 | } 57 | ) 58 | 59 | test.failing.each([['06b5fcd14cae2211e884a0914b6f81c0458a90efae8cf317bf09e9cd057164c']])( 60 | 'Validate invalid hex private key', 61 | (privateKey) => { 62 | expect(validatePrivateKey(privateKey)).toBe(true) 63 | } 64 | ) 65 | 66 | describe('getAddress', () => { 67 | test.each([ 68 | { 69 | mnemonic: 'install melt spy tiny dose spot close van oven sibling misery symptom', 70 | chain: SocotraJVMChain, 71 | address: 'JVM-socotra17p4punu4589yqfzgv044tl546dnwvf2pd2k6j4' 72 | }, 73 | { 74 | mnemonic: 'install melt spy tiny dose spot close van oven sibling misery symptom', 75 | chain: SocotraJUNEChain, 76 | address: '0xf44b80bf950058b087F47d88fDB71686c4beFef8' 77 | } 78 | ])('$chain.name address: $address', ({ mnemonic, chain, address }): void => { 79 | const wallet: MCNWallet = new MCNWallet(SocotraNetwork.hrp, mnemonic) 80 | expect(wallet.getAddress(chain)).toBe(address) 81 | }) 82 | }) 83 | 84 | describe('getWallet', () => { 85 | test.each([ 86 | { 87 | mnemonic: 'install melt spy tiny dose spot close van oven sibling misery symptom', 88 | chain: SocotraJVMChain 89 | }, 90 | { 91 | mnemonic: 'install melt spy tiny dose spot close van oven sibling misery symptom', 92 | chain: SocotraJUNEChain 93 | } 94 | ])('$chain.name wallet from: $mnemonic', ({ mnemonic, chain }): void => { 95 | const wallet: MCNWallet = new MCNWallet(SocotraNetwork.hrp, mnemonic) 96 | expect(wallet.getWallet(chain)).toBeDefined() 97 | }) 98 | }) 99 | 100 | test('getWallets', () => { 101 | const wallet = new MCNWallet(SocotraNetwork.hrp, 12) 102 | wallet.getAddress(SocotraJUNEChain) 103 | wallet.getAddress(SocotraPlatformChain) 104 | const wallets = wallet.getWallets() 105 | expect(wallets.length).toBe(2) 106 | }) 107 | }) 108 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./_dist/", 4 | "target": "ES2022", 5 | "module": "commonjs", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "lib": ["ES5", "ES2022"] 14 | }, 15 | "exclude": ["node_modules/"], 16 | "include": ["src/", "tests/"] 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "target": "ES2022", 5 | "module": "commonjs", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "lib": ["ES5", "ES2022"] 14 | }, 15 | "exclude": ["node_modules/", "tests/"], 16 | "include": ["src/"] 17 | } 18 | --------------------------------------------------------------------------------