diff --git a/.github/workflows/cla.yaml b/.github/workflows/cla.yaml new file mode 100644 index 00000000..ab8973f9 --- /dev/null +++ b/.github/workflows/cla.yaml @@ -0,0 +1,52 @@ +# This workflow automates the process of signing our CLA. It makes use of +# the action at https://github.com/contributor-assistant/github-action in +# order to provide automations. +# +# This workflow file should be present in every repository that wants to +# use the Contributor License Agreement automation process. Ideally, it +# would remain more-or-less synchronized across each repository as updates +# are rolled out. +# +# Since the database of signatories is maintained in a remote repository, +# each repository that wishes to make use of the CLA Assistant will also +# need to have a repository secret (named `CLA_ASSISTANT_LITE_PAT`) that +# grants permission to write to the "signatures" file in that repository. +--- +name: "CLA Assistant" +on: + issue_comment: + types: + - created + pull_request_target: + types: + - opened + - closed + - synchronize + +# Explicitly configure permissions, in case the GITHUB_TOKEN workflow permissions +# are set to read-only in the repository's settings. +permissions: + actions: write + contents: read # We only need to `read` since signatures are in a remote repo. + pull-requests: write + statuses: write + +jobs: + CLAAssistant: + runs-on: ubuntu-latest + steps: + - name: "CLA Assistant" + if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' + uses: entropyxyz/contributor-assistant-github-action@c5f4628ffe1edb97724edb64e0dd4795394d33e5 # exemptRepoOrgMembers + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Required, so that the bot in this repository has `write` permissions to Contents of remote repo. + PERSONAL_ACCESS_TOKEN: ${{ secrets.CLA_ASSISTANT_LITE_PAT }} + with: + path-to-signatures: 'legal/cla/v1/signatures.json' + path-to-document: 'https://github.com/entropyxyz/.github/blob/main/legal/cla/v1/cla.md' + branch: 'main' + allowlist: dependabot[bot] + exemptRepoOrgMembers: true + remote-organization-name: entropyxyz + remote-repository-name: .github diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 532170d8..11406d18 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -28,6 +28,6 @@ jobs: run: yarn run build - name: Add TSS server host mappings run: | - echo "127.0.0.1 alice-tss-server bob-tss-server" | sudo tee -a /etc/hosts + echo "127.0.0.1 alice-tss-server bob-tss-server charlie-tss-server dave-tss-server" | sudo tee -a /etc/hosts - name: Test run: yarn run test diff --git a/CHANGELOG.md b/CHANGELOG.md index cbf423ea..4db65ed9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,24 +10,126 @@ The format extends [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). Version header format: `[version] Name - year-month-day (entropy-core compatibility: version [range])` +## [0.0.4] Carnage - 2024-10-23 (entropy-core compatibility: 0.3.0) + +### Added + +- programmatic CLI commands + - new: `entropy account create` + - new: `entropy account import` + - new: `entropy account list` + - new: `entropy account register` + - new: `entropy program deploy` + - new: option to display cli and core version + +- TUI + - new: added faucet to main menu for TUI + - updated faucet to use loading spinner to indicate to user the progress of the transfer + - new: menu item to trigger a jumpstart to the network (needs to be run once for fresh test networks) + +- documentation + - updated: `./README.md` + - new: `./src/README.md` - an guide to the source of the project + - new: `./src/_template/*` - an example "domain" with lots of notes + +- tests + - new: `./tests/account.test.ts` - tests for `./src/account/` + - updated: `./tests/balance.test.ts` - tests for `./src/balance/` + - new: `./tests/common.test.ts` - tests for `./src/common/` + - updated: `./tests/config.test.ts` - tests for `./src/config/` + - new: `./tests/e2e.cli.sh` - a shell script which is an early test for programmatic usage + + - new: `./tests/faucet.test.ts` - tests `./src/faucet/` + - new: `./tests/global.test.ts` - + - new: `./tests/program.test.ts` - tests for `./src/program/` + +- programs + - new: `./tests/programs/faucet_program.wasm` - the faucet program! + +- packages + - new: `yocto-spinner` for adding loading spinners to the cli + - some minor package updates + +- github actions + - new: CLA action + +### Changed + +- updated SDK version to v0.3.0 (entropy-core 0.3.0) + - updated us to use `four-nodes` docker setup +- logger to handle nested contexts for better organization of logs +- update: `./src/common/utils.ts` - removed isValidSubstrateAddress and imported the method in from the sdk +- file restructure: + - removed: `src/flows/*` + - added + - `./src/common/entropy-base.ts` - base abstract class for all our domains `main.js` files + - `./src/_template` - docs explaining the new file structure pattern + - `./src/account` - new file structure for our CLI/TUI flows + - NOTE: this contains register flow + - `./src/balance` - new file structure for our CLI/TUI flows + - `./src/faucet` - new file structure for our CLI/TUI flows + - `./src/program` - new file structure for our CLI/TUI flows + - NOTE: this merges user-program + dev-program domains into a single domain + - `./src/sign` - new file structure for our CLI/TUI flows + - `./src/transfer` - new file structure for our CLI/TUI flows +- folder name for user programs to match the kebab-case style for folder namespace +- ascii art print out now shows up to date core version based, coming from SDK + +### Broke + +- network now uses `four-nodes` docker setup + - requires an update to `/etc/hosts` for local testing, should include line: + ``` + 127.0.0.1 alice-tss-server bob-tss-server charlie-tss-server dave-tss-server + ``` +- for programmatic CLI + - change account listing: + - old: `entropy list` + - new: `entropy account list [options]` + - changed transfer: + - old: `entropy transfer [options] ` + - new: `entropy transfer [options] ` + - changed env: `ENDPOINT` => `ENTROPY_ENDPOINT` + +- for TUI + - "endpoint" configuration has changed + - see `entropy --help` + - change flag: `--endpoint` => `--tui-endpiont` + - change env: `ENTROPY_ENDPOINT` => `ENTROPY_TUI_ENDPOINT` + - This is because of [collisions we were seeing](https://github.com/entropyxyz/cli/issues/265) with `commander` flags. + - Does not effect programmatic CLI usage + - We may revert this in a future release. + - deploying programs now requires + - `*.wasm` file for `bytecode` + - `*.json` file path for `configurationSchema` + - `*.json` file path for `auxillaryDataSchema` + ## [0.0.3] Blade - 2024-07-17 (entropy-core compatibility: 0.2.0) ### Fixed + - HOT-FIX programmatic balance error [183](https://github.com/entropyxyz/cli/pull/183) ## [0.0.2] AntMan - 2024-07-12 (entropy-core compatibility: 0.2.0) ### Added -- new: './src/flows/balance/balance.ts' - service file separated out of main flow containing the pure functions to perform balance requests for one or multiple addresses -- new: './tests/balance.test.ts' - new unit tests file for balance pure functions -- new: './src/common/logger.ts' - utility file consisting of the logger used throughout the entropy cli -- new: './src/common/masking.ts' - utility helper file for EntropyLogger, used to mask private data in the payload (message) of the logging method + +- new: `./src/flows/balance/balance.ts` - service file separated out of main flow containing the pure functions to perform balance requests for one or multiple addresses +- new: `./tests/balance.test.ts` - new unit tests file for balance pure functions +- new: `./src/common/logger.ts` - utility file consisting of the logger used throughout the entropy cli +- new: `./src/common/masking.ts` - utility helper file for EntropyLogger, used to mask private data in the payload (message) of the logging method + ### Fixed + - keyring retrieval method was incorrectly returning the default keyring when no keyring was found, which is not the intended flow + ### Changed + - conditional when initializing entropy object to only error if no seed AND admin account is not found in the account data, new unit test caught bug with using OR condition + ### Broke ### Meta/Dev + - new: `./dev/README.md` - `./.github`: their is now a check list you should fill out for creating a PR diff --git a/README.md b/README.md index 2ff79b1a..dbec39cb 100644 --- a/README.md +++ b/README.md @@ -5,137 +5,250 @@ A straightforward command-line interface (CLI) tool to showcase how to perform b > :warning: This tool is in early development. As such, a lot of things do not work. Feel free to play around with it and report any issues at [github.com/entropyxyz/cli](https://github.com/entropyxyz/cli). - [Install](#install) -- [Basic usage](#basic-usage) +- [Usage](#usage) + - [Text-based user interface](#text-based-user-interface) + - [Programmatic mode](#programmatic-mode) +- [Available functions](#available-functions) +- [Development contributions](#development-contributions) - [Support](#support) -- [Contributions](#contributions) - [License](#license) ## Install ```bash -npm install -g @entropyxyz/cli +npm install --global @entropyxyz/cli ``` ## Usage -### Interactive mode +There are two ways to interact with this CLI: + +- [Using the text-based user interface (TUI)](#text-based-user-interface). +- [Programmatically from the command line](#programmatic-use). + +### Text-based user interface + +You can use this tool interactively by calling the `entropy` executable without any arguments or options: ```bash entropy ``` This command will bring you to the main menu: - ```output - ? Select Action (Use arrow keys) - > Manage Accounts - Balance - Register - Sign - Transfer - Deploy Program - User Programs - Exit - ``` + +```output +? Select Action (Use arrow keys) +> Manage Accounts +Balance +Register +Sign +Transfer +Deploy Program +User Programs +Exit +``` + +To exit this text-based user interface anytime, press `CTRL` + `c`. ### Programmatic mode +You can interact with the Entropy network using the CLI's programmatic mode. Simply call the `entropy` executable followed by the command you wish to use: + ```bash entropy balance 5GYvMHuB8J4mpJFCJ7scdR8AXGbT69B2bAqbNxPEa9ZSgEJm ``` -See help on programmatic usage: +For more information on the commands available, see the [CLI documentation](https://docs.entropy.xyz/reference/cli/). + +#### Help + +Use the `help` command to get information on what commands are available and what specific options or arguments they require: + +**General help**: + ```bash -entropy --help # all commands -entropy balance --help # a specific command +entropy --help +``` + +```output +Usage: entropy [options] [command] + +CLI interface for interacting with entropy.xyz. Running this binary without any commands or arguments starts a text-based +interface. + +Options: + -et, --tui-endpoint Runs entropy with the given endpoint and ignores network endpoints in config. + Can also be given a stored endpoint name from config eg: `entropy --endpoint + test-net`. (default: "wss://testnet.entropy.xyz/", env: ENTROPY_TUI_ENDPOINT) + -h, --help Display help for command + -v, --version Display current cli version + -cv, --core-version Display current core protocol version + +Commands: + balance [options] > Command to retrieive the balance of an account on the Entropy Network + account Commands to work with accounts on the Entropy Network + transfer [options] Transfer funds between two Entropy accounts. + sign [options] Sign a message using the Entropy network. Output is a JSON { verifyingKey, + signature } + program Commands for working with programs deployed to the Entropy Network +``` + +**Command-specific help**: + +```shell +entropy balance --help ``` +```output +Usage: entropy balance [options] + +Command to retrieive the balance of an account on the Entropy Network + +Arguments: + account The address an account address whose balance you want to query. Can also be the human-readable name of + one of your accounts + +Options: + -e, --endpoint Runs entropy with the given endpoint and ignores network endpoints in config. Can also be given a + stored endpoint name from config eg: `entropy --endpoint test-net`. (default: + "wss://testnet.entropy.xyz/", env: ENTROPY_ENDPOINT) + -h, --help display help for command +``` ### Available functions -| Function | Description | -| -------- | ----------- | -| Manage accounts | Create, import, and list your locally stored Entropy accounts. | -| Balance | Show the balance of any locally stored accounts. | -| Register | Register a locally stored account with the Entropy network. | -| Sign | Get a signature from the Entropy network using a locally stored, registered account. | -| Transfer | Transfer funds from a locally stored account to any other address. | -| Deploy Program | Deploy a program to the Entropy network using a locally stored account. | -| User Programs | List the programs stored on the Entropy network by any locally stored accounts. | +| Function | Description | +| --------------- | ------------------------------------------------------------------------------------ | +| Manage accounts | Create, import, and list your locally stored Entropy accounts. | +| Balance | Show the balance of any locally stored accounts. | +| Register | Register a locally stored account with the Entropy network. | +| Sign | Get a signature from the Entropy network using a locally stored, registered account. | +| Transfer | Transfer funds from a locally stored account to any other address. | +| Deploy Program | Deploy a program to the Entropy network using a locally stored account. | +| User Programs | List the programs stored on the Entropy network by any locally stored accounts. | -For more instructions on using the CLI, check out [docs.entropy.xyz/reference/cli](https://docs.entropy.xyz/reference/cli). +For more CLI instructions, check out [docs.entropy.xyz/reference/cli](https://docs.entropy.xyz/reference/cli). -## Support +## Development contributions -Need help with something? [Head over to the Entropy Community repository for support or to raise a ticket →](https://github.com/entropyxyz/community#support) +All changes to this repo should be based off the `dev` branch. The `main` branch should not be directly edited, unless a hotfix is necessary. All PRs should follow this workflow: -## License +```plaintext +feature_branch -> dev -> main +``` -This project is licensed under [GNU Affero General Public License v3.0](./LICENSE). +If you want to make changes to this CLI tool, you should install it by following these steps: -## Development +1. Ensure you have Node.js version 20.9.0 or above and Yarn version 1.22.22 installed: -
- - Development install - + ```shell + node --version && yarn --version + ``` -1. Install Node + yarn 1.22.x + ```output + v22.2.0 + 1.22.22 + ``` - - we recommend installing Node with e.g. [NVM](https://github.com/nvm-sh/nvm) - - enable yarn by running `corepack enable` +1. Clone the Entropy CLI repository and move into the new `cli` directory: -1. Grab this repository and move into the new directory: + ```shell + git clone https://github.com/entropyxyz/cli + cd cli + ``` - ```bash - git clone https://github.com/entropyxyz/cli - cd cli - ``` +1. Use Yarn to install the dependencies and build the project. -1. Build the CLI with Yarn: + ```shell + yarn + ``` - ```bash - yarn - ``` + This command pulls in the necessary packages and builds the CLI locally. -1. Start the CLI: +1. Run the CLI using `yarn`: -For an interactive text user interface: + ```shell + yarn start + ``` -```bash -yarn start -``` +1. Start the CLI: -You should now see the main menu: - ```output - ? Select Action (Use arrow keys) - > Manage Accounts - Balance - Register - Sign - Transfer - Deploy Program - User Programs - Exit - ``` - -For programmatic use, see: -```bash -yarn start --help -``` + For an interactive text user interface: + + ```bash + yarn start + ``` + + You should now see the main menu: + + ```output + ? Select Action (Use arrow keys) + ❯ Manage Accounts + Entropy Faucet + Balance + Register + Sign + Transfer + Deploy Program + User Programs + Exit + ``` + + For programmatic use, see: + + ```bash + yarn start --help + ``` + + ```output + yarn run v1.22.1 + $ yarn build:global && entropy --help + $ tsup && npm install -g + CLI Building entry: src/cli.ts + CLI Using tsconfig: tsconfig.json + CLI tsup v6.7.0 + CLI Using tsup config: /home/mixmix/projects/ENTROPY/cli/tsup.config.ts + CLI Target: es2022 + CLI Cleaning output folder + ESM Build start + ESM dist/cli.js 576.07 KB + ESM ⚡️ Build success in 38ms + DTS Build start + DTS ⚡️ Build success in 985ms + DTS dist/cli.d.ts 21.00 B + + up to date in 234ms + Usage: entropy [options] [command] + + CLI interface for interacting with entropy.xyz. Running this binary without any commands or arguments starts a text-based + interface. + + Options: + -et, --tui-endpoint Runs entropy with the given endpoint and ignores network endpoints in config. + Can also be given a stored endpoint name from config eg: `entropy --endpoint + test-net`. (default: "wss://testnet.entropy.xyz/", env: ENTROPY_TUI_ENDPOINT) + -h, --help display help for command + + Commands: + balance [options] > Command to retrieive the balance of an account on the Entropy Network + account Commands to work with accounts on the Entropy Network + transfer [options] Transfer funds between two Entropy accounts. + sign [options] Sign a message using the Entropy network. Output is a JSON { verifyingKey, + signature } + program Commands for working with programs deployed to the Entropy Network + Done in 2.06s. + ``` + + You can see more detail on specific commands using `--help` e.g. + + ```bash + + ``` -
-
- - Global install - +## Support -```bash -npm install -g -``` -This will register the `entropy` bin script globally so that you can run +Need help with something? [Head over to the Entropy Community repository for support or to raise a ticket →](https://github.com/entropyxyz/community#support) -```bash -entropy --help -``` +## License -
+This project is licensed under [GNU Affero General Public License v3.0](./LICENSE). diff --git a/dev/README.md b/dev/README.md index ba633b34..faf6ed4c 100644 --- a/dev/README.md +++ b/dev/README.md @@ -2,7 +2,14 @@ ## Tests -DONT HAVE ANY YET LETS MAKE SOME! +For the tests to run you **must** edit your `/etc/hosts` file, adding: + +``` +127.0.0.1 alice-tss-server +127.0.0.1 bob-tss-server +127.0.0.1 dave-tss-server +127.0.0.1 charlie-tss-server +``` ## Linting @@ -44,3 +51,14 @@ git push origin main --tags ``` go create a release on github if possible. + + +## Deploying new faucet + +#### Requirements +- faucet program + - build from repo or use binary in tests/programs/faucet_program.wasm +- configuration and aux data schema +- program mod account with funds to deploy +- child funded accounts to be used as issuers of funds for faucet + - child accounts must be registered with deployed faucet program diff --git a/dev/bin/build.sh b/dev/bin/build.sh new file mode 100755 index 00000000..3a16ebfe --- /dev/null +++ b/dev/bin/build.sh @@ -0,0 +1,4 @@ +#! /usr/bin/env bash + +source ./node_modules/@entropyxyz/sdk/dev/bin/ENTROPY_CORE_VERSION.sh +tsup --env.ENTROPY_CORE_VERSION $ENTROPY_CORE_VERSION \ No newline at end of file diff --git a/dev/bin/test-hosts.sh b/dev/bin/test-hosts.sh new file mode 100755 index 00000000..fb3b07c9 --- /dev/null +++ b/dev/bin/test-hosts.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# For local tests to work, we need to have aliases set up to point to 127.0.0.1 +# see: dev/README.md + +ALIASES=( + "alice-tss-server" + "bob-tss-server" + "charlie-tss-server" + "dave-tss-server" +) +EXPECTED_IP="127.0.0.1" # IP for localhost +ERROR=0 + +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +printf " + \e[4m/etc/hosts\e[0m + +" + +# Check each alias +for ALIAS in "${ALIASES[@]}"; do + resolved_ip=$(getent hosts "$ALIAS" | awk '{ print $1 }') + + if [ "$resolved_ip" == "$EXPECTED_IP" ]; then + printf " ${GREEN}✓${NC} ${ALIAS}\n" + else + printf " ${RED}✗ ${ALIAS}${NC} is NOT aliased to localhost.\n" + ERROR=1 + fi +done + +echo "" + +# Exit with an error code if any alias is incorrect +if [ $ERROR -ne 0 ]; then + exit 1 +fi diff --git a/package.json b/package.json index 55bf10ac..754de9e2 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,16 @@ { "name": "@entropyxyz/cli", - "version": "0.0.3", + "version": "0.0.4", "description": "cli and tui for interacting with the entropy protocol", "type": "module", "scripts": { - "start": "yarn build && npm install -g && entropy", + "start": "yarn build:global && entropy", "start:debug": "DEBUG=@entropyxyz/cli yarn start", - "build": "tsup", - "test": "yarn test:types && yarn test:ts && yarn test:only", + "build": "./dev/bin/build.sh", + "build:global": "yarn build && npm install -g", "lint": "eslint . --ext .ts --fix", + "test": "yarn test:types && yarn build:global && yarn test:hosts && yarn test:ts && yarn test:only", + "test:hosts": "./dev/bin/test-hosts.sh", "test:only": "./dev/bin/test-only.sh", "test:ts": "yarn removedb && ./dev/bin/test-ts.sh", "test:types": "tsc --project tsconfig.json", @@ -19,7 +21,7 @@ "link:sdk": "yarn link @entropyxyz/sdk", "unlink:sdk": "yarn unlink @entropyxyz/sdk", "re-link:sdk": "yarn unlink:sdk && yarn link:sdk", - "removedb": "rm -rf .entropy && docker compose --file node_modules/@entropyxyz/sdk/dev/docker-scripts/two-nodes.yaml down 2> /dev/null" + "removedb": "rm -rf .entropy && docker compose --file node_modules/@entropyxyz/sdk/dev/docker-scripts/four-nodes.yaml down 2> /dev/null" }, "files": [ "dist" @@ -43,20 +45,16 @@ }, "homepage": "https://github.com/entropyxyz/cli#readme", "dependencies": { - "@entropyxyz/sdk": "^0.2.2", - "@polkadot/util": "^12.6.2", - "@types/node": "^20.12.12", + "@entropyxyz/sdk": "0.3.0", "ansi-colors": "^4.1.3", "cli-progress": "^3.12.0", - "commander": "^12.0.0", + "commander": "^12.1.0", "env-paths": "^3.0.0", "inquirer": "8.0.0", - "lodash.clonedeep": "^4.5.0", "mkdirp": "^3.0.1", - "typescript": "^4.8.4", - "viem": "^2.7.8", "winston": "^3.13.0", - "x25519": "^0.1.0" + "x25519": "^0.1.0", + "yocto-spinner": "^0.1.1" }, "devDependencies": { "@swc/core": "^1.4.0", @@ -64,6 +62,7 @@ "@types/cli-progress": "^3", "@types/inquirer": "^9.0.2", "@types/node": "^20.12.12", + "@types/tape": "^5.6.4", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", "eslint": "^8.56.0", diff --git a/src/README.md b/src/README.md new file mode 100644 index 00000000..62416288 --- /dev/null +++ b/src/README.md @@ -0,0 +1,30 @@ +# src/ docs + +- `src/cli.ts` - the entry-point for the application. Where all CLI and TUI + (text user interface) functions are registered. +- `src/tui.ts` - the entry-point for the TUI. + +## Special Folders + +- `src/_template/` - a template and guide for "User Flow" folders +- `src/common/` - helper functions used accross the application +- `src/config/` - utils for entropy config +- `src/types/` - types used across the application + +## "Domain" Folders + +CLI functionality is grouped into "domains". Within these we pool common +resources needed for programmatic CLI and TUI usage (see `src/_template/` for +detail) + +- `src/account/` - account creation, querying, manipulation etc. +- `src/balance/` - account balance querying +- `src/faucet/` - faucet functions for test-net +- `src/program/` - program deploying, querying, manipulation +- `src/sign/` - message signing +- `src/transfer/` - fund transfers + +### Legacy Folders + +- `src/flows` - a collection of functions leftover from an earier version + diff --git a/src/_template/README.md b/src/_template/README.md new file mode 100644 index 00000000..fe6d5c40 --- /dev/null +++ b/src/_template/README.md @@ -0,0 +1,40 @@ +# Domain Template + +This folder described how we structure our "Domain" folders. + +```mermaid +flowchart +direction TB + +entropy-base:::base + +subgraph example[ ] + direction TB + + types + main + command + interaction + + constants:::optional + utils:::optional + + main --> command & interaction +end + +entropy-base --> main + +classDef default fill:#B0B, stroke:none, color:#FFF; +classDef base fill:#FFF, stroke:#000, color:#000; +classDef optional fill:#B9B, stroke:none; +classDef cluster fill:none, stroke:#B0B; +``` +_Diagram showing the required + optional files, and key dependencies._ + + +- `main.ts` - the core functions used by the flow (inherits from `EntropyBase`) +- `command.ts` - the programmatic CLI functions (depends on `main.ts`) +- `interactions.ts` - the TUI (text user interface) functions (depends on `main.ts`) +- `types.ts` - all the types/interfaces used in this flow +- `constants.ts` (optional) - constants used in this flow +- `utils.ts` (optional) - help function used in this flow diff --git a/src/_template/command.ts b/src/_template/command.ts new file mode 100644 index 00000000..76bb67d3 --- /dev/null +++ b/src/_template/command.ts @@ -0,0 +1,84 @@ +import { Command } from "commander" + +import { EntropyDance } from "./main" +import { loadByteCode, loadDanceConfig } from "./utils" +import { accountOption, endpointOption, cliWrite } from "../common/utils-cli" + +/* + This file is responsible for building up the commands related to our domain + + There is a single export, which will be registered in src/cli.ts + This example has sub-commands (e.g. entropy dance learn), though not all do. + + The descriptions written here will be readable when users type e.g. + - entropy --help + - entropy dance --help + - entropy dance learn --help + + + ## References + + https://www.npmjs.com/package/commander + + + ## This example + + We use the made-up domain "dance" so we will have names like + - entropyDanceCommand + - entropyDanceLearn (for command: `entropy dance learn`) + +*/ + +export function entropyDanceCommand () { + return new Command('dance') + .description('Commands to query/ manipulate dances on the Entropy Network') + .addCommand(entropyDanceLearn()) + .addCommand(entropyDanceAdd()) +} + + +function entropyDanceLearn () { + return new Command('learn') + // description + .description('Have the Entropy network learn a new dance function.') + + // arguments + .argument('', 'the path to the bytecode being learnt') + + // options / flags + .addOption(accountOption()) + .addOption(endpointOption()) + + // what is run: + .action(async (byteCodePath, opts) => { + const danceMoveByteCode = await loadByteCode(byteCodePath) + const dance = new EntropyDance(opts.account, opts.endpoint) + + const pointer = await dance.learn(danceMoveByteCode) + + // We write output simply so other programs can parse + consume output + cliWrite(pointer) + + // NOTE: must exit the program! + process.exit(0) + }) +} + +function entropyDanceAdd () { + return new Command('add') + .description('Add a dance to your verifyingKey.') + .argument('', 'verifiying key to add the dance to') + .argument('', 'pointer for the dance bytecode that is already learn') + .argument('[danceConfigPath]', 'path to a config file for your dance') // optional + .addOption(accountOption()) + .addOption(endpointOption()) + .action(async (verifyingKey, dancePionter, danceConfigPath, opts) => { + const danceConfig = await loadDanceConfig(danceConfigPath) + const dance = new EntropyDance(opts.account, opts.endpoint) + + await dance.add(verifyingKey, dancePionter, danceConfig) + + // NOTE: must exit the program! + process.exit(0) + }) +} diff --git a/src/_template/constants.ts b/src/_template/constants.ts new file mode 100644 index 00000000..8fc58435 --- /dev/null +++ b/src/_template/constants.ts @@ -0,0 +1,27 @@ + +// These are for interaction.ts +// - content is grouped by flow (e.g. "add") +// - then questions within that flow +// +// This makes our code easy to import into translation tools in the future +// and keeps the groupings in line with the UI + +export const PROMPT = { + learn: { + byteCodePath: { + name: "byteCodePath", + // NOTE: this is a duplicate of the parent key. A little messy, but works for the moment + message: "please give the path your dance's bytecode" + } + }, + add: { + dancePointer: { + name: "dancePointer", + message: "please give the pointer to your dance" + }, + danceConfigPath: { + name: "danceConfigPath", + message: "please give the path your dance's config (as JSON)" + } + } +} diff --git a/src/_template/interaction.ts b/src/_template/interaction.ts new file mode 100644 index 00000000..ba6ae92f --- /dev/null +++ b/src/_template/interaction.ts @@ -0,0 +1,60 @@ +import inquirer from "inquirer"; +import Entropy from "@entropyxyz/sdk"; + +import { EntropyDance } from './main' +import { loadByteCode, loadDanceConfig } from './utils' +import { print } from "../common/utils" + +import { + learnDanceQuestions, + addDanceQuestions, +} from "./utils" + +export async function entropyDance (entropy: Entropy, endpoint: string) { + const dance = new EntropyDance(entropy, endpoint) + const { action } = await inquirer.prompt([ + { + type: "list", + name: "action", + message: "What would you like to do?", + choices: [ + "Learn a dance", + "Add an existing dance to my account", + "Exit" + ], + }, + ]) + + + switch (action) { + + case 'Learn a dance': { + const { danceMoveByteCodePath } = await inquirer.prompt(learnDanceQuestions) + const danceMoveByteCode = await loadByteCode(danceMoveByteCodePath) + + await dance.learn(danceMoveByteCode) + + print('dance learnt!') + return + } + + case 'Add an existing dance to my account': { + // example code. users should probably select which vk they want to use + const verifyingKey = entropy.keyring.accounts.registration.verifyingKeys[0] + const { dancePointer, danceConfigPath } = await inquirer.prompt(addDanceQuestions) + const danceConfig = await loadDanceConfig(danceConfigPath) + + await dance.add(verifyingKey, dancePointer, danceConfig) + + print('Dance added to account!') + return + } + + case 'exit': { + return 'exit' + } + + default: + throw new Error('DanceError: Unknown interaction action') + } +} diff --git a/src/_template/main.ts b/src/_template/main.ts new file mode 100644 index 00000000..26087aaa --- /dev/null +++ b/src/_template/main.ts @@ -0,0 +1,60 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import Entropy from "@entropyxyz/sdk"; +import { EntropyBase } from "../common/entropy-base"; + +/* + + This file provides core functions consumed by both ./command.ts and ./interaction.ts + + ## Conventions + + 1. unit tested + 2. tight interface + - strict typing + - no this-or-that function signatures + 3. minimal side-effects + - ✓ logging + - ✓ substrate queries/ mutations + - ✗ config mutation + - ✗ printing + + + ## This example + + We use the made-up domain "dance" so we will have names like + - ENTROPY_DANCE + - EntropyDance + - dance = new EntropyDance(entroyp, endpoint) + - tests: tests/dance.test.ts + +*/ + + +// this is for logging output +const FLOW_CONTEXT = 'ENTROPY_DANCE' + +export class EntropyDance extends EntropyBase { + static isDanceMove (danceName: string) { + // stateless function - useful if you do not have/ need an entropy instance + // NOTE: no logging + return Boolean(danceName) + } + + constructor (entropy: Entropy, endpoint: string) { + super({ entropy, endpoint, flowContext: FLOW_CONTEXT }) + } + + async learn (danceMoveByteCode) { + // write code requiring the use of entropy + // + // return this.entropy... + } + + async add (verifyingKey, dancePointer, danceConfig) { + // .. + + // logging + this.logger.debug(`add: ${dancePointer} to ${verifyingKey}`, `${FLOW_CONTEXT}::add_dance`); + } +} diff --git a/src/_template/types.ts b/src/_template/types.ts new file mode 100644 index 00000000..ec164f70 --- /dev/null +++ b/src/_template/types.ts @@ -0,0 +1,6 @@ +// @ts-ignore +/* + +RECORD TYPES HERE + +*/ \ No newline at end of file diff --git a/src/_template/utils.ts b/src/_template/utils.ts new file mode 100644 index 00000000..e32e9f4f --- /dev/null +++ b/src/_template/utils.ts @@ -0,0 +1,35 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import { PROMPT } from "./constants" + +export async function loadByteCode (path) { + // TODO +} + +export async function loadDanceConfig (path) { + // TODO +} + +// For advanced question options (different types, validation, ...): +// - https://www.npmjs.com/package/inquirer + +export const learnDanceQuestions = [ + { + type: 'input', + name: PROMPT.learn.byteCodePath.name, + message: PROMPT.learn.byteCodePath.message + }, +] + +export const addDanceQuestions = [ + { + type: 'input', + name: PROMPT.add.dancePointer.name, + message: PROMPT.add.dancePointer.message + }, + { + type: 'input', + name: PROMPT.add.danceConfigPath.name, + message: PROMPT.add.danceConfigPath.message + }, +] diff --git a/src/account/command.ts b/src/account/command.ts new file mode 100644 index 00000000..87ef635f --- /dev/null +++ b/src/account/command.ts @@ -0,0 +1,122 @@ +import Entropy from "@entropyxyz/sdk" +import { Command, Option } from 'commander' +import { EntropyAccount } from "./main"; +import { selectAndPersistNewAccount, addVerifyingKeyToAccountAndSelect } from "./utils"; +import { ACCOUNTS_CONTENT } from './constants' +import * as config from '../config' +import { accountOption, endpointOption, cliWrite, loadEntropy } from "../common/utils-cli"; + +export function entropyAccountCommand () { + return new Command('account') + .description('Commands to work with accounts on the Entropy Network') + .addCommand(entropyAccountCreate()) + .addCommand(entropyAccountImport()) + .addCommand(entropyAccountList()) + .addCommand(entropyAccountRegister()) + // .addCommand(entropyAccountAlias()) + // IDEA: support aliases for remote accounts (those we don't have seeds for) + // this would make transfers safer/ easier from CLI +} + +function entropyAccountCreate () { + return new Command('create') + .alias('new') + .description('Create a new entropy account from scratch. Output is JSON of form {name, address}') + .argument('', 'A user friendly name for your new account.') + .addOption( + new Option( + '--path', + 'Derivation path' + ).default(ACCOUNTS_CONTENT.path.default) + ) + .action(async (name, opts) => { + const { path } = opts + const newAccount = await EntropyAccount.create({ name, path }) + + await selectAndPersistNewAccount(newAccount) + + cliWrite({ + name: newAccount.name, + address: newAccount.address, + verifyingKeys: [] + }) + process.exit(0) + }) +} + +function entropyAccountImport () { + return new Command('import') + .description('Import an existing entropy account from seed. Output is JSON of form {name, address}') + .argument('', 'A user friendly name for your new account.') + .argument('', 'The seed for the account you are importing') + .addOption( + new Option( + '--path', + 'Derivation path' + ).default(ACCOUNTS_CONTENT.path.default) + ) + .action(async (name, seed, opts) => { + const { path } = opts + const newAccount = await EntropyAccount.import({ name, seed, path }) + + await selectAndPersistNewAccount(newAccount) + + cliWrite({ + name: newAccount.name, + address: newAccount.address, + verifyingKeys: [] + }) + process.exit(0) + }) +} + +function entropyAccountList () { + return new Command('list') + .alias('ls') + .description('List all accounts. Output is JSON of form [{ name, address, verifyingKeys }]') + .action(async () => { + // TODO: test if it's an encrypted account, if no password provided, throw because later on there's no protection from a prompt coming up + const accounts = await config.get() + .then(storedConfig => EntropyAccount.list(storedConfig)) + .catch((err) => { + if (err.message.includes('currently no accounts')) return [] + + throw err + }) + + cliWrite(accounts) + process.exit(0) + }) +} + +/* register */ +function entropyAccountRegister () { + return new Command('register') + .description('Register an entropy account with a program') + .addOption(accountOption()) + .addOption(endpointOption()) + // Removing these options for now until we update the design to accept program configs + // .addOption( + // new Option( + // '-pointer, --pointer', + // 'Program pointer of program to be used for registering' + // ) + // ) + // .addOption( + // new Option( + // '-data, --program-data', + // 'Path to file containing program data in JSON format' + // ) + // ) + .action(async (opts) => { + // NOTE: loadEntropy throws if it can't find opts.account + const entropy: Entropy = await loadEntropy(opts.account, opts.endpoint) + const accountService = new EntropyAccount(entropy, opts.endpoint) + + const verifyingKey = await accountService.register() + await addVerifyingKeyToAccountAndSelect(verifyingKey, opts.account) + + cliWrite(verifyingKey) + process.exit(0) + }) +} diff --git a/src/account/constants.ts b/src/account/constants.ts new file mode 100644 index 00000000..432c58db --- /dev/null +++ b/src/account/constants.ts @@ -0,0 +1,36 @@ +export const FLOW_CONTEXT = 'ENTROPY_ACCOUNT' + +export const ACCOUNTS_CONTENT = { + seed: { + name: 'seed', + message: 'Enter seed:', + invalidSeed: 'Seed provided is not valid' + }, + path: { + name: 'path', + message: 'derivation path:', + default: 'none', + }, + importKey: { + name: 'importKey', + message: 'Would you like to import your own seed?', + default: false + }, + name: { + name: 'name', + default: 'My Key', + }, + selectAccount: { + name: "selectedAccount", + message: "Choose account:", + }, + interactionChoice: { + name: 'interactionChoice', + choices: [ + { name: 'Create/Import Account', value: 'create-import' }, + { name: 'Select Account', value: 'select-account' }, + { name: 'List Accounts', value: 'list-account' }, + { name: 'Exit to Main Menu', value: 'exit' } + ] + } +} diff --git a/src/account/interaction.ts b/src/account/interaction.ts new file mode 100644 index 00000000..b26103dd --- /dev/null +++ b/src/account/interaction.ts @@ -0,0 +1,88 @@ +import inquirer from "inquirer"; +import Entropy from "@entropyxyz/sdk"; + +import { EntropyAccount } from './main' +import { selectAndPersistNewAccount, addVerifyingKeyToAccountAndSelect } from "./utils"; +import { findAccountByAddressOrName, print } from "../common/utils" +import { EntropyConfig } from "../config/types"; +import * as config from "../config"; + +import { + accountManageQuestions, + accountNewQuestions, + accountSelectQuestions +} from "./utils" + +/* + * @returns partialConfigUpdate | "exit" | undefined + */ +export async function entropyAccount (endpoint: string, storedConfig: EntropyConfig) { + const { accounts } = storedConfig + const { interactionChoice } = await inquirer.prompt(accountManageQuestions) + + switch (interactionChoice) { + + case 'create-import': { + const answers = await inquirer.prompt(accountNewQuestions) + const { name, path, importKey } = answers + let { seed } = answers + if (importKey && seed.includes('#debug')) { + // isDebugMode = true + seed = seed.split('#debug')[0] + } + + const newAccount = seed + ? await EntropyAccount.import({ seed, name, path }) + : await EntropyAccount.create({ name, path }) + + await selectAndPersistNewAccount(newAccount) + return + } + + case 'select-account': { + if (!accounts.length) { + console.error('There are currently no accounts available, please create or import a new account using the Manage Accounts feature') + return + } + const { selectedAccount } = await inquirer.prompt(accountSelectQuestions(accounts)) + await config.setSelectedAccount(selectedAccount) + + print('Current selected account is ' + selectedAccount) + return + } + + case 'list-account': { + try { + EntropyAccount.list({ accounts }) + .forEach((account) => print(account)) + } catch (error) { + console.error(error.message.split('AccountsError: ')[1]) + } + return + } + + case 'exit': { + return 'exit' + } + + default: + throw new Error('AccountsError: Unknown interaction action') + } +} + +export async function entropyRegister (entropy: Entropy, endpoint: string, storedConfig: EntropyConfig): Promise> { + const accountService = new EntropyAccount(entropy, endpoint) + + const { accounts, selectedAccount } = storedConfig + const account = findAccountByAddressOrName(accounts, selectedAccount) + if (!account) { + print("No account selected to register") + return + } + + print("Attempting to register the address:", account.address) + const verifyingKey = await accountService.register() + await addVerifyingKeyToAccountAndSelect(verifyingKey, account.address) + + print("Your address", account.address, "has been successfully registered.") +} diff --git a/src/account/main.ts b/src/account/main.ts new file mode 100644 index 00000000..702beeab --- /dev/null +++ b/src/account/main.ts @@ -0,0 +1,105 @@ +import Entropy, { wasmGlobalsReady } from "@entropyxyz/sdk"; +// @ts-expect-error +import Keyring from '@entropyxyz/sdk/keys' +import { randomAsHex } from '@polkadot/util-crypto' + +import { FLOW_CONTEXT } from "./constants"; +import { AccountCreateParams, AccountImportParams, AccountRegisterParams } from "./types"; + +import { EntropyBase } from "../common/entropy-base"; +import { EntropyAccountConfig, EntropyAccountConfigFormatted } from "../config/types"; + +export class EntropyAccount extends EntropyBase { + constructor (entropy: Entropy, endpoint: string) { + super({ entropy, endpoint, flowContext: FLOW_CONTEXT }) + } + + static async create ({ name, path }: AccountCreateParams): Promise { + const seed = randomAsHex(32) + return EntropyAccount.import({ name, seed, path }) + } + + // WARNING: #create depends on #import => be careful modifying this function + static async import ({ name, seed, path }: AccountImportParams ): Promise { + await wasmGlobalsReady() + const keyring = new Keyring({ seed, path, debug: true }) + + const fullAccount = keyring.getAccount() + // TODO: sdk should create account on constructor + const data = fixData(fullAccount) + const maybeEncryptedData = data + // const maybeEncryptedData = password ? passwordFlow.encrypt(data, password) : data + + const { admin } = keyring.getAccount() + delete admin.pair + + return { + name, + address: admin.address, + data: maybeEncryptedData, + } + } + + static list ({ accounts }: { accounts: EntropyAccountConfig[] }): EntropyAccountConfigFormatted[] { + if (!accounts.length) + throw new Error( + 'AccountsError: There are currently no accounts available, please create or import a new account using the Manage Accounts feature' + ) + + return accounts.map((account: EntropyAccountConfig) => ({ + name: account.name, + address: account.address, + verifyingKeys: account?.data?.registration?.verifyingKeys || [] + })) + } + + async register (params?: AccountRegisterParams): Promise { + let programModAddress: string + let programData: any + if (params) { + ({ programModAddress, programData } = params) + } + const registerParams = programModAddress && programData + ? { + programDeployer: programModAddress, + programData + } + : undefined + + this.logger.debug(`registering with params: ${registerParams}`, 'REGISTER') + return this.entropy.register(registerParams) + } +} + +// TODO: there is a bug in SDK that is munting this data +function fixData (data) { + if (data.admin?.pair) { + const { addressRaw, secretKey, publicKey } = data.admin.pair + Object.assign(data.admin.pair, { + addressRaw: objToUint8Array(addressRaw), + secretKey: objToUint8Array(secretKey), + publicKey: objToUint8Array(publicKey) + }) + } + + if (data.registration?.pair) { + const { addressRaw, secretKey, publicKey } = data.registration.pair + Object.assign(data.registration.pair, { + addressRaw: objToUint8Array(addressRaw), + secretKey: objToUint8Array(secretKey), + publicKey: objToUint8Array(publicKey) + }) + } + + return data +} + +function objToUint8Array (input) { + if (input instanceof Uint8Array) return input + + const values: any = Object.entries(input) + .sort((a, b) => Number(a[0]) - Number(b[0])) // sort entries by keys + .map(entry => entry[1]) + + return new Uint8Array(values) +} diff --git a/src/account/types.ts b/src/account/types.ts new file mode 100644 index 00000000..f1d1cf30 --- /dev/null +++ b/src/account/types.ts @@ -0,0 +1,22 @@ +export interface AccountCreateParams { + name: string + path?: string +} + +export interface AccountImportParams { + seed: string + name: string + path?: string +} + +export type AccountListResults = { + name: string + address: string + verifyingKeys: string[] +} + +export interface AccountRegisterParams { + programModAddress?: string + // TODO: Export ProgramInstance type from sdk + programData?: any +} diff --git a/src/account/utils.ts b/src/account/utils.ts new file mode 100644 index 00000000..e0e008db --- /dev/null +++ b/src/account/utils.ts @@ -0,0 +1,95 @@ +import { ACCOUNTS_CONTENT } from './constants'; +import { EntropyAccountConfig } from "../config/types"; +import * as config from "../config"; +import { generateAccountChoices, findAccountByAddressOrName } from '../common/utils'; + +export async function selectAndPersistNewAccount (newAccount: EntropyAccountConfig) { + const storedConfig = await config.get() + const { accounts } = storedConfig + + const isExistingName = accounts.find(account => account.name === newAccount.name) + if (isExistingName) { + throw Error(`An account with name "${newAccount.name}" already exists. Choose a different name`) + } + const isExistingAddress = accounts.find(account => account.address === newAccount.address) + if (isExistingAddress) { + throw Error(`An account with address "${newAccount.address}" already exists.`) + } + + // persist to config, set selectedAccount + accounts.push(newAccount) + await config.set({ + ...storedConfig, + selectedAccount: newAccount.name + }) +} + +export async function addVerifyingKeyToAccountAndSelect (verifyingKey: string, accountNameOrAddress: string) { + const storedConfig = await config.get() + const { accounts } = storedConfig + + const account = findAccountByAddressOrName(accounts, accountNameOrAddress) + if (!account) throw Error(`Unable to persist verifyingKey "${verifyingKey}" to unknown account "${accountNameOrAddress}"`) + + // persist to config, set selectedAccount + account.data.registration.verifyingKeys.push(verifyingKey) + await config.set({ + ...storedConfig, + setSelectedAccount: account.name + }) +} + +function validateSeedInput (seed) { + if (seed.includes('#debug')) return true + if (seed.length === 66 && seed.startsWith('0x')) return true + if (seed.length === 64) return true + return ACCOUNTS_CONTENT.seed.invalidSeed +} + +export const accountImportQuestions = [ + { + type: 'input', + name: ACCOUNTS_CONTENT.seed.name, + message: ACCOUNTS_CONTENT.seed.message, + validate: validateSeedInput, + when: ({ importKey }) => importKey + }, + { + type: 'input', + name: ACCOUNTS_CONTENT.path.name, + message: ACCOUNTS_CONTENT.path.message, + default: ACCOUNTS_CONTENT.path.default, + when: ({ importKey }) => importKey + }, +] + +export const accountNewQuestions = [ + { + type: 'confirm', + name: ACCOUNTS_CONTENT.importKey.name, + message: ACCOUNTS_CONTENT.importKey.message, + default: ACCOUNTS_CONTENT.importKey.default, + }, + ...accountImportQuestions, + { + type: 'input', + name: ACCOUNTS_CONTENT.name.name, + default: ACCOUNTS_CONTENT.name.default, + }, +] + +export const accountSelectQuestions = (accounts: EntropyAccountConfig[]) => [{ + type: 'list', + name: ACCOUNTS_CONTENT.selectAccount.name, + message: ACCOUNTS_CONTENT.selectAccount.message, + choices: generateAccountChoices(accounts) +}] + +export const accountManageQuestions = [ + { + type: 'list', + name: ACCOUNTS_CONTENT.interactionChoice.name, + pageSize: ACCOUNTS_CONTENT.interactionChoice.choices.length, + choices: ACCOUNTS_CONTENT.interactionChoice.choices + } +] diff --git a/src/balance/command.ts b/src/balance/command.ts new file mode 100644 index 00000000..d99ba2e2 --- /dev/null +++ b/src/balance/command.ts @@ -0,0 +1,31 @@ +import { Command } from "commander"; +import Entropy from "@entropyxyz/sdk"; + +import { EntropyBalance } from "./main"; +import { endpointOption, cliWrite, loadEntropy } from "../common/utils-cli"; +import { findAccountByAddressOrName } from "../common/utils"; +import * as config from "../config"; + +export function entropyBalanceCommand () { + const balanceCommand = new Command('balance') + balanceCommand + .description('Command to retrieive the balance of an account on the Entropy Network') + .argument('', [ + 'The address an account address whose balance you want to query.', + 'Can also be the human-readable name of one of your accounts' + ].join(' ')) + .addOption(endpointOption()) + .action(async (account, opts) => { + const entropy: Entropy = await loadEntropy(account, opts.endpoint) + const BalanceService = new EntropyBalance(entropy, opts.endpoint) + + const { accounts } = await config.get() + const address = findAccountByAddressOrName(accounts, account)?.address + + const balance = await BalanceService.getBalance(address) + cliWrite(`${balance.toLocaleString('en-US')} BITS`) + process.exit(0) + }) + + return balanceCommand +} diff --git a/src/balance/interaction.ts b/src/balance/interaction.ts new file mode 100644 index 00000000..334c00c7 --- /dev/null +++ b/src/balance/interaction.ts @@ -0,0 +1,13 @@ +import { findAccountByAddressOrName, print } from "src/common/utils" +import { EntropyBalance } from "./main" + +export async function entropyBalance (entropy, endpoint, storedConfig) { + try { + const balanceService = new EntropyBalance(entropy, endpoint) + const address = findAccountByAddressOrName(storedConfig.accounts, storedConfig.selectedAccount)?.address + const balance = await balanceService.getBalance(address) + print(`Entropy Account [${storedConfig.selectedAccount}] (${address}) has a balance of: ${balance.toLocaleString('en-US')} BITS`) + } catch (error) { + console.error('There was an error retrieving balance', error) + } +} diff --git a/src/balance/main.ts b/src/balance/main.ts new file mode 100644 index 00000000..7f7345f4 --- /dev/null +++ b/src/balance/main.ts @@ -0,0 +1,34 @@ +import Entropy from "@entropyxyz/sdk" +import { EntropyBase } from "../common/entropy-base" +import * as BalanceUtils from "./utils" +import { BalanceInfo } from "./types" + +const FLOW_CONTEXT = 'ENTROPY-BALANCE' +export class EntropyBalance extends EntropyBase { + constructor (entropy: Entropy, endpoint: string) { + super({ entropy, endpoint, flowContext: FLOW_CONTEXT }) + } + + async getBalance (address: string): Promise { + const accountInfo = (await this.entropy.substrate.query.system.account(address)) as any + const balance = parseInt(BalanceUtils.hexToBigInt(accountInfo.data.free).toString()) + + this.logger.log(`Current balance of ${address}: ${balance}`, EntropyBalance.name) + return balance + } + + async getBalances (addresses: string[]): Promise { + const balanceInfo: BalanceInfo = {} + await Promise.all(addresses.map(async address => { + try { + const balance = await this.getBalance(address) + + balanceInfo[address] = { balance } + } catch (error) { + balanceInfo[address] = { error: error.message } + } + })) + + return balanceInfo + } +} diff --git a/src/flows/balance/types.ts b/src/balance/types.ts similarity index 100% rename from src/flows/balance/types.ts rename to src/balance/types.ts diff --git a/src/balance/utils.ts b/src/balance/utils.ts new file mode 100644 index 00000000..2fe9b038 --- /dev/null +++ b/src/balance/utils.ts @@ -0,0 +1 @@ +export const hexToBigInt = (hexString: string) => BigInt(hexString) \ No newline at end of file diff --git a/src/cli.ts b/src/cli.ts index 7c6704fa..6a4e185b 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2,53 +2,25 @@ /* NOTE: calling this file entropy.ts helps commander parse process.argv */ import { Command, Option } from 'commander' -import launchTui from './tui' -import * as config from './config' + import { EntropyTuiOptions } from './types' +import { coreVersion, loadEntropy, tuiEndpointOption, versionOption } from './common/utils-cli' +import * as config from './config' -import { cliGetBalance } from './flows/balance/cli' -import { cliListAccounts } from './flows/manage-accounts/cli' -import { cliEntropyTransfer } from './flows/entropyTransfer/cli' -import { cliSign } from './flows/sign/cli' +import launchTui from './tui' +import { entropyAccountCommand } from './account/command' +import { entropyTransferCommand } from './transfer/command' +import { entropySignCommand } from './sign/command' +import { entropyBalanceCommand } from './balance/command' +import { entropyProgramCommand } from './program/command' +import { print } from './common/utils' const program = new Command() -function endpointOption (){ - return new Option( - '-e, --endpoint ', - [ - 'Runs entropy with the given endpoint and ignores network endpoints in config.', - 'Can also be given a stored endpoint name from config eg: `entropy --endpoint test-net`.' - ].join(' ') - ) - .env('ENDPOINT') - .argParser(aliasOrEndpoint => { - /* see if it's a raw endpoint */ - if (aliasOrEndpoint.match(/^wss?:\/\//)) return aliasOrEndpoint - - /* look up endpoint-alias */ - const storedConfig = config.getSync() - const endpoint = storedConfig.endpoints[aliasOrEndpoint] - if (!endpoint) throw Error('unknown endpoint alias: ' + aliasOrEndpoint) - - return endpoint - }) - .default('ws://testnet.entropy.xyz:9944/') - // NOTE: argParser is only run IF an option is provided, so this cannot be 'test-net' -} - -function passwordOption (description?: string) { - return new Option( - '-p, --password ', - description || 'Password for the account' - ) -} - /* no command */ program .name('entropy') .description('CLI interface for interacting with entropy.xyz. Running this binary without any commands or arguments starts a text-based interface.') - .addOption(endpointOption()) .addOption( new Option( '-d, --dev', @@ -57,65 +29,33 @@ program .env('DEV_MODE') .hideHelp() ) - .action((options: EntropyTuiOptions) => { - launchTui(options) + .addOption(tuiEndpointOption()) + .addOption(versionOption()) + .addOption(coreVersion()) + .addCommand(entropyBalanceCommand()) + .addCommand(entropyAccountCommand()) + .addCommand(entropyTransferCommand()) + .addCommand(entropySignCommand()) + .addCommand(entropyProgramCommand()) + + .action(async (opts: EntropyTuiOptions) => { + const { account, tuiEndpoint, version, coreVersion } = opts + const entropy = account + ? await loadEntropy(account, tuiEndpoint) + : undefined + if (version) { + print(`v${version}`) + process.exit(0) + } else if (coreVersion) { + print(coreVersion) + process.exit(0) + } + // NOTE: on initial startup you have no account + launchTui(entropy, opts) }) - -/* list */ -program.command('list') - .alias('ls') - .description('List all accounts. Output is JSON of form [{ name, address, data }]') - .action(async () => { - // TODO: test if it's an encrypted account, if no password provided, throw because later on there's no protection from a prompt coming up - const accounts = await cliListAccounts() - writeOut(accounts) - process.exit(0) + .hook('preAction', async () => { + // set up config file, run migrations + return config.init() }) -/* balance */ -program.command('balance') - .description('Get the balance of an Entropy account. Output is a number') - .argument('address', 'Account address whose balance you want to query') - .addOption(passwordOption()) - .addOption(endpointOption()) - .action(async (address, opts) => { - const balance = await cliGetBalance({ address, ...opts }) - writeOut(balance) - process.exit(0) - }) - -/* Transfer */ -program.command('transfer') - .description('Transfer funds between two Entropy accounts.') // TODO: name the output - .argument('source', 'Account address funds will be drawn from') - .argument('destination', 'Account address funds will be sent to') - .argument('amount', 'Amount of funds to be moved') - .addOption(passwordOption('Password for the source account (if required)')) - .addOption(endpointOption()) - .action(async (source, destination, amount, opts) => { - await cliEntropyTransfer({ source, destination, amount, ...opts }) - // writeOut(??) // TODO: write the output - process.exit(0) - }) - -/* Sign */ -program.command('sign') - .description('Sign a message using the Entropy network. Output is a signature (string)') - .argument('address', 'Account address to use to sign') - .argument('message', 'Message you would like to sign') - .addOption(passwordOption('Password for the source account (if required)')) - .addOption(endpointOption()) - .action(async (address, message, opts) => { - const signature = await cliSign({ address, message, ...opts }) - writeOut(signature) - process.exit(0) - }) - -function writeOut (result) { - const prettyResult = typeof result === 'object' - ? JSON.stringify(result, null, 2) - : result - process.stdout.write(prettyResult) -} - -program.parse() +program.parseAsync() diff --git a/src/common/ascii.ts b/src/common/ascii.ts index 67ee5f97..8fb6c6b8 100644 --- a/src/common/ascii.ts +++ b/src/common/ascii.ts @@ -17,5 +17,5 @@ export const logo = @@@@@@ TEST @@@@@@ *NET @@@@@@ ENTROPY-CLI - @@@@@@ COREv0.1.0 + @@@@@@ CORE${process.env.ENTROPY_CORE_VERSION.split('-')[1]} ` diff --git a/src/common/constants.ts b/src/common/constants.ts new file mode 100644 index 00000000..a83a61ab --- /dev/null +++ b/src/common/constants.ts @@ -0,0 +1,7 @@ +/* + A "bit" is the smallest indivisible unit of account value we track. + A "token" is the human readable unit of value value + This constant is then "the number of bits that make up 1 token", or said differently + "how many decimal places our token has". +*/ +export const BITS_PER_TOKEN = 1e10 diff --git a/src/common/entropy-base.ts b/src/common/entropy-base.ts new file mode 100644 index 00000000..e24c9479 --- /dev/null +++ b/src/common/entropy-base.ts @@ -0,0 +1,14 @@ +import Entropy from "@entropyxyz/sdk"; +import { EntropyLogger } from "./logger"; + +export abstract class EntropyBase { + protected logger: EntropyLogger + protected entropy: Entropy + protected endpoint: string + + constructor ({ entropy, endpoint, flowContext }: { entropy: Entropy, endpoint: string, flowContext: string }) { + this.logger = new EntropyLogger(flowContext, endpoint) + this.entropy = entropy + this.endpoint = endpoint + } +} diff --git a/src/common/initializeEntropy.ts b/src/common/initializeEntropy.ts index 13c2ffdf..7d8e9c05 100644 --- a/src/common/initializeEntropy.ts +++ b/src/common/initializeEntropy.ts @@ -2,8 +2,6 @@ import Entropy, { wasmGlobalsReady } from "@entropyxyz/sdk" // TODO: fix importing of types from @entropy/sdk/keys // @ts-ignore import Keyring from "@entropyxyz/sdk/keys" -import inquirer from "inquirer" -import { decrypt, encrypt } from "../flows/password" import * as config from "../config" import { EntropyAccountData } from "../config/types" import { EntropyLogger } from "./logger" @@ -27,20 +25,19 @@ export function getKeyring (address?: string) { interface InitializeEntropyOpts { keyMaterial: MaybeKeyMaterial, - password?: string, endpoint: string, configPath?: string // for testing } type MaybeKeyMaterial = EntropyAccountData | string -// WARNING: in programatic cli mode this function should NEVER prompt users, but it will if no password was provided -// This is currently caught earlier in the code -export const initializeEntropy = async ({ keyMaterial, password, endpoint, configPath }: InitializeEntropyOpts): Promise => { +// WARNING: in programatic cli mode this function should NEVER prompt users + +export const initializeEntropy = async ({ keyMaterial, endpoint, configPath }: InitializeEntropyOpts): Promise => { const logger = new EntropyLogger('initializeEntropy', endpoint) try { await wasmGlobalsReady() - const { accountData, password: successfulPassword } = await getAccountDataAndPassword(keyMaterial, password) + const { accountData } = await getAccountData(keyMaterial) // check if there is no admin account and no seed so that we can throw an error if (!accountData.seed && !accountData.admin) { throw new Error("Data format is not recognized as either encrypted or unencrypted") @@ -52,12 +49,9 @@ export const initializeEntropy = async ({ keyMaterial, password, endpoint, confi const store = await config.get(configPath) store.accounts = store.accounts.map((account) => { if (account.address === accountData.admin.address) { - let data = accountData - // @ts-ignore - if (typeof account.data === 'string' ) data = encrypt(accountData, successfulPassword) account = { ...account, - data, + data: accountData, } } return account @@ -75,11 +69,9 @@ export const initializeEntropy = async ({ keyMaterial, password, endpoint, confi const store = await config.get(configPath) store.accounts = store.accounts.map((account) => { if (account.address === store.selectedAccount) { - let data = newAccountData - if (typeof account.data === 'string') data = encrypt(newAccountData, successfulPassword) const newAccount = { ...account, - data, + data: newAccountData, } return newAccount } @@ -104,11 +96,10 @@ export const initializeEntropy = async ({ keyMaterial, password, endpoint, confi const entropy = new Entropy({ keyring: selectedAccount, endpoint }) await entropy.ready - + if (!entropy?.keyring?.accounts?.registration?.seed) { throw new Error("Keys are undefined") } - return entropy } catch (error) { @@ -122,10 +113,9 @@ export const initializeEntropy = async ({ keyMaterial, password, endpoint, confi // NOTE: frankie this was prettier before I had to refactor it for merge conflicts, promise -async function getAccountDataAndPassword (keyMaterial: MaybeKeyMaterial, password?: string): Promise<{ password: string | null, accountData: EntropyAccountData }> { +async function getAccountData (keyMaterial: MaybeKeyMaterial): Promise<{ accountData: EntropyAccountData }> { if (isEntropyAccountData(keyMaterial)) { return { - password: null, accountData: keyMaterial as EntropyAccountData } } @@ -133,54 +123,6 @@ async function getAccountDataAndPassword (keyMaterial: MaybeKeyMaterial, passwor if (typeof keyMaterial !== 'string') { throw new Error("Data format is not recognized as either encrypted or unencrypted") } - - /* Programmatic Mode */ - if (password) { - const decryptedData = decrypt(keyMaterial, password) - if (!isEntropyAccountData(decryptedData)) { - throw new Error("Failed to decrypt keyMaterial or decrypted keyMaterial is invalid") - } - // @ts-ignore TODO: some type work here - return { password, accountData: decryptedData } - } - - /* Interactive Mode */ - let sucessfulPassword: string - let decryptedData - let attempts = 0 - - while (attempts < 3) { - const answers = await inquirer.prompt([ - { - type: 'password', - name: 'password', - message: 'Enter password to decrypt keyMaterial:', - mask: '*', - } - ]) - - try { - decryptedData = decrypt(keyMaterial, answers.password) - //@ts-ignore - if (!isEntropyAccountData(decryptedData)) { - throw new Error("Failed to decrypt keyMaterial or decrypted keyMaterial is invalid") - } - - sucessfulPassword = answers.password - break - } catch (error) { - console.error("Incorrect password. Try again") - attempts++ - if (attempts >= 3) { - throw new Error("Failed to decrypt keyMaterial after 3 attempts.") - } - } - } - - return { - password: sucessfulPassword, - accountData: decryptedData as EntropyAccountData - } } function isEntropyAccountData (maybeAccountData: any) { diff --git a/src/common/logger.ts b/src/common/logger.ts index 436f3b4b..4e699012 100644 --- a/src/common/logger.ts +++ b/src/common/logger.ts @@ -1,6 +1,7 @@ import envPaths from 'env-paths' import { join } from 'path' import * as winston from 'winston' +import { replacer } from './utils' import { maskPayload } from './masking' import { EntropyLoggerOptions } from 'src/types' @@ -38,7 +39,7 @@ export class EntropyLogger { winston.format.splat(), // Uses safe-stable-stringify to finalize full object message as string // (prevents circular references from crashing) - winston.format.json(), + winston.format.json({ replacer }), ); if (isTesting) { @@ -88,27 +89,27 @@ export class EntropyLogger { } // maps to winston:error - public error (description: string, error: Error): void { - this.writeLogMsg('error', error?.message || error, this.context, description, error.stack); + error (description: string, error: Error, context?: string): void { + this.writeLogMsg('error', error?.message || error, context, description, error.stack); } // maps to winston:info - public log (message: any, context?: string): void { + log (message: any, context?: string): void { this.writeLogMsg('info', message, context); } // maps to winston:warn - public warn (message: any, context?: string): void { + warn (message: any, context?: string): void { this.writeLogMsg('warn', message, context); } // maps to winston:debug - public debug (message: any, context?: string): void { + debug (message: any, context?: string): void { this.writeLogMsg('debug', message, context); } // maps to winston:verbose - public verbose (message: any, context?: string): void { + verbose (message: any, context?: string): void { this.writeLogMsg('verbose', message, context); } @@ -116,11 +117,10 @@ export class EntropyLogger { this.winstonLogger.log({ level, message: maskPayload(message), - context: context || this.context, + context: context ? `${this.context}:${context}` : this.context, endpoint: this.endpoint, description, stack, }); } - -} \ No newline at end of file +} diff --git a/src/common/masking.ts b/src/common/masking.ts index 4118ddb8..3ee66295 100644 --- a/src/common/masking.ts +++ b/src/common/masking.ts @@ -1,35 +1,38 @@ -import cloneDeep from 'lodash.clonedeep' - -const DEFAULT_MASKED_FIELDS = [ +const PREFIX = 'data:application/UI8A;base64,' +const DEFAULT_MASKED_FIELDS = new Set([ 'seed', 'secretKey', 'addressRaw', -]; +]); export function maskPayload (payload: any): any { - const clonedPayload = cloneDeep(payload); - const maskedPayload = {} + if ( + typeof payload === 'string' || + typeof payload === 'boolean' || + payload === null + ) return payload - if (!clonedPayload) { - return clonedPayload; - } + const maskedPayload = Array.isArray(payload) + ? [] + : {} // maskJSONFields doesn't handle nested objects very well so we'll // need to recursively walk to object and mask them one by one - for (const [property, value] of Object.entries(clonedPayload)) { - if (value && typeof value === 'object') { - if (Object.keys(clonedPayload[property]).filter(key => isNaN(parseInt(key))).length === 0) { - const reconstructedUintArr: number[] = Object.values(clonedPayload[property]) - maskedPayload[property] = "base64:" + Buffer.from(reconstructedUintArr).toString("base64"); - } else { - maskedPayload[property] = maskPayload(value); - } - } else if (value && typeof value === 'string' && DEFAULT_MASKED_FIELDS.includes(property)) { - maskedPayload[property] = "*".repeat(clonedPayload[property].length) - } else { - maskedPayload[property] = value + return Object.entries(payload).reduce((acc, [property, value]) => { + if (DEFAULT_MASKED_FIELDS.has(property)) { + // @ts-expect-error .length does not exist on type "unknown" + acc[property] = "*".repeat(value?.length || 32) + } + else if (value instanceof Uint8Array) { + acc[property] = PREFIX + Buffer.from(value).toString('base64') + } + else if (typeof value === 'object') { + acc[property] = maskPayload(value); + } + else { + acc[property] = value } - } - return maskedPayload; + return acc + }, maskedPayload) } diff --git a/src/common/progress.ts b/src/common/progress.ts index a2410501..bbdd6564 100644 --- a/src/common/progress.ts +++ b/src/common/progress.ts @@ -11,10 +11,10 @@ export function setupProgress (label: string): { start: () => void; stop: () => }) const start = () => { - // 160 was found through trial and error, don't believe there is a formula to + // 150 was found through trial and error, don't believe there is a formula to // determine the exact time it takes for the transaction to be processed and finalized // TO-DO: Change progress bar to loading animation? - b1.start(160, 0, { + b1.start(150, 0, { speed: "N/A" }) // update values diff --git a/src/common/utils-cli.ts b/src/common/utils-cli.ts new file mode 100644 index 00000000..d00b8b04 --- /dev/null +++ b/src/common/utils-cli.ts @@ -0,0 +1,136 @@ +import Entropy from '@entropyxyz/sdk' +import { Option } from 'commander' +import { findAccountByAddressOrName, stringify } from './utils' +import * as config from '../config' +import { initializeEntropy } from './initializeEntropy' + +const entropyPackage = require('../../package.json') + +export function cliWrite (result) { + const prettyResult = stringify(result, 0) + process.stdout.write(prettyResult) +} + +function getConfigOrNull () { + try { + return config.getSync() + } catch (err) { + if (config.isDangerousReadError(err)) throw err + return null + } +} + +export function versionOption () { + const { version } = entropyPackage + + return new Option( + '-v, --version', + 'Displays the current running version of Entropy CLI' + ).argParser(() => version) +} + +export function coreVersion () { + const coreVersion = process.env.ENTROPY_CORE_VERSION.split('-')[1] + + return new Option( + '-cv, --core-version', + 'Displays the current running version of the Entropy Protocol' + ).argParser(() => coreVersion) +} + +export function endpointOption () { + return new Option( + '-e, --endpoint ', + [ + 'Runs entropy with the given endpoint and ignores network endpoints in config.', + 'Can also be given a stored endpoint name from config eg: `entropy --endpoint test-net`.' + ].join(' ') + ) + .env('ENTROPY_ENDPOINT') + .argParser(aliasOrEndpoint => { + /* see if it's a raw endpoint */ + if (aliasOrEndpoint.match(/^wss?:\/\//)) return aliasOrEndpoint + + /* look up endpoint-alias */ + const storedConfig = getConfigOrNull() + const endpoint = storedConfig?.endpoints?.[aliasOrEndpoint] + if (!endpoint) throw Error('unknown endpoint alias: ' + aliasOrEndpoint) + + return endpoint + }) + .default('wss://testnet.entropy.xyz/') + // NOTE: default cannot be "test-net" as argParser only runs if the -e/--endpoint flag + // or ENTROPY_ENDPOINT env set +} + +export function tuiEndpointOption () { + return new Option( + '-et, --tui-endpoint ', + [ + 'Runs entropy with the given endpoint and ignores network endpoints in config.', + 'Can also be given a stored endpoint name from config eg: `entropy --endpoint test-net`.' + ].join(' ') + ) + .env('ENTROPY_TUI_ENDPOINT') + .argParser(aliasOrEndpoint => { + /* see if it's a raw endpoint */ + if (aliasOrEndpoint.match(/^wss?:\/\//)) return aliasOrEndpoint + + /* look up endpoint-alias */ + const storedConfig = getConfigOrNull() + const endpoint = storedConfig?.endpoints?.[aliasOrEndpoint] + if (!endpoint) throw Error('unknown endpoint alias: ' + aliasOrEndpoint) + + return endpoint + }) + .default('wss://testnet.entropy.xyz/') + // NOTE: default cannot be "test-net" as argParser only runs if the -e/--endpoint flag + // or ENTROPY_ENDPOINT env set +} + +export function accountOption () { + const storedConfig = getConfigOrNull() + + return new Option( + '-a, --account ', + [ + 'Sets the account for the session.', + 'Defaults to the last set account (or the first account if one has not been set before).' + ].join(' ') + ) + .env('ENTROPY_ACCOUNT') + .argParser(addressOrName => { + // We try to map addressOrName to an account we have stored + if (!storedConfig) return addressOrName + + const account = findAccountByAddressOrName(storedConfig.accounts, addressOrName) + if (!account) return addressOrName + + // If we find one, we set this account as the future default + config.setSelectedAccount(account) + // NOTE: argParser cannot be an async function, so we cannot await this call + // WARNING: this will lead to a race-condition if functions are called in quick succession + // and assume the selectedAccount has been persisted + // + // RISK: doesn't seem likely as most of our functions will await at slow other steps.... + // SOLUTION: write a scynchronous version? + + // We finally return the account name to be as consistent as possible (using name, not address) + return account.name + }) + .default(storedConfig?.selectedAccount) +} + +export async function loadEntropy (addressOrName: string, endpoint: string): Promise { + const accounts = getConfigOrNull()?.accounts || [] + const selectedAccount = findAccountByAddressOrName(accounts, addressOrName) + if (!selectedAccount) throw new Error(`No account with name or address: "${addressOrName}"`) + + const entropy = await initializeEntropy({ keyMaterial: selectedAccount.data, endpoint }) + + if (!entropy?.keyring?.accounts?.registration?.pair) { + throw new Error("Signer keypair is undefined or not properly initialized.") + } + + return entropy +} diff --git a/src/common/utils.ts b/src/common/utils.ts index b7e03451..da1b4b3d 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -1,22 +1,28 @@ -import { decodeAddress, encodeAddress } from "@polkadot/keyring" -import { hexToU8a, isHex } from "@polkadot/util" +import { Entropy } from '@entropyxyz/sdk' import { Buffer } from 'buffer' import { EntropyAccountConfig } from "../config/types" +import { EntropyLogger } from './logger' export function stripHexPrefix (str: string): string { if (str.startsWith('0x')) return str.slice(2) return str } +export function stringify (thing, indent = 2) { + return (typeof thing === 'object') + ? JSON.stringify(thing, replacer, indent) + : thing +} + export function replacer (key, value) { - if(value instanceof Uint8Array ){ + if (value instanceof Uint8Array) { return Buffer.from(value).toString('base64') } else return value } export function print (...args) { - console.log(...args) + console.log(...args.map(arg => stringify(arg))) } // hardcoding for now instead of querying chain @@ -48,17 +54,7 @@ export function buf2hex (buffer: ArrayBuffer): string { return Buffer.from(buffer).toString("hex") } -export function isValidSubstrateAddress (address: any) { - try { - encodeAddress(isHex(address) ? hexToU8a(address) : decodeAddress(address)) - - return true - } catch (error) { - return false - } -} - -export function accountChoices (accounts: EntropyAccountConfig[]) { +export function generateAccountChoices (accounts: EntropyAccountConfig[]) { return accounts .map((account) => ({ name: `${account.name} (${account.address})`, @@ -67,10 +63,49 @@ export function accountChoices (accounts: EntropyAccountConfig[]) { } export function accountChoicesWithOther (accounts: EntropyAccountConfig[]) { - return accountChoices(accounts) + return generateAccountChoices(accounts) .concat([{ name: "Other", value: null }]) } -export function getSelectedAccount (accounts: EntropyAccountConfig[], address: string) { - return accounts.find(account => account.address === address) +export function findAccountByAddressOrName (accounts: EntropyAccountConfig[], aliasOrAddress: string) { + if (!aliasOrAddress || !aliasOrAddress.length) throw Error('account name or address required') + + return ( + accounts.find(account => account.address === aliasOrAddress) || + accounts.find(account => account.name === aliasOrAddress) + ) +} + +export function formatDispatchError (entropy: Entropy, dispatchError) { + let msg: string + if (dispatchError.isModule) { + // for module errors, we have the section indexed, lookup + const decoded = entropy.substrate.registry.findMetaError( + dispatchError.asModule + ) + const { docs, name, section } = decoded + + msg = `${section}.${name}: ${docs.join(' ')}` + } else { + // Other, CannotLookup, BadOrigin, no extra info + msg = dispatchError.toString() + } + + return Error(msg) +} + +export async function jumpStartNetwork (entropy, endpoint): Promise { + const logger = new EntropyLogger('JUMPSTART_NETWORK', endpoint) + return new Promise((resolve, reject) => { + entropy.substrate.tx.registry.jumpStartNetwork() + .signAndSend(entropy.keyring.accounts.registration.pair, ({ status, dispatchError }) => { + if (dispatchError) { + const error = formatDispatchError(entropy, dispatchError) + logger.error('There was an issue jump starting the network', error) + return reject(error) + } + + if (status.isFinalized) resolve(status) + }) + }) } diff --git a/src/config/encoding.ts b/src/config/encoding.ts new file mode 100644 index 00000000..11fd1f45 --- /dev/null +++ b/src/config/encoding.ts @@ -0,0 +1,30 @@ +const PREFIX = 'data:application/UI8A;base64,' +// was a UInt8Array, but is stored as base64 + +export function serialize (config: object) { + return JSON.stringify(config, replacer, 2) +} + +export function deserialize (config: string) { + try { + return JSON.parse(config, reviver) + } catch (err) { + console.log('broken config:', config) + throw err + } +} + +function replacer (_key: string, value: any) { + if (value instanceof Uint8Array) { + return PREFIX + Buffer.from(value).toString('base64') + } + else return value +} + +function reviver (key, value) { + if (typeof value === 'string' && value.startsWith(PREFIX)) { + const data = value.slice(PREFIX.length) + return Uint8Array.from(Buffer.from(data, 'base64')) + } + else return value +} diff --git a/src/config/index.ts b/src/config/index.ts index e502b780..a38859a4 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -4,9 +4,9 @@ import { mkdirp } from 'mkdirp' import { join, dirname } from 'path' import envPaths from 'env-paths' - import allMigrations from './migrations' -import { replacer } from 'src/common/utils' +import { serialize, deserialize } from './encoding' +import { EntropyConfig, EntropyAccountConfig } from './types' const paths = envPaths('entropy-cryptography', { suffix: '' }) const CONFIG_PATH = join(paths.config, 'entropy-cli.json') @@ -35,9 +35,10 @@ function hasRunMigration (config: any, version: number) { export async function init (configPath = CONFIG_PATH, oldConfigPath = OLD_CONFIG_PATH) { const currentConfig = await get(configPath) - .catch(async (err) => { - if (err && err.code !== 'ENOENT') throw err + .catch(async (err ) => { + if (isDangerousReadError(err)) throw err + // If there is no current config, try loading the old one const oldConfig = await get(oldConfigPath).catch(noop) // drop errors if (oldConfig) { // move the config @@ -54,19 +55,48 @@ export async function init (configPath = CONFIG_PATH, oldConfigPath = OLD_CONFIG await set(newConfig, configPath) } } -function noop () {} export async function get (configPath = CONFIG_PATH) { - const configBuffer = await readFile(configPath) - return JSON.parse(configBuffer.toString()) + return readFile(configPath, 'utf-8') + .then(deserialize) } export function getSync (configPath = CONFIG_PATH) { - const configBuffer = readFileSync(configPath, 'utf8') - return JSON.parse(configBuffer) + const configStr = readFileSync(configPath, 'utf8') + return deserialize(configStr) } -export async function set (config = {}, configPath = CONFIG_PATH) { +export async function set (config: EntropyConfig, configPath = CONFIG_PATH) { + assertConfigPath(configPath) + await mkdirp(dirname(configPath)) - await writeFile(configPath, JSON.stringify(config, replacer)) + await writeFile(configPath, serialize(config)) +} + +export async function setSelectedAccount (account: EntropyAccountConfig, configPath = CONFIG_PATH) { + const storedConfig = await get(configPath) + + if (storedConfig.selectedAccount === account.name) return storedConfig + // no need for update + + const newConfig = { + ...storedConfig, + selectedAccount: account.name + } + await set(newConfig, configPath) + return newConfig +} + +/* util */ +function noop () {} +function assertConfigPath (configPath: string) { + if (!configPath.endsWith('.json')) { + throw Error(`configPath must be of form *.json, got ${configPath}`) + } +} +export function isDangerousReadError (err: any) { + // file not found: + if (err.code === 'ENOENT') return false + + return true } diff --git a/src/config/migrations/02.ts b/src/config/migrations/02.ts new file mode 100644 index 00000000..c84b7c78 --- /dev/null +++ b/src/config/migrations/02.ts @@ -0,0 +1,43 @@ +export const version = 2 + +const targetKeys = new Set(['secretKey', 'publicKey', 'addressRaw']) + +export function migrate (data = {}) { + if (!isObject(data)) return data + if (isUI8A(data)) return data + + const initial = isArray(data) ? [] : {} + + return Object.entries(data).reduce((acc, [key, value]) => { + if (targetKeys.has(key) && !isUI8A(value)) { + acc[key] = objToUI8A(value) + } + else { + acc[key] = migrate(value) + } + + return acc + }, initial) +} + + +function isObject (thing) { + return typeof thing === 'object' +} + +function isArray (thing) { + return Array.isArray(thing) +} + +function isUI8A (thing) { + return thing instanceof Uint8Array +} + + +function objToUI8A (obj) { + const bytes = Object.keys(obj) + .sort((a, b) => Number(a) > Number(b) ? 1 : -1) + .map(arrayIndex => obj[arrayIndex]) + + return new Uint8Array(bytes) +} diff --git a/src/config/migrations/03.ts b/src/config/migrations/03.ts new file mode 100644 index 00000000..ffc5a2f7 --- /dev/null +++ b/src/config/migrations/03.ts @@ -0,0 +1,18 @@ +export const version = 3 + +export function migrate (data = {}) { + try { + const migratedData = { + ...data, + endpoints: { + // @ts-ignore + ...data.endpoints, + 'stg': 'wss://api.staging.testnet.testnet-2024.infrastructure.entropy.xyz' + } + } + return migratedData + } catch (e) { + console.error(`error in migration ${version}: e.message`) + } + return data +} diff --git a/src/config/migrations/index.ts b/src/config/migrations/index.ts index 5d1f18b7..72a3e975 100644 --- a/src/config/migrations/index.ts +++ b/src/config/migrations/index.ts @@ -1,9 +1,13 @@ import * as migration00 from './00' import * as migration01 from './01' +import * as migration02 from './02' +import * as migration03 from './03' const migrations = [ migration00, migration01, + migration02, + migration03 ] export default migrations diff --git a/src/config/types.ts b/src/config/types.ts index 74d52ffc..26aa6678 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -1,6 +1,11 @@ export interface EntropyConfig { accounts: EntropyAccountConfig[] - endpoints: { dev: string; 'test-net': string } + endpoints: { + dev: string; + 'test-net': string + } + // selectedAccount is account.name (alias) for the account + selectedAccount: string 'migration-version': string } @@ -10,6 +15,14 @@ export interface EntropyAccountConfig { data: EntropyAccountData } +// Safe output format +export interface EntropyAccountConfigFormatted { + name: string + address: string + verifyingKeys: string[] +} + +// TODO: document this whole thing export interface EntropyAccountData { debug?: boolean seed: string @@ -33,4 +46,4 @@ export enum EntropyAccountContextType { registration = 'ADMIN_KEY', deviceKey = 'CONSUMER_KEY', undefined = 'MIXED_KEY', -} \ No newline at end of file +} diff --git a/src/faucet/helpers/signer.ts b/src/faucet/helpers/signer.ts new file mode 100644 index 00000000..dd86e47a --- /dev/null +++ b/src/faucet/helpers/signer.ts @@ -0,0 +1,70 @@ +import Entropy from "@entropyxyz/sdk"; +import type { Signer, SignerResult } from "@polkadot/api/types"; +import { Registry, SignerPayloadJSON } from "@polkadot/types/types"; +import { u8aToHex } from "@polkadot/util"; +import { blake2AsHex, decodeAddress, encodeAddress, signatureVerify } from "@polkadot/util-crypto"; +import { stripHexPrefix } from "../../common/utils"; + +let id = 0 +export default class FaucetSigner implements Signer { + readonly #registry: Registry + readonly #entropy: Entropy + readonly amount: number + readonly chosenVerifyingKey: any + readonly globalTest: any + + constructor ( + registry: Registry, + entropy: Entropy, + amount: number, + chosenVerifyingKey: any, + ) { + this.#registry = registry + this.#entropy = entropy + this.amount = amount + this.chosenVerifyingKey = chosenVerifyingKey + } + + async signPayload (payload: SignerPayloadJSON): Promise { + // toU8a(true) is important as it strips the scale encoding length prefix from the payload + // without it transactions will fail + // ref: https://github.com/polkadot-js/api/issues/4446#issuecomment-1013213962 + const raw = this.#registry.createType('ExtrinsicPayload', payload, { + version: payload.version, + }).toU8a(true); + + const auxData = { + spec_version: 100, + transaction_version: 6, + string_account_id: this.#entropy.keyring.accounts.registration.address, + amount: this.amount + } + + const signature = await this.#entropy.sign({ + sigRequestHash: u8aToHex(raw), + // @ts-ignore + hash: { custom: 0 }, // NOTE: this is the custom hashing algo used for the faucet program. + auxiliaryData: [auxData], + signatureVerifyingKey: this.chosenVerifyingKey + }) + + let sigHex = u8aToHex(signature); + // the 02 prefix is needed for signature type edcsa (00 = ed25519, 01 = sr25519, 02 = ecdsa) + // ref: https://github.com/polkadot-js/tools/issues/175#issuecomment-767496439 + sigHex = `0x02${stripHexPrefix(sigHex)}` + + const hashedKey = blake2AsHex(this.chosenVerifyingKey) + const faucetAddress = encodeAddress(hashedKey) + const publicKey = decodeAddress(faucetAddress); + + const hexPublicKey = u8aToHex(publicKey); + + const signatureValidation = signatureVerify(u8aToHex(raw), sigHex, hexPublicKey) + + if (signatureValidation.isValid) { + return { id: id++, signature: sigHex } + } else { + throw new Error('FaucetSignerError: Signature is not valid') + } + } +} diff --git a/src/faucet/interaction.ts b/src/faucet/interaction.ts new file mode 100644 index 00000000..648f2c04 --- /dev/null +++ b/src/faucet/interaction.ts @@ -0,0 +1,65 @@ +import Entropy from "@entropyxyz/sdk" +import yoctoSpinner from 'yocto-spinner'; +import { EntropyLogger } from '../common/logger' +import { TESTNET_PROGRAM_HASH } from "./utils" +import { EntropyFaucet } from "./main" +import { print } from "src/common/utils" + +let chosenVerifyingKeys = [] +// Sending only 1e10 BITS does not allow user's to register after receiving funds +// there are limits in place to ensure user's are leftover with a certain balance in their accounts +// increasing amount send here, will allow user's to register right away +const amount = "20000000000" +// context for logging file +const FLOW_CONTEXT = 'ENTROPY_FAUCET_INTERACTION' +const SPINNER_TEXT = 'Funding account…' +const faucetSpinner = yoctoSpinner() +export async function entropyFaucet (entropy: Entropy, options, logger: EntropyLogger) { + faucetSpinner.text = SPINNER_TEXT + if (faucetSpinner.isSpinning) { + faucetSpinner.stop() + } + const { endpoint } = options + if (!entropy.registrationManager.signer.pair) { + throw new Error("Keys are undefined") + } + const faucetService = new EntropyFaucet(entropy, endpoint) + const verifyingKeys = await faucetService.getAllFaucetVerifyingKeys() + // @ts-expect-error + return sendMoneyFromRandomFaucet(entropy, options.endpoint, verifyingKeys, logger) +} + +// Method that takes in the initial list of verifying keys (to avoid multiple calls to the rpc) and recursively retries each faucet until +// a successful transfer is made +async function sendMoneyFromRandomFaucet (entropy: Entropy, endpoint: string, verifyingKeys: string[], logger: EntropyLogger) { + if (!faucetSpinner.isSpinning) { + faucetSpinner.start() + } + const faucetService = new EntropyFaucet(entropy, endpoint) + const selectedAccountAddress = entropy.keyring.accounts.registration.address + let chosenVerifyingKey: string + try { + const randomFaucet = faucetService.getRandomFaucet(chosenVerifyingKeys, verifyingKeys) + chosenVerifyingKey = randomFaucet.chosenVerifyingKey + const { faucetAddress } = randomFaucet + await faucetService.sendMoney({ amount, addressToSendTo: selectedAccountAddress, faucetAddress, chosenVerifyingKey, faucetProgramPointer: TESTNET_PROGRAM_HASH }) + // reset chosen keys after successful transfer + if (faucetSpinner.isSpinning) faucetSpinner.stop() + chosenVerifyingKeys = [] + print(`Account: ${selectedAccountAddress} has been successfully funded with ${parseInt(amount).toLocaleString('en-US')} BITS`) + } catch (error) { + logger.error('Error issuing funds through faucet', error, FLOW_CONTEXT) + chosenVerifyingKeys.push(chosenVerifyingKey) + if (error.message.includes('FaucetError')) { + faucetSpinner.text = 'Faucet has failed...' + if (faucetSpinner.isSpinning) { + faucetSpinner.stop() + } + console.error('ERR::', error.message) + return + } else { + // Check for non faucet errors (FaucetError) and retry faucet + await sendMoneyFromRandomFaucet(entropy, endpoint, verifyingKeys, logger) + } + } +} \ No newline at end of file diff --git a/src/faucet/main.ts b/src/faucet/main.ts new file mode 100644 index 00000000..729898d1 --- /dev/null +++ b/src/faucet/main.ts @@ -0,0 +1,98 @@ +import Entropy from "@entropyxyz/sdk"; +import { EntropyBase } from "../common/entropy-base"; +import { blake2AsHex, encodeAddress } from "@polkadot/util-crypto"; +import { FAUCET_PROGRAM_MOD_KEY, TESTNET_PROGRAM_HASH } from "./utils"; +import { EntropyBalance } from "src/balance/main"; +import { EntropyProgram } from "src/program/main"; +import FaucetSigner from "./helpers/signer"; +import { SendMoneyParams } from "./types"; +import { formatDispatchError } from "src/common/utils"; + +const FLOW_CONTEXT = 'ENTROPY-FAUCET' + +function pickRandom (items: string[]): string { + const i = Math.floor(Math.random() * items.length) + return items[i] +} + +export class EntropyFaucet extends EntropyBase { + constructor (entropy: Entropy, endpoint: string) { + super({ entropy, endpoint, flowContext: FLOW_CONTEXT }) + } + + // Method used to sign and send the transfer request (transfer request = call argument) using the custom signer + // created to overwrite how we sign the payload that is sent up chain + async faucetSignAndSend (call: any, amount: number, senderAddress: string, chosenVerifyingKey: any): Promise { + // QUESTION: call has "amount" encoded, but we also take amount in again... why? + const api = this.entropy.substrate + const faucetSigner = new FaucetSigner(api.registry, this.entropy, amount, chosenVerifyingKey) + + const sig = await call.signAsync(senderAddress, { signer: faucetSigner }) + + return new Promise((resolve, reject) => { + sig.send(({ status, dispatchError }: any) => { + // status would still be set, but in the case of error we can shortcut + // to just check it (so an error would indicate InBlock or Finalized) + if (dispatchError) { + const error = formatDispatchError(this.entropy, dispatchError) + return reject(error) + } + if (status.isFinalized) resolve(status) + }) + }) + } + + async getAllFaucetVerifyingKeys (programModKey = FAUCET_PROGRAM_MOD_KEY) { + return this.entropy.substrate.query.registry.modifiableKeys(programModKey) + .then(res => res.toJSON()) + } + + // To handle overloading the individual faucet, multiple faucet accounts have been generated, and here is + // where we choose one of those faucet's at random + getRandomFaucet (previousVerifyingKeys: string[] = [], allVerifyingKeys: string[] = []) { + if (allVerifyingKeys.length === previousVerifyingKeys.length) { + throw new Error('FaucetError: There are no more faucets to choose from') + } + + const unusedVerifyingKeys = allVerifyingKeys.filter((key) => !previousVerifyingKeys.includes(key)) + const chosenVerifyingKey = pickRandom(unusedVerifyingKeys) + const hashedKey = blake2AsHex(chosenVerifyingKey) + const faucetAddress = encodeAddress(hashedKey, 42).toString() + + return { chosenVerifyingKey, faucetAddress } + } + + async sendMoney ( + { + amount, + addressToSendTo, + faucetAddress, + chosenVerifyingKey, + faucetProgramPointer = TESTNET_PROGRAM_HASH + }: SendMoneyParams + ): Promise { + const balanceService = new EntropyBalance(this.entropy, this.endpoint) + const programService = new EntropyProgram(this.entropy, this.endpoint) + + // check balance of faucet address + const balance = await balanceService.getBalance(faucetAddress) + if (balance <= 0) throw new Error('FundsError: Faucet Account does not have funds') + + // check verifying key has ONLY the exact program installed + const programs = await programService.list({ verifyingKey: chosenVerifyingKey }) + if (programs.length === 1) { + if (programs[0].program_pointer !== faucetProgramPointer) { + throw new Error('ProgramsError: Faucet Account does not have the correct faucet program') + } + } + else { + throw new Error( + `ProgramsError: Faucet Account has ${programs.length} programs attached, expected 1.` + ) + } + + const transfer = this.entropy.substrate.tx.balances.transferAllowDeath(addressToSendTo, BigInt(amount)); + const transferStatus = await this.faucetSignAndSend(transfer, parseInt(amount), faucetAddress, chosenVerifyingKey) + if (transferStatus.isFinalized) return transferStatus + } +} diff --git a/src/faucet/types.ts b/src/faucet/types.ts new file mode 100644 index 00000000..9e874085 --- /dev/null +++ b/src/faucet/types.ts @@ -0,0 +1,7 @@ +export interface SendMoneyParams { + amount: string + addressToSendTo: string + faucetAddress: string + chosenVerifyingKey: string + faucetProgramPointer: string +} \ No newline at end of file diff --git a/src/faucet/utils.ts b/src/faucet/utils.ts new file mode 100644 index 00000000..63eb4864 --- /dev/null +++ b/src/faucet/utils.ts @@ -0,0 +1,9 @@ +// Testnet address used to deploy program on chain +// Used to derive various accounts registered to faucet program in order to be used for +// issuing Faucet Funds +export const FAUCET_PROGRAM_MOD_KEY = '5GWamxgW4XWcwGsrUynqnFq2oNZPqNXQhMDfgNH9xNsg2Yj7' +// Faucet program pointer +// To-DO: Look into deriving this program from owned programs of Faucet Program Mod Acct +// this is differnt from tests because the fauce that is live now was lazily deployed without schemas +// TO-DO: update this when faucet is deployed properly +export const TESTNET_PROGRAM_HASH = '0x12af0bd1f2d91f12e34aeb07ea622c315dbc3c2bdc1e25ff98c23f1e61106c77' diff --git a/src/flows/DeployPrograms/index.ts b/src/flows/DeployPrograms/index.ts deleted file mode 100644 index 479add72..00000000 --- a/src/flows/DeployPrograms/index.ts +++ /dev/null @@ -1,101 +0,0 @@ -import Entropy from "@entropyxyz/sdk" -import * as util from "@polkadot/util" -import inquirer from "inquirer" -import { readFileSync } from "fs" -import { initializeEntropy } from "../../common/initializeEntropy" -import { print, getSelectedAccount } from "../../common/utils" -import { EntropyTuiOptions } from "src/types" - -export async function devPrograms ({ accounts, selectedAccount: selectedAccountAddress }, options: EntropyTuiOptions) { - const { endpoint } = options - const selectedAccount = getSelectedAccount(accounts, selectedAccountAddress) - - const choices = { - "Deploy": deployProgram, - "Get Owned Programs": getOwnedPrograms, - "Exit to Main Menu": () => 'exit' - } - - const actionChoice = await inquirer.prompt([ - { - type: "list", - name: "action", - message: "Select your action:", - choices: Object.keys(choices) - }, - ]) - - const entropy = await initializeEntropy({ - keyMaterial: selectedAccount.data, - endpoint - }) - - const flow = choices[actionChoice.action] - await flow(entropy, selectedAccount) -} - -async function deployProgram (entropy: Entropy, account: any) { - const deployQuestions = [ - { - type: "input", - name: "programPath", - message: "Please provide the path to your program:", - }, - { - type: "confirm", - name: "hasConfig", - message: "Does your program have a configuration file?", - default: false, - }, - ] - - const deployAnswers = await inquirer.prompt(deployQuestions) - const userProgram = readFileSync(deployAnswers.programPath) - - let programConfig = "" - - if (deployAnswers.hasConfig) { - const configAnswers = await inquirer.prompt([ - { - type: "input", - name: "config", - message: "Please provide your program configuration as a JSON string:", - }, - ]) - - // Convert JSON string to bytes and then to hex - const encoder = new TextEncoder() - const byteArray = encoder.encode(configAnswers.config) - programConfig = util.u8aToHex(new Uint8Array(byteArray)) - } - - try { - // Deploy the program with config - const pointer = await entropy.programs.dev.deploy( - userProgram, - programConfig - ) - print("Program deployed successfully with pointer:", pointer) - } catch (deployError) { - console.error("Deployment failed:", deployError) - } - - print("Deploying from account:", account.address) -} - -async function getOwnedPrograms (entropy: Entropy, account: any) { - const userAddress = account.address - if (!userAddress) return - - try { - const fetchedPrograms = await entropy.programs.dev.get(userAddress) - if (fetchedPrograms.length) { - print("Retrieved program pointers:") - print(fetchedPrograms) - } else { - print("There are no programs to show") - } - } catch (error) { - console.error("Failed to retrieve program pointers:", error) - } -} diff --git a/src/flows/UserPrograms/index.ts b/src/flows/UserPrograms/index.ts deleted file mode 100644 index da4e7c4f..00000000 --- a/src/flows/UserPrograms/index.ts +++ /dev/null @@ -1,158 +0,0 @@ -import inquirer from "inquirer" -import * as util from "@polkadot/util" -import { initializeEntropy } from "../../common/initializeEntropy" -import { getSelectedAccount, print } from "../../common/utils" -import { EntropyLogger } from "src/common/logger"; - -let verifyingKey: string; - -export async function userPrograms ({ accounts, selectedAccount: selectedAccountAddress }, options, logger: EntropyLogger) { - const FLOW_CONTEXT = 'USER_PROGRAMS' - const { endpoint } = options - const selectedAccount = getSelectedAccount(accounts, selectedAccountAddress) - - const actionChoice = await inquirer.prompt([ - { - type: "list", - name: "action", - message: "What would you like to do?", - choices: [ - "View My Programs", - "Add a Program to My List", - "Remove a Program from My List", - "Check if Program Exists", - "Exit to Main Menu", - ], - }, - ]) - - const entropy = await initializeEntropy({ - keyMaterial: selectedAccount.data, - endpoint - }) - - if (!entropy.registrationManager?.signer?.pair) { - throw new Error("Keys are undefined") - } - - const verifyingKeyQuestion = [{ - type: 'list', - name: 'verifyingKey', - message: 'Select the key to proceeed', - choices: entropy.keyring.accounts.registration.verifyingKeys, - default: entropy.keyring.accounts.registration.verifyingKeys[0] - }] - - switch (actionChoice.action) { - case "View My Programs": { - try { - if (!verifyingKey && entropy.keyring.accounts.registration.verifyingKeys.length) { - ({ verifyingKey } = await inquirer.prompt(verifyingKeyQuestion)) - } else { - print('You currently have no verifying keys, please register this account to generate the keys') - break - } - const programs = await entropy.programs.get(verifyingKey) - if (programs.length === 0) { - print("You currently have no programs set.") - } else { - print("Your Programs:") - programs.forEach((program, index) => { - print( - `${index + 1}. Pointer: ${ - program.program_pointer - }, Config: ${JSON.stringify(program.program_config)}` - ) - }) - } - } catch (error) { - console.error(error.message) - } - break - } - case "Check if Program Exists": { - try { - const { programPointer } = await inquirer.prompt([{ - type: "input", - name: "programPointer", - message: "Enter the program pointer you wish to check:", - validate: (input) => (input ? true : "Program pointer is required!"), - }]) - logger.debug(`program pointer: ${programPointer}`, `${FLOW_CONTEXT}::PROGRAM_PRESENCE_CHECK`); - const program = await entropy.programs.dev.get(programPointer); - print(program); - } catch (error) { - console.error(error.message); - } - break; - } - - case "Add a Program to My List": { - try { - const { programPointerToAdd, programConfigJson } = await inquirer.prompt([ - { - type: "input", - name: "programPointerToAdd", - message: "Enter the program pointer you wish to add:", - validate: (input) => (input ? true : "Program pointer is required!"), - }, - { - type: "editor", - name: "programConfigJson", - message: - "Enter the program configuration as a JSON string (this will open your default editor):", - validate: (input) => { - try { - JSON.parse(input) - return true - } catch (e) { - return "Please enter a valid JSON string for the configuration." - } - }, - }, - ]) - - const encoder = new TextEncoder() - const byteArray = encoder.encode(programConfigJson) - const programConfigHex = util.u8aToHex(byteArray) - - await entropy.programs.add( - { - program_pointer: programPointerToAdd, - program_config: programConfigHex, - } - ) - - print("Program added successfully.") - } catch (error) { - console.error(error.message) - } - break - } - case "Remove a Program from My List": { - try { - if (!verifyingKey) { - ({ verifyingKey } = await inquirer.prompt(verifyingKeyQuestion)) - } - const { programPointerToRemove } = await inquirer.prompt([ - { - type: "input", - name: "programPointerToRemove", - message: "Enter the program pointer you wish to remove:", - }, - ]) - await entropy.programs.remove( - programPointerToRemove, - verifyingKey - ) - print("Program removed successfully.") - } catch (error) { - console.error(error.message) - - } - break - } - case 'Exit to Main Menu': - return 'exit' - } -} diff --git a/src/flows/balance/balance.ts b/src/flows/balance/balance.ts deleted file mode 100644 index d2a5ce3b..00000000 --- a/src/flows/balance/balance.ts +++ /dev/null @@ -1,36 +0,0 @@ -import Entropy from "@entropyxyz/sdk"; -import { BalanceInfo } from "./types"; - -const hexToBigInt = (hexString: string) => BigInt(hexString) - -export async function getBalance (entropy: Entropy, address: string): Promise { - try { - const accountInfo = (await entropy.substrate.query.system.account(address)) as any - - return parseInt(hexToBigInt(accountInfo.data.free).toString()) - } catch (error) { - // console.error(`There was an error getting balance for [acct = ${address}]`, error); - throw new Error(error.message) - } -} - -export async function getBalances (entropy: Entropy, addresses: string[]): Promise { - const balanceInfo: BalanceInfo = {} - try { - await Promise.all(addresses.map(async address => { - try { - const balance = await getBalance(entropy, address) - - balanceInfo[address] = { balance } - } catch (error) { - // console.error(`Error retrieving balance for ${address}`, error); - balanceInfo[address] = { error: error.message } - } - })) - - return balanceInfo - } catch (error) { - // console.error(`There was an error getting balances for [${addresses}]`, error); - throw new Error(error.message) - } -} \ No newline at end of file diff --git a/src/flows/balance/cli.ts b/src/flows/balance/cli.ts deleted file mode 100644 index 4bcb208d..00000000 --- a/src/flows/balance/cli.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { initializeEntropy } from '../../common/initializeEntropy' -import * as config from '../../config' -import { getBalance } from './balance' -import { EntropyLogger } from 'src/common/logger' - -export async function cliGetBalance ({ address, password, endpoint }) { - const logger = new EntropyLogger('CLI::CHECK_BALANCE', endpoint) - const storedConfig = await config.get() - const account = storedConfig.accounts.find(account => account.address === address) - if (!account) throw Error(`No account with address ${address}`) - // QUESTION: is throwing the right response? - logger.debug('account', account) - - // check if data is encrypted + we have a password - if (typeof account.data === 'string' && !password) { - throw Error('This account requires a password, add --password ') - } - - const entropy = await initializeEntropy({ keyMaterial: account.data, password, endpoint }) - const balance = await getBalance(entropy, address) - - return `${balance.toLocaleString('en-US')} BITS` -} - diff --git a/src/flows/balance/index.ts b/src/flows/balance/index.ts deleted file mode 100644 index 0481c86e..00000000 --- a/src/flows/balance/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { EntropyLogger } from "src/common/logger"; -import { initializeEntropy } from "../../common/initializeEntropy" -import { print, getSelectedAccount } from "../../common/utils" -import { getBalance } from "./balance"; - -// TO-DO setup flow method to provide options to allow users to select account, -// use external address, or get balances for all accounts in config -export async function checkBalance ({ accounts, selectedAccount: selectedAccountAddress }, options, logger: EntropyLogger) { - const FLOW_CONTEXT = 'CHECK_BALANCE' - const { endpoint } = options - logger.debug(`endpoint: ${endpoint}`, FLOW_CONTEXT) - - const selectedAccount = getSelectedAccount(accounts, selectedAccountAddress) - logger.log(selectedAccount, FLOW_CONTEXT) - const entropy = await initializeEntropy({ keyMaterial: selectedAccount.data, endpoint }); - const accountAddress = selectedAccountAddress - const freeBalance = await getBalance(entropy, accountAddress) - print(`Address ${accountAddress} has a balance of: ${freeBalance.toLocaleString('en-US')} BITS`) -} diff --git a/src/flows/entropyFaucet/index.ts b/src/flows/entropyFaucet/index.ts deleted file mode 100644 index 3a38942c..00000000 --- a/src/flows/entropyFaucet/index.ts +++ /dev/null @@ -1,48 +0,0 @@ -import inquirer from "inquirer" -import { print, accountChoices } from "../../common/utils" -import { initializeEntropy } from "../../common/initializeEntropy" - -export async function entropyFaucet ({ accounts }, options) { - const { endpoint } = options - - const accountQuestion = { - type: "list", - name: "selectedAccount", - message: "Choose account:", - choices: accountChoices(accounts), - } - - const answers = await inquirer.prompt([accountQuestion]) - const selectedAccount = answers.selectedAccount - - const recipientAddress = selectedAccount.address - const aliceAccount = { - data: { - // type: "seed", - seed: "0xe5be9a5092b81bca64be81d212e7f2f9eba183bb7a90954f7b76361f6edb5c0a", - // admin TODO: missing this field - }, - } - - // @ts-ignore (see TODO on aliceAccount) - const entropy = await initializeEntropy({ keyMaterial: aliceAccount.data, endpoint }) - - if (!entropy.registrationManager.signer.pair) { - throw new Error("Keys are undefined") - } - - const amount = "10000000000000000" - const tx = entropy.substrate.tx.balances.transferAllowDeath( - recipientAddress, - amount - ) - - await tx.signAndSend( - entropy.registrationManager.signer.pair, - async ({ status }) => { - if (status.isInBlock || status.isFinalized) { - print(recipientAddress, "funded") - } - } - ) -} diff --git a/src/flows/entropyTransfer/cli.ts b/src/flows/entropyTransfer/cli.ts deleted file mode 100644 index 1a0abe51..00000000 --- a/src/flows/entropyTransfer/cli.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { initializeEntropy } from '../../common/initializeEntropy' -import * as config from '../../config' -import { formatAmountAsHex } from '../../common/utils' -import { EntropyLogger } from 'src/common/logger' - -export async function cliEntropyTransfer ({ source, password, destination, amount, endpoint }) { - const logger = new EntropyLogger('CLI::TRANSFER', endpoint) - // NOTE: password is optional, is only for source account (if that is encrypted) - - const storedConfig = await config.get() - const account = storedConfig.accounts.find(account => account.address === source) - if (!account) throw Error(`No account with address ${source}`) - // QUESTION: is throwing the right response? - logger.debug('account', account) - - const entropy = await initializeEntropy({ keyMaterial: account.data, password, endpoint }) - - if (!entropy?.registrationManager?.signer?.pair) { - throw new Error("Signer keypair is undefined or not properly initialized.") - } - const formattedAmount = formatAmountAsHex(amount) - const tx = await entropy.substrate.tx.balances.transferAllowDeath( - destination, - BigInt(formattedAmount), - // WARNING: this is moving ... a lot? What? - ) - - await tx.signAndSend (entropy.registrationManager.signer.pair, ({ status }) => { - logger.debug('signAndSend status:') - logger.debug(status) - }) -} diff --git a/src/flows/entropyTransfer/index.ts b/src/flows/entropyTransfer/index.ts deleted file mode 100644 index d376703f..00000000 --- a/src/flows/entropyTransfer/index.ts +++ /dev/null @@ -1,64 +0,0 @@ -import inquirer from "inquirer" -import { getSelectedAccount, print } from "../../common/utils" -import { initializeEntropy } from "../../common/initializeEntropy" -import { transfer } from "./transfer" -import { setupProgress } from "src/common/progress" - -const question = [ - { - type: "input", - name: "amount", - message: "Input amount to transfer:", - default: "1", - validate: (amount) => { - if (isNaN(amount) || parseInt(amount) <= 0) { - return 'Please enter a value greater than 0' - } - return true - } - }, - { - type: "input", - name: "recipientAddress", - message: "Input recipient's address:", - }, -] - -export async function entropyTransfer ({ accounts, selectedAccount: selectedAccountAddress }, options) { - const { endpoint } = options - const selectedAccount = getSelectedAccount(accounts, selectedAccountAddress) - - const { start: startProgress, stop: stopProgress } = setupProgress('Transferring Funds') - - try { - const entropy = await initializeEntropy({ - keyMaterial: selectedAccount.data, - endpoint - }) - - const { amount, recipientAddress } = await inquirer.prompt(question) - - if (!entropy?.keyring?.accounts?.registration?.pair) { - throw new Error("Signer keypair is undefined or not properly initialized.") - } - const formattedAmount = BigInt(parseInt(amount) * 1e10) - startProgress() - const transferStatus = await transfer( - entropy, - { - from: entropy.keyring.accounts.registration.pair, - to: recipientAddress, - amount: formattedAmount - } - ) - if (transferStatus.isFinalized) stopProgress() - - print( - `\nTransaction successful: Sent ${amount} to ${recipientAddress}` - ) - print('\nPress enter to return to main menu') - } catch (error) { - stopProgress() - console.error('ERR:::', error); - } -} \ No newline at end of file diff --git a/src/flows/entropyTransfer/transfer.ts b/src/flows/entropyTransfer/transfer.ts deleted file mode 100644 index d8663eee..00000000 --- a/src/flows/entropyTransfer/transfer.ts +++ /dev/null @@ -1,34 +0,0 @@ -import Entropy from "@entropyxyz/sdk"; -import { TransferOptions } from "./types"; - -export async function transfer (entropy: Entropy, payload: TransferOptions): Promise { - const { from, to, amount } = payload - - return new Promise((resolve, reject) => { - // WARN: await signAndSend is dangerous as it does not resolve - // after transaction is complete :melt: - entropy.substrate.tx.balances - .transferAllowDeath(to, amount) - // @ts-ignore - .signAndSend(from, ({ status, dispatchError }) => { - if (dispatchError) { - let msg: string - if (dispatchError.isModule) { - // for module errors, we have the section indexed, lookup - const decoded = entropy.substrate.registry.findMetaError( - dispatchError.asModule - ) - const { docs, name, section } = decoded - - msg = `${section}.${name}: ${docs.join(' ')}` - } else { - // Other, CannotLookup, BadOrigin, no extra info - msg = dispatchError.toString() - } - return reject(Error(msg)) - } - - if (status.isFinalized) resolve(status) - }) - }) -} \ No newline at end of file diff --git a/src/flows/index.ts b/src/flows/index.ts deleted file mode 100644 index af41e7bf..00000000 --- a/src/flows/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { entropyFaucet } from './entropyFaucet' -export { checkBalance } from './balance' -export { register } from './register' -export { userPrograms } from './UserPrograms' -export { devPrograms } from './DeployPrograms' -export { sign } from './sign' -export { entropyTransfer } from './entropyTransfer' -export { manageAccounts } from './manage-accounts' diff --git a/src/flows/manage-accounts/cli.ts b/src/flows/manage-accounts/cli.ts deleted file mode 100644 index 34c6ec57..00000000 --- a/src/flows/manage-accounts/cli.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as config from '../../config' - -export async function cliListAccounts () { - const storedConfig = await config.get() - - return storedConfig.accounts - .map(account => ({ - name: account.name, - address: account.address, - verifyingKeys: account?.data?.admin?.verifyingKeys - })) -} diff --git a/src/flows/manage-accounts/helpers/import-key.ts b/src/flows/manage-accounts/helpers/import-key.ts deleted file mode 100644 index 50667d01..00000000 --- a/src/flows/manage-accounts/helpers/import-key.ts +++ /dev/null @@ -1,40 +0,0 @@ -// import { mnemonicValidate, mnemonicToMiniSecret } from '@polkadot/util-crypto' - -export const importQuestions = [ - // { - // type: 'list', - // name: 'secretType', - // message: 'select secret type:', - // choices: ['seed'], - // when: ({ importKey }) => importKey - // }, - { - type: 'input', - name: 'secret', - // message: ({ secretType }) => `${secretType}:`, - message: 'Enter seed:', - validate: (secret) => { - // validate: (secret, { secretType }) => { - // if (secretType === 'mnemonic') return mnemonicValidate(secret) ? true : 'not a valid mnemonic' - if (secret.includes('#debug')) return true - if (secret.length === 66 && secret.startsWith('0x')) return true - if (secret.length === 64) return true - return 'not a valid seed' - }, - filter: (secret) => { - // filter: (secret, { secretType }) => { - // if (secretType === 'mnemonic') { - // return mnemonicToMiniSecret(secret) - // } - return secret - }, - when: ({ importKey }) => importKey - }, - { - type: 'input', - name: 'path', - meesage: 'derivation path:', - default: 'none', - when: ({ importKey }) => importKey - }, -] diff --git a/src/flows/manage-accounts/index.ts b/src/flows/manage-accounts/index.ts deleted file mode 100644 index 413f2d16..00000000 --- a/src/flows/manage-accounts/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -import inquirer from 'inquirer' -import { print } from '../../common/utils' -import { newKey } from './new-key' -import { selectAccount } from './select-account' -import { listAccounts } from './list' -import { EntropyTuiOptions } from 'src/types' -import { EntropyLogger } from 'src/common/logger' - -const actions = { - 'Create/Import Account': newKey, - 'Select Account': selectAccount, - 'List Accounts': (config) => { - try { - const accountsArray = listAccounts(config) - accountsArray?.forEach(account => print(account)) - return - } catch (error) { - console.error(error.message); - } - }, -} - -const choices = Object.keys(actions) - -const questions = [{ - type: 'list', - name: 'choice', - pageSize: choices.length, - choices, -}] - -export async function manageAccounts (config, _options: EntropyTuiOptions, logger: EntropyLogger) { - const FLOW_CONTEXT = 'MANAGE_ACCOUNTS' - const { choice } = await inquirer.prompt(questions) - const responses = await actions[choice](config, logger) || {} - logger.debug('returned config update', FLOW_CONTEXT) - logger.debug({ accounts: responses.accounts ? responses.accounts : config.accounts, selectedAccount: responses.selectedAccount || config.selectedAccount }, FLOW_CONTEXT) - return { accounts: responses.accounts ? responses.accounts : config.accounts, selectedAccount: responses.selectedAccount || config.selectedAccount } -} diff --git a/src/flows/manage-accounts/list.ts b/src/flows/manage-accounts/list.ts deleted file mode 100644 index 63ae5995..00000000 --- a/src/flows/manage-accounts/list.ts +++ /dev/null @@ -1,12 +0,0 @@ -export function listAccounts (config) { - const accountsArray = Array.isArray(config.accounts) ? config.accounts : [config.accounts] - if (!accountsArray.length) - throw new Error( - 'There are currently no accounts available, please create or import your new account using the Manage Accounts feature' - ) - return accountsArray.map((account) => ({ - name: account.name, - address: account.address, - verifyingKeys: account?.data?.admin?.verifyingKeys - })) -} \ No newline at end of file diff --git a/src/flows/manage-accounts/new-key.ts b/src/flows/manage-accounts/new-key.ts deleted file mode 100644 index f0a781a6..00000000 --- a/src/flows/manage-accounts/new-key.ts +++ /dev/null @@ -1,89 +0,0 @@ -import inquirer from 'inquirer' -import { randomAsHex } from '@polkadot/util-crypto' -// @ts-ignore -import Keyring from '@entropyxyz/sdk/keys' -import { importQuestions } from './helpers/import-key' -// import * as passwordFlow from '../password' -import { print } from '../../common/utils' -import { EntropyLogger } from 'src/common/logger' - -export async function newKey ({ accounts }, logger: EntropyLogger) { - const FLOW_CONTEXT = 'MANAGE_ACCOUNTS::NEW_KEY' - accounts = Array.isArray(accounts) ? accounts : [] - - const questions = [ - { - type: 'confirm', - name: 'importKey', - message: 'Would you like to import a key?', - default: false, - }, - ...importQuestions, - { - type: 'input', - name: 'name', - default: 'My Key' - }, - // { - // type: 'confirm', - // name: 'newPassword', - // message: 'Would you like to password protect this key?', - // default: true, - // } - ] - - const answers = await inquirer.prompt(questions) - - // if (answers.newPassword) { - // const passwordAnswer = await inquirer.prompt([ - // { - // type: 'password', - // name: 'password', - // mask: '*', - // message: 'Enter a password for the key:', - // } - // ]) - // answers = { ...answers, ...passwordAnswer } - // } - // The below conditional resolves as true, but the passwordFlow questions never get asked - // most likely due to the when field criteria not being satified on the individual questions - // if (passwordFlow.questions.length > 0) { - // const passwordFlowAnswers = await inquirer.prompt(passwordFlow.questions) - // answers = { ...answers, ...passwordFlowAnswers } - // } - - // const { secret, name, path, password, importKey } = answers - const { secret, name, path, importKey } = answers - // let isDebugMode = false - let seed - // never create debug keys only ever import them - if (importKey && secret.includes('#debug')) { - // isDebugMode = true - seed = secret.split('#debug')[0] - } else { - seed = importKey ? secret : randomAsHex(32) - } - - const keyring = new Keyring({ seed, path, debug: true }) - const fullAccount = keyring.getAccount() - // TO-DO: sdk should create account on constructor - const { admin } = keyring.getAccount() - logger.debug('fullAccount:', FLOW_CONTEXT) - logger.debug(fullAccount, FLOW_CONTEXT) - - const data = fullAccount - delete admin.pair - // const encryptedData = password ? passwordFlow.encrypt(data, password) : data - - const newAccount = { - name: name, - address: admin.address, - // TODO: replace with data: encryptedData once pasword input is added back - data, - } - - print(`New account:\n{\n\tname: ${newAccount.name}\n\taddress: ${newAccount.address}\n}`) - - accounts.push(newAccount) - return { accounts, selectedAccount: newAccount.address } -} diff --git a/src/flows/manage-accounts/select-account.ts b/src/flows/manage-accounts/select-account.ts deleted file mode 100644 index c0746c15..00000000 --- a/src/flows/manage-accounts/select-account.ts +++ /dev/null @@ -1,15 +0,0 @@ -import inquirer from "inquirer"; -import { accountChoices } from "../../common/utils"; - -export async function selectAccount ({ accounts }) { - const accountQuestion = { - type: "list", - name: "selectedAccount", - message: "Choose account:", - choices: accountChoices(accounts) - } - - const answers = await inquirer.prompt([accountQuestion]) - - return { selectedAccount: answers.selectedAccount.address } -} \ No newline at end of file diff --git a/src/flows/password/index.ts b/src/flows/password/index.ts deleted file mode 100644 index 876481c9..00000000 --- a/src/flows/password/index.ts +++ /dev/null @@ -1,61 +0,0 @@ -import * as crypto from 'crypto' - -export const questions = [ - { - type: 'password:', - name: 'password', - mask: '*', - when: ({ askForPassword }) => askForPassword, - }, - { - type: 'password:', - name: 'password', - mask: '*', - when: ({ newPassword }) => newPassword, - }, - { - type: 'password:', - message: 'confirm password', - name: 'confirmPassword', - mask: '*', - validate: (confirmPassword, { password }) => { return confirmPassword === password ? true : 'incorrect password' }, - when: ({ newPassword }) => newPassword - }, -] - - - -// Function to generate a key and IV using a password and salt -function generateKeyAndIV (password, salt) { - return crypto.scryptSync(password, salt, 32, { N: 2 ** 14 }).slice(0, 32) -} - -// Function to encrypt data with a password -export function encrypt (anyData, password) { - const data = JSON.stringify(anyData) - const salt = crypto.randomBytes(16) - const key = generateKeyAndIV(password, salt) - const cipher = crypto.createCipheriv('aes-256-gcm', key, Buffer.alloc(16)) - const encrypted = Buffer.concat([cipher.update(data, 'utf8'), cipher.final()]) - const tag = cipher.getAuthTag() - - return Buffer.concat([salt, tag, encrypted]).toString('base64') -} - -// Function to decrypt data with a password -export function decrypt (encryptedData, password) { - const buffer = Buffer.from(encryptedData, 'base64') - const salt = buffer.slice(0, 16) - const tag = buffer.slice(16, 32) - const data = buffer.slice(32) - - const key = generateKeyAndIV(password, salt) - const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.alloc(16)) - decipher.setAuthTag(tag) - - let returnData = decipher.update(data, null, 'utf8') + decipher.final('utf8') - try { - returnData = JSON.parse(returnData) - } catch (e) {/*swallo all parse errors*/} - return returnData -} diff --git a/src/flows/register/index.ts b/src/flows/register/index.ts deleted file mode 100644 index d15d6f88..00000000 --- a/src/flows/register/index.ts +++ /dev/null @@ -1,62 +0,0 @@ -// import inquirer from "inquirer" -import { getSelectedAccount, print, /*accountChoices*/ } from "../../common/utils" -import { initializeEntropy } from "../../common/initializeEntropy" -import { EntropyLogger } from "src/common/logger"; - -export async function register (storedConfig, options, logger: EntropyLogger) { - const FLOW_CONTEXT = 'REGISTER' - const { accounts, selectedAccount: selectedFromConfig } = storedConfig; - const { endpoint } = options - - if (!selectedFromConfig) return - const selectedAccount = getSelectedAccount(accounts, selectedFromConfig) - - const entropy = await initializeEntropy({ keyMaterial: selectedAccount.data, endpoint }) - // TO-DO: investigate this a little more - // const filteredAccountChoices = accountChoices(accounts) - // Not going to ask for a pointer from the user just yet - // const { programPointer } = await inquirer.prompt([{ - // type: 'input', - // message: 'Enter the program pointer here:', - // name: 'programPointer', - // // Setting default to default key proxy program - // default: '0x0000000000000000000000000000000000000000000000000000000000000000' - // }]) - //@ts-ignore: - logger.debug('about to register selectedAccount.address' + selectedAccount.address + 'keyring:' + entropy.keyring.getLazyLoadAccountProxy('registration').pair.address, FLOW_CONTEXT) - print("Attempting to register the address:", selectedAccount.address, ) - let verifyingKey: string - try { - // For now we are forcing users to only register with the default info before having to format the config for them - // verifyingKey = await entropy.register({ - // programDeployer: entropy.keyring.accounts.registration.address, - // programData: [{ - // program_pointer: programPointer, - // program_config: '0x', - // }] - // }) - verifyingKey = await entropy.register() - if (verifyingKey) { - print("Your address", selectedAccount.address, "has been successfully registered.") - selectedAccount?.data?.registration?.verifyingKeys?.push(verifyingKey) - const arrIdx = accounts.indexOf(selectedAccount) - accounts.splice(arrIdx, 1, selectedAccount) - return { accounts, selectedAccount: selectedAccount.address } - } - } catch (error) { - console.error('error', error); - if (!verifyingKey) { - logger.debug('Pruning Registration', FLOW_CONTEXT) - try { - const tx = await entropy.substrate.tx.registry.pruneRegistration() - await tx.signAndSend(entropy.keyring.accounts.registration.pair, ({ status }) => { - if (status.isFinalized) { - print('Successfully pruned registration'); - } - }) - } catch (error) { - console.error('Unable to prune registration due to:', error.message); - } - } - } -} diff --git a/src/flows/sign/cli.ts b/src/flows/sign/cli.ts deleted file mode 100644 index 5fb4c6aa..00000000 --- a/src/flows/sign/cli.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { EntropyLogger } from "src/common/logger" -import { initializeEntropy } from "../../common/initializeEntropy" -import * as config from '../../config' -import { signWithAdapters } from './sign' - -// TODO: revisit this file, rename as signEthTransaction? -export async function cliSign ({ address, message, endpoint }) { - const logger = new EntropyLogger('CLI::SIGN', endpoint) - const storedConfig = await config.get() - const account = storedConfig.accounts.find(account => account.address === address) - if (!account) throw Error(`No account with address ${address}`) - // QUESTION: is throwing the right response? - logger.debug('account:') - logger.debug(account) - - const entropy = await initializeEntropy({ keyMaterial: account.data, endpoint }) - - return signWithAdapters(entropy, { - msg: message - }) -} diff --git a/src/flows/sign/index.ts b/src/flows/sign/index.ts deleted file mode 100644 index d5371692..00000000 --- a/src/flows/sign/index.ts +++ /dev/null @@ -1,127 +0,0 @@ -import inquirer from "inquirer" -import { u8aToHex } from '@polkadot/util' -import { initializeEntropy } from "../../common/initializeEntropy" -import { getSelectedAccount, print } from "../../common/utils" -import { signWithAdapters } from './sign' -import { EntropyLogger } from "src/common/logger" - -async function signWithAdaptersInOrder (entropy, msg?: string, signingAttempts = 0) { - try { - const messageQuestion = { - type: 'list', - name: 'messageAction', - message: 'Please choose how you would like to input your message to sign:', - choices: [ - 'Text Input', - /* DO NOT DELETE THIS */ - // 'From a File', - ], - } - const userInputQuestion = { - type: "editor", - name: "userInput", - message: "Enter the message you wish to sign (this will open your default editor):", - } - /* DO NOT DELETE THIS */ - // const pathToFileQuestion = { - // type: 'input', - // name: 'pathToFile', - // message: 'Enter the path to the file you wish to sign:', - // } - if (!msg) { - const { messageAction } = await inquirer.prompt([messageQuestion]) - switch (messageAction) { - case 'Text Input': { - const { userInput } = await inquirer.prompt([userInputQuestion]) - msg = userInput - break - } - /* DO NOT DELETE THIS */ - // case 'From a File': { - // break - // } - default: { - console.error('Unsupported Action') - return - } - } - } - - print('msg to be signed:', msg) - print('verifying key:', entropy.signingManager.verifyingKey) - const signature = await signWithAdapters(entropy, { msg }) - const signatureHexString = u8aToHex(signature) - print('signature:', signatureHexString) - } catch (error) { - const { message } = error - // See https://github.com/entropyxyz/sdk/issues/367 for reasoning behind adding this retry mechanism - if ((message.includes('Invalid Signer') || message.includes('Invalid Signer in Signing group')) && signingAttempts <= 1) { - // Recursively retries signing with a reverse order in the subgroups list - await signWithAdaptersInOrder(entropy, msg, signingAttempts + 1) - } - console.error(message) - return - } -} - -export async function sign ({ accounts, selectedAccount: selectedAccountAddress }, options, logger: EntropyLogger) { - const FLOW_CONTEXT = 'SIGN' - const { endpoint } = options - const actionChoice = await inquirer.prompt([ - { - type: "list", - name: "action", - message: "What would you like to do?", - choices: [ - // Removing the option to select Raw Sign until we fully release signing. - // We will need to update the flow to ask the user to input the auxilary data for the signature request - // "Raw Sign", - "Sign With Adapter", - "Exit to Main Menu", - ], - }, - ]) - - const selectedAccount = getSelectedAccount(accounts, selectedAccountAddress) - logger.debug("selectedAccount:", FLOW_CONTEXT) - logger.debug(selectedAccount, FLOW_CONTEXT) - const keyMaterial = selectedAccount?.data; - - const entropy = await initializeEntropy({ keyMaterial, endpoint }) - const { address } = entropy.keyring.accounts.registration - logger.debug("address:", FLOW_CONTEXT) - logger.debug(address, FLOW_CONTEXT) - if (address == undefined) { - throw new Error("address issue") - } - switch (actionChoice.action) { - // case 'Raw Sign': { - // const msg = Buffer.from('Hello world: new signature from entropy!').toString('hex') - // debug('msg', msg); - // const signature = await entropy.sign({ - // sigRequestHash: msg, - // hash: 'sha3', - // naynay does not think he is doing this properly - // auxiliaryData: [ - // { - // public_key_type: 'sr25519', - // public_key: Buffer.from(entropy.keyring.accounts.registration.pair.publicKey).toString('base64'), - // signature: entropy.keyring.accounts.registration.pair.sign(msg), - // context: 'substrate', - // }, - // ], - // }) - - // print('signature:', signature) - // return - // } - case 'Sign With Adapter': { - await signWithAdaptersInOrder(entropy) - return - } - case 'Exit to Main Menu': - return 'exit' - default: - throw new Error('Unrecognizable action') - } -} diff --git a/src/flows/sign/sign.ts b/src/flows/sign/sign.ts deleted file mode 100644 index e51e6924..00000000 --- a/src/flows/sign/sign.ts +++ /dev/null @@ -1,23 +0,0 @@ -interface SignWithAdapterInput { - /** the message as a utf-8 encoded string */ - msg: string, - verifyingKey?: string, - // LATER: - // auxillaryData: any -} - -function stringToHex (str: string): string { - return Buffer.from(str).toString('hex') -} - -export async function signWithAdapters (entropy, input: SignWithAdapterInput) { - return entropy.signWithAdaptersInOrder({ - msg: { - msg: stringToHex(input.msg) - }, - // type - order: ['deviceKeyProxy', 'noop'], - signatureVerifyingKey: input.verifyingKey - // auxillaryData - }) -} diff --git a/src/program/command.ts b/src/program/command.ts new file mode 100644 index 00000000..ad71ed2b --- /dev/null +++ b/src/program/command.ts @@ -0,0 +1,62 @@ +import { Command } from 'commander' + +import { EntropyProgram } from './main' +import { accountOption, endpointOption, cliWrite, loadEntropy } from '../common/utils-cli' + +export function entropyProgramCommand () { + return new Command('program') + .description('Commands for working with programs deployed to the Entropy Network') + .addCommand(entropyProgramDeploy()) + // TODO: + // .addCommand(entropyProgramGet()) + // .addCommand(entropyProgramListDeployed()) + // .addCommand(entropyProgramAdd()) + // .addCommand(entropyProgramRemove()) + // .addCommand(entropyProgramList()) +} + +function entropyProgramDeploy () { + return new Command('deploy') + .description([ + 'Deploys a program to the Entropy network, returning a program pointer.', + 'Requires funds.' + ].join(' ')) + .argument( + '', + [ + 'The path to your program bytecode.', + 'Must be a .wasm file.' + ].join(' ') + ) + .argument( + '', + [ + 'The path to the JSON Schema for validating configurations passed in by users installing this program.', + 'Must be a .json file.' + ].join(' ') + ) + .argument( + '', + [ + 'The path to the JSON Schema for validating auxillary data passed to the program on calls to "sign".', + 'Must be a .json file.' + ].join(' ') + ) + .addOption(accountOption()) + .addOption(endpointOption()) + + .action(async (bytecodePath, configurationSchemaPath, auxillaryDataSchemaPath, opts) => { // eslint-disable-line + const entropy = await loadEntropy(opts.account, opts.endpoint) + + const program = new EntropyProgram(entropy, opts.endpoint) + + const pointer = await program.deploy({ + bytecodePath, + configurationSchemaPath, + auxillaryDataSchemaPath + }) + cliWrite(pointer) + + process.exit(0) + }) +} diff --git a/src/program/constants.ts b/src/program/constants.ts new file mode 100644 index 00000000..6b34c72e --- /dev/null +++ b/src/program/constants.ts @@ -0,0 +1 @@ +export const FLOW_CONTEXT = 'ENTROPY_PROGRAM' diff --git a/src/program/interaction.ts b/src/program/interaction.ts new file mode 100644 index 00000000..ccb3fdc3 --- /dev/null +++ b/src/program/interaction.ts @@ -0,0 +1,184 @@ +import Entropy from "@entropyxyz/sdk" +import inquirer from "inquirer" +import { u8aToHex } from "@polkadot/util" + +import { displayPrograms, addQuestions, getProgramPointerInput, verifyingKeyQuestion } from "./utils"; +import { EntropyProgram } from "./main"; +import { print } from "../common/utils" + +let verifyingKey: string; + +export async function entropyProgram (entropy: Entropy, endpoint: string) { + const actionChoice = await inquirer.prompt([ + { + type: "list", + name: "action", + message: "What would you like to do?", + choices: [ + "View My Programs", + "Add a Program to My List", + "Remove a Program from My List", + "Check if Program Exists", + "Exit to Main Menu", + ], + }, + ]) + + if (!entropy.registrationManager?.signer?.pair) { + throw new Error("Keys are undefined") + } + + const program = new EntropyProgram(entropy, endpoint) + + switch (actionChoice.action) { + case "View My Programs": { + try { + if (!verifyingKey && entropy.keyring.accounts.registration.verifyingKeys.length) { + ({ verifyingKey } = await inquirer.prompt(verifyingKeyQuestion(entropy))) + } else { + print('You currently have no verifying keys, please register this account to generate the keys') + break + } + const programs = await program.list({ verifyingKey }) + if (programs.length === 0) { + print("You currently have no programs set.") + } else { + print("Your Programs:") + displayPrograms(programs) + } + } catch (error) { + console.error(error.message) + } + break + } + case "Check if Program Exists": { + try { + const { programPointer } = await inquirer.prompt([{ + type: "input", + name: "programPointer", + message: "Enter the program pointer you wish to check:", + validate: (input) => (input ? true : "Program pointer is required!"), + }]) + const info = await program.get(programPointer); + print(info); + } catch (error) { + console.error(error.message); + } + break; + } + + case "Add a Program to My List": { + try { + const { programPointerToAdd, programConfigJson } = await inquirer.prompt(addQuestions) + + const encoder = new TextEncoder() + const byteArray = encoder.encode(programConfigJson) + const programConfigHex = u8aToHex(byteArray) + + await program.add({ programPointer: programPointerToAdd, programConfig: programConfigHex }) + + print("Program added successfully.") + } catch (error) { + console.error(error.message) + } + break + } + case "Remove a Program from My List": { + try { + if (!verifyingKey) { + ({ verifyingKey } = await inquirer.prompt(verifyingKeyQuestion(entropy))) + } + const { programPointer: programPointerToRemove } = await inquirer.prompt(getProgramPointerInput) + await program.remove({ programPointer: programPointerToRemove, verifyingKey }) + print("Program removed successfully.") + } catch (error) { + console.error(error.message) + } + break + } + case 'Exit to Main Menu': + return 'exit' + } +} + +// eslint-disable-next-line +export async function entropyProgramDev (entropy, endpoint) { + const choices = { + "Deploy": deployProgramTUI, + "Get Owned Programs": getOwnedProgramsTUI, + "Exit to Main Menu": () => 'exit' + } + + const actionChoice = await inquirer.prompt([ + { + type: "list", + name: "action", + message: "Select your action:", + choices: Object.keys(choices) + }, + ]) + + const flow = choices[actionChoice.action] + await flow(entropy, endpoint) +} + +async function deployProgramTUI (entropy: Entropy, endpoint: string) { + const program = new EntropyProgram(entropy, endpoint) + + const answers = await inquirer.prompt([ + { + type: "input", + name: "bytecodePath", + message: "Please provide the path to your program binary:", + validate (input: string) { + return input.endsWith('.wasm') + ? true + : 'program binary must be .wasm file' + } + }, + { + type: "input", + name: "configurationSchemaPath", + message: "Please provide the path to your configuration schema:", + validate (input: string) { + return input.endsWith('.json') + ? true + : 'configuration schema must be a .json file' + } + }, + { + type: "input", + name: "auxillaryDataSchemaPath", + message: "Please provide the path to your auxillary data schema:", + validate (input: string) { + return input.endsWith('.json') + ? true + : 'configuration schema must be a .json file' + } + }, + ]) + + try { + const pointer = await program.deploy(answers) + + print("Program deployed successfully with pointer:", pointer) + } catch (deployError) { + console.error("Deployment failed:", deployError) + } +} + +async function getOwnedProgramsTUI (entropy: Entropy, endpoint: string) { + const program = new EntropyProgram(entropy, endpoint) + + try { + const fetchedPrograms = await program.listDeployed() + if (fetchedPrograms.length) { + print("Retrieved program pointers:") + print(fetchedPrograms) + } else { + print("There are no programs to show") + } + } catch (error) { + console.error("Failed to retrieve program pointers:", error) + } +} diff --git a/src/program/main.ts b/src/program/main.ts new file mode 100644 index 00000000..ac241a85 --- /dev/null +++ b/src/program/main.ts @@ -0,0 +1,67 @@ +import Entropy from "@entropyxyz/sdk" + +import { EntropyBase } from "../common/entropy-base" +import { FLOW_CONTEXT } from "./constants" +import { loadFile, jsonToHex } from "./utils" +import { + EntropyProgramDeployParams, + EntropyProgramAddParams, + EntropyProgramRemoveParams, + EntropyProgramViewProgramsParams +} from "./types" + +export class EntropyProgram extends EntropyBase { + constructor (entropy: Entropy, endpoint: string) { + super({ entropy, endpoint, flowContext: FLOW_CONTEXT }) + } + + // User Methods: + + async add ({ programPointer, programConfig, verifyingKey }: EntropyProgramAddParams): Promise { + return this.entropy.programs.add( + { + program_pointer: programPointer, + program_config: programConfig, + }, + verifyingKey + ) + } + + async remove ({ programPointer, verifyingKey }: EntropyProgramRemoveParams): Promise { + return this.entropy.programs.remove( + programPointer, + verifyingKey + ) + } + + async list ({ verifyingKey }: EntropyProgramViewProgramsParams): Promise { + return this.entropy.programs.get(verifyingKey) + } + + // Dev Methods: + + async deploy (params: EntropyProgramDeployParams) { + const bytecode = await loadFile(params.bytecodePath) + const configurationSchema = await loadFile(params.configurationSchemaPath, 'json') + const auxillaryDataSchema = await loadFile(params.auxillaryDataSchemaPath, 'json') + // QUESTION: where / how are schema validated? + + return this.entropy.programs.dev.deploy( + bytecode, + jsonToHex(configurationSchema), + jsonToHex(auxillaryDataSchema) + ) + } + + async get (programPointer: string): Promise { + this.logger.debug(`program pointer: ${programPointer}`, `${FLOW_CONTEXT}::PROGRAM_PRESENCE_CHECK`); + return this.entropy.programs.dev.get(programPointer) + } + + async listDeployed () { + const address = this.entropy.keyring.accounts.registration.address + // QUESTION: will we always be wanting this address? + return this.entropy.programs.dev.getByDeployer(address) + } +} + diff --git a/src/program/types.ts b/src/program/types.ts new file mode 100644 index 00000000..16573e8d --- /dev/null +++ b/src/program/types.ts @@ -0,0 +1,22 @@ + +export interface EntropyProgramDeployParams { + bytecodePath: string, + configurationSchemaPath?: string + auxillaryDataSchemaPath?: string + // TODO: confirm which of these are optional +} + +export interface EntropyProgramAddParams { + programPointer: string + programConfig: string + verifyingKey?: string +} + +export interface EntropyProgramRemoveParams { + programPointer: string + verifyingKey: string +} + +export interface EntropyProgramViewProgramsParams { + verifyingKey: string +} diff --git a/src/program/utils.ts b/src/program/utils.ts new file mode 100644 index 00000000..3cc60746 --- /dev/null +++ b/src/program/utils.ts @@ -0,0 +1,107 @@ +import Entropy from "@entropyxyz/sdk" +import fs from "node:fs/promises" +import { isAbsolute, join } from "node:path" +import { u8aToHex } from "@polkadot/util" + +import { print } from "../common/utils" + +export async function loadFile (path?: string, encoding?: string) { + if (path === undefined) return + + const absolutePath = isAbsolute(path) + ? path + : join(process.cwd(), path) + + switch (encoding) { + case undefined: + return fs.readFile(absolutePath) + + case 'json': + return fs.readFile(absolutePath, 'utf-8') + .then(string => JSON.parse(string)) + + default: + throw Error('unknown encoding: ' + encoding) + // return fs.readFile(absolutePath, encoding) + } +} + +export function jsonToHex (obj?: object) { + if (obj === undefined) return + + const encoder = new TextEncoder() + const byteArray = encoder.encode(JSON.stringify(obj)) + + return u8aToHex(new Uint8Array(byteArray)) +} + + +export function displayPrograms (programs): void { + programs.forEach((program, index) => { + print(`${index + 1}.`) + print({ + pointer: program.program_pointer, + config: parseProgramConfig(program.program_config) + }) + print('') + }) + + // private + + function parseProgramConfig (rawConfig: unknown) { + if (typeof rawConfig !== 'string') return rawConfig + if (!rawConfig.startsWith('0x')) return rawConfig + + const hex = rawConfig.slice(2) + const utf8 = Buffer.from(hex, 'hex').toString() + const output = JSON.parse(utf8) + Object.keys(output).forEach(key => { + output[key] = output[key].map(base64toHex) + }) + + return output + } + function base64toHex (base64: string): string { + return Buffer.from(base64, 'base64').toString('hex') + } +} + + +export const addQuestions = [ + { + type: "input", + name: "programPointerToAdd", + message: "Enter the program pointer you wish to add:", + validate: (input) => (input ? true : "Program pointer is required!"), + }, + { + type: "editor", + name: "programConfigJson", + message: + "Enter the program configuration as a JSON string (this will open your default editor):", + validate: (input) => { + try { + JSON.parse(input) + return true + } catch (e) { + return "Please enter a valid JSON string for the configuration." + } + }, + }, +] + +export const getProgramPointerInput = [ + { + type: "input", + name: "programPointer", + message: "Enter the program pointer you wish to remove:", + }, +] + +export const verifyingKeyQuestion = (entropy: Entropy) => [{ + type: 'list', + name: 'verifyingKey', + message: 'Select the key to proceeed', + choices: entropy.keyring.accounts.registration.verifyingKeys, + default: entropy.keyring.accounts.registration.verifyingKeys[0] +}] diff --git a/src/sign/command.ts b/src/sign/command.ts new file mode 100644 index 00000000..9fffa433 --- /dev/null +++ b/src/sign/command.ts @@ -0,0 +1,31 @@ +import { Command, /* Option */ } from 'commander' +import { accountOption, endpointOption, cliWrite, loadEntropy } from '../common/utils-cli' +import { EntropySign } from './main' + +export function entropySignCommand () { + const signCommand = new Command('sign') + .description('Sign a message using the Entropy network. Output is a JSON { verifyingKey, signature }') + .argument('', 'Message you would like to sign (string)') + .addOption(accountOption()) + .addOption(endpointOption()) + // .addOption( + // new Option( + // '-r, --raw', + // 'Signs the provided message using the Raw Signing method. Output is a signature (string)' + // ) + // ) + .action(async (msg, opts) => { + const entropy = await loadEntropy(opts.account, opts.endpoint) + const SigningService = new EntropySign(entropy, opts.endpoint) + // TO-DO: Add ability for raw signing here, maybe? new raw option can be used for the conditional + /** + * if (opts.raw) { + * implement raw sign here + * } + */ + const { verifyingKey, signature } = await SigningService.signMessageWithAdapters({ msg }) + cliWrite({ verifyingKey, signature }) + process.exit(0) + }) + return signCommand +} diff --git a/src/sign/constants.ts b/src/sign/constants.ts new file mode 100644 index 00000000..4d497f0e --- /dev/null +++ b/src/sign/constants.ts @@ -0,0 +1,38 @@ +export const FLOW_CONTEXT = 'ENTROPY_SIGN' + +export const SIGNING_CONTENT = { + messageAction: { + name: 'messageAction', + message: 'Please choose how you would like to input your message to sign:', + choices: [ + 'Text Input', + // Input from file requires more design + // 'From a File', + ], + }, + textInput: { + name: "userInput", + message: "Enter the message you wish to sign (this will open your default editor):", + }, + pathToFile: { + name: 'pathToFile', + message: 'Enter the path to the file you wish to sign:', + }, + interactionChoice: { + name: "interactionChoice", + message: "What would you like to do?", + choices: [ + // "Raw Sign", + "Sign With Adapter", + "Exit to Main Menu", + ], + }, + hashingAlgorithmInput: { + name: 'hashingAlgorithm', + message: 'Enter the hashing algorigthm to be used:', + }, + auxiliaryDataInput: { + name: 'auxiliaryDataFile', + message: 'Enter path to file containing auxiliary data for signing:' + }, +} \ No newline at end of file diff --git a/src/sign/interaction.ts b/src/sign/interaction.ts new file mode 100644 index 00000000..1cec6a67 --- /dev/null +++ b/src/sign/interaction.ts @@ -0,0 +1,48 @@ +import { print } from "src/common/utils" +import { getMsgFromUser, /* interactionChoiceQuestions */ } from "./utils" +import inquirer from "inquirer" +import Entropy from "@entropyxyz/sdk" +import { EntropySign } from "./main" + +export async function entropySign (entropy: Entropy, endpoint: string) { + const signingService = new EntropySign(entropy, endpoint) + // const { interactionChoice } = await inquirer.prompt(interactionChoiceQuestions) + // switch (interactionChoice) { + // case 'Raw Sign': { + // const { msg, msgPath } = await getMsgFromUser(inquirer) + // const { hashingAlgorithm, auxiliaryDataFile } = await inquirer.prompt(rawSignParamsQuestions) + // let hash = hashingAlgorithm + // const auxiliaryData = JSON.parse(readFileSync(auxiliaryDataFile).toString()) + // if (JSON.parse(hashingAlgorithm)) { + // hash = JSON.parse(hashingAlgorithm) + // } + + // const { signature, verifyingKey } = await Sign.rawSignMessage({ msg, msgPath, hashingAlgorithm: hash, auxiliaryData }) + // print('msg to be signed:', msg) + // print('verifying key:', verifyingKey) + // print('signature:', signature) + // return + // } + // case 'Sign With Adapter': { + try { + const { msg } = await getMsgFromUser(inquirer) + const { signature, verifyingKey } = await signingService.signMessageWithAdapters({ msg }) + print('msg to be signed:', msg) + print('verifying key:', verifyingKey) + print('signature:', signature) + return + } catch (error) { + if (!entropy.signingManager.verifyingKey) { + console.error('Please register your Entropy account before signing'); + return 'exit' + } + throw error + } + // return + // } + // case 'Exit to Main Menu': + // return 'exit' + // default: + // throw new Error('Unrecognizable action') + // } +} diff --git a/src/sign/main.ts b/src/sign/main.ts new file mode 100644 index 00000000..e2210043 --- /dev/null +++ b/src/sign/main.ts @@ -0,0 +1,67 @@ +import Entropy from "@entropyxyz/sdk" +import { u8aToHex } from '@polkadot/util' +import { EntropyBase } from "../common/entropy-base"; +import { SignResult } from "./types"; +import { FLOW_CONTEXT } from "./constants"; +import { stringToHex } from "./utils"; + +export class EntropySign extends EntropyBase { + constructor (entropy: Entropy, endpoint: string) { + super({ entropy, endpoint, flowContext: FLOW_CONTEXT }) + } + + // async rawSign (entropy: Entropy, payload: RawSignPayload) { + // return entropy.sign(payload) + // } + + // async rawSignMessage ({ msg, msgPath, auxiliaryData, hashingAlgorithm }): Promise { + // const message = getMsgFromInputOrFile(msg, msgPath) + + // try { + // this.logger.log(`Msg to be signed: ${msg}`, 'SIGN_MSG') + // this.logger.log( `Verifying Key used: ${this.entropy.signingManager.verifyingKey}`) + // const signature = await rawSign( + // this.entropy, + // { + // sigRequestHash: stringAsHex(message), + // hash: hashingAlgorithm, + // auxiliaryData + // } + // ) + // const signatureHexString = u8aToHex(signature) + // this.logger.log(`Signature: ${signatureHexString}`) + + // return { signature: signatureHexString, verifyingKey: this.entropy.signingManager.verifyingKey } + // } catch (error) { + // this.logger.error('Error signing message', error) + // throw error + // } + // } + + async signMessageWithAdapters ({ msg }: { msg: string }): Promise { + try { + this.logger.log(`Msg to be signed: ${msg}`, 'SIGN_MSG') + this.logger.log( `Verifying Key used: ${this.entropy.signingManager.verifyingKey}`) + const signature: any = await this.entropy.signWithAdaptersInOrder({ + msg: { + msg: stringToHex(msg) + }, + // type + order: ['deviceKeyProxy', 'noop'], + // signatureVerifyingKey: verifyingKey + // auxillaryData + }) + + const signatureHexString = u8aToHex(signature) + this.logger.log(`Signature: ${signatureHexString}`) + + return { + signature: signatureHexString, + verifyingKey: this.entropy.signingManager.verifyingKey + } + } catch (error) { + this.logger.error('Error signing message', error) + throw error + } + } +} diff --git a/src/sign/types.ts b/src/sign/types.ts new file mode 100644 index 00000000..4e222b82 --- /dev/null +++ b/src/sign/types.ts @@ -0,0 +1,11 @@ +export interface SignResult { + signature: string + verifyingKey: string +} + +export interface RawSignPayload { + sigRequestHash: string + hash: any + auxiliaryData: any + signatureVerifyingKey?: string +} diff --git a/src/sign/utils.ts b/src/sign/utils.ts new file mode 100644 index 00000000..0b63020d --- /dev/null +++ b/src/sign/utils.ts @@ -0,0 +1,96 @@ +import { readFileSync } from "fs" +import { SIGNING_CONTENT } from "./constants" +import { isHex } from '@polkadot/util' + +export function stringToHex (str: string): string { + if (isHex(str)) return str; + return Buffer.from(str).toString('hex') +} + +export const interactionChoiceQuestions = [{ + type: "list", + name: SIGNING_CONTENT.interactionChoice.name, + message: SIGNING_CONTENT.interactionChoice.message, + choices: SIGNING_CONTENT.interactionChoice.choices +}] + +export const messageActionQuestions = [{ + type: 'list', + name: SIGNING_CONTENT.messageAction.name, + message: SIGNING_CONTENT.messageAction.message, + choices: SIGNING_CONTENT.messageAction.choices, +}] + +export const userInputQuestions = [{ + type: "editor", + name: SIGNING_CONTENT.textInput.name, + message: SIGNING_CONTENT.textInput.message, +}] + +export const filePathInputQuestions = [{ + type: 'input', + name: SIGNING_CONTENT.pathToFile.name, + message: SIGNING_CONTENT.pathToFile.message, +}] + +export const hashingAlgorithmQuestions = [{ + type: 'input', + name: SIGNING_CONTENT.hashingAlgorithmInput.name, + message: SIGNING_CONTENT.hashingAlgorithmInput.message, +}] + +export const auxiliaryDataQuestions = [{ + type: 'input', + name: SIGNING_CONTENT.auxiliaryDataInput.name, + message: SIGNING_CONTENT.auxiliaryDataInput.message, +}] + +export const rawSignParamsQuestions = [ + ...hashingAlgorithmQuestions, + ...auxiliaryDataQuestions +] + +export async function getMsgFromUser (inquirer) { + // let msg: string + // let msgPath: string + // const { messageAction } = await inquirer.prompt(messageActionQuestions) + // switch (messageAction) { + // case 'Text Input': { + const { userInput } = await inquirer.prompt(userInputQuestions) + const msg = userInput + // break + // } + // Msg input from a file requires more design + // case 'From a File': { + // const { pathToFile } = await inquirer.prompt(filePathInputQuestions) + // // TODO: relative/absolute path? encoding? + // msgPath = pathToFile + // break + // } + // default: { + // const error = new Error('SigningError: Unsupported User Input Action') + // this.logger.error('Error signing with adapter', error) + // return + // } + // } + return { + msg, + // msgPath + }; +} + +export function getMsgFromInputOrFile (msg?: string, msgPath?: string) { + let result: string = msg + if (!msg && !msgPath) { + throw new Error('SigningError: You must provide a message or path to a file') + } + if (!msg && msgPath) { + try { + result = readFileSync(msgPath, 'utf-8') + } catch (error) { + // noop + } + } + + return result +} diff --git a/src/transfer/command.ts b/src/transfer/command.ts new file mode 100644 index 00000000..0cdfe6fe --- /dev/null +++ b/src/transfer/command.ts @@ -0,0 +1,24 @@ +import { Command } from "commander" +import { accountOption, endpointOption, loadEntropy } from "src/common/utils-cli" +import { EntropyTransfer } from "./main" + +export function entropyTransferCommand () { + const transferCommand = new Command('transfer') + transferCommand + .description('Transfer funds between two Entropy accounts.') // TODO: name the output + .argument('', 'Account address funds will be sent to') + .argument('', 'Amount of funds to be moved (in "tokens")') + .addOption(accountOption()) + .addOption(endpointOption()) + .action(async (destination, amount, opts) => { + // TODO: destination as ? + const entropy = await loadEntropy(opts.account, opts.endpoint) + const transferService = new EntropyTransfer(entropy, opts.endpoint) + + await transferService.transfer(destination, amount) + + // cliWrite(??) // TODO: write the output + process.exit(0) + }) + return transferCommand +} diff --git a/src/transfer/constants.ts b/src/transfer/constants.ts new file mode 100644 index 00000000..5529b5be --- /dev/null +++ b/src/transfer/constants.ts @@ -0,0 +1,12 @@ +export const TRANSFER_CONTENT = { + amount: { + name: 'amount', + message: 'Input amount to transfer:', + default: '1', + invalidError: 'Please enter a value greater than 0', + }, + recipientAddress: { + name: 'recipientAddress', + message: `Input recipient's address:`, + }, +} \ No newline at end of file diff --git a/src/transfer/interaction.ts b/src/transfer/interaction.ts new file mode 100644 index 00000000..e0e43097 --- /dev/null +++ b/src/transfer/interaction.ts @@ -0,0 +1,16 @@ +import inquirer from "inquirer" +import { print } from "../common/utils" +import { EntropyTransfer } from "./main" +import { transferInputQuestions } from "./utils" +import { setupProgress } from "src/common/progress" + +export async function entropyTransfer (entropy, endpoint) { + const progressTracker = setupProgress('Transferring Funds') + const transferService = new EntropyTransfer(entropy, endpoint) + const { amount, recipientAddress } = await inquirer.prompt(transferInputQuestions) + await transferService.transfer(recipientAddress, amount, progressTracker) + print('') + print(`Transaction successful: Sent ${amount} to ${recipientAddress}`) + print('') + print('Press enter to return to main menu') +} diff --git a/src/transfer/main.ts b/src/transfer/main.ts new file mode 100644 index 00000000..e796fec4 --- /dev/null +++ b/src/transfer/main.ts @@ -0,0 +1,59 @@ +import Entropy from "@entropyxyz/sdk"; + +import { EntropyBase } from "../common/entropy-base"; +import { formatDispatchError } from "../common/utils"; +import { BITS_PER_TOKEN } from "../common/constants"; +import { TransferOptions } from "./types"; + +const FLOW_CONTEXT = 'ENTROPY_TRANSFER' + +export class EntropyTransfer extends EntropyBase { + constructor (entropy: Entropy, endpoint: string) { + super({ entropy, endpoint, flowContext: FLOW_CONTEXT }) + } + + // NOTE: a more accessible function which handles + // - setting `from` + // - converting `amount` (string => BigInt) + // - progress callbacks (optional) + + async transfer (toAddress: string, amount: string, progress?: { start: ()=>void, stop: ()=>void }) { + const formattedAmount = BigInt(Number(amount) * BITS_PER_TOKEN) + + if (progress) progress.start() + try { + await this.rawTransfer({ + from: this.entropy.keyring.accounts.registration.pair, + to: toAddress, + amount: formattedAmount + }) + if (progress) return progress.stop() + } catch (error) { + if (progress) return progress.stop() + throw error + } + } + + private async rawTransfer (payload: TransferOptions): Promise { + const { from, to, amount } = payload + + return new Promise((resolve, reject) => { + // WARN: await signAndSend is dangerous as it does not resolve + // after transaction is complete :melt: + this.entropy.substrate.tx.balances + .transferAllowDeath(to, amount) + // @ts-ignore + .signAndSend(from, ({ status, dispatchError }) => { + if (dispatchError) { + const error = formatDispatchError(this.entropy, dispatchError) + this.logger.error('There was an issue sending this transfer', error) + return reject(error) + } + + if (status.isFinalized) resolve(status) + }) + }) + } + +} + diff --git a/src/flows/entropyTransfer/types.ts b/src/transfer/types.ts similarity index 85% rename from src/flows/entropyTransfer/types.ts rename to src/transfer/types.ts index c23b6b77..365cdaac 100644 --- a/src/flows/entropyTransfer/types.ts +++ b/src/transfer/types.ts @@ -1,7 +1,8 @@ // @ts-ignore import { Pair } from '@entropyxyz/sdk/keys' + export interface TransferOptions { from: Pair to: string - amount: bigint -} \ No newline at end of file + amount: bigint +} diff --git a/src/transfer/utils.ts b/src/transfer/utils.ts new file mode 100644 index 00000000..d14ed76a --- /dev/null +++ b/src/transfer/utils.ts @@ -0,0 +1,27 @@ +import { TRANSFER_CONTENT } from "./constants"; + +function validateAmount (amount: string | number) { + if (isNaN(amount as number) || parseInt(amount as string) <= 0) { + return TRANSFER_CONTENT.amount.invalidError + } + return true +} + +const amountQuestion = { + type: 'input', + name: TRANSFER_CONTENT.amount.name, + message: TRANSFER_CONTENT.amount.message, + default: TRANSFER_CONTENT.amount.default, + validate: validateAmount +} + +const recipientAddressQuestion = { + type: 'input', + name: TRANSFER_CONTENT.recipientAddress.name, + message: TRANSFER_CONTENT.recipientAddress.message, +} + +export const transferInputQuestions = [ + amountQuestion, + recipientAddressQuestion +] \ No newline at end of file diff --git a/src/tui.ts b/src/tui.ts index 872fd5ee..9624cd64 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -1,65 +1,89 @@ import inquirer from 'inquirer' +import Entropy from '@entropyxyz/sdk' import * as config from './config' -import * as flows from './flows' import { EntropyTuiOptions } from './types' import { logo } from './common/ascii' -import { print } from './common/utils' +import { jumpStartNetwork, print } from './common/utils' +import { loadEntropy } from './common/utils-cli' import { EntropyLogger } from './common/logger' +import { entropyAccount, entropyRegister } from './account/interaction' +import { entropySign } from './sign/interaction' +import { entropyBalance } from './balance/interaction' +import { entropyTransfer } from './transfer/interaction' +import { entropyFaucet } from './faucet/interaction' +import { entropyProgram, entropyProgramDev } from './program/interaction' +import yoctoSpinner from 'yocto-spinner' -let shouldInit = true +async function setupConfig () { + let storedConfig = await config.get() + + // set selectedAccount if we can + if (!storedConfig.selectedAccount && storedConfig.accounts.length) { + storedConfig = await config.setSelectedAccount(storedConfig.accounts[0]) + } + + return storedConfig +} // tui = text user interface -export default function tui (options: EntropyTuiOptions) { +export default function tui (entropy: Entropy, options: EntropyTuiOptions) { const logger = new EntropyLogger('TUI', options.endpoint) console.clear() console.log(logo) // the Entropy logo logger.debug(options) - const choices = { - 'Manage Accounts': flows.manageAccounts, - 'Balance': flows.checkBalance, - 'Register': flows.register, - 'Sign': flows.sign, - 'Transfer': flows.entropyTransfer, - 'Deploy Program': flows.devPrograms, - 'User Programs': flows.userPrograms, - // 'Construct an Ethereum Tx': flows.ethTransaction, + let choices = [ + 'Manage Accounts', + 'Entropy Faucet', + 'Balance', + 'Register', + 'Sign', + 'Transfer', + // TODO: design programs in TUI (merge deploy+user programs) + 'Deploy Program', + 'User Programs', + ] + + const devChoices = [ + 'Jumpstart Network', + // 'Create and Fund Faucet(s)' + ] + + if (options.dev) { + choices = [...choices, ...devChoices] } - // const devChoices = { - // // 'Entropy Faucet': flows.entropyFaucet, - // } - - // if (options.dev) Object.assign(choices, devChoices) - // assign exit so its last - Object.assign(choices, { 'Exit': async () => {} }) + choices = [...choices, 'Exit'] - main(choices, options, logger) + main(entropy, choices, options, logger) } +const loader = yoctoSpinner() -async function main (choices, options, logger: EntropyLogger) { - if (shouldInit) { - await config.init() - shouldInit = false - } - - let storedConfig = await config.get() +async function main (entropy: Entropy, choices: string[], options: EntropyTuiOptions, logger: EntropyLogger) { + if (loader.isSpinning) loader.stop() + const storedConfig = await setupConfig() - // if there are accounts available and selected account is not set, - // first account in list is set as the selected account - if (!storedConfig.selectedAccount && storedConfig.accounts.length) { - await config.set({ ...storedConfig, ...{ selectedAccount: storedConfig.accounts[0].address } }) - storedConfig = await config.get() + // Entropy is undefined on initial install, after user creates their first account, + // entropy should be loaded + if (storedConfig.selectedAccount && !entropy) { + entropy = await loadEntropy(storedConfig.selectedAccount, options.tuiEndpoint) } + // If the selected account changes within the TUI we need to reset the entropy instance being used + const currentAccount = entropy?.keyring?.accounts?.registration?.address + if (currentAccount && currentAccount !== storedConfig.selectedAccount) { + await entropy.close() + entropy = await loadEntropy(storedConfig.selectedAccount, options.tuiEndpoint); + } + const answers = await inquirer.prompt([{ type: 'list', name: 'choice', message: 'Select Action', - pageSize: Object.keys(choices).length, - choices: Object.keys(choices), + pageSize: choices.length, + choices, }]) if (answers.choice === 'Exit') { @@ -73,16 +97,83 @@ async function main (choices, options, logger: EntropyLogger) { console.error('There are currently no accounts available, please create or import your new account using the Manage Accounts feature') } else { logger.debug(answers) - const newConfigUpdates = await choices[answers.choice](storedConfig, options, logger) - if (typeof newConfigUpdates === 'string' && newConfigUpdates === 'exit') { - returnToMain = true - } else { - await config.set({ ...storedConfig, ...newConfigUpdates }) + + switch (answers.choice) { + case 'Manage Accounts': { + const response = await entropyAccount(options.tuiEndpoint, storedConfig) + if (response === 'exit') { returnToMain = true } + break + } + case 'Register': { + await entropyRegister(entropy, options.tuiEndpoint, storedConfig) + break + } + case 'Balance': { + await entropyBalance(entropy, options.tuiEndpoint, storedConfig) + .catch(err => console.error('There was an error retrieving balance', err)) + break + } + case 'Transfer': { + await entropyTransfer(entropy, options.tuiEndpoint) + .catch(err => console.error('There was an error sending the transfer', err)) + break + } + case 'Sign': { + await entropySign(entropy, options.tuiEndpoint) + .catch(err => console.error('There was an issue with signing', err)) + break + } + case 'Entropy Faucet': { + try { + await entropyFaucet(entropy, options, logger) + } catch (error) { + console.error('There was an issue with running the faucet', error); + } + break + } + case 'User Programs': { + await entropyProgram(entropy, options.tuiEndpoint) + .catch(err => console.error('There was an error with programs', err)) + break + } + case 'Deploy Program': { + await entropyProgramDev(entropy, options.tuiEndpoint) + .catch(err => console.error('There was an error with program dev', err)) + break + } + case 'Jumpstart Network': { + // TO-DO: possibly move this to it's own directory similar to the other actions + // could create a new system directory for system/network level functionality + // i.e jumpstarting, deploy faucet, etc. + loader.text = 'Jumpstarting Network...' + try { + loader.start() + const jumpStartStatus = await jumpStartNetwork(entropy, options.tuiEndpoint) + + if (jumpStartStatus.isFinalized) { + loader.clear() + loader.success('Network jumpstarted!') + // running into an issue where the loader displays the success message but the return to main menu + // prompt does not display, so for now exiting process + process.exit(0) + } + } catch (error) { + loader.text = 'Jumpstart Failed' + loader.stop() + loader.clear() + console.error('There was an issue jumpstarting the network', error); + process.exit(1) + } + break + } + default: { + console.error('Unsupported Action:' + answers.choice) + break + } } - storedConfig = await config.get() } - if (!returnToMain) { + if (returnToMain === undefined) { ({ returnToMain } = await inquirer.prompt([{ type: 'confirm', name: 'returnToMain', @@ -90,7 +181,7 @@ async function main (choices, options, logger: EntropyLogger) { }])) } - if (returnToMain) main(choices, options, logger) + if (returnToMain) main(entropy, choices, options, logger) else { print('Have a nice day') process.exit() diff --git a/src/types/index.ts b/src/types/index.ts index af964cd7..9a912b54 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,6 +1,10 @@ export interface EntropyTuiOptions { - dev: boolean + account: string endpoint: string + tuiEndpoint: string + dev: boolean + version: string + coreVersion: string } type EntropyLoggerLogLevel = 'error' | 'warn' | 'info' | 'debug' @@ -9,4 +13,4 @@ export interface EntropyLoggerOptions { debug?: boolean level?: EntropyLoggerLogLevel isTesting?: boolean -} \ No newline at end of file +} diff --git a/tests/account.test.ts b/tests/account.test.ts new file mode 100644 index 00000000..8eec5faa --- /dev/null +++ b/tests/account.test.ts @@ -0,0 +1,138 @@ +import test from 'tape' +import { Entropy, wasmGlobalsReady } from '@entropyxyz/sdk' +// @ts-ignore +import { isValidSubstrateAddress } from '@entropyxyz/sdk/utils' +import { jumpStartNetwork } from '@entropyxyz/sdk/testing' +// @ts-ignore +import Keyring from '@entropyxyz/sdk/keys' +import { randomAsHex } from '@polkadot/util-crypto' +import { EntropyAccount } from '../src/account/main' +import { EntropyTransfer } from '../src/transfer/main' +import { EntropyAccountConfig, EntropyConfig } from '../src/config/types' +import * as config from '../src/config' +import { promiseRunner, setupTest } from './testing-utils' +import { charlieStashAddress, charlieStashSeed, eveSeed } from './testing-utils/constants' +import { readFileSync } from 'fs' + +test('Account - list', async t => { + const account: EntropyAccountConfig = { + name: 'Test Config', + address: charlieStashAddress, + data: { + seed: charlieStashSeed, + registration: { + verifyingKeys: ['this-is-a-verifying-key'], + seed: charlieStashSeed, + address: charlieStashAddress, + path: '//Charlie' + } + } + } + const config: EntropyConfig = { + accounts: [account], + endpoints: { + dev: 'ws://127.0.0.1:9944', + 'test-net': 'wss://testnet.entropy.xyz', + }, + selectedAccount: account.name, + 'migration-version': '0' + } + + const accountsArray = EntropyAccount.list(config) + + t.deepEqual(accountsArray, [{ + name: account.name, + address: account.address, + verifyingKeys: account?.data?.registration?.verifyingKeys + }]) + + // Resetting accounts on config to test for empty list + config.accounts = [] + try { + EntropyAccount.list(config) + } catch (error) { + const msg = error.message + t.equal(msg, 'AccountsError: There are currently no accounts available, please create or import a new account using the Manage Accounts feature') + } + + t.end() +}) + +let counter = 0 + +test('Account - import', async t => { + const configPath = `/tmp/entropy-cli-${Date.now()}_${counter++}.json` + /* Setup */ + const run = promiseRunner(t) + await run('wasm', wasmGlobalsReady()) + await run('config.init', config.init(configPath)) + const testAccountSeed = randomAsHex(32) + const testAccountName = 'Test Account' + const newAccount = await EntropyAccount.import({ name: testAccountName, seed: testAccountSeed }) + + const testKeyring = new Keyring({ seed: testAccountSeed, path: 'none', debug: true }) + const { admin } = testKeyring.getAccount() + + const isValidAddress = isValidSubstrateAddress(newAccount.address) + + t.ok(isValidAddress, 'Valid address created') + t.equal(newAccount.address, admin?.address, 'Generated Account matches Account created by Keyring') + t.end() +}) + +const endpoint = 'ws://127.0.0.1:9944' + +async function fundAccount (t, entropy: Entropy) { + const { entropy: charlie } = await setupTest(t, { seed: charlieStashSeed }) + const transfer = new EntropyTransfer(charlie, endpoint) + + await transfer.transfer(entropy.keyring.accounts.registration.address, "1000") +} + + +test('Account - Register: Default Program', async (t) => { + const { run, entropy: naynay } = await setupTest(t) + // NOTE: we fund a new account "naynay" because jumpStart has problems with charlie (T_T) + await run('fund naynay', fundAccount(t, naynay)) + + await run('jump-start network', jumpStartNetwork(naynay)) + + const account = new EntropyAccount(naynay, endpoint) + const verifyingKey = await run('register account', account.register()) + + const fullAccount = naynay.keyring.getAccount() + t.equal(verifyingKey, fullAccount?.registration?.verifyingKeys?.[0], 'verifying key matches key added to registration account') + + t.end() +}) + +test('Account - Register: Barebones Program', async t => { + const { run, entropy: naynay } = await setupTest(t) + await run('fund naynay', fundAccount(t, naynay)) + // NOTE: we fund a new account "naynay" because jumpStart has problems with charlie (T_T) + + const dummyProgram: any = readFileSync( + new URL('./programs/template_barebones.wasm', import.meta.url) + ) + + await run('jump-start network', jumpStartNetwork(naynay)) + + const account = new EntropyAccount(naynay, endpoint) + const pointer = await run( + 'deploy program', + naynay.programs.dev.deploy(dummyProgram) + ) + const verifyingKey = await run( + 'register account - with custom params', + account.register({ + programModAddress: naynay.keyring.accounts.registration.address, + programData: [{ program_pointer: pointer, program_config: '0x' }], + }) + ) + + const fullAccount = naynay.keyring.getAccount() + t.equal(verifyingKey, fullAccount?.registration?.verifyingKeys?.[0], 'verifying key matches key added to registration account') + + t.end() +}) + diff --git a/tests/balance.test.ts b/tests/balance.test.ts index 2d05b25e..56b2c3c3 100644 --- a/tests/balance.test.ts +++ b/tests/balance.test.ts @@ -1,32 +1,32 @@ import test from 'tape' -import { getBalance, getBalances } from '../src/flows/balance/balance' import { setupTest, charlieStashAddress as richAddress } from './testing-utils' +import { EntropyBalance } from '../src/balance/main' -const networkType = 'two-nodes' +const networkType = 'four-nodes' test('getBalance + getBalances', async (t) => { - const { run, entropy } = await setupTest(t, { networkType }) - + const { run, entropy, endpoint } = await setupTest(t, { networkType }) + const balanceService = new EntropyBalance(entropy, endpoint) const newAddress = entropy.keyring.accounts.registration.address /* getBalance */ const newAddressBalance = await run( 'getBalance (newSeed)', - getBalance(entropy, newAddress) + balanceService.getBalance(newAddress) ) t.equal(newAddressBalance, 0, 'newSeed balance = 0') const richAddressBalance = await run( 'getBalance (richAddress)', - getBalance(entropy, richAddress) + balanceService.getBalance(richAddress) ) t.true(richAddressBalance > BigInt(10e10), 'richAddress balance >>> 0') /* getBalances */ const balances = await run( 'getBalances', - getBalances(entropy, [newAddress, richAddress]) + balanceService.getBalances([newAddress, richAddress]) ) t.deepEqual( balances, @@ -40,7 +40,7 @@ test('getBalance + getBalances', async (t) => { const badAddresses = ['5Cz6BfUaxxXCA3jninzxdan4JdmC1NVpgkiRPYhXbhr', '5Cz6BfUaxxXCA3jninzxdan4JdmC1NVpgkiRPYhXbhrfnD'] const balancesWithNoGoodAddress = await run( 'getBalances::one good address', - getBalances(entropy, badAddresses) + balanceService.getBalances(badAddresses) ) badAddresses.forEach(addr => { diff --git a/tests/common.test.ts b/tests/common.test.ts new file mode 100644 index 00000000..8ec05314 --- /dev/null +++ b/tests/common.test.ts @@ -0,0 +1,53 @@ +import test from 'tape' + +import { maskPayload } from '../src/common/masking' + +test('common/masking', async (t) => { + + t.deepEqual( + maskPayload('dog'), + 'dog', + 'handles string' + ) + + t.deepEqual( + maskPayload(null), + null, + 'handles null' + ) + + t.deepEqual( + maskPayload(true), + true, + 'handles bool' + ) + + const buildPayload = () => { + return { + nested: { + seed: 'secrets', + secretKey: new Uint8Array([1,2,3]), + publicKey: new Uint8Array([4,5,6]), + arr: ['a', 'b', 'c'], + obj: { "0": 17, "1": 23 } + } + } + } + const payload = buildPayload() + const expected = { + nested: { + seed: '*'.repeat('secrets'.length), + secretKey: '***', + publicKey: 'data:application/UI8A;base64,BAUG', + arr: ['a', 'b', 'c'], + obj: { "0": 17, "1": 23 } + } + } + t.deepEqual(maskPayload(payload), expected, 'nested mess') + t.deepEqual(payload, buildPayload(), 'maskPayload does not mutate') + + + t.deepEqual(maskPayload([payload, payload]), [expected, expected], 'arrays') + + t.end() +}) diff --git a/tests/config.test.ts b/tests/config.test.ts index 767f214c..5e2d0215 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -2,6 +2,7 @@ import test from 'tape' import { writeFile } from 'node:fs/promises' import migrations from '../src/config/migrations' import { migrateData, init, get, set } from '../src/config' +import * as encoding from '../src/config/encoding' // used to ensure unique test ids let id = Date.now() @@ -65,29 +66,41 @@ test('config - migrateData', async t => { t.end() }) +const makeKey = () => new Uint8Array( + Array(32).fill(0).map((_, i) => i * 2 + 1) +) + test('config - get', async t => { const configPath = makeTmpPath() - const config = { boop: 'doop' } - await writeFile(configPath, JSON.stringify(config)) + const config = { + boop: 'doop', + secretKey: makeKey() + } + await writeFile(configPath, encoding.serialize(config)) const result = await get(configPath) t.deepEqual(result, config, 'get works') + const MSG = 'path that does not exist fails' await get('/tmp/junk') - .then(() => t.fail('bad path should fail')) + .then(() => t.fail(MSG)) .catch(err => { - t.match(err.message, /no such file/, 'bad path should fail') + t.match(err.message, /ENOENT/, MSG) }) }) test('config - set', async t => { const configPath = makeTmpPath() - const config = { dog: true } + const config = { + dog: true, + secretKey: makeKey() + } + // @ts-expect-error : this is a breaking test await set(config, configPath) const actual = await get(configPath) - t.deepEqual(actual, config, 'set works') + t.deepEqual(config, actual, 'set works') t.end() }) @@ -146,3 +159,110 @@ test('config - init (migration)', async t => { t.end() }) + + +test('config/migrattions/02', t => { + const initial = JSON.parse( + '{"accounts":[{"name":"Mix","address":"5GCaN3fcL6vAQQKamHzVSorwv2XqtM3WcxosCLd9JqGVrtS8","data":{"debug":true,"seed":"0xc4c466182b86ff1f4a16548df79c5808ab9bcde87c22c27938ac9aabc4300840","admin":{"address":"5GCaN3fcL6vAQQKamHzVSorwv2XqtM3WcxosCLd9JqGVrtS8","type":"registration","verifyingKeys":["0x03b225d2032e1dbff26316cc8b7d695b3386400d30ce004c1b42e2c28bcd834039"],"userContext":"ADMIN_KEY","seed":"0xc4c466182b86ff1f4a16548df79c5808ab9bcde87c22c27938ac9aabc4300840","path":"","pair":{"address":"5GCaN3fcL6vAQQKamHzVSorwv2XqtM3WcxosCLd9JqGVrtS8","addressRaw":{"0":182,"1":241,"2":171,"3":246,"4":239,"5":100,"6":192,"7":41,"8":49,"9":32,"10":10,"11":84,"12":241,"13":225,"14":183,"15":152,"16":164,"17":182,"18":176,"19":244,"20":39,"21":237,"22":74,"23":225,"24":250,"25":244,"26":187,"27":129,"28":97,"29":222,"30":33,"31":116},"isLocked":false,"meta":{},"publicKey":{"0":182,"1":241,"2":171,"3":246,"4":239,"5":100,"6":192,"7":41,"8":49,"9":32,"10":10,"11":84,"12":241,"13":225,"14":183,"15":152,"16":164,"17":182,"18":176,"19":244,"20":39,"21":237,"22":74,"23":225,"24":250,"25":244,"26":187,"27":129,"28":97,"29":222,"30":33,"31":116},"type":"sr25519","secretKey":{"0":120,"1":247,"2":1,"3":38,"4":246,"5":195,"6":0,"7":49,"8":84,"9":240,"10":226,"11":144,"12":66,"13":172,"14":130,"15":168,"16":237,"17":74,"18":121,"19":243,"20":49,"21":217,"22":208,"23":70,"24":160,"25":220,"26":125,"27":114,"28":230,"29":17,"30":254,"31":71,"32":158,"33":68,"34":133,"35":24,"36":119,"37":34,"38":46,"39":154,"40":85,"41":62,"42":178,"43":69,"44":206,"45":217,"46":132,"47":184,"48":8,"49":219,"50":89,"51":165,"52":189,"53":106,"54":6,"55":51,"56":112,"57":76,"58":42,"59":157,"60":146,"61":130,"62":203,"63":241}},"used":true},"registration":{"address":"5GCaN3fcL6vAQQKamHzVSorwv2XqtM3WcxosCLd9JqGVrtS8","type":"registration","verifyingKeys":["0x03b225d2032e1dbff26316cc8b7d695b3386400d30ce004c1b42e2c28bcd834039"],"userContext":"ADMIN_KEY","seed":"0xc4c466182b86ff1f4a16548df79c5808ab9bcde87c22c27938ac9aabc4300840","path":"","pair":{"address":"5GCaN3fcL6vAQQKamHzVSorwv2XqtM3WcxosCLd9JqGVrtS8","addressRaw":{"0":182,"1":241,"2":171,"3":246,"4":239,"5":100,"6":192,"7":41,"8":49,"9":32,"10":10,"11":84,"12":241,"13":225,"14":183,"15":152,"16":164,"17":182,"18":176,"19":244,"20":39,"21":237,"22":74,"23":225,"24":250,"25":244,"26":187,"27":129,"28":97,"29":222,"30":33,"31":116},"isLocked":false,"meta":{},"publicKey":{"0":182,"1":241,"2":171,"3":246,"4":239,"5":100,"6":192,"7":41,"8":49,"9":32,"10":10,"11":84,"12":241,"13":225,"14":183,"15":152,"16":164,"17":182,"18":176,"19":244,"20":39,"21":237,"22":74,"23":225,"24":250,"25":244,"26":187,"27":129,"28":97,"29":222,"30":33,"31":116},"type":"sr25519","secretKey":{"0":120,"1":247,"2":1,"3":38,"4":246,"5":195,"6":0,"7":49,"8":84,"9":240,"10":226,"11":144,"12":66,"13":172,"14":130,"15":168,"16":237,"17":74,"18":121,"19":243,"20":49,"21":217,"22":208,"23":70,"24":160,"25":220,"26":125,"27":114,"28":230,"29":17,"30":254,"31":71,"32":158,"33":68,"34":133,"35":24,"36":119,"37":34,"38":46,"39":154,"40":85,"41":62,"42":178,"43":69,"44":206,"45":217,"46":132,"47":184,"48":8,"49":219,"50":89,"51":165,"52":189,"53":106,"54":6,"55":51,"56":112,"57":76,"58":42,"59":157,"60":146,"61":130,"62":203,"63":241}},"used":true},"deviceKey":{"address":"5GCaN3fcL6vAQQKamHzVSorwv2XqtM3WcxosCLd9JqGVrtS8","type":"deviceKey","verifyingKeys":["0x03b225d2032e1dbff26316cc8b7d695b3386400d30ce004c1b42e2c28bcd834039"],"userContext":"CONSUMER_KEY","seed":"0xc4c466182b86ff1f4a16548df79c5808ab9bcde87c22c27938ac9aabc4300840","path":"","pair":{"address":"5GCaN3fcL6vAQQKamHzVSorwv2XqtM3WcxosCLd9JqGVrtS8","addressRaw":{"0":182,"1":241,"2":171,"3":246,"4":239,"5":100,"6":192,"7":41,"8":49,"9":32,"10":10,"11":84,"12":241,"13":225,"14":183,"15":152,"16":164,"17":182,"18":176,"19":244,"20":39,"21":237,"22":74,"23":225,"24":250,"25":244,"26":187,"27":129,"28":97,"29":222,"30":33,"31":116},"isLocked":false,"meta":{},"publicKey":{"0":182,"1":241,"2":171,"3":246,"4":239,"5":100,"6":192,"7":41,"8":49,"9":32,"10":10,"11":84,"12":241,"13":225,"14":183,"15":152,"16":164,"17":182,"18":176,"19":244,"20":39,"21":237,"22":74,"23":225,"24":250,"25":244,"26":187,"27":129,"28":97,"29":222,"30":33,"31":116},"type":"sr25519","secretKey":{"0":120,"1":247,"2":1,"3":38,"4":246,"5":195,"6":0,"7":49,"8":84,"9":240,"10":226,"11":144,"12":66,"13":172,"14":130,"15":168,"16":237,"17":74,"18":121,"19":243,"20":49,"21":217,"22":208,"23":70,"24":160,"25":220,"26":125,"27":114,"28":230,"29":17,"30":254,"31":71,"32":158,"33":68,"34":133,"35":24,"36":119,"37":34,"38":46,"39":154,"40":85,"41":62,"42":178,"43":69,"44":206,"45":217,"46":132,"47":184,"48":8,"49":219,"50":89,"51":165,"52":189,"53":106,"54":6,"55":51,"56":112,"57":76,"58":42,"59":157,"60":146,"61":130,"62":203,"63":241}},"used":true}}}],"selectedAccount":"5GCaN3fcL6vAQQKamHzVSorwv2XqtM3WcxosCLd9JqGVrtS8","endpoints":{"dev":"ws://127.0.0.1:9944","test-net":"wss://testnet.entropy.xyz"},"migration-version":1}' + ) + + const migrated = migrations[2].migrate(initial) + + // console.log(encoding.serialize(migrated)) + // { + // "accounts": [ + // { + // "name": "Mix", + // "address": "5GCaN3fcL6vAQQKamHzVSorwv2XqtM3WcxosCLd9JqGVrtS8", + // "data": { + // "debug": true, + // "seed": "0xc4c466182b86ff1f4a16548df79c5808ab9bcde87c22c27938ac9aabc4300840", + // "admin": { + // "address": "5GCaN3fcL6vAQQKamHzVSorwv2XqtM3WcxosCLd9JqGVrtS8", + // "type": "registration", + // "verifyingKeys": [ + // "0x03b225d2032e1dbff26316cc8b7d695b3386400d30ce004c1b42e2c28bcd834039" + // ], + // "userContext": "ADMIN_KEY", + // "seed": "0xc4c466182b86ff1f4a16548df79c5808ab9bcde87c22c27938ac9aabc4300840", + // "path": "", + // "pair": { + // "address": "5GCaN3fcL6vAQQKamHzVSorwv2XqtM3WcxosCLd9JqGVrtS8", + // "addressRaw": "data:application/UI8A;base64,tvGr9u9kwCkxIApU8eG3mKS2sPQn7Urh+vS7gWHeIXQ=", + // "isLocked": false, + // "meta": {}, + // "publicKey": "data:application/UI8A;base64,tvGr9u9kwCkxIApU8eG3mKS2sPQn7Urh+vS7gWHeIXQ=", + // "type": "sr25519", + // "secretKey": "data:application/UI8A;base64,ePcBJvbDADFU8OKQQqyCqO1KefMx2dBGoNx9cuYR/keeRIUYdyIumlU+skXO2YS4CNtZpb1qBjNwTCqdkoLL8Q==" + // }, + // "used": true + // }, + // "registration": { + // "address": "5GCaN3fcL6vAQQKamHzVSorwv2XqtM3WcxosCLd9JqGVrtS8", + // "type": "registration", + // "verifyingKeys": [ + // "0x03b225d2032e1dbff26316cc8b7d695b3386400d30ce004c1b42e2c28bcd834039" + // ], + // "userContext": "ADMIN_KEY", + // "seed": "0xc4c466182b86ff1f4a16548df79c5808ab9bcde87c22c27938ac9aabc4300840", + // "path": "", + // "pair": { + // "address": "5GCaN3fcL6vAQQKamHzVSorwv2XqtM3WcxosCLd9JqGVrtS8", + // "addressRaw": "data:application/UI8A;base64,tvGr9u9kwCkxIApU8eG3mKS2sPQn7Urh+vS7gWHeIXQ=", + // "isLocked": false, + // "meta": {}, + // "publicKey": "data:application/UI8A;base64,tvGr9u9kwCkxIApU8eG3mKS2sPQn7Urh+vS7gWHeIXQ=", + // "type": "sr25519", + // "secretKey": "data:application/UI8A;base64,ePcBJvbDADFU8OKQQqyCqO1KefMx2dBGoNx9cuYR/keeRIUYdyIumlU+skXO2YS4CNtZpb1qBjNwTCqdkoLL8Q==" + // }, + // "used": true + // }, + // "deviceKey": { + // "address": "5GCaN3fcL6vAQQKamHzVSorwv2XqtM3WcxosCLd9JqGVrtS8", + // "type": "deviceKey", + // "verifyingKeys": [ + // "0x03b225d2032e1dbff26316cc8b7d695b3386400d30ce004c1b42e2c28bcd834039" + // ], + // "userContext": "CONSUMER_KEY", + // "seed": "0xc4c466182b86ff1f4a16548df79c5808ab9bcde87c22c27938ac9aabc4300840", + // "path": "", + // "pair": { + // "address": "5GCaN3fcL6vAQQKamHzVSorwv2XqtM3WcxosCLd9JqGVrtS8", + // "addressRaw": "data:application/UI8A;base64,tvGr9u9kwCkxIApU8eG3mKS2sPQn7Urh+vS7gWHeIXQ=", + // "isLocked": false, + // "meta": {}, + // "publicKey": "data:application/UI8A;base64,tvGr9u9kwCkxIApU8eG3mKS2sPQn7Urh+vS7gWHeIXQ=", + // "type": "sr25519", + // "secretKey": "data:application/UI8A;base64,ePcBJvbDADFU8OKQQqyCqO1KefMx2dBGoNx9cuYR/keeRIUYdyIumlU+skXO2YS4CNtZpb1qBjNwTCqdkoLL8Q==" + // }, + // "used": true + // } + // } + // } + // ], + // "selectedAccount": "5GCaN3fcL6vAQQKamHzVSorwv2XqtM3WcxosCLd9JqGVrtS8", + // "endpoints": { + // "dev": "ws://127.0.0.1:9944", + // "test-net": "wss://testnet.entropy.xyz" + // }, + // "migration-version": 1 + // } + + const targetKeys = ['addressRaw', 'publicKey', 'secretKey'] + + // @ts-ignore + migrated.accounts.forEach(account => { + return Object.keys(account.data).forEach(subAccount => { + if (typeof account.data[subAccount] !== 'object') return + + t.true( + targetKeys.every(targetKey => { + return account.data[subAccount].pair[targetKey] instanceof Uint8Array + }), + `migrated: ${subAccount}` + ) + }) + }) + + t.end() +}) diff --git a/tests/e2e.cli.sh b/tests/e2e.cli.sh new file mode 100755 index 00000000..119f1ad7 --- /dev/null +++ b/tests/e2e.cli.sh @@ -0,0 +1,90 @@ +#! /usr/bin/bash + +# WARNING - this script nukes your config! +# +# Dependencies +# - internet connection +# - jq - see https://jqlang.github.io/jq +# +# Run +# $ yarn build && ./tests/e2e.cli.sh + +rm ~/.config/entropy-cryptography/entropy-cli.json + +print () { + COLOR='\033[0;35m' + RESET='\033[0m' + echo "" + echo -e "${COLOR}> $1${RESET}" +} + +print "// ACCOUNT /////////////////////////////////////////////////" + +print "account ls" +entropy account ls | jq + +print "account create" +entropy account create naynay | jq + +print "account import" +entropy account import faucet 0x358f394d157e31be23313a1500f5e2c8871e514e530a35aa5c05334be7a39ba6 | jq + +print "account list" +entropy account list | jq + + + +print "// BALANCE ///////////////////////////////////////////////// " + +print "balance (name)" +entropy balance naynay + +print "balance (address)" +entropy balance 5CqJyjALDFz4sKjQgK8NXBQGHCWAiV63xXn2Dye393Y6Vghz + + + +print "// TRANSFER ////////////////////////////////////////////////" + +print "transfer" +NAYNAY_ADDRESS=`entropy account ls | jq --raw-output ".[0].address"` +entropy transfer -a faucet ${NAYNAY_ADDRESS} 2.5 + +print "balance" +entropy balance naynay + + + +print "// REGISTER ////////////////////////////////////////////////" + +print "register" +entropy account register -a naynay +# NOTE, this does not work: +# entropy account -a naynay register + +print "account ls" +entropy account ls | jq + +# print "entropy register (again)" +# entropy account register -a naynay + + + +print "// SIGN ////////////////////////////////////////////////////" + + +print "entropy sign" +entropy sign -a naynay "some content!\nNICE&SIMPLE" + + + +print "// PROGRAM /////////////////////////////////////////////////" + +print "program deploy" +echo "wasm junk - $(date)" > /tmp/entropy.fake.wasm +echo '{"type": "object"}' > /tmp/entropy.configSchema.fake.json +echo '{"type": "object"}' > /tmp/entropy.auxDataSchema.fake.json +entropy program deploy -a naynay \ + /tmp/entropy.fake.wasm \ + /tmp/entropy.configSchema.fake.json \ + /tmp/entropy.auxDataSchema.fake.json diff --git a/tests/faucet.test.ts b/tests/faucet.test.ts new file mode 100644 index 00000000..8247d78c --- /dev/null +++ b/tests/faucet.test.ts @@ -0,0 +1,156 @@ +import test from 'tape' +import { readFileSync } from 'fs' +// @ts-expect-error : type export... +import { jumpStartNetwork } from '@entropyxyz/sdk/testing' + +import { eveSeed, setupTest } from './testing-utils' +import { stripHexPrefix } from '../src/common/utils' +import { EntropyBalance } from '../src/balance/main' +import { EntropyTransfer } from '../src/transfer/main' +import { EntropyFaucet } from '../src/faucet/main' +import { EntropyAccount } from '../src/account/main' + +async function setupAndFundFaucet (t) { + const { run, entropy, endpoint } = await setupTest(t, { seed: eveSeed }) + await run('jump-start network', jumpStartNetwork(entropy)) + + const account = new EntropyAccount(entropy, endpoint) + const transfer = new EntropyTransfer(entropy, endpoint) + const faucet = new EntropyFaucet(entropy, endpoint) + + // Deploy faucet program + const faucetProgram = readFileSync('tests/programs/faucet_program.wasm') + const configurationSchema = { + type: 'object', + properties: { + max_transfer_amount: { type: "number" }, + genesis_hash: { type: "string" } + } + } + const auxDataSchema = { + type: 'object', + properties: { + amount: { type: "number" }, + string_account_id: { type: "string" }, + spec_version: { type: "number" }, + transaction_version: { type: "number" }, + } + } + const faucetProgramPointer = await run( + 'Deploy faucet program', + entropy.programs.dev.deploy(faucetProgram, configurationSchema, auxDataSchema) + ) + + const POINTER = '0x3a1d45fecdee990925286ccce71f78693ff2bb27eae62adf8cfb7d3d61e142aa' + // TODO: record the schema deployed to testnet for faucet, as this will help + // us have a deterministic pointer (which we can test against) + t.equal(faucetProgramPointer, POINTER, 'Program pointer matches') + + // Register with faucet program + const genesisHash = await entropy.substrate.rpc.chain.getBlockHash(0) + const userConfig = { + max_transfer_amount: 20_000_000_000, + genesis_hash: stripHexPrefix(genesisHash.toString()) + } + await run( + 'Register Faucet Program for eve', + account.register({ + programModAddress: entropy.keyring.accounts.registration.address, + programData: [{ + program_pointer: faucetProgramPointer, + program_config: userConfig + }] + }) + ) + + // Fund the Faucet + const eveAddress = entropy.keyring.accounts.registration.address + const verifyingKeys = await faucet.getAllFaucetVerifyingKeys(eveAddress) + // @ts-expect-error + const { chosenVerifyingKey, faucetAddress } = faucet.getRandomFaucet([], verifyingKeys) + await run('Transfer funds to faucet address', transfer.transfer(faucetAddress, "1000")) + + return { faucetProgramPointer, chosenVerifyingKey, faucetAddress } +} + +test('Faucet Tests: Successfully send funds and register', async t => { + const { faucetAddress, chosenVerifyingKey, faucetProgramPointer } = await setupAndFundFaucet(t) + + const { run, endpoint, entropy: naynay } = await setupTest(t) + const naynayAddress = naynay.keyring.accounts.registration.address + + const faucet = new EntropyFaucet(naynay, endpoint) + const balance = new EntropyBalance(naynay, endpoint) + + let naynayBalance = await balance.getBalance(naynayAddress) + t.equal(naynayBalance, 0, 'Naynay is broke af') + + const amount = 20000000000 + const transferStatus = await run( + 'Sending faucet funds to account', + faucet.sendMoney({ + amount: `${amount}`, + addressToSendTo: naynay.keyring.accounts.registration.address, + faucetAddress, + chosenVerifyingKey, + faucetProgramPointer + }) + ) + t.ok(transferStatus.isFinalized, 'Transfer is good') + + naynayBalance = await balance.getBalance(naynayAddress) + t.equal(naynayBalance, amount, 'Naynay is drippin in faucet tokens') + + // Test if user can register after receiving funds + const account = new EntropyAccount(naynay, endpoint) + const verifyingKey = await run('register account', account.register()) + t.ok(!!verifyingKey, 'Verifying key exists and is returned from register method') + + const fullAccount = naynay.keyring.getAccount() + t.equal(verifyingKey, fullAccount?.registration?.verifyingKeys?.[0], 'verifying key matches key added to registration account') + + t.end() +}) + +// TODO: @naynay fix below test for register failing when only sending 1e10 bits +// test('Faucet Tests: Successfully send funds but cannot register', async t => { +// const { run, endpoint, entropy: naynayEntropy } = await setupTest(t) + +// const faucetService = new EntropyFaucet(naynayEntropy, endpoint) +// const balanceService = new EntropyBalance(naynayEntropy, endpoint) + +// const { faucetAddress, chosenVerifyingKey, faucetProgramPointer } = await setupAndFundFaucet(t, naynayEntropy) + +// let naynayBalance = await balanceService.getBalance(naynayEntropy.keyring.accounts.registration.address) +// t.equal(naynayBalance, 0, 'Naynay is broke af') + +// const transferStatus = await run('Sending faucet funds to account', faucetService.sendMoney( +// { +// amount: "10000000000", +// addressToSendTo: naynayEntropy.keyring.accounts.registration.address, +// faucetAddress, +// chosenVerifyingKey, +// faucetProgramPointer +// } +// )) + +// t.ok(transferStatus.isFinalized, 'Transfer is good') + +// naynayBalance = await balanceService.getBalance(naynayEntropy.keyring.accounts.registration.address) + +// t.ok(naynayBalance > 0, 'Naynay is drippin in faucet tokens') + +// // Test if user can register after receiving funds +// const naynayAccountService = new EntropyAccount(naynayEntropy, endpoint) +// try { +// const verifyingKey = await naynayAccountService.register() +// console.log('ver key', verifyingKey); + +// // t.fail('Register should fail') +// } catch (error) { +// console.log('error', error); + +// t.pass('Regsitration failed') +// t.end() +// } +// }) diff --git a/tests/global.test.ts b/tests/global.test.ts new file mode 100644 index 00000000..bff68d43 --- /dev/null +++ b/tests/global.test.ts @@ -0,0 +1,14 @@ +import test from 'tape' + +import { + promiseRunner, + execPromise +} from './testing-utils' + +test('Global: entropy --help', async (t) => { + /* Setup */ + const run = promiseRunner(t) + + await run('should run the global entropy --help', execPromise('entropy --help')) + t.end() +}) \ No newline at end of file diff --git a/tests/manage-accounts.test.ts b/tests/manage-accounts.test.ts deleted file mode 100644 index b2931e8d..00000000 --- a/tests/manage-accounts.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { EntropyAccountConfig, EntropyConfig } from 'src/config/types' -import test from 'tape' -import { charlieStashAddress, charlieStashSeed } from './testing-utils/constants' -import { listAccounts } from 'src/flows/manage-accounts/list' - -test('List Accounts', async t => { - const account: EntropyAccountConfig = { - name: 'Test Config', - address: charlieStashAddress, - data: { - seed: charlieStashSeed, - admin: { - verifyingKeys: ['this-is-a-verifying-key'], - seed: charlieStashSeed, - address: charlieStashAddress, - path: '//Charlie' - } - } - } - const config: EntropyConfig = { - accounts: [account], - endpoints: { - dev: 'ws://127.0.0.1:9944', - 'test-net': 'wss://testnet.entropy.xyz', - }, - 'migration-version': '0' - } - - const accountsArray = listAccounts(config) - - t.deepEqual(accountsArray, [{ - name: account.name, - address: account.address, - verifyingKeys: account.data.admin.verifyingKeys - }]) - - // Resetting accounts on config to test for empty list - config.accounts = [] - try { - listAccounts(config) - } catch (error) { - const msg = error.message - t.equal(msg, 'There are currently no accounts available, please create or import your new account using the Manage Accounts feature') - } - - t.end() -}) \ No newline at end of file diff --git a/tests/program.test.ts b/tests/program.test.ts new file mode 100644 index 00000000..6f89849e --- /dev/null +++ b/tests/program.test.ts @@ -0,0 +1,85 @@ +import test from 'tape' +import { jumpStartNetwork } from '@entropyxyz/sdk/testing' + +import { promiseRunner, eveSeed, setupTest } from './testing-utils' +import { EntropyProgram } from '../src/program/main' + +const networkType = 'four-nodes' +const endpoint = 'ws://127.0.0.1:9944' + +test('program', async t => { + const { run, entropy } = await setupTest(t, { seed: eveSeed, networkType }) + await run('jump-start network', jumpStartNetwork(entropy)) + await run('register', entropy.register()) // TODO: consider removing this in favour of just testing add + + const program = new EntropyProgram(entropy, endpoint) + + let programPointer1 + + t.test('program - deploy', async t => { + const run = promiseRunner(t) + + programPointer1 = await run ( + 'deploy!', + program.deploy({ + bytecodePath: './tests/programs/program_noop.wasm' + }) + ) + + t.end() + }) + + const getPrograms = () => program.list({ verifyingKey: entropy.programs.verifyingKey }) + const verifyingKey = entropy.programs.verifyingKey + + t.test('program - add', async t => { + const run = promiseRunner(t) + + const programsBeforeAdd = await run('get programs initial', getPrograms()) + t.equal(programsBeforeAdd.length, 1, 'eve has 1 program') + + await run( + 'adding program', + program.add({ + programPointer: programPointer1, + programConfig: '' + }) + ) + const programsAfterAdd = await run('get programs after add', getPrograms()) + t.equal(programsAfterAdd.length, 2, 'eve has 2 programs') + + t.end() + }) + + t.test('program - remove', async t => { + const run = promiseRunner(t) + + const programsBeforeRemove = await run('get programs initial', getPrograms()) + t.equal(programsBeforeRemove.length, 2, 'eve has 2 programs') + + await run( + 'removing noop program', + program.remove({ + programPointer: programPointer1, + verifyingKey + }) + ) + const programsAfterRemove = await run('get programs initial', getPrograms()) + t.equal(programsAfterRemove.length, 1, 'eve has 1 less program') + + t.end() + }) + + t.test('program - view', async t => { + const run = promiseRunner(t) + + const programs = await run( + 'get eve programs', + program.list({ verifyingKey }) + ) + + t.equal(programs.length, 1, 'eve has 1 program') + + t.end() + }) +}) diff --git a/tests/programs/faucet_program.wasm b/tests/programs/faucet_program.wasm new file mode 100644 index 00000000..cf1da081 Binary files /dev/null and b/tests/programs/faucet_program.wasm differ diff --git a/src/programs/program_noop.wasm b/tests/programs/program_noop.wasm similarity index 100% rename from src/programs/program_noop.wasm rename to tests/programs/program_noop.wasm diff --git a/src/programs/template_barebones.wasm b/tests/programs/template_barebones.wasm similarity index 100% rename from src/programs/template_barebones.wasm rename to tests/programs/template_barebones.wasm diff --git a/tests/sign.test.ts b/tests/sign.test.ts index aef6795d..f9b59ef2 100644 --- a/tests/sign.test.ts +++ b/tests/sign.test.ts @@ -1,20 +1,39 @@ -import test from 'tape' +import test from 'tape' +import { jumpStartNetwork } from '@entropyxyz/sdk/testing' -import { signWithAdapters } from '../src/flows/sign/sign' -import { setupTest, charlieStashSeed } from './testing-utils' +import { EntropySign } from '../src/sign/main' +import { setupTest, eveSeed } from './testing-utils' -test('Sign - signWithAdapter', async (t) => { - const { run, entropy } = await setupTest(t, { seed: charlieStashSeed }) +const endpoint = 'ws://127.0.0.1:9944' - await run('register', entropy.register()) +test('Sign - signMessageWithAdapters', async (t) => { + const { run, entropy } = await setupTest(t, { seed: eveSeed }) + await run('jump-start network', jumpStartNetwork(entropy)) + const signService = new EntropySign(entropy, endpoint) - const signature = await run( + await run('register', entropy.register()) + const result = await run( 'sign', - signWithAdapters(entropy, { msg: "heyo!" }) + signService.signMessageWithAdapters({ msg: "heyo!" }) ) - t.true(signature && signature.length > 32, 'signature has some body!') - signature && console.log(signature) + t.true(result?.signature?.length > 32, 'signature has some body!') + console.log(result) + + t.end() +}) + +test('Sign - signMessageWithAdapters (no verifying key)', async (t) => { + const { entropy } = await setupTest(t) + const signService = new EntropySign(entropy, endpoint) + + const description = 'Unregistered user gets error when they try to sign.' + await signService.signMessageWithAdapters({ msg: "heyo!" }) + .then(() => t.fail(description)) + .catch((err) => { + t.match(err.message, /Cannot read properties of undefined/, description) + return + }) t.end() }) diff --git a/tests/testing-utils/constants.ts b/tests/testing-utils/constants.ts index 0593c40a..53090266 100644 --- a/tests/testing-utils/constants.ts +++ b/tests/testing-utils/constants.ts @@ -1,5 +1,11 @@ -export const charlieStashAddress = - '5Ck5SLSHYac6WFt5UZRSsdJjwmpSZq85fd5TRNAdZQVzEAPT' - +export const charlieStashAddress = '5Ck5SLSHYac6WFt5UZRSsdJjwmpSZq85fd5TRNAdZQVzEAPT' export const charlieStashSeed = '0x66256c4e2f90e273bf387923a9a7860f2e9f47a1848d6263de512f7fb110fc08' + +export const charlieSeed = + '0xbc1ede780f784bb6991a585e4f6e61522c14e1cae6ad0895fb57b9a205a8f938' +export const charlieAddress = '5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y' + +export const eveSeed = + '0x786ad0e2df456fe43dd1f91ebca22e235bc162e0bb8d53c633e8c85b2af68b7a' +export const eveAddress = '5HGjWAeFDfFCWPsjFQdVV2Msvz2XtMktvgocEZcCj68kUMaw' \ No newline at end of file diff --git a/tests/testing-utils/index.ts b/tests/testing-utils/index.ts index 99cfde31..55b3c640 100644 --- a/tests/testing-utils/index.ts +++ b/tests/testing-utils/index.ts @@ -1,3 +1,4 @@ +import { exec } from 'node:child_process' // @ts-ignore import { spinNetworkUp, spinNetworkDown, } from "@entropyxyz/sdk/testing" import * as readline from 'readline' @@ -11,6 +12,21 @@ export { export * from './constants' export * from './setup-test' +/* Promise wrapper function of [exec](https://nodejs.org/api/child_process.html#child_processexeccommand-options-callback) + * + * @param {string} command - a string command to run in child process + */ + +export function execPromise (command: string): Promise { + return new Promise((res, rej) => { + exec(command, (error, stdout, stderr) => { + if (!error && !stderr) res(stdout) + else if (!!stderr && !error) rej(stderr) + else if (!!error) rej(error) + }) + }) +} + /* Helper for wrapping promises which makes it super clear in logging if the promise * resolves or threw. * @@ -32,13 +48,15 @@ export function promiseRunner(t: any, keepThrowing = false) { return promise .then((result) => { const time = (Date.now() - startTime) / 1000 - const pad = Array(40 - message.length) + const noPad = message.length > 40 + const pad = noPad ? '' : Array(40 - message.length) .fill('-') .join('') t.pass(`${message} ${pad} ${time}s`) return result }) .catch((err) => { + console.log('error', err); t.error(err, message) if (keepThrowing) throw err }) diff --git a/tests/testing-utils/setup-test.ts b/tests/testing-utils/setup-test.ts index b2e3206f..f63593c8 100644 --- a/tests/testing-utils/setup-test.ts +++ b/tests/testing-utils/setup-test.ts @@ -1,27 +1,40 @@ import { Test } from 'tape' import { Entropy, wasmGlobalsReady } from '@entropyxyz/sdk' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + // @ts-ignore -import { spinNetworkUp, spinNetworkDown, } from "@entropyxyz/sdk/testing" +import { spinNetworkUp, spinNetworkDown, jumpStartNetwork } from "@entropyxyz/sdk/testing" // @ts-ignore import Keyring from '@entropyxyz/sdk/keys' import { initializeEntropy } from '../../src/common/initializeEntropy' import * as config from '../../src/config' -import { makeSeed, promiseRunner, sleep } from './' +import { makeSeed, promiseRunner } from './' interface SetupTestOpts { configPath?: string networkType?: string - seed?: string, + seed?: string + endpoint?: string + createAccountOnly?: boolean +} + +const NETWORK_TYPE_DEFAULT = 'four-nodes' +let count = 0 +function uniqueConfigPath () { + return join( + tmpdir(), + `entropy-cli-${Date.now()}_${count++}.json` + ) } -const NETWORK_TYPE_DEFAULT = 'two-nodes' -let counter = 0 -export async function setupTest (t: Test, opts?: SetupTestOpts): Promise<{ entropy: Entropy; run: any }> { +export async function setupTest (t: Test, opts?: SetupTestOpts): Promise<{ entropy: Entropy; run: any; endpoint: string }> { const { - configPath = `/tmp/entropy-cli-${Date.now()}_${counter++}.json`, + configPath = uniqueConfigPath(), networkType = NETWORK_TYPE_DEFAULT, - seed = makeSeed() + seed = makeSeed(), + endpoint = 'ws://127.0.0.1:9944', } = opts || {} const run = promiseRunner(t) @@ -38,18 +51,17 @@ export async function setupTest (t: Test, opts?: SetupTestOpts): Promise<{ entro await run('config.init', config.init(configPath)) - // TODO: remove this after new SDK is published - await sleep(process.env.GITHUB_WORKSPACE ? 30_000 : 5_000) // To follow the same way we initiate entropy within the cli we must go through the same process of creating an initial keyring // as done in src/flows/manage-accounts/new-key.ts const keyring = new Keyring({ seed, debug: true }) const entropy = await initializeEntropy({ + // @ts-expect-error keyMaterial: keyring.getAccount(), - endpoint: 'ws://127.0.0.1:9944', + endpoint, configPath }) await run('entropy ready', entropy.ready) - return { entropy, run } + return { entropy, run, endpoint } } diff --git a/tests/transfer.test.ts b/tests/transfer.test.ts index 83379bc2..8423adb7 100644 --- a/tests/transfer.test.ts +++ b/tests/transfer.test.ts @@ -1,22 +1,23 @@ import test from 'tape' import { wasmGlobalsReady } from '@entropyxyz/sdk' -// WIP: I'm seeing problems importing this? // @ts-ignore import Keyring from '@entropyxyz/sdk/keys' import { makeSeed, promiseRunner, - sleep, spinNetworkUp, spinNetworkDown } from './testing-utils' -import { getBalance } from '../src/flows/balance/balance' -import { initializeEntropy } from 'src/common/initializeEntropy' +import { initializeEntropy } from '../src/common/initializeEntropy' +import { BITS_PER_TOKEN } from "../src/common/constants"; +import { EntropyTransfer } from '../src/transfer/main' +import { EntropyBalance } from '../src/balance/main' import { charlieStashAddress, charlieStashSeed } from './testing-utils/constants' -import { transfer } from 'src/flows/entropyTransfer/transfer' +import { EntropyAccountData } from '../src/config/types' -const networkType = 'two-nodes' +const networkType = 'four-nodes' +const endpoint = 'ws://127.0.0.1:9944' test('Transfer', async (t) => { /* Setup */ @@ -25,58 +26,53 @@ test('Transfer', async (t) => { await run('network up', spinNetworkUp(networkType)) // this gets called after all tests are run t.teardown(async () => { - await entropy.close() + await naynayEntropy.close() await charlieEntropy.close() await spinNetworkDown(networkType).catch((error) => console.error('Error while spinning network down', error.message) ) }) - await sleep(process.env.GITHUB_WORKSPACE ? 30_000 : 5_000) - const naynaySeed = makeSeed() - const naynayKeyring = new Keyring({ seed: naynaySeed, debug: true }) const charlieKeyring = new Keyring({ seed: charlieStashSeed, debug: true }) - - const entropy = await initializeEntropy({ keyMaterial: naynayKeyring.getAccount(), endpoint: 'ws://127.0.0.1:9944', }) - const charlieEntropy = await initializeEntropy({ keyMaterial: charlieKeyring.getAccount(), endpoint: 'ws://127.0.0.1:9944', }) - await run('entropy ready', entropy.ready) + const charlieEntropy = await initializeEntropy({ keyMaterial: charlieKeyring.getAccount() as EntropyAccountData, endpoint, }) await run('charlie ready', charlieEntropy.ready) - - const recipientAddress = entropy.keyring.accounts.registration.address - // Check Balance of new account + const naynaySeed = makeSeed() + const naynayKeyring = new Keyring({ seed: naynaySeed, debug: true }) + const naynayEntropy = await initializeEntropy({ keyMaterial: naynayKeyring.getAccount() as EntropyAccountData, endpoint, }) + await run('naynay ready', naynayEntropy.ready) + + const naynayAddress = naynayEntropy.keyring.accounts.registration.address + + // Check initial balances + const balanceService = new EntropyBalance(naynayEntropy, endpoint) let naynayBalance = await run( 'getBalance (naynay)', - getBalance(entropy, recipientAddress) + balanceService.getBalance(naynayAddress) ) - t.equal(naynayBalance, 0, 'naynay is broke') let charlieBalance = await run( 'getBalance (charlieStash)', - getBalance(entropy, charlieStashAddress) + balanceService.getBalance(charlieStashAddress) ) - t.equal(charlieBalance, 1e17, 'charlie got bank') - const transferStatus = await run( + // Do transer + const transferService = new EntropyTransfer(charlieEntropy, endpoint) + const inputAmount = "1.5" + await run( 'transfer', - transfer(entropy, { - from: charlieEntropy.keyring.accounts.registration.pair, - to: recipientAddress, - amount: BigInt(1000 * 10e10) - }) + transferService.transfer(naynayAddress, inputAmount) ) - // @ts-ignore - t.true(transferStatus?.isFinalized, 'Funds transferred successfully') - // Re-Check Balance of new account + // Re-Check balance naynayBalance = await run( 'getBalance (naynay)', - getBalance(entropy, recipientAddress) + balanceService.getBalance(naynayAddress) ) + const expected = Number(inputAmount) * BITS_PER_TOKEN + t.equal(naynayBalance, expected,'naynay is rolling in it!') - t.equal(naynayBalance, 1000 * 10e10, 'naynay is rolling in it!') - t.end() -}) \ No newline at end of file +}) diff --git a/tsconfig.json b/tsconfig.json index a0d38874..b35dedd6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,6 @@ "exclude": [ "node_modules", "dist", - "build", - "examples" + "tests" ] } diff --git a/yarn.lock b/yarn.lock index 7a9863ff..7e6beba6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,11 +2,6 @@ # yarn lockfile v1 -"@adraffy/ens-normalize@1.10.0": - version "1.10.0" - resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.10.0.tgz#d2a39395c587e092d77cbbc80acf956a54f38bf7" - integrity sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q== - "@colors/colors@1.6.0", "@colors/colors@^1.6.0": version "1.6.0" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.6.0.tgz#ec6cd237440700bc23ca23087f513c75508958b0" @@ -31,17 +26,19 @@ resolved "https://registry.yarnpkg.com/@entropyxyz/entropy-protocol-web/-/entropy-protocol-web-0.2.0.tgz#b9478438386fefb4b821dbac95ec81d251d2dd55" integrity sha512-lLa/lLNJnwH1R8fJvLlUn1kw7d4Rbnt9LjhUC69HKxkU69J+bw/EY6fAjBnpVbgNmqCnYpf/DBLtMyOayZeNDQ== -"@entropyxyz/sdk@^0.2.2": - version "0.2.2" - resolved "https://registry.yarnpkg.com/@entropyxyz/sdk/-/sdk-0.2.2.tgz#c1602767c2bee5248e6df87c437db302fb246f1d" - integrity sha512-144x0Zkbfgc4MY7M7gL6TA8wliZDZn5Qqv4syLuON9x+FThRI1flubOcOstG97HppJJH5XwZQouTLcPPCCYjjg== +"@entropyxyz/sdk@0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@entropyxyz/sdk/-/sdk-0.3.0.tgz#845ea46f4181882cfb48e3eedcf53812e44ce436" + integrity sha512-gEpiVJOJADiqr1tBqOgMUxufKD19xp7R03TkrxoTW/zzB0tEWFtWD+yW12Sq1Zzl3xBH5QXI4v/UiyDFkqaiUQ== dependencies: "@entropyxyz/entropy-protocol-nodejs" "^0.2.0" "@entropyxyz/entropy-protocol-web" "^0.2.0" - "@polkadot/api" "^10.13.1" + "@polkadot/api" "^14.0.1" + "@types/lodash.clonedeep" "^4.5.9" "@types/node" "^20.12.12" debug "^4.3.4" hpke-js "^1.2.7" + lodash.clonedeep "^4.5.0" uuid "^9.0.1" xtend "^4.0.2" @@ -385,13 +382,6 @@ resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-0.4.1.tgz#977fc35f563a4ca315ebbc4cbb1f9b670bd54456" integrity sha512-QCOA9cgf3Rc33owG0AYBB9wszz+Ul2kramWN8tXG44Gyciud/tbkEqvxRF/IpqQaBpRBNi9f4jdNxqB2CQCIXg== -"@noble/curves@1.2.0", "@noble/curves@~1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.2.0.tgz#92d7e12e4e49b23105a2555c6984d41733d65c35" - integrity sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw== - dependencies: - "@noble/hashes" "1.3.2" - "@noble/curves@1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.3.0.tgz#01be46da4fd195822dab821e72f71bf4aeec635e" @@ -406,12 +396,7 @@ dependencies: "@noble/hashes" "1.4.0" -"@noble/hashes@1.3.2": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" - integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== - -"@noble/hashes@1.3.3", "@noble/hashes@~1.3.0", "@noble/hashes@~1.3.2": +"@noble/hashes@1.3.3": version "1.3.3" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.3.tgz#39908da56a4adc270147bb07968bf3b16cfe1699" integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA== @@ -447,268 +432,270 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== -"@polkadot-api/client@0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0": - version "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0" - resolved "https://registry.yarnpkg.com/@polkadot-api/client/-/client-0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0.tgz#5d6b863f63f5c6ecd4183fcf0c5c84dd349f7627" - integrity sha512-0fqK6pUKcGHSG2pBvY+gfSS+1mMdjd/qRygAcKI5d05tKsnZLRnmhb9laDguKmGEIB0Bz9vQqNK3gIN/cfvVwg== - dependencies: - "@polkadot-api/metadata-builders" "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0" - "@polkadot-api/substrate-bindings" "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0" - "@polkadot-api/substrate-client" "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0" - "@polkadot-api/utils" "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0" +"@polkadot-api/json-rpc-provider-proxy@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@polkadot-api/json-rpc-provider-proxy/-/json-rpc-provider-proxy-0.1.0.tgz#6e191f28e7d0fbbe8b540fc51d12a0adaeba297e" + integrity sha512-8GSFE5+EF73MCuLQm8tjrbCqlgclcHBSRaswvXziJ0ZW7iw3UEMsKkkKvELayWyBuOPa2T5i1nj6gFOeIsqvrg== -"@polkadot-api/json-rpc-provider-proxy@0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0": - version "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0" - resolved "https://registry.yarnpkg.com/@polkadot-api/json-rpc-provider-proxy/-/json-rpc-provider-proxy-0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0.tgz#cc28fb801db6a47824261a709ab924ec6951eb96" - integrity sha512-0hZ8vtjcsyCX8AyqP2sqUHa1TFFfxGWmlXJkit0Nqp9b32MwZqn5eaUAiV2rNuEpoglKOdKnkGtUF8t5MoodKw== +"@polkadot-api/json-rpc-provider@0.0.1", "@polkadot-api/json-rpc-provider@^0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@polkadot-api/json-rpc-provider/-/json-rpc-provider-0.0.1.tgz#333645d40ccd9bccfd1f32503f17e4e63e76e297" + integrity sha512-/SMC/l7foRjpykLTUTacIH05H3mr9ip8b5xxfwXlVezXrNVLp3Cv0GX6uItkKd+ZjzVPf3PFrDF2B2/HLSNESA== -"@polkadot-api/json-rpc-provider@0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0": - version "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0" - resolved "https://registry.yarnpkg.com/@polkadot-api/json-rpc-provider/-/json-rpc-provider-0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0.tgz#2f71bfb192d28dd4c400ef8b1c5f934c676950f3" - integrity sha512-EaUS9Fc3wsiUr6ZS43PQqaRScW7kM6DYbuM/ou0aYjm8N9MBqgDbGm2oL6RE1vAVmOfEuHcXZuZkhzWtyvQUtA== +"@polkadot-api/metadata-builders@0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@polkadot-api/metadata-builders/-/metadata-builders-0.3.2.tgz#007f158c9e0546cf79ba440befc0c753ab1a6629" + integrity sha512-TKpfoT6vTb+513KDzMBTfCb/ORdgRnsS3TDFpOhAhZ08ikvK+hjHMt5plPiAX/OWkm1Wc9I3+K6W0hX5Ab7MVg== + dependencies: + "@polkadot-api/substrate-bindings" "0.6.0" + "@polkadot-api/utils" "0.1.0" -"@polkadot-api/metadata-builders@0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0": - version "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0" - resolved "https://registry.yarnpkg.com/@polkadot-api/metadata-builders/-/metadata-builders-0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0.tgz#085db2a3c7b100626b2fae3be35a32a24ea7714f" - integrity sha512-BD7rruxChL1VXt0icC2gD45OtT9ofJlql0qIllHSRYgama1CR2Owt+ApInQxB+lWqM+xNOznZRpj8CXNDvKIMg== +"@polkadot-api/observable-client@^0.3.0": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@polkadot-api/observable-client/-/observable-client-0.3.2.tgz#fd91efee350595a6e0ecfd3f294cc80de86c0cf7" + integrity sha512-HGgqWgEutVyOBXoGOPp4+IAq6CNdK/3MfQJmhCJb8YaJiaK4W6aRGrdQuQSTPHfERHCARt9BrOmEvTXAT257Ug== dependencies: - "@polkadot-api/substrate-bindings" "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0" - "@polkadot-api/utils" "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0" + "@polkadot-api/metadata-builders" "0.3.2" + "@polkadot-api/substrate-bindings" "0.6.0" + "@polkadot-api/utils" "0.1.0" -"@polkadot-api/substrate-bindings@0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0": - version "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0" - resolved "https://registry.yarnpkg.com/@polkadot-api/substrate-bindings/-/substrate-bindings-0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0.tgz#f836a554a9ead6fb6356079c725cd53f87238932" - integrity sha512-N4vdrZopbsw8k57uG58ofO7nLXM4Ai7835XqakN27MkjXMp5H830A1KJE0L9sGQR7ukOCDEIHHcwXVrzmJ/PBg== +"@polkadot-api/substrate-bindings@0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@polkadot-api/substrate-bindings/-/substrate-bindings-0.6.0.tgz#889b0c3ba19dc95282286506bf6e370a43ce119a" + integrity sha512-lGuhE74NA1/PqdN7fKFdE5C1gNYX357j1tWzdlPXI0kQ7h3kN0zfxNOpPUN7dIrPcOFZ6C0tRRVrBylXkI6xPw== dependencies: "@noble/hashes" "^1.3.1" - "@polkadot-api/utils" "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0" + "@polkadot-api/utils" "0.1.0" "@scure/base" "^1.1.1" scale-ts "^1.6.0" -"@polkadot-api/substrate-client@0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0": - version "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0" - resolved "https://registry.yarnpkg.com/@polkadot-api/substrate-client/-/substrate-client-0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0.tgz#55ae463f4143495e328465dd16b03e71663ef4c4" - integrity sha512-lcdvd2ssUmB1CPzF8s2dnNOqbrDa+nxaaGbuts+Vo8yjgSKwds2Lo7Oq+imZN4VKW7t9+uaVcKFLMF7PdH0RWw== - -"@polkadot-api/utils@0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0": - version "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0" - resolved "https://registry.yarnpkg.com/@polkadot-api/utils/-/utils-0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0.tgz#759698dcf948745ea37cc5ab6abd49a00f1b0c31" - integrity sha512-0CYaCjfLQJTCRCiYvZ81OncHXEKPzAexCMoVloR+v2nl/O2JRya/361MtPkeNLC6XBoaEgLAG9pWQpH3WePzsw== - -"@polkadot/api-augment@10.13.1": - version "10.13.1" - resolved "https://registry.yarnpkg.com/@polkadot/api-augment/-/api-augment-10.13.1.tgz#dd3670a2f1a581c38b857ad3b0805b6581099c63" - integrity sha512-IAKaCp19QxgOG4HKk9RAgUgC/VNVqymZ2GXfMNOZWImZhxRIbrK+raH5vN2MbWwtVHpjxyXvGsd1RRhnohI33A== - dependencies: - "@polkadot/api-base" "10.13.1" - "@polkadot/rpc-augment" "10.13.1" - "@polkadot/types" "10.13.1" - "@polkadot/types-augment" "10.13.1" - "@polkadot/types-codec" "10.13.1" - "@polkadot/util" "^12.6.2" - tslib "^2.6.2" - -"@polkadot/api-base@10.13.1": - version "10.13.1" - resolved "https://registry.yarnpkg.com/@polkadot/api-base/-/api-base-10.13.1.tgz#efed5bb31e38244b6a68ce56138b97ad82101426" - integrity sha512-Okrw5hjtEjqSMOG08J6qqEwlUQujTVClvY1/eZkzKwNzPelWrtV6vqfyJklB7zVhenlxfxqhZKKcY7zWSW/q5Q== +"@polkadot-api/substrate-client@^0.1.2": + version "0.1.4" + resolved "https://registry.yarnpkg.com/@polkadot-api/substrate-client/-/substrate-client-0.1.4.tgz#7a808e5cb85ecb9fa2b3a43945090a6c807430ce" + integrity sha512-MljrPobN0ZWTpn++da9vOvt+Ex+NlqTlr/XT7zi9sqPtDJiQcYl+d29hFAgpaeTqbeQKZwz3WDE9xcEfLE8c5A== dependencies: - "@polkadot/rpc-core" "10.13.1" - "@polkadot/types" "10.13.1" - "@polkadot/util" "^12.6.2" - rxjs "^7.8.1" - tslib "^2.6.2" + "@polkadot-api/json-rpc-provider" "0.0.1" + "@polkadot-api/utils" "0.1.0" -"@polkadot/api-derive@10.13.1": - version "10.13.1" - resolved "https://registry.yarnpkg.com/@polkadot/api-derive/-/api-derive-10.13.1.tgz#d8827ee83124f3b3f664c415cdde9c6b909e5145" - integrity sha512-ef0H0GeCZ4q5Om+c61eLLLL29UxFC2/u/k8V1K2JOIU+2wD5LF7sjAoV09CBMKKHfkLenRckVk2ukm4rBqFRpg== - dependencies: - "@polkadot/api" "10.13.1" - "@polkadot/api-augment" "10.13.1" - "@polkadot/api-base" "10.13.1" - "@polkadot/rpc-core" "10.13.1" - "@polkadot/types" "10.13.1" - "@polkadot/types-codec" "10.13.1" - "@polkadot/util" "^12.6.2" - "@polkadot/util-crypto" "^12.6.2" +"@polkadot-api/utils@0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@polkadot-api/utils/-/utils-0.1.0.tgz#d36937cdc465c2ea302f3278cf53157340ab33a0" + integrity sha512-MXzWZeuGxKizPx2Xf/47wx9sr/uxKw39bVJUptTJdsaQn/TGq+z310mHzf1RCGvC1diHM8f593KrnDgc9oNbJA== + +"@polkadot/api-augment@14.0.1": + version "14.0.1" + resolved "https://registry.yarnpkg.com/@polkadot/api-augment/-/api-augment-14.0.1.tgz#77ace2ba0c8ead942243e66e3c621c9219bb0e97" + integrity sha512-+ZHq3JaQZ/3Q45r6/YQBeLfoP8S5ibgkOvLKnKA9cJeF7oP5Qgi6pAEnGW0accfnT9PyCEco9fD/ZOLR9Yka7w== + dependencies: + "@polkadot/api-base" "14.0.1" + "@polkadot/rpc-augment" "14.0.1" + "@polkadot/types" "14.0.1" + "@polkadot/types-augment" "14.0.1" + "@polkadot/types-codec" "14.0.1" + "@polkadot/util" "^13.1.1" + tslib "^2.7.0" + +"@polkadot/api-base@14.0.1": + version "14.0.1" + resolved "https://registry.yarnpkg.com/@polkadot/api-base/-/api-base-14.0.1.tgz#2adaea94ac0b021d3b13c1f2b2f1d47de93815d0" + integrity sha512-OVnDiztKx/1ktae9eCzO1q8lmKEfnQ71fipo8JkDJOMIN4vT1IqL9KQo4e/Xz8UtOfTJ0H8kZ6evaLqdA3ZYOA== + dependencies: + "@polkadot/rpc-core" "14.0.1" + "@polkadot/types" "14.0.1" + "@polkadot/util" "^13.1.1" rxjs "^7.8.1" - tslib "^2.6.2" - -"@polkadot/api@10.13.1", "@polkadot/api@^10.13.1": - version "10.13.1" - resolved "https://registry.yarnpkg.com/@polkadot/api/-/api-10.13.1.tgz#47586c070d3fe13a0acc93a8aa9c3a53791284fb" - integrity sha512-YrKWR4TQR5CDyGkF0mloEUo7OsUA+bdtENpJGOtNavzOQUDEbxFE0PVzokzZfVfHhHX2CojPVmtzmmLxztyJkg== - dependencies: - "@polkadot/api-augment" "10.13.1" - "@polkadot/api-base" "10.13.1" - "@polkadot/api-derive" "10.13.1" - "@polkadot/keyring" "^12.6.2" - "@polkadot/rpc-augment" "10.13.1" - "@polkadot/rpc-core" "10.13.1" - "@polkadot/rpc-provider" "10.13.1" - "@polkadot/types" "10.13.1" - "@polkadot/types-augment" "10.13.1" - "@polkadot/types-codec" "10.13.1" - "@polkadot/types-create" "10.13.1" - "@polkadot/types-known" "10.13.1" - "@polkadot/util" "^12.6.2" - "@polkadot/util-crypto" "^12.6.2" + tslib "^2.7.0" + +"@polkadot/api-derive@14.0.1": + version "14.0.1" + resolved "https://registry.yarnpkg.com/@polkadot/api-derive/-/api-derive-14.0.1.tgz#3d76f8191568c174f909781c85d18b804edd3502" + integrity sha512-ADQMre3DRRW/0rhJqxOVhQ1vqtyafP2dSZJ0qEAsto12q2WMSF8CZWo7pXe4DxiniDkZx3zVq4z5lqw2aBRLfg== + dependencies: + "@polkadot/api" "14.0.1" + "@polkadot/api-augment" "14.0.1" + "@polkadot/api-base" "14.0.1" + "@polkadot/rpc-core" "14.0.1" + "@polkadot/types" "14.0.1" + "@polkadot/types-codec" "14.0.1" + "@polkadot/util" "^13.1.1" + "@polkadot/util-crypto" "^13.1.1" + rxjs "^7.8.1" + tslib "^2.7.0" + +"@polkadot/api@14.0.1", "@polkadot/api@^14.0.1": + version "14.0.1" + resolved "https://registry.yarnpkg.com/@polkadot/api/-/api-14.0.1.tgz#2882280d69e1b2f121ece118137fbc8a70f3f465" + integrity sha512-CDSaUiJpXu9aE6MaTg14K+9Trf8K2PBHcD3Xl5m5KOvJperWgYFxoCqV3rXLIBWt69LgHhMYlq5JSPRHxejIsw== + dependencies: + "@polkadot/api-augment" "14.0.1" + "@polkadot/api-base" "14.0.1" + "@polkadot/api-derive" "14.0.1" + "@polkadot/keyring" "^13.1.1" + "@polkadot/rpc-augment" "14.0.1" + "@polkadot/rpc-core" "14.0.1" + "@polkadot/rpc-provider" "14.0.1" + "@polkadot/types" "14.0.1" + "@polkadot/types-augment" "14.0.1" + "@polkadot/types-codec" "14.0.1" + "@polkadot/types-create" "14.0.1" + "@polkadot/types-known" "14.0.1" + "@polkadot/util" "^13.1.1" + "@polkadot/util-crypto" "^13.1.1" eventemitter3 "^5.0.1" rxjs "^7.8.1" - tslib "^2.6.2" - -"@polkadot/keyring@^12.6.2": - version "12.6.2" - resolved "https://registry.yarnpkg.com/@polkadot/keyring/-/keyring-12.6.2.tgz#6067e6294fee23728b008ac116e7e9db05cecb9b" - integrity sha512-O3Q7GVmRYm8q7HuB3S0+Yf/q/EB2egKRRU3fv9b3B7V+A52tKzA+vIwEmNVaD1g5FKW9oB97rmpggs0zaKFqHw== - dependencies: - "@polkadot/util" "12.6.2" - "@polkadot/util-crypto" "12.6.2" - tslib "^2.6.2" - -"@polkadot/networks@12.6.2", "@polkadot/networks@^12.6.2": - version "12.6.2" - resolved "https://registry.yarnpkg.com/@polkadot/networks/-/networks-12.6.2.tgz#791779fee1d86cc5b6cd371858eea9b7c3f8720d" - integrity sha512-1oWtZm1IvPWqvMrldVH6NI2gBoCndl5GEwx7lAuQWGr7eNL+6Bdc5K3Z9T0MzFvDGoi2/CBqjX9dRKo39pDC/w== - dependencies: - "@polkadot/util" "12.6.2" - "@substrate/ss58-registry" "^1.44.0" - tslib "^2.6.2" - -"@polkadot/rpc-augment@10.13.1": - version "10.13.1" - resolved "https://registry.yarnpkg.com/@polkadot/rpc-augment/-/rpc-augment-10.13.1.tgz#83317b46c5ab86104cca2bdc336199db0c25b798" - integrity sha512-iLsWUW4Jcx3DOdVrSHtN0biwxlHuTs4QN2hjJV0gd0jo7W08SXhWabZIf9mDmvUJIbR7Vk+9amzvegjRyIf5+A== - dependencies: - "@polkadot/rpc-core" "10.13.1" - "@polkadot/types" "10.13.1" - "@polkadot/types-codec" "10.13.1" - "@polkadot/util" "^12.6.2" - tslib "^2.6.2" - -"@polkadot/rpc-core@10.13.1": - version "10.13.1" - resolved "https://registry.yarnpkg.com/@polkadot/rpc-core/-/rpc-core-10.13.1.tgz#a7ea9db8997b68aa6724f28ba76125a73e925575" - integrity sha512-eoejSHa+/tzHm0vwic62/aptTGbph8vaBpbvLIK7gd00+rT813ROz5ckB1CqQBFB23nHRLuzzX/toY8ID3xrKw== - dependencies: - "@polkadot/rpc-augment" "10.13.1" - "@polkadot/rpc-provider" "10.13.1" - "@polkadot/types" "10.13.1" - "@polkadot/util" "^12.6.2" + tslib "^2.7.0" + +"@polkadot/keyring@^13.1.1": + version "13.1.1" + resolved "https://registry.yarnpkg.com/@polkadot/keyring/-/keyring-13.1.1.tgz#14b85d4e73ebfa8ccb0fadcdee127e102624dc11" + integrity sha512-Wm+9gn946GIPjGzvueObLGBBS9s541HE6mvKdWGEmPFMzH93ESN931RZlOd67my5MWryiSP05h5SHTp7bSaQTA== + dependencies: + "@polkadot/util" "13.1.1" + "@polkadot/util-crypto" "13.1.1" + tslib "^2.7.0" + +"@polkadot/networks@13.1.1", "@polkadot/networks@^13.1.1": + version "13.1.1" + resolved "https://registry.yarnpkg.com/@polkadot/networks/-/networks-13.1.1.tgz#e1a05ef6f78ffc37272c6474df7b55244b311f9c" + integrity sha512-eEQ4+Mfl1xFtApeU5PdXZ2XBhxNSvUz9yW+YQVGUCkXRjWFbqNRsTOYWGd9uFbiAOXiiiXbtqfZpxSDzIm4XOg== + dependencies: + "@polkadot/util" "13.1.1" + "@substrate/ss58-registry" "^1.50.0" + tslib "^2.7.0" + +"@polkadot/rpc-augment@14.0.1": + version "14.0.1" + resolved "https://registry.yarnpkg.com/@polkadot/rpc-augment/-/rpc-augment-14.0.1.tgz#e6bc1cf0cdb2839fb709027e4fce6b113a5b06af" + integrity sha512-M0CbN/IScqiedYI2TmoQ+SoeEdJHfxGeQD1qJf9uYv9LILK+x1/5fyr5DrZ3uCGVmLuObWAJLnHTs0BzJcSHTQ== + dependencies: + "@polkadot/rpc-core" "14.0.1" + "@polkadot/types" "14.0.1" + "@polkadot/types-codec" "14.0.1" + "@polkadot/util" "^13.1.1" + tslib "^2.7.0" + +"@polkadot/rpc-core@14.0.1": + version "14.0.1" + resolved "https://registry.yarnpkg.com/@polkadot/rpc-core/-/rpc-core-14.0.1.tgz#9f4272354c65e2e34e6e542336c26c14aa5a21e8" + integrity sha512-SfgC6WU7RxaFFgm/GUpsqTywyaDeb7+r5GU3GlwC+QR148h3a7UcQ3sssOpB0MiZ2gIXngJuyIcIQm/3GfHnJw== + dependencies: + "@polkadot/rpc-augment" "14.0.1" + "@polkadot/rpc-provider" "14.0.1" + "@polkadot/types" "14.0.1" + "@polkadot/util" "^13.1.1" rxjs "^7.8.1" - tslib "^2.6.2" - -"@polkadot/rpc-provider@10.13.1": - version "10.13.1" - resolved "https://registry.yarnpkg.com/@polkadot/rpc-provider/-/rpc-provider-10.13.1.tgz#7e17f7be7d9a104b797d8f5aa8f1ed69f800f841" - integrity sha512-oJ7tatVXYJ0L7NpNiGd69D558HG5y5ZDmH2Bp9Dd4kFTQIiV8A39SlWwWUPCjSsen9lqSvvprNLnG/VHTpenbw== - dependencies: - "@polkadot/keyring" "^12.6.2" - "@polkadot/types" "10.13.1" - "@polkadot/types-support" "10.13.1" - "@polkadot/util" "^12.6.2" - "@polkadot/util-crypto" "^12.6.2" - "@polkadot/x-fetch" "^12.6.2" - "@polkadot/x-global" "^12.6.2" - "@polkadot/x-ws" "^12.6.2" + tslib "^2.7.0" + +"@polkadot/rpc-provider@14.0.1": + version "14.0.1" + resolved "https://registry.yarnpkg.com/@polkadot/rpc-provider/-/rpc-provider-14.0.1.tgz#69c98bd2ef7a243d0544494bc28b206889e46161" + integrity sha512-mNfaKZUHPXGSY7TwgOfV05RN3Men21Dw7YXrSZDFkJYsZ55yOAYdmLg9anPZGHW100YnNWrXj+3uhQOw8JgqkA== + dependencies: + "@polkadot/keyring" "^13.1.1" + "@polkadot/types" "14.0.1" + "@polkadot/types-support" "14.0.1" + "@polkadot/util" "^13.1.1" + "@polkadot/util-crypto" "^13.1.1" + "@polkadot/x-fetch" "^13.1.1" + "@polkadot/x-global" "^13.1.1" + "@polkadot/x-ws" "^13.1.1" eventemitter3 "^5.0.1" mock-socket "^9.3.1" - nock "^13.5.0" - tslib "^2.6.2" + nock "^13.5.4" + tslib "^2.7.0" optionalDependencies: - "@substrate/connect" "0.8.8" - -"@polkadot/types-augment@10.13.1": - version "10.13.1" - resolved "https://registry.yarnpkg.com/@polkadot/types-augment/-/types-augment-10.13.1.tgz#8f39a46a1a3e100be03cbae06f43a043cb25c337" - integrity sha512-TcrLhf95FNFin61qmVgOgayzQB/RqVsSg9thAso1Fh6pX4HSbvI35aGPBAn3SkA6R+9/TmtECirpSNLtIGFn0g== - dependencies: - "@polkadot/types" "10.13.1" - "@polkadot/types-codec" "10.13.1" - "@polkadot/util" "^12.6.2" - tslib "^2.6.2" - -"@polkadot/types-codec@10.13.1": - version "10.13.1" - resolved "https://registry.yarnpkg.com/@polkadot/types-codec/-/types-codec-10.13.1.tgz#f70cd617160b467685ef3ce5195a04142255ba7b" - integrity sha512-AiQ2Vv2lbZVxEdRCN8XSERiWlOWa2cTDLnpAId78EnCtx4HLKYQSd+Jk9Y4BgO35R79mchK4iG+w6gZ+ukG2bg== - dependencies: - "@polkadot/util" "^12.6.2" - "@polkadot/x-bigint" "^12.6.2" - tslib "^2.6.2" - -"@polkadot/types-create@10.13.1": - version "10.13.1" - resolved "https://registry.yarnpkg.com/@polkadot/types-create/-/types-create-10.13.1.tgz#99470816d0d2ca32a6a5ce6d701b4199e8700f66" - integrity sha512-Usn1jqrz35SXgCDAqSXy7mnD6j4RvB4wyzTAZipFA6DGmhwyxxIgOzlWQWDb+1PtPKo9vtMzen5IJ+7w5chIeA== - dependencies: - "@polkadot/types-codec" "10.13.1" - "@polkadot/util" "^12.6.2" - tslib "^2.6.2" - -"@polkadot/types-known@10.13.1": - version "10.13.1" - resolved "https://registry.yarnpkg.com/@polkadot/types-known/-/types-known-10.13.1.tgz#8cca2d3f2c4ef67849f66ba4a35856063ec61f5f" - integrity sha512-uHjDW05EavOT5JeU8RbiFWTgPilZ+odsCcuEYIJGmK+es3lk/Qsdns9Zb7U7NJl7eJ6OWmRtyrWsLs+bU+jjIQ== - dependencies: - "@polkadot/networks" "^12.6.2" - "@polkadot/types" "10.13.1" - "@polkadot/types-codec" "10.13.1" - "@polkadot/types-create" "10.13.1" - "@polkadot/util" "^12.6.2" - tslib "^2.6.2" - -"@polkadot/types-support@10.13.1": - version "10.13.1" - resolved "https://registry.yarnpkg.com/@polkadot/types-support/-/types-support-10.13.1.tgz#d4b58c8d9bcbb8e897a255d9a66c217dcaa6ead4" - integrity sha512-4gEPfz36XRQIY7inKq0HXNVVhR6HvXtm7yrEmuBuhM86LE0lQQBkISUSgR358bdn2OFSLMxMoRNoh3kcDvdGDQ== - dependencies: - "@polkadot/util" "^12.6.2" - tslib "^2.6.2" - -"@polkadot/types@10.13.1": - version "10.13.1" - resolved "https://registry.yarnpkg.com/@polkadot/types/-/types-10.13.1.tgz#979d652dc11af9cb8b32e7a55839e9762532755d" - integrity sha512-Hfvg1ZgJlYyzGSAVrDIpp3vullgxrjOlh/CSThd/PI4TTN1qHoPSFm2hs77k3mKkOzg+LrWsLE0P/LP2XddYcw== - dependencies: - "@polkadot/keyring" "^12.6.2" - "@polkadot/types-augment" "10.13.1" - "@polkadot/types-codec" "10.13.1" - "@polkadot/types-create" "10.13.1" - "@polkadot/util" "^12.6.2" - "@polkadot/util-crypto" "^12.6.2" + "@substrate/connect" "0.8.11" + +"@polkadot/types-augment@14.0.1": + version "14.0.1" + resolved "https://registry.yarnpkg.com/@polkadot/types-augment/-/types-augment-14.0.1.tgz#06b5eed44e53cfc42cb98e624581170076c6ab2f" + integrity sha512-PGo81444J5tGJxP3tu060Jx1kkeuo8SmBIt9S/w626Se49x4RLM5a7Pa5fguYVsg4TsJa9cgVPMuu6Y0F/2aCQ== + dependencies: + "@polkadot/types" "14.0.1" + "@polkadot/types-codec" "14.0.1" + "@polkadot/util" "^13.1.1" + tslib "^2.7.0" + +"@polkadot/types-codec@14.0.1": + version "14.0.1" + resolved "https://registry.yarnpkg.com/@polkadot/types-codec/-/types-codec-14.0.1.tgz#9b354295c4b565a18034c82fa0f86928b2514a99" + integrity sha512-IyUlkrRZ6uppbHVlMJL+btKP7dfgW65K06ggQxH7Y/IyRAQVDNjXecAZrCUMB/gtjUXNPyTHEIfPGDlg8E6rig== + dependencies: + "@polkadot/util" "^13.1.1" + "@polkadot/x-bigint" "^13.1.1" + tslib "^2.7.0" + +"@polkadot/types-create@14.0.1": + version "14.0.1" + resolved "https://registry.yarnpkg.com/@polkadot/types-create/-/types-create-14.0.1.tgz#5695c80f292ab7a9d766bd7cdbc1b51ef0e84392" + integrity sha512-R9/ac3CHKrFhvPKVUdpjnCDFSaGjfrNwtuY+AzvExAMIq7pM9dxo2N8UfnLbyFaG/n1hfYPXDIS3hLHvOZsLbw== + dependencies: + "@polkadot/types-codec" "14.0.1" + "@polkadot/util" "^13.1.1" + tslib "^2.7.0" + +"@polkadot/types-known@14.0.1": + version "14.0.1" + resolved "https://registry.yarnpkg.com/@polkadot/types-known/-/types-known-14.0.1.tgz#fe3ef88819aac9cf7c3627a74f21fbabb8c73338" + integrity sha512-oGypUOQNxZ6bq10czpVadZYeDM2NBB2kX3VFHLKLEpjaRbnVYtKXL6pl8B0uHR8GK/2Z8AmPOj6kuRjaC86qXg== + dependencies: + "@polkadot/networks" "^13.1.1" + "@polkadot/types" "14.0.1" + "@polkadot/types-codec" "14.0.1" + "@polkadot/types-create" "14.0.1" + "@polkadot/util" "^13.1.1" + tslib "^2.7.0" + +"@polkadot/types-support@14.0.1": + version "14.0.1" + resolved "https://registry.yarnpkg.com/@polkadot/types-support/-/types-support-14.0.1.tgz#a726bc36e6d3fe7e2d9710d6e8b7559f9d6aee3b" + integrity sha512-lcZEyOf5e3WLLtrFlLTvFfUpO0Vx/Gh5lhLLjdx1W9Xs0KJUlOxSAKxvjVieJJj6HifL0Jh6tDYOUeEc4TOrvA== + dependencies: + "@polkadot/util" "^13.1.1" + tslib "^2.7.0" + +"@polkadot/types@14.0.1": + version "14.0.1" + resolved "https://registry.yarnpkg.com/@polkadot/types/-/types-14.0.1.tgz#a562ef1b49203bfbe640a608e0e23c6c77a0153b" + integrity sha512-DOMzHsyVbCa12FT2Fng8iGiQJhHW2ONpv5oieU+Z2o0gFQqwNmIDXWncScG5mAUBNcDMXLuvWIKLKtUDOq8msg== + dependencies: + "@polkadot/keyring" "^13.1.1" + "@polkadot/types-augment" "14.0.1" + "@polkadot/types-codec" "14.0.1" + "@polkadot/types-create" "14.0.1" + "@polkadot/util" "^13.1.1" + "@polkadot/util-crypto" "^13.1.1" rxjs "^7.8.1" - tslib "^2.6.2" + tslib "^2.7.0" -"@polkadot/util-crypto@12.6.2", "@polkadot/util-crypto@^12.6.2": - version "12.6.2" - resolved "https://registry.yarnpkg.com/@polkadot/util-crypto/-/util-crypto-12.6.2.tgz#d2d51010e8e8ca88951b7d864add797dad18bbfc" - integrity sha512-FEWI/dJ7wDMNN1WOzZAjQoIcCP/3vz3wvAp5QQm+lOrzOLj0iDmaIGIcBkz8HVm3ErfSe/uKP0KS4jgV/ib+Mg== +"@polkadot/util-crypto@13.1.1", "@polkadot/util-crypto@^13.1.1": + version "13.1.1" + resolved "https://registry.yarnpkg.com/@polkadot/util-crypto/-/util-crypto-13.1.1.tgz#26960046a9bd6b3b63dc9b006c1a24dc6391b875" + integrity sha512-FG68rrLPdfLcscEyH10vnGkakM4O2lqr71S3GDhgc9WXaS8y9jisLgMPg8jbMHiQBJ3iKYkmtPKiLBowRslj2w== dependencies: "@noble/curves" "^1.3.0" "@noble/hashes" "^1.3.3" - "@polkadot/networks" "12.6.2" - "@polkadot/util" "12.6.2" + "@polkadot/networks" "13.1.1" + "@polkadot/util" "13.1.1" "@polkadot/wasm-crypto" "^7.3.2" "@polkadot/wasm-util" "^7.3.2" - "@polkadot/x-bigint" "12.6.2" - "@polkadot/x-randomvalues" "12.6.2" - "@scure/base" "^1.1.5" - tslib "^2.6.2" - -"@polkadot/util@12.6.2", "@polkadot/util@^12.6.2": - version "12.6.2" - resolved "https://registry.yarnpkg.com/@polkadot/util/-/util-12.6.2.tgz#9396eff491221e1f0fd28feac55fc16ecd61a8dc" - integrity sha512-l8TubR7CLEY47240uki0TQzFvtnxFIO7uI/0GoWzpYD/O62EIAMRsuY01N4DuwgKq2ZWD59WhzsLYmA5K6ksdw== - dependencies: - "@polkadot/x-bigint" "12.6.2" - "@polkadot/x-global" "12.6.2" - "@polkadot/x-textdecoder" "12.6.2" - "@polkadot/x-textencoder" "12.6.2" + "@polkadot/x-bigint" "13.1.1" + "@polkadot/x-randomvalues" "13.1.1" + "@scure/base" "^1.1.7" + tslib "^2.7.0" + +"@polkadot/util@13.1.1", "@polkadot/util@^13.1.1": + version "13.1.1" + resolved "https://registry.yarnpkg.com/@polkadot/util/-/util-13.1.1.tgz#9cbf81e8c48e2ac549dbe2a40384624870016658" + integrity sha512-M4iQ5Um8tFdDmD7a96nPzfrEt+kxyWOqQDPqXyaax4QBnq/WCbq0jo8IO61uz55mdMQnGZvq8jd8uge4V6JzzQ== + dependencies: + "@polkadot/x-bigint" "13.1.1" + "@polkadot/x-global" "13.1.1" + "@polkadot/x-textdecoder" "13.1.1" + "@polkadot/x-textencoder" "13.1.1" "@types/bn.js" "^5.1.5" bn.js "^5.2.1" - tslib "^2.6.2" + tslib "^2.7.0" "@polkadot/wasm-bridge@7.3.2": version "7.3.2" @@ -763,122 +750,110 @@ dependencies: tslib "^2.6.2" -"@polkadot/x-bigint@12.6.2", "@polkadot/x-bigint@^12.6.2": - version "12.6.2" - resolved "https://registry.yarnpkg.com/@polkadot/x-bigint/-/x-bigint-12.6.2.tgz#59b7a615f205ae65e1ac67194aefde94d3344580" - integrity sha512-HSIk60uFPX4GOFZSnIF7VYJz7WZA7tpFJsne7SzxOooRwMTWEtw3fUpFy5cYYOeLh17/kHH1Y7SVcuxzVLc74Q== +"@polkadot/x-bigint@13.1.1", "@polkadot/x-bigint@^13.1.1": + version "13.1.1" + resolved "https://registry.yarnpkg.com/@polkadot/x-bigint/-/x-bigint-13.1.1.tgz#1a9036c9529ce15deab808bee7333bcbd3ab0078" + integrity sha512-Cq4Y6fd9UWtRBZz8RX2tWEBL1IFwUtY6cL8p6HC9yhZtUR6OPjKZe6RIZQa9gSOoIuqZWd6PmtvSNGVH32yfkQ== dependencies: - "@polkadot/x-global" "12.6.2" - tslib "^2.6.2" + "@polkadot/x-global" "13.1.1" + tslib "^2.7.0" -"@polkadot/x-fetch@^12.6.2": - version "12.6.2" - resolved "https://registry.yarnpkg.com/@polkadot/x-fetch/-/x-fetch-12.6.2.tgz#b1bca028db90263bafbad2636c18d838d842d439" - integrity sha512-8wM/Z9JJPWN1pzSpU7XxTI1ldj/AfC8hKioBlUahZ8gUiJaOF7K9XEFCrCDLis/A1BoOu7Ne6WMx/vsJJIbDWw== +"@polkadot/x-fetch@^13.1.1": + version "13.1.1" + resolved "https://registry.yarnpkg.com/@polkadot/x-fetch/-/x-fetch-13.1.1.tgz#df05a3405537accab76000d99aa32cbea790aed9" + integrity sha512-qA6mIUUebJbS+oWzq/EagZflmaoa9b25WvsxSFn7mCvzKngXzr+GYCY4XiDwKY/S+/pr/kvSCKZ1ia8BDqPBYQ== dependencies: - "@polkadot/x-global" "12.6.2" + "@polkadot/x-global" "13.1.1" node-fetch "^3.3.2" - tslib "^2.6.2" + tslib "^2.7.0" -"@polkadot/x-global@12.6.2", "@polkadot/x-global@^12.6.2": - version "12.6.2" - resolved "https://registry.yarnpkg.com/@polkadot/x-global/-/x-global-12.6.2.tgz#31d4de1c3d4c44e4be3219555a6d91091decc4ec" - integrity sha512-a8d6m+PW98jmsYDtAWp88qS4dl8DyqUBsd0S+WgyfSMtpEXu6v9nXDgPZgwF5xdDvXhm+P0ZfVkVTnIGrScb5g== +"@polkadot/x-global@13.1.1", "@polkadot/x-global@^13.1.1": + version "13.1.1" + resolved "https://registry.yarnpkg.com/@polkadot/x-global/-/x-global-13.1.1.tgz#1db0c16e45a20eddf682c98b1d3487619203c8a9" + integrity sha512-DViIMmmEs29Qlsp058VTg2Mn7e3/CpGazNnKJrsBa0o1Ptxl13/4Z0fjqCpNi2GB+kaOsnREzxUORrHcU+PqcQ== dependencies: - tslib "^2.6.2" + tslib "^2.7.0" -"@polkadot/x-randomvalues@12.6.2": - version "12.6.2" - resolved "https://registry.yarnpkg.com/@polkadot/x-randomvalues/-/x-randomvalues-12.6.2.tgz#13fe3619368b8bf5cb73781554859b5ff9d900a2" - integrity sha512-Vr8uG7rH2IcNJwtyf5ebdODMcr0XjoCpUbI91Zv6AlKVYOGKZlKLYJHIwpTaKKB+7KPWyQrk4Mlym/rS7v9feg== +"@polkadot/x-randomvalues@13.1.1": + version "13.1.1" + resolved "https://registry.yarnpkg.com/@polkadot/x-randomvalues/-/x-randomvalues-13.1.1.tgz#e3fc6e77cdfe6f345fca7433dd92a914807a7e4f" + integrity sha512-cXj4omwbgzQQSiBtV1ZBw+XhJUU3iz/DS6ghUnGllSZEK+fGqiyaNgeFQzDY0tKjm6kYaDpvtOHR3mHsbzDuTg== dependencies: - "@polkadot/x-global" "12.6.2" - tslib "^2.6.2" + "@polkadot/x-global" "13.1.1" + tslib "^2.7.0" -"@polkadot/x-textdecoder@12.6.2": - version "12.6.2" - resolved "https://registry.yarnpkg.com/@polkadot/x-textdecoder/-/x-textdecoder-12.6.2.tgz#b86da0f8e8178f1ca31a7158257e92aea90b10e4" - integrity sha512-M1Bir7tYvNappfpFWXOJcnxUhBUFWkUFIdJSyH0zs5LmFtFdbKAeiDXxSp2Swp5ddOZdZgPac294/o2TnQKN1w== +"@polkadot/x-textdecoder@13.1.1": + version "13.1.1" + resolved "https://registry.yarnpkg.com/@polkadot/x-textdecoder/-/x-textdecoder-13.1.1.tgz#305e9a1be38aa435942bc2a73b088a2ca1c1c89b" + integrity sha512-LpZ9KYc6HdBH+i86bCmun4g4GWMiWN/1Pzs0hNdanlQMfqp3UGzl1Dqp0nozMvjWAlvyG7ip235VgNMd8HEbqg== dependencies: - "@polkadot/x-global" "12.6.2" - tslib "^2.6.2" + "@polkadot/x-global" "13.1.1" + tslib "^2.7.0" -"@polkadot/x-textencoder@12.6.2": - version "12.6.2" - resolved "https://registry.yarnpkg.com/@polkadot/x-textencoder/-/x-textencoder-12.6.2.tgz#81d23bd904a2c36137a395c865c5fefa21abfb44" - integrity sha512-4N+3UVCpI489tUJ6cv3uf0PjOHvgGp9Dl+SZRLgFGt9mvxnvpW/7+XBADRMtlG4xi5gaRK7bgl5bmY6OMDsNdw== +"@polkadot/x-textencoder@13.1.1": + version "13.1.1" + resolved "https://registry.yarnpkg.com/@polkadot/x-textencoder/-/x-textencoder-13.1.1.tgz#2588c57c1fae68493a5588a156313d25b91a577e" + integrity sha512-w1mT15B9ptN5CJNgN/A0CmBqD5y9OePjBdU6gmAd8KRhwXCF0MTBKcEZk1dHhXiXtX+28ULJWLrfefC5gxy69Q== dependencies: - "@polkadot/x-global" "12.6.2" - tslib "^2.6.2" + "@polkadot/x-global" "13.1.1" + tslib "^2.7.0" -"@polkadot/x-ws@^12.6.2": - version "12.6.2" - resolved "https://registry.yarnpkg.com/@polkadot/x-ws/-/x-ws-12.6.2.tgz#b99094d8e53a03be1de903d13ba59adaaabc767a" - integrity sha512-cGZWo7K5eRRQCRl2LrcyCYsrc3lRbTlixZh3AzgU8uX4wASVGRlNWi/Hf4TtHNe1ExCDmxabJzdIsABIfrr7xw== +"@polkadot/x-ws@^13.1.1": + version "13.1.1" + resolved "https://registry.yarnpkg.com/@polkadot/x-ws/-/x-ws-13.1.1.tgz#cff0356c75e64f0221706e34f831126287354ac1" + integrity sha512-E/xFmJTiFzu+IK5M3/8W/9fnvNJFelcnunPv/IgO6UST94SDaTsN/Gbeb6SqPb6CsrTHRl3WD+AZ3ErGGwQfEA== dependencies: - "@polkadot/x-global" "12.6.2" - tslib "^2.6.2" - ws "^8.15.1" + "@polkadot/x-global" "13.1.1" + tslib "^2.7.0" + ws "^8.16.0" -"@scure/base@^1.1.1", "@scure/base@^1.1.5", "@scure/base@~1.1.0", "@scure/base@~1.1.2": +"@scure/base@^1.1.1": version "1.1.7" resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.7.tgz#fe973311a5c6267846aa131bc72e96c5d40d2b30" integrity sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g== -"@scure/bip32@1.3.2": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.2.tgz#90e78c027d5e30f0b22c1f8d50ff12f3fb7559f8" - integrity sha512-N1ZhksgwD3OBlwTv3R6KFEcPojl/W4ElJOeCZdi+vuI5QmTFwLq3OFf2zd2ROpKvxFdgZ6hUpb0dx9bVNEwYCA== - dependencies: - "@noble/curves" "~1.2.0" - "@noble/hashes" "~1.3.2" - "@scure/base" "~1.1.2" - -"@scure/bip39@1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.1.tgz#5cee8978656b272a917b7871c981e0541ad6ac2a" - integrity sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg== - dependencies: - "@noble/hashes" "~1.3.0" - "@scure/base" "~1.1.0" +"@scure/base@^1.1.7": + version "1.1.9" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.9.tgz#e5e142fbbfe251091f9c5f1dd4c834ac04c3dbd1" + integrity sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg== "@substrate/connect-extension-protocol@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@substrate/connect-extension-protocol/-/connect-extension-protocol-2.0.0.tgz#badaa6e6b5f7c7d56987d778f4944ddb83cd9ea7" integrity sha512-nKu8pDrE3LNCEgJjZe1iGXzaD6OSIDD4Xzz/yo4KO9mQ6LBvf49BVrt4qxBFGL6++NneLiWUZGoh+VSd4PyVIg== -"@substrate/connect-known-chains@^1.1.1": - version "1.1.6" - resolved "https://registry.yarnpkg.com/@substrate/connect-known-chains/-/connect-known-chains-1.1.6.tgz#2627d329b82b46c7d745752c48c73e1b8ce6ac81" - integrity sha512-JwtdGbnK3ZqrY1qp3Ifr/p648sp9hG0Q715h4nRghnqZJnMQIiLKaFkcLnvrAiYQD3zNTYDztHidy5Q/u0TcbQ== +"@substrate/connect-known-chains@^1.1.5": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@substrate/connect-known-chains/-/connect-known-chains-1.6.0.tgz#e5a6daa0b8796a436c7f1b5676736b3b3dad3d4b" + integrity sha512-ImPIaaQjSs07qI+gfP6sV/HnupexqgPnyicsPax3Pc6mqDp2HUNMDVdaoWjR84yPbgN8+un/P4KOEb5g4wqHSg== -"@substrate/connect@0.8.8": - version "0.8.8" - resolved "https://registry.yarnpkg.com/@substrate/connect/-/connect-0.8.8.tgz#80879f2241e2bd4f24a9aa25d7997fd91a5e68e3" - integrity sha512-zwaxuNEVI9bGt0rT8PEJiXOyebLIo6QN1SyiAHRPBOl6g3Sy0KKdSN8Jmyn++oXhVRD8aIe75/V8ZkS81T+BPQ== +"@substrate/connect@0.8.11": + version "0.8.11" + resolved "https://registry.yarnpkg.com/@substrate/connect/-/connect-0.8.11.tgz#983ec69a05231636e217b573b8130a6b942af69f" + integrity sha512-ofLs1PAO9AtDdPbdyTYj217Pe+lBfTLltdHDs3ds8no0BseoLeAGxpz1mHfi7zB4IxI3YyAiLjH6U8cw4pj4Nw== dependencies: "@substrate/connect-extension-protocol" "^2.0.0" - "@substrate/connect-known-chains" "^1.1.1" - "@substrate/light-client-extension-helpers" "^0.0.4" - smoldot "2.0.22" - -"@substrate/light-client-extension-helpers@^0.0.4": - version "0.0.4" - resolved "https://registry.yarnpkg.com/@substrate/light-client-extension-helpers/-/light-client-extension-helpers-0.0.4.tgz#a5958d5c1aac7df69f55bd90991aa935500f8124" - integrity sha512-vfKcigzL0SpiK+u9sX6dq2lQSDtuFLOxIJx2CKPouPEHIs8C+fpsufn52r19GQn+qDhU8POMPHOVoqLktj8UEA== - dependencies: - "@polkadot-api/client" "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0" - "@polkadot-api/json-rpc-provider" "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0" - "@polkadot-api/json-rpc-provider-proxy" "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0" - "@polkadot-api/substrate-client" "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0" + "@substrate/connect-known-chains" "^1.1.5" + "@substrate/light-client-extension-helpers" "^1.0.0" + smoldot "2.0.26" + +"@substrate/light-client-extension-helpers@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@substrate/light-client-extension-helpers/-/light-client-extension-helpers-1.0.0.tgz#7b60368c57e06e5cf798c6557422d12e6d81f1ff" + integrity sha512-TdKlni1mBBZptOaeVrKnusMg/UBpWUORNDv5fdCaJklP4RJiFOzBCrzC+CyVI5kQzsXBisZ+2pXm+rIjS38kHg== + dependencies: + "@polkadot-api/json-rpc-provider" "^0.0.1" + "@polkadot-api/json-rpc-provider-proxy" "^0.1.0" + "@polkadot-api/observable-client" "^0.3.0" + "@polkadot-api/substrate-client" "^0.1.2" "@substrate/connect-extension-protocol" "^2.0.0" - "@substrate/connect-known-chains" "^1.1.1" + "@substrate/connect-known-chains" "^1.1.5" rxjs "^7.8.1" -"@substrate/ss58-registry@^1.44.0": - version "1.48.0" - resolved "https://registry.yarnpkg.com/@substrate/ss58-registry/-/ss58-registry-1.48.0.tgz#b50b577b491274dbab55711d2e933456637e73d0" - integrity sha512-lE9TGgtd93fTEIoHhSdtvSFBoCsvTbqiCvQIMvX4m6BO/hESywzzTzTFMVP1doBwDDMAN4lsMfIM3X3pdmt7kQ== +"@substrate/ss58-registry@^1.50.0": + version "1.51.0" + resolved "https://registry.yarnpkg.com/@substrate/ss58-registry/-/ss58-registry-1.51.0.tgz#39e0341eb4069c2d3e684b93f0d8cb0bec572383" + integrity sha512-TWDurLiPxndFgKjVavCniytBIw+t4ViOi7TYp9h/D0NMmkEc9klFTo+827eyEJ0lELpqO207Ey7uGxUa+BS1jQ== "@swc/core-darwin-arm64@1.6.1": version "1.6.1" @@ -995,6 +970,18 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== +"@types/lodash.clonedeep@^4.5.9": + version "4.5.9" + resolved "https://registry.yarnpkg.com/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.9.tgz#ea48276c7cc18d080e00bb56cf965bcceb3f0fc1" + integrity sha512-19429mWC+FyaAhOLzsS8kZUsI+/GmBAQ0HFiCPsKGU+7pBXOQWhyrY6xNNDwUSX8SMZMJvuFVMF9O5dQOlQK9Q== + dependencies: + "@types/lodash" "*" + +"@types/lodash@*": + version "4.17.11" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.11.tgz#8d01705ee14865015a0520e80b7ce95f3c2f7060" + integrity sha512-jzqWo/uQP/iqeGGTjhgFp2yaCrCYTauASQcpdzESNCkHjSprBJVcZP9KG9aQ0q+xcsXiKd/iuw/4dLjS3Odc7Q== + "@types/node@*", "@types/node@^20.12.12": version "20.14.5" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.5.tgz#fe35e3022ebe58b8f201580eb24e1fcfc0f2487d" @@ -1007,6 +994,14 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ== +"@types/tape@^5.6.4": + version "5.6.4" + resolved "https://registry.yarnpkg.com/@types/tape/-/tape-5.6.4.tgz#efae4202493043457b1900dceb4808c8f04c7d8f" + integrity sha512-EmL4fJpZyByNCkupLLcJhneqcnT+rQUG5fWKNCsZyBK1x7nUuDTwwEerc4biEMZgvSK2+FXr775aLeXhKXK4Yw== + dependencies: + "@types/node" "*" + "@types/through" "*" + "@types/through@*": version "0.0.33" resolved "https://registry.yarnpkg.com/@types/through/-/through-0.0.33.tgz#14ebf599320e1c7851e7d598149af183c6b9ea56" @@ -1110,11 +1105,6 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -abitype@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.0.tgz#237176dace81d90d018bebf3a45cb42f2a2d9e97" - integrity sha512-NMeMah//6bJ56H5XRj8QCV4AwuW6hB6zqz2LnhhLdcWVQOsXki6/Pn3APeqxCma62nXIcmZWdu1DlHWS74umVQ== - acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" @@ -1453,7 +1443,7 @@ colorspace@1.1.x: color "^3.1.3" text-hex "1.0.x" -commander@^12.0.0, commander@~12.1.0: +commander@^12.1.0, commander@~12.1.0: version "12.1.0" resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3" integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== @@ -2573,11 +2563,6 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -isows@1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/isows/-/isows-1.0.4.tgz#810cd0d90cc4995c26395d2aa4cfa4037ebdf061" - integrity sha512-hEzjY+x9u9hPmBom9IIAqdJCwNLax+xrPb51vEPpERoFlIxgmZcHzsT5jKG06nvInKOBGvReAVz80Umed5CczQ== - jackspeak@^3.1.2: version "3.4.0" resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.0.tgz#a75763ff36ad778ede6a156d8ee8b124de445b4a" @@ -2852,10 +2837,10 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -nock@^13.5.0: - version "13.5.4" - resolved "https://registry.yarnpkg.com/nock/-/nock-13.5.4.tgz#8918f0addc70a63736170fef7106a9721e0dc479" - integrity sha512-yAyTfdeNJGGBFxWdzSKCBYxs5FxLbCg5X5Q4ets974hcQzG1+qCxvIyOo4j2Ry6MUlhWVMX4OoYDefAIIwupjw== +nock@^13.5.4: + version "13.5.5" + resolved "https://registry.yarnpkg.com/nock/-/nock-13.5.5.tgz#cd1caaca281d42be17d51946367a3d53a6af3e78" + integrity sha512-XKYnqUrCwXC8DGG1xX4YH5yNIrlh9c065uaMZZHUoeUUINTOyt+x/G+ezYk0Ft6ExSREVIs+qBJDK503viTfFA== dependencies: debug "^4.1.0" json-stringify-safe "^5.0.1" @@ -3406,10 +3391,10 @@ slice-ansi@^7.0.0: ansi-styles "^6.2.1" is-fullwidth-code-point "^5.0.0" -smoldot@2.0.22: - version "2.0.22" - resolved "https://registry.yarnpkg.com/smoldot/-/smoldot-2.0.22.tgz#1e924d2011a31c57416e79a2b97a460f462a31c7" - integrity sha512-B50vRgTY6v3baYH6uCgL15tfaag5tcS2o/P5q1OiXcKGv1axZDfz2dzzMuIkVpyMR2ug11F6EAtQlmYBQd292g== +smoldot@2.0.26: + version "2.0.26" + resolved "https://registry.yarnpkg.com/smoldot/-/smoldot-2.0.26.tgz#0e64c7fcd26240fbe4c8d6b6e4b9a9aca77e00f6" + integrity sha512-F+qYmH4z2s2FK+CxGj8moYcd1ekSIKH8ywkdqlOz88Dat35iB1DIYL11aILN46YSGMzQW/lbJNS307zBSDN5Ig== dependencies: ws "^8.8.1" @@ -3731,6 +3716,11 @@ tslib@^2.1.0, tslib@^2.4.0, tslib@^2.6.2: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== +tslib@^2.7.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.0.tgz#d124c86c3c05a40a91e6fdea4021bd31d377971b" + integrity sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA== + tsup@^6.7.0: version "6.7.0" resolved "https://registry.yarnpkg.com/tsup/-/tsup-6.7.0.tgz#416f350f32a07b6ae86792ad7e52b0cafc566d64" @@ -3859,20 +3849,6 @@ uuid@^9.0.1: resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== -viem@^2.7.8: - version "2.15.1" - resolved "https://registry.yarnpkg.com/viem/-/viem-2.15.1.tgz#05a9ef5fd74661bd77d865c334477a900e59b436" - integrity sha512-Vrveen3vDOJyPf8Q8TDyWePG2pTdK6IpSi4P6qlvAP+rXkAeqRvwYBy9AmGm+BeYpCETAyTT0SrCP6458XSt+w== - dependencies: - "@adraffy/ens-normalize" "1.10.0" - "@noble/curves" "1.2.0" - "@noble/hashes" "1.3.2" - "@scure/bip32" "1.3.2" - "@scure/bip39" "1.2.1" - abitype "1.0.0" - isows "1.0.4" - ws "8.17.1" - web-streams-polyfill@^3.0.3: version "3.3.3" resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b" @@ -3994,7 +3970,12 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== -ws@8.17.1, ws@^8.15.1, ws@^8.8.1: +ws@^8.16.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + +ws@^8.8.1: version "8.17.1" resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== @@ -4023,3 +4004,15 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +yocto-spinner@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/yocto-spinner/-/yocto-spinner-0.1.1.tgz#bc3b841ebd74f8ec4bc9d36eddf025d3841cda9f" + integrity sha512-vb6yztJdmbX9BwiR2NlKim7roGM5xFFhiTO6UstNiKBnh8NT6uFNjpXYC6DWTnLgRRyHh2nDNEM8kLHSRLw4kg== + dependencies: + yoctocolors "^2.1.1" + +yoctocolors@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/yoctocolors/-/yoctocolors-2.1.1.tgz#e0167474e9fbb9e8b3ecca738deaa61dd12e56fc" + integrity sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==