├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── run-tests.yaml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── jest.config.base.js ├── jest.config.integration.js ├── jest.config.js ├── lerna.json ├── openapitools.json ├── package.json ├── packages ├── account │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── _types.ts │ │ ├── addresses │ │ │ ├── _types.ts │ │ │ ├── abstractAddress.ts │ │ │ ├── accountAddress.ts │ │ │ ├── index.ts │ │ │ ├── resourceIdentifier.ts │ │ │ └── validatorAddress.ts │ │ ├── bech32 │ │ │ ├── _types.ts │ │ │ ├── bech32.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── signingKey.ts │ │ └── signingKeychain.ts │ ├── test │ │ ├── accountAddress.test.ts │ │ ├── bech32.test.ts │ │ ├── resourceIdentifier.test.ts │ │ ├── signingKey.test.ts │ │ ├── signingKeychain.test.ts │ │ ├── utils.ts │ │ └── validatorAddress.test.ts │ ├── tsconfig.json │ └── tsconfig.test.json ├── application │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── _types.ts │ │ ├── account.ts │ │ ├── actions │ │ │ ├── _types.ts │ │ │ ├── index.ts │ │ │ ├── intendedStakeTokensAction.ts │ │ │ ├── intendedTransferTokensAction.ts │ │ │ └── intendedUnstakeTokensAction.ts │ │ ├── api │ │ │ ├── _types.ts │ │ │ ├── api.ts │ │ │ ├── decoders.ts │ │ │ ├── index.ts │ │ │ ├── json-rpc │ │ │ │ ├── _types.ts │ │ │ │ ├── index.ts │ │ │ │ ├── interface.ts │ │ │ │ └── responseHandlers.ts │ │ │ ├── open-api │ │ │ │ ├── _types.ts │ │ │ │ ├── interface.ts │ │ │ │ └── responseHandlers.ts │ │ │ ├── radixCoreAPI.ts │ │ │ └── utils.ts │ │ ├── dto │ │ │ ├── _types.ts │ │ │ ├── index.ts │ │ │ ├── transactionIdentifier.ts │ │ │ └── transactionIntentBuilder.ts │ │ ├── errors.ts │ │ ├── global-fetch.d.ts │ │ ├── index.ts │ │ ├── mockRadix.ts │ │ ├── radix.ts │ │ └── wallet.ts │ ├── test │ │ ├── _load-rpc.ts │ │ ├── atomIdentifier.test.ts │ │ ├── integration-tests │ │ │ └── API.test.integration.ts │ │ ├── json-rpc.deprecatedTest.ts │ │ ├── open-api.test.ts │ │ ├── radix.deprecatedTest.ts │ │ ├── responseHandlers.test.ts │ │ ├── stringifyTypes.ts │ │ ├── transactionIntentBuilder.deprecatedTest.ts │ │ ├── transferTokensAction.deprecatedTest.ts │ │ ├── util.ts │ │ └── wallet.test.ts │ ├── tsconfig.json │ └── tsconfig.test.json ├── crypto │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── _types.ts │ │ ├── elliptic-curve │ │ │ ├── _types.ts │ │ │ ├── ecPointOnCurve.ts │ │ │ ├── hd │ │ │ │ ├── bip32 │ │ │ │ │ ├── _types.ts │ │ │ │ │ ├── bip32.ts │ │ │ │ │ ├── bip32PathComponent.ts │ │ │ │ │ ├── bip44 │ │ │ │ │ │ ├── _types.ts │ │ │ │ │ │ ├── bip44.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── bip39 │ │ │ │ │ ├── _types.ts │ │ │ │ │ ├── hdMasterSeed.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mnemonic.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── indutnyEllipticImportDER.js │ │ │ ├── keyPair.ts │ │ │ ├── privateKey.ts │ │ │ ├── publicKey.ts │ │ │ ├── secp256k1.ts │ │ │ └── signature.ts │ │ ├── encryption │ │ │ ├── _types.ts │ │ │ ├── index.ts │ │ │ ├── message.ts │ │ │ ├── messageEncryption.ts │ │ │ └── sealedMessage.ts │ │ ├── hash │ │ │ ├── index.ts │ │ │ └── sha.ts │ │ ├── index.ts │ │ ├── key-derivation-functions │ │ │ ├── _types.ts │ │ │ ├── index.ts │ │ │ └── scrypt.ts │ │ ├── keystore │ │ │ ├── _types.ts │ │ │ ├── index.ts │ │ │ └── keystore.ts │ │ ├── symmetric-encryption │ │ │ ├── aes │ │ │ │ ├── _types.ts │ │ │ │ ├── aesGCM.ts │ │ │ │ ├── aesGCMSealedBox.ts │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ └── utils.ts │ ├── test │ │ ├── aesGCM.test.ts │ │ ├── bip32.test.ts │ │ ├── bip39.test.ts │ │ ├── bip44.test.ts │ │ ├── diffiehellman.test.ts │ │ ├── ellipticCurveCryptography.test.ts │ │ ├── hash.test.ts │ │ ├── keystore.test.ts │ │ ├── message.test.ts │ │ ├── messageEncryption.test.ts │ │ ├── scrypt.test.ts │ │ ├── sealedMessage.test.ts │ │ └── utils.ts │ ├── tsconfig.json │ └── tsconfig.test.json ├── data-formats │ ├── CHANGELOG.md │ ├── README.md │ ├── customTypes │ │ └── cbor.d.ts │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── json │ │ │ ├── _types.ts │ │ │ ├── decoding.ts │ │ │ └── index.ts │ ├── test │ │ └── serialization.test.ts │ ├── tsconfig.json │ └── tsconfig.test.json ├── hardware-ledger │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.ledger.js │ ├── package.json │ ├── src │ │ ├── _types.ts │ │ ├── apdu.ts │ │ ├── device-connection.ts │ │ ├── hardwareWalletFromLedger.ts │ │ ├── index.ts │ │ └── ledgerNano.ts │ ├── test │ │ └── physical-devices │ │ │ ├── ledgerNano.test.integration.ts │ │ │ ├── radix.test.integration.ts │ │ │ ├── signingKeychain.test.integration.ts │ │ │ └── utils.ts │ ├── tsconfig.json │ └── tsconfig.test.json ├── hardware-wallet │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── _types.ts │ │ ├── hardwareWallet.ts │ │ ├── index.ts │ │ └── semVer.ts │ ├── tsconfig.json │ └── tsconfig.test.json ├── networking │ ├── CHANGELOG.md │ ├── README.md │ ├── extract-api-version.js │ ├── generate-open-api.sh │ ├── openapitools.json │ ├── package.json │ ├── schema.yaml │ ├── src │ │ ├── _types.ts │ │ ├── global-fetch.d.ts │ │ ├── index.ts │ │ ├── open-api-client.ts │ │ ├── open-api │ │ │ ├── .gitignore │ │ │ ├── .npmignore │ │ │ ├── .openapi-generator-ignore │ │ │ ├── .openapi-generator │ │ │ │ ├── FILES │ │ │ │ └── VERSION │ │ │ ├── api-version.ts │ │ │ ├── api.ts │ │ │ ├── base.ts │ │ │ ├── common.ts │ │ │ ├── configuration.ts │ │ │ ├── git_push.sh │ │ │ └── index.ts │ │ └── open-rpc-client.ts │ ├── tsconfig.json │ └── tsconfig.test.json ├── primitives │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── _types.ts │ │ ├── amount.ts │ │ ├── index.ts │ │ ├── resultAsync.ts │ │ └── uint256-extensions.ts │ ├── test │ │ └── amount.test.ts │ ├── tsconfig.json │ └── tsconfig.test.json ├── tx-parser │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── _types.ts │ │ ├── bytes.ts │ │ ├── index.ts │ │ ├── instruction.ts │ │ ├── preparedStake.ts │ │ ├── preparedUnstake.ts │ │ ├── reAddress.ts │ │ ├── removeWhitespace.ts │ │ ├── stakeOwnership.ts │ │ ├── substate.ts │ │ ├── substateId.ts │ │ ├── tokens.ts │ │ ├── transaction.ts │ │ ├── txSignature.ts │ │ ├── validatorAllowDelegationFlag.ts │ │ └── validatorOwnerCopy.ts │ ├── test │ │ └── txParser.test.ts │ ├── tsconfig.json │ └── tsconfig.test.json └── util │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ ├── _types.ts │ ├── arrays.ts │ ├── bufferReader.ts │ ├── buffers.ts │ ├── byte.ts │ ├── enumTypeGuard.ts │ ├── extractErrorMessage.ts │ ├── functional.ts │ ├── index.ts │ ├── isNode.ts │ ├── logging.ts │ ├── objectEquals.ts │ ├── resultAsync_observable.ts │ ├── resultHelpers.ts │ ├── secureRandomGenerator.ts │ └── typeGuards.ts │ ├── test │ ├── arrays.test.ts │ ├── mapEquals.test.ts │ ├── neverthrow-extensions.test.ts │ ├── util.test.ts │ └── util.ts │ ├── tsconfig.json │ └── tsconfig.test.json ├── tsconfig.base.json ├── tsconfig.json ├── tsconfig.test.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | # don't ever lint node_modules 2 | node_modules 3 | # don't lint build output (make sure it's set to your correct build folder name) 4 | dist 5 | # don't lint nyc coverage output 6 | coverage 7 | 8 | .eslintrc.js 9 | jest.*.js 10 | jest*.js 11 | *.test.ts 12 | **/_load-rpc.ts -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | parserOptions: { 5 | tsconfigRootDir: __dirname, 6 | project: [ 7 | './packages/*/tsconfig.json', 8 | './packages/*/tsconfig.test.json', 9 | ], 10 | }, 11 | plugins: ['@typescript-eslint', 'jest', 'functional', 'jsdoc'], 12 | extends: [ 13 | 'eslint:recommended', 14 | 'plugin:@typescript-eslint/recommended', 15 | 'plugin:@typescript-eslint/recommended-requiring-type-checking', 16 | 'plugin:jest/recommended', 17 | 'plugin:jsdoc/recommended', 18 | 'plugin:functional/external-recommended', // https://github.com/jonaskello/eslint-plugin-functional 19 | 'plugin:functional/recommended', // https://github.com/jonaskello/eslint-plugin-functional 20 | 'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier 21 | 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 22 | ], 23 | env: { 24 | node: true, 25 | }, 26 | rules: { 27 | complexity: ['error', 5], 28 | 29 | 'max-depth': ['error', 3], 30 | 31 | 'max-lines': [ 32 | 'error', 33 | { 34 | max: 200, 35 | skipBlankLines: true, 36 | skipComments: true, 37 | }, 38 | ], 39 | 40 | 'max-lines-per-function': 'off', 41 | 42 | 'max-nested-callbacks': ['error', 3], 43 | 44 | 'max-params': ['error', 1], 45 | 46 | '@typescript-eslint/no-unused-vars': [ 47 | 'warn', 48 | { argsIgnorePattern: '^_' }, 49 | ], 50 | 51 | '@typescript-eslint/require-await': 'off', 52 | '@typescript-eslint/ban-ts-comment': 'off', 53 | '@typescript-eslint/no-non-null-assertion': 'off', 54 | 55 | '@typescript-eslint/no-inferrable-types': 'off', 56 | 57 | // ESLint-Plugin-Functional RULES 58 | 'functional/immutable-data': 'error', 59 | 'functional/no-let': 'off', 60 | 'functional/no-loop-statement': 'off', 61 | 'no-param-reassign': 'error', 62 | 'functional/no-try-statement': 'off', 63 | 'jsdoc/require-returns': 'off', 64 | 65 | // ESLint-Plugin-JSDoc RULES 66 | 'jsdoc/check-param-names': 'off', 67 | 'jsdoc/require-param-type': 'off', 68 | 'jsdoc/require-param': 'off', 69 | '@typescript-eslint/explicit-function-return-type': 'off', 70 | 'jsdoc/require-returns-type': 'off', 71 | '@typescript-eslint/explicit-module-boundary-types': 'off', 72 | 73 | 'functional/no-expression-statement': 'off', 74 | 'functional/no-conditional-statement': 'off', // we like switch statements 75 | 'functional/no-mixed-type': 'off', 76 | 'functional/functional-parameters': 'off', 77 | 'functional/prefer-readonly-type': 'off', // false positive trigger on `Readonly<>` - which we like. 78 | 'jest/no-export': 'off', 79 | 'functional/no-return-void': 'off', 80 | 'max-params': 'off', 81 | 'functional/no-throw-statement': 'off', 82 | complexity: 'off', 83 | 'functional/immutable-data': 'off', 84 | '@typescript-eslint/no-unsafe-return': 'warn', 85 | '@typescript-eslint/no-unsafe-member-access': 'warn', 86 | 'max-lines': 'off', 87 | '@typescript-eslint/no-unsafe-assignment': 'off', 88 | '@typescript-eslint/no-namespace': 'off', 89 | 'functional/no-this-expression': 'off', 90 | '@typescript-eslint/no-var-requires': 'off', 91 | 'arrow-body-style': ['warn', 'as-needed'], 92 | 'no-useless-return': 1, 93 | '@typescript-eslint/restrict-template-expressions': 'off', 94 | 'no-param-reassign': 'off', 95 | '@typescript-eslint/no-floating-promises': 'off', 96 | }, 97 | } 98 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yaml: -------------------------------------------------------------------------------- 1 | name: Build, lint and run tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [15.5.x] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - name: Install 25 | run: | 26 | yarn policies set-version '^1.22.10' 27 | - run: yarn setup-dev 28 | - run: yarn sanity -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | build 4 | .idea/* 5 | .vscode 6 | lerna-debug.log 7 | yarn-error.log 8 | .yarn/ 9 | !.yarn/releases 10 | !.yarn/plugins 11 | .pnp.* 12 | 13 | dist 14 | includes 15 | tsconfig.tsbuildinfo 16 | coverage 17 | devTestOnly/ 18 | .eslintcache 19 | 20 | combined.log 21 | error.log 22 | dev.log -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 15.5.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .tmp/ 2 | node_modules/ 3 | package.json 4 | .travis.yml 5 | *.md 6 | dist/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "typescript", 3 | "printWidth": 80, 4 | "tabWidth": 4, 5 | "useTabs": true, 6 | "singleQuote": true, 7 | "trailingComma": "all", 8 | "arrowParens": "avoid", 9 | "semi": false, 10 | "overrides": [ 11 | { 12 | "files": "*.json", 13 | "options": { 14 | "parser": "json" 15 | } 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This library is intended for use specifically within the Radix Desktop Wallet, not for general use. It is undocumented, unsupported, and may be changed at any time. Use at your own risk. We have disabled issues, although PRs with bug fixes will be considered. 2 | 3 | For the Olympia mainnet release, no language-specific libraries are offered and we instead recommend use of the simple JSON RPC and REST API endpoints offered by the Radix node. These cover the full token and staking functionality of Olympia, sufficient for exchange or wallet integration. 4 | 5 | The later Alexandria and Babylon releases will focus more on developer functionality and so heavily refactored APIs and new libraries are expected to start to be introduced at that time. We expect these new APIs and libraries to become the primary method of programmatic interaction with the Radix network at its Babylon release. 6 | 7 | ## Build and run tests 8 | 9 | ``` 10 | yarn build && yarn bootstrap 11 | yarn test 12 | ``` 13 | 14 | ## Publish a new release 15 | 16 | When new changes have been merged into the main branch, and you want to publish a new version, make sure you're up to date with main branch locally and do: 17 | 18 | ``` 19 | yarn release 20 | ``` 21 | 22 | It will automatically bump the versions in the changed packages, update the changelog, commit and publish to npm. 23 | 24 | For commits to main branch, please follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). 25 | 26 | -------------------------------------------------------------------------------- /jest.config.base.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | transform: { 3 | '^.+\\.ts$': 'ts-jest', 4 | }, 5 | moduleDirectories: [ 6 | 'packages/*/src', 7 | '/includes', 8 | '/node_modules', 9 | '/*/node_modules', 10 | ], 11 | moduleFileExtensions: ['js', 'ts', 'node', 'json'], 12 | moduleNameMapper: { 13 | '^@radix-javascript/(.*)$': '/packages/$1/src/index.ts', 14 | 'cross-fetch': 'jest-fetch-mock', 15 | }, 16 | globals: { 17 | 'ts-jest': { 18 | babelConfig: true, 19 | tsconfig: './tsconfig.test.json', 20 | }, 21 | }, 22 | testURL: 'http://localhost', 23 | collectCoverage: true, 24 | collectCoverageFrom: [ 25 | '/packages/*/src/**/*.{ts,js}', 26 | '!**/node_modules/**', 27 | ], 28 | watchPlugins: [ 29 | 'jest-watch-typeahead/filename', 30 | 'jest-watch-typeahead/testname', 31 | ], 32 | modulePathIgnorePatterns: [], 33 | } 34 | 35 | module.exports = config 36 | -------------------------------------------------------------------------------- /jest.config.integration.js: -------------------------------------------------------------------------------- 1 | const sharedConfig = require('./jest.config.base') 2 | module.exports = { 3 | ...sharedConfig, 4 | testMatch: [ 5 | '/packages/**/test/integration-tests/?(*.)+(spec|test).integration.ts', 6 | ], 7 | } 8 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const sharedConfig = require('./jest.config.base') 2 | module.exports = { 3 | ...sharedConfig, 4 | testMatch: ['/packages/**/test/?(*.)+(spec|test).ts'], 5 | } 6 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmClient": "yarn", 3 | "packages": [ 4 | "packages/*" 5 | ], 6 | "useWorkspaces": true, 7 | "version": "independent" 8 | } 9 | -------------------------------------------------------------------------------- /openapitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json", 3 | "spaces": 2, 4 | "generator-cli": { 5 | "version": "5.3.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "bugs": { 3 | "url": "https://github.com/radixdlt/radixdlt-javascript/issues" 4 | }, 5 | "devDependencies": { 6 | "@octokit/core": "^3.2.4", 7 | "@trust/webcrypto": "^0.9.2", 8 | "@typescript-eslint/eslint-plugin": "^4.11.0", 9 | "@typescript-eslint/parser": "^4.11.0", 10 | "cross-env": "^7.0.3", 11 | "dotenv": "^8.2.0", 12 | "eslint": "^7.16.0", 13 | "eslint-config-prettier": "^7.1.0", 14 | "eslint-plugin-functional": "^3.1.0", 15 | "eslint-plugin-jest": "^24.1.3", 16 | "eslint-plugin-jsdoc": "^30.7.13", 17 | "eslint-plugin-prettier": "^3.3.0", 18 | "jest": "^26.6.3", 19 | "jest-environment-jsdom": "25", 20 | "jest-fetch-mock": "^3.0.3", 21 | "jest-json-schema": "^2.1.0", 22 | "jest-runner-groups": "^2.0.1", 23 | "jest-watch-typeahead": "^0.6.1", 24 | "lerna": "^3.22.1", 25 | "lint-staged": ">=10", 26 | "pinst": "^2.1.1", 27 | "prettier": "^2.2.1", 28 | "prettier-eslint": "^12.0.0", 29 | "ts-jest": "^26.4.4", 30 | "typescript": "^4.5.2" 31 | }, 32 | "engines": { 33 | "yarn": "^1.22.10", 34 | "node": "^15.5.0" 35 | }, 36 | "homepage": "https://github.com/radixdlt/radixdlt-javascript#README", 37 | "keywords": [ 38 | "eXRD", 39 | "Decentralized", 40 | "DeFi", 41 | "DLT", 42 | "Distributed Ledger Technology", 43 | "Crypto", 44 | "Cryptocurrency", 45 | "Ledger", 46 | "Money", 47 | "Public Network", 48 | "Radix", 49 | "RadixDLT", 50 | "Radix DLT", 51 | "XRD" 52 | ], 53 | "license": "Apache-2.0", 54 | "private": true, 55 | "repository": { 56 | "type": "git", 57 | "url": "git+https://github.com/radixdlt/radixdlt-javascript.git" 58 | }, 59 | "scripts": { 60 | "setup-dev": "yarn && yarn bootstrap && yarn link-all && yarn build", 61 | "bootstrap": "lerna bootstrap", 62 | "link-all": "yarn lerna exec --parallel yarn link", 63 | "unlink-all": "lerna exec --parallel --bail=false yarn unlink", 64 | "lint": "yarn eslint . --ext .js,.jsx,.ts,.tsx", 65 | "lint:fix": "yarn eslint --fix . --ext .js,.jsx,.ts,.tsx", 66 | "build": "yarn build:ts", 67 | "build:ts": "tsc -b -f tsconfig.json", 68 | "bundle": "ts-node -P scripts/tsconfig.json scripts/bundle.ts umd,esm", 69 | "clean": "lerna clean --yes && lerna run clean && rm -rf includes", 70 | "baseTest": "jest --rootDir=.", 71 | "test": "yarn baseTest -c jest.config.js --testTimeout=10000", 72 | "test:baseIntegration": "yarn baseTest --runInBand --testTimeout=20000 --verbose true", 73 | "test:integration": "NODE_TLS_REJECT_UNAUTHORIZED=0 yarn test:baseIntegration -c jest.config.integration.js", 74 | "test:ledger": "yarn test:baseIntegration --coverage=false -c ./packages/hardware-ledger/jest.config.ledger.js", 75 | "test:ledger:hw": "yarn test:ledger ./packages/hardware-ledger/test/physical-devices/ledgerNano.test.integration.ts", 76 | "test:ledger:account": "yarn test:ledger ./packages/hardware-ledger/test/physical-devices/signingKeychain.test.integration.ts", 77 | "test:ledger:app": "yarn test:ledger ./packages/hardware-ledger/test/physical-devices/radix.test.integration.ts", 78 | "release": "yarn build && yarn bootstrap && lerna publish --conventional-commits", 79 | "format": "prettier --write '**/*.{ts,tsx,js}' --config .prettierrc", 80 | "sanity": "yarn format && yarn build && yarn test" 81 | }, 82 | "workspaces": [ 83 | "packages/*" 84 | ] 85 | } 86 | -------------------------------------------------------------------------------- /packages/account/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@radixdlt/account", 3 | "version": "3.0.14", 4 | "description": "A JavaScript client library for interacting with the Radix Distributed Ledger.", 5 | "keywords": [ 6 | "Account", 7 | "Radix", 8 | "RadixDLT" 9 | ], 10 | "author": "Alexander Cyon ", 11 | "homepage": "https://github.com/radixdlt/radixdlt-javascript/tree/master/packages/account#readme", 12 | "license": "Apache-2.0", 13 | "main": "dist/index.js", 14 | "publishConfig": { 15 | "access": "public" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/radixdlt/radixdlt-javascript.git" 20 | }, 21 | "scripts": { 22 | "test": "echo \"Error: run tests from root\" && exit 1" 23 | }, 24 | "bugs": { 25 | "url": "https://github.com/radixdlt/radixdlt-javascript/issues" 26 | }, 27 | "engines": { 28 | "node": ">=15.5.0 <16" 29 | }, 30 | "dependencies": { 31 | "@radixdlt/crypto": "^2.1.17", 32 | "@radixdlt/data-formats": "1.0.30", 33 | "@radixdlt/hardware-wallet": "^2.1.20", 34 | "@radixdlt/primitives": "^3.0.9", 35 | "@radixdlt/uint256": "1.1.0", 36 | "@radixdlt/util": "1.0.29", 37 | "bech32": "^2.0.0", 38 | "bip39": "^3.0.3", 39 | "hdkey": "^2.0.1", 40 | "long": "^4.0.0", 41 | "neverthrow": "^4.0.1", 42 | "prelude-ts": "^1.0.2", 43 | "rxjs": "7.0.0" 44 | }, 45 | "devDependencies": { 46 | "@types/hdkey": "^0.7.1", 47 | "@types/long": "^4.0.1", 48 | "ethereum-public-key-to-address": "^0.0.5" 49 | }, 50 | "gitHead": "f8026eb0c310402ba395025e6a544f7c6baffd66" 51 | } 52 | -------------------------------------------------------------------------------- /packages/account/src/addresses/_types.ts: -------------------------------------------------------------------------------- 1 | import { PublicKeyT } from '@radixdlt/crypto' 2 | import { Network } from '@radixdlt/primitives' 3 | 4 | export type ResourceIdentifierT = Readonly<{ 5 | hash: Buffer 6 | __witness: 'isRRI' 7 | network: Network 8 | name: string 9 | toString: () => string 10 | equals: (other: ResourceIdentifierT) => boolean 11 | }> 12 | 13 | export enum AddressTypeT { 14 | VALIDATOR = 'VALIDATOR_ADDRESS', 15 | ACCOUNT = 'ACCOUNT_ADDRESS', 16 | } 17 | 18 | export type AbstractAddressT = Readonly<{ 19 | addressType: AddressTypeT 20 | network: Network 21 | publicKey: PublicKeyT 22 | toString: () => string 23 | equals: (other: AbstractAddressT) => boolean 24 | }> 25 | 26 | export type AccountAddressT = AbstractAddressT & 27 | Readonly<{ 28 | addressType: AddressTypeT.ACCOUNT 29 | equals: (other: AccountAddressT) => boolean 30 | }> 31 | 32 | export type ValidatorAddressT = AbstractAddressT & 33 | Readonly<{ 34 | addressType: AddressTypeT.VALIDATOR 35 | equals: (other: ValidatorAddressT) => boolean 36 | }> 37 | -------------------------------------------------------------------------------- /packages/account/src/addresses/index.ts: -------------------------------------------------------------------------------- 1 | export * from './_types' 2 | 3 | export * from './accountAddress' 4 | export * from './validatorAddress' 5 | export * from './resourceIdentifier' 6 | -------------------------------------------------------------------------------- /packages/account/src/bech32/_types.ts: -------------------------------------------------------------------------------- 1 | export type Bech32T = Readonly<{ 2 | hrp: string 3 | 4 | // excluding checksum 5 | data: Buffer 6 | 7 | // including checksum 8 | toString: () => string 9 | 10 | equals: (other: Bech32T) => boolean 11 | }> 12 | -------------------------------------------------------------------------------- /packages/account/src/bech32/bech32.ts: -------------------------------------------------------------------------------- 1 | import { bech32, bech32m, BechLib, Decoded } from 'bech32' 2 | import { log, msgFromError } from '@radixdlt/util' 3 | import { err, ok, Result } from 'neverthrow' 4 | import { Bech32T } from './_types' 5 | 6 | export enum Encoding { 7 | BECH32 = 'bech32', 8 | BECH32m = 'bech32m', 9 | } 10 | 11 | export const defaultEncoding = Encoding.BECH32 12 | 13 | const convertDataFromBech32 = (bech32Data: Buffer): Result => { 14 | try { 15 | const data = bech32.fromWords(bech32Data) 16 | return ok(Buffer.from(data)) 17 | } catch (e) { 18 | const underlyingError = msgFromError(e) 19 | const errMsg = `Failed to converted bech32 data to Buffer, underlying error: '${underlyingError}'` 20 | return err(new Error(errMsg)) 21 | } 22 | } 23 | 24 | const convertDataToBech32 = (data: Buffer): Result => { 25 | try { 26 | const bech32Data = bech32.toWords(data) 27 | return ok(Buffer.from(bech32Data)) 28 | } catch (e) { 29 | const underlyingError = msgFromError(e) 30 | const errMsg = `Failed to converted buffer to bech32 data, underlying error: '${underlyingError}'` 31 | return err(new Error(errMsg)) 32 | } 33 | } 34 | 35 | const __unsafeCreate = ( 36 | input: Readonly<{ 37 | bech32String: string 38 | hrp: string 39 | data: Buffer 40 | }>, 41 | ): Bech32T => { 42 | const toString = (): string => input.bech32String 43 | const equals = (other: Bech32T): boolean => toString() === other.toString() 44 | return { hrp: input.hrp, data: input.data, equals, toString } 45 | } 46 | 47 | export type Bech32EncodeInput = Readonly<{ 48 | hrp: string 49 | data: Buffer 50 | encoding?: Encoding 51 | maxLength?: number 52 | }> 53 | 54 | const encode = (input: Bech32EncodeInput): Result => { 55 | const { hrp, data, maxLength } = input 56 | const encoding = input.encoding ?? defaultEncoding 57 | 58 | const impl: BechLib = encoding === Encoding.BECH32 ? bech32 : bech32m 59 | 60 | try { 61 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access 62 | const bech32String: string = impl.encode(hrp, data, maxLength) 63 | return ok( 64 | __unsafeCreate({ 65 | bech32String: bech32String.toLowerCase(), 66 | hrp, 67 | data, 68 | }), 69 | ) 70 | } catch (e) { 71 | const errMsg = msgFromError(e) 72 | log.error(errMsg) 73 | return err(new Error(errMsg)) 74 | } 75 | } 76 | 77 | export type Bech32DecodeInput = Readonly<{ 78 | bechString: string 79 | encoding?: Encoding 80 | maxLength?: number 81 | }> 82 | 83 | const decode = (input: Bech32DecodeInput): Result => { 84 | const { bechString, maxLength } = input 85 | const encoding = input.encoding ?? defaultEncoding 86 | 87 | const impl: BechLib = encoding === Encoding.BECH32 ? bech32 : bech32m 88 | 89 | try { 90 | const decoded: Decoded = impl.decode(bechString, maxLength) 91 | return ok( 92 | __unsafeCreate({ 93 | bech32String: bechString, 94 | hrp: decoded.prefix, 95 | data: Buffer.from(decoded.words), 96 | }), 97 | ) 98 | } catch (e) { 99 | const errMsg = msgFromError(e) 100 | log.error(errMsg) 101 | return err(new Error(errMsg)) 102 | } 103 | } 104 | 105 | export const Bech32 = { 106 | convertDataToBech32, 107 | convertDataFromBech32, 108 | decode, 109 | encode, 110 | } 111 | -------------------------------------------------------------------------------- /packages/account/src/bech32/index.ts: -------------------------------------------------------------------------------- 1 | export * from './_types' 2 | export * from './bech32' 3 | -------------------------------------------------------------------------------- /packages/account/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './bech32' 2 | export * from './addresses' 3 | 4 | export * from './_types' 5 | 6 | export * from './signingKeychain' 7 | export * from './signingKey' 8 | -------------------------------------------------------------------------------- /packages/account/test/bech32.test.ts: -------------------------------------------------------------------------------- 1 | import { Bech32 } from '../src' 2 | 3 | describe('bech32', () => { 4 | it('to from bech32 data', () => { 5 | const plaintext = 'Hello Radix!' 6 | const bech32DataHex = '09011216181b030f04010906021903090f001010' 7 | const bech32Data = Buffer.from(bech32DataHex, 'hex') 8 | const decodedBech32Data = Bech32.convertDataFromBech32( 9 | bech32Data, 10 | )._unsafeUnwrap() 11 | expect(decodedBech32Data.toString('utf8')).toBe(plaintext) 12 | 13 | const convertedToBech32Data = Bech32.convertDataToBech32( 14 | Buffer.from(plaintext, 'utf8'), 15 | )._unsafeUnwrap() 16 | 17 | expect(convertedToBech32Data.toString('hex')).toBe(bech32DataHex) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /packages/account/test/utils.ts: -------------------------------------------------------------------------------- 1 | import { UInt256 } from '@radixdlt/uint256' 2 | import { Mnemonic, PrivateKey } from '@radixdlt/crypto' 3 | import { SigningKeychain, SigningKeychainT } from '../src' 4 | 5 | export const makeSigningKeyChainWithFunds = (): SigningKeychainT => { 6 | const signingKeychain = SigningKeychain.create({ 7 | startWithInitialSigningKey: false, 8 | mnemonic: Mnemonic.generateNew(), // not used, 9 | }) 10 | 11 | const addPK = (privateKeyScalar: number): void => { 12 | const privateKey = PrivateKey.fromScalar( 13 | UInt256.valueOf(privateKeyScalar), 14 | )._unsafeUnwrap() 15 | signingKeychain.addSigningKeyFromPrivateKey({ 16 | privateKey, 17 | alsoSwitchTo: true, 18 | name: `SigningKey with funds, privateKey: ${privateKeyScalar}`, 19 | }) 20 | } 21 | 22 | addPK(1) 23 | addPK(2) 24 | addPK(3) 25 | addPK(4) 26 | addPK(5) 27 | 28 | signingKeychain.switchSigningKey('first') 29 | if ( 30 | signingKeychain.__unsafeGetSigningKey().publicKey.toString(true) !== 31 | '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798' 32 | ) { 33 | throw new Error('incorrect imple') 34 | } 35 | 36 | return signingKeychain 37 | } 38 | -------------------------------------------------------------------------------- /packages/account/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist" 6 | }, 7 | "include": [ 8 | "src" 9 | ], 10 | "references": [ 11 | {"path": "../util"}, 12 | {"path": "../crypto"}, 13 | {"path": "../primitives"}, 14 | {"path": "../data-formats"}, 15 | {"path": "../hardware-wallet"}, 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /packages/account/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.test.json", 3 | "include": [ 4 | "src", 5 | "test" 6 | ], 7 | "references": [ 8 | {"path": "../util"}, 9 | {"path": "../crypto"}, 10 | {"path": "../primitives"}, 11 | {"path": "../data-formats"} 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/application/README.md: -------------------------------------------------------------------------------- 1 | # `@radixdlt/application` 2 | -------------------------------------------------------------------------------- /packages/application/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@radixdlt/application", 3 | "version": "4.0.22", 4 | "description": "A JavaScript client library for interacting with the Radix Distributed Ledger.", 5 | "keywords": [ 6 | "RadixApplicationClient", 7 | "ApplicationLayer", 8 | "Radix", 9 | "RadixDLT" 10 | ], 11 | "author": "Alexander Cyon ", 12 | "homepage": "https://github.com/radixdlt/radixdlt-javascript/tree/master/packages/application#readme", 13 | "license": "Apache-2.0", 14 | "main": "dist/index.js", 15 | "publishConfig": { 16 | "access": "public" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/radixdlt/radixdlt-javascript.git" 21 | }, 22 | "scripts": { 23 | "test": "jest -c ./jest.config.js --rootDir=.", 24 | "server": "open-rpc-mock-server -d ./test/rpc-spec.json -p 3333 &", 25 | "postserver": "kill $(lsof -t -i:3333)" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/radixdlt/radixdlt-javascript/issues" 29 | }, 30 | "engines": { 31 | "node": ">=15.5.0 <16" 32 | }, 33 | "dependencies": { 34 | "@open-rpc/mock-server": "1.7.2", 35 | "@open-rpc/schema-utils-js": "1.14.3", 36 | "jest": "^26.6.3", 37 | "json-schema-faker": "^0.5.0-rcv.34", 38 | "neverthrow": "^4.0.1", 39 | "node-fetch": "^2.6.1", 40 | "ramda": "^0.27.1", 41 | "rxjs": "7.0.0" 42 | }, 43 | "devDependencies": { 44 | "@types/bignumber.js": "^5.0.0", 45 | "axios-mock-adapter": "^1.20.0", 46 | "bignumber.js": "^9.0.1", 47 | "fetch-mock-jest": "^1.5.1" 48 | }, 49 | "gitHead": "f8026eb0c310402ba395025e6a544f7c6baffd66" 50 | } 51 | -------------------------------------------------------------------------------- /packages/application/src/account.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SigningKeyT, 3 | AccountAddressT, 4 | isSigningKey, 5 | isAccountAddress, 6 | } from '@radixdlt/account' 7 | import { AccountT } from './_types' 8 | 9 | export const isAccount = (something: unknown): something is AccountT => { 10 | const inspection = something as AccountT 11 | return ( 12 | inspection.signingKey !== undefined && 13 | isSigningKey(inspection.signingKey) && 14 | inspection.address !== undefined && 15 | isAccountAddress(inspection.address) 16 | ) 17 | } 18 | 19 | const create = ( 20 | input: Readonly<{ 21 | address: AccountAddressT 22 | signingKey: SigningKeyT 23 | }>, 24 | ): AccountT => { 25 | const { signingKey, address } = input 26 | if (!signingKey.publicKey.equals(address.publicKey)) { 27 | const errMsg = `Incorrect implementation, publicKey of address does not match publicKey of signingKey.` 28 | console.error(errMsg) 29 | throw new Error(errMsg) 30 | } 31 | const network = address.network 32 | const publicKey = signingKey.publicKey 33 | const hdPath = signingKey.hdPath 34 | return { 35 | ...signingKey, // encrypt, decrypt, sign 36 | equals: (other: AccountT): boolean => other.publicKey.equals(publicKey), 37 | signingKey: signingKey, 38 | type: signingKey.type, 39 | address, 40 | network, 41 | publicKey, 42 | hdPath, 43 | } 44 | } 45 | 46 | export const Account = { 47 | create, 48 | } 49 | -------------------------------------------------------------------------------- /packages/application/src/actions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './_types' 2 | export * from './intendedStakeTokensAction' 3 | export * from './intendedTransferTokensAction' 4 | export * from './intendedUnstakeTokensAction' 5 | -------------------------------------------------------------------------------- /packages/application/src/actions/intendedStakeTokensAction.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActionType, 3 | IntendedStakeTokensAction, 4 | StakeTokensInput, 5 | } from './_types' 6 | import { 7 | AccountAddressT, 8 | isValidatorAddressOrUnsafeInput, 9 | ValidatorAddress, 10 | ValidatorAddressT, 11 | ResourceIdentifier, 12 | ResourceIdentifierT, 13 | isResourceIdentifierOrUnsafeInput, 14 | } from '@radixdlt/account' 15 | import { Amount, AmountT, isAmountOrUnsafeInput } from '@radixdlt/primitives' 16 | import { combine, Result } from 'neverthrow' 17 | 18 | export const isStakeTokensInput = ( 19 | something: unknown, 20 | ): something is StakeTokensInput => { 21 | const inspection = something as StakeTokensInput 22 | return ( 23 | isValidatorAddressOrUnsafeInput(inspection.to_validator) && 24 | isAmountOrUnsafeInput(inspection.amount) && 25 | isResourceIdentifierOrUnsafeInput(inspection.tokenIdentifier) 26 | ) 27 | } 28 | 29 | const create = ( 30 | input: StakeTokensInput, 31 | from_account: AccountAddressT, 32 | ): Result => 33 | combine([ 34 | ValidatorAddress.fromUnsafe(input.to_validator), 35 | Amount.fromUnsafe(input.amount), 36 | ResourceIdentifier.fromUnsafe(input.tokenIdentifier), 37 | ]).map( 38 | (resultList): IntendedStakeTokensAction => { 39 | const to_validator = resultList[0] as ValidatorAddressT 40 | const amount = resultList[1] as AmountT 41 | const rri = resultList[2] as ResourceIdentifierT 42 | 43 | return { 44 | to_validator: to_validator.toString(), 45 | amount, 46 | type: ActionType.STAKE_TOKENS, 47 | from_account: from_account.toString(), 48 | rri, 49 | } 50 | }, 51 | ) 52 | 53 | export const IntendedStakeTokens = { 54 | create, 55 | } 56 | -------------------------------------------------------------------------------- /packages/application/src/actions/intendedTransferTokensAction.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActionType, 3 | IntendedTransferTokensAction, 4 | TransferTokensInput, 5 | } from './_types' 6 | import { 7 | AccountAddress, 8 | AccountAddressT, 9 | isAccountAddressOrUnsafeInput, 10 | ResourceIdentifierT, 11 | ResourceIdentifier, 12 | isResourceIdentifierOrUnsafeInput, 13 | } from '@radixdlt/account' 14 | import { Amount, AmountT, isAmountOrUnsafeInput } from '@radixdlt/primitives' 15 | import { combine, Result } from 'neverthrow' 16 | 17 | export const isTransferTokensInput = ( 18 | something: unknown, 19 | ): something is TransferTokensInput => { 20 | const inspection = something as TransferTokensInput 21 | return ( 22 | isAccountAddressOrUnsafeInput(inspection.to_account) && 23 | isAmountOrUnsafeInput(inspection.amount) && 24 | isResourceIdentifierOrUnsafeInput(inspection.tokenIdentifier) 25 | ) 26 | } 27 | 28 | const create = ( 29 | input: TransferTokensInput, 30 | from_account: AccountAddressT, 31 | ): Result => 32 | combine([ 33 | AccountAddress.fromUnsafe(input.to_account), 34 | Amount.fromUnsafe(input.amount), 35 | ResourceIdentifier.fromUnsafe(input.tokenIdentifier), 36 | ]).map( 37 | (resultList): IntendedTransferTokensAction => { 38 | const to_account = resultList[0] as AccountAddressT 39 | const amount = resultList[1] as AmountT 40 | const rri = resultList[2] as ResourceIdentifierT 41 | 42 | return { 43 | to_account: to_account.toString(), 44 | amount, 45 | rri, 46 | type: ActionType.TOKEN_TRANSFER, 47 | from_account: from_account.toString(), 48 | } 49 | }, 50 | ) 51 | 52 | export const IntendedTransferTokens = { 53 | create, 54 | } 55 | -------------------------------------------------------------------------------- /packages/application/src/actions/intendedUnstakeTokensAction.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActionType, 3 | IntendedUnstakeTokensAction, 4 | UnstakeTokensInput, 5 | } from './_types' 6 | import { 7 | AccountAddressT, 8 | isResourceIdentifierOrUnsafeInput, 9 | isValidatorAddressOrUnsafeInput, 10 | ResourceIdentifier, 11 | ResourceIdentifierT, 12 | ValidatorAddress, 13 | ValidatorAddressT, 14 | } from '@radixdlt/account' 15 | import { combine, Result } from 'neverthrow' 16 | import { Amount, AmountT } from '@radixdlt/primitives' 17 | 18 | const create = ( 19 | input: UnstakeTokensInput, 20 | to_account: AccountAddressT, 21 | ): Result => 22 | combine([ 23 | ValidatorAddress.fromUnsafe(input.from_validator), 24 | Amount.fromUnsafe(input.amount ?? 0), 25 | Amount.fromUnsafe(input.unstake_percentage ?? 0), 26 | ResourceIdentifier.fromUnsafe(input.tokenIdentifier), 27 | ]).map( 28 | (resultList): IntendedUnstakeTokensAction => { 29 | const from_validator = resultList[0] as ValidatorAddressT 30 | const amount = resultList[1] as AmountT 31 | const unstake_percentage = resultList[2] as AmountT 32 | const rri = resultList[3] as ResourceIdentifierT 33 | 34 | return { 35 | from_validator: from_validator.toString(), 36 | amount, 37 | unstake_percentage, 38 | type: ActionType.UNSTAKE_TOKENS, 39 | to_account: to_account.toString(), 40 | rri, 41 | } 42 | }, 43 | ) 44 | 45 | export const IntendedUnstakeTokens = { 46 | create, 47 | } 48 | -------------------------------------------------------------------------------- /packages/application/src/api/_types.ts: -------------------------------------------------------------------------------- 1 | import { nodeAPI } from '.' 2 | import { getAPI } from './json-rpc' 3 | import { radixCoreAPI } from './radixCoreAPI' 4 | 5 | type JsonRpcAPI = { 6 | [Property in keyof ReturnType]: ReturnType< 7 | typeof getAPI 8 | >[Property] 9 | } 10 | 11 | export type NodeAPI = ReturnType 12 | 13 | export type NodeT = Readonly<{ 14 | url: URL 15 | }> 16 | 17 | export type RadixAPI = Omit 18 | 19 | export type RadixCoreAPI = ReturnType 20 | -------------------------------------------------------------------------------- /packages/application/src/api/api.ts: -------------------------------------------------------------------------------- 1 | import { openApiClient } from '@radixdlt/networking' 2 | import { getAPI } from './open-api/interface' 3 | 4 | export const nodeAPI = (url: URL) => ({ 5 | ...getAPI(openApiClient(url).call), 6 | }) 7 | -------------------------------------------------------------------------------- /packages/application/src/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from './_types' 2 | export * from './api' 3 | export * from './json-rpc' 4 | export * from './radixCoreAPI' 5 | -------------------------------------------------------------------------------- /packages/application/src/api/json-rpc/index.ts: -------------------------------------------------------------------------------- 1 | export * from './_types' 2 | 3 | export * from './interface' 4 | export * from './responseHandlers' 5 | -------------------------------------------------------------------------------- /packages/application/src/api/open-api/interface.ts: -------------------------------------------------------------------------------- 1 | import { 2 | InputOfAPICall, 3 | MethodName, 4 | OpenApiClientCall, 5 | ReturnOfAPICall, 6 | } from '@radixdlt/networking' 7 | import { 8 | handleAccountBalancesResponse, 9 | handleNativeTokenResponse, 10 | handleGatewayResponse, 11 | handleStakePositionsResponse, 12 | handleTokenInfoResponse, 13 | handleUnstakePositionsResponse, 14 | handleBuildTransactionResponse, 15 | handleFinalizeTransactionResponse, 16 | handleSubmitTransactionResponse, 17 | handleTransactionResponse, 18 | handleAccountTransactionsResponse, 19 | handleValidatorResponse, 20 | handleValidatorsResponse, 21 | handleRecentTransactionResponse, 22 | } from './responseHandlers' 23 | import { pipe } from 'ramda' 24 | import { Result, ResultAsync } from 'neverthrow' 25 | 26 | const callAPIWith = (call: OpenApiClientCall) => ( 27 | method: M, 28 | ) => ( 29 | handleResponse: ( 30 | response: ReturnOfAPICall, 31 | ) => Result, 32 | ) => ( 33 | params: InputOfAPICall, 34 | headers?: Record, 35 | ): ResultAsync => 36 | pipe( 37 | () => call(method, params, headers), 38 | result => result.mapErr(e => [e]).andThen(handleResponse), 39 | )() 40 | 41 | export const getAPI = pipe( 42 | (call: OpenApiClientCall) => callAPIWith(call), 43 | 44 | callAPI => ({ 45 | gateway: callAPI('gatewayPost')(handleGatewayResponse), 46 | tokenInfo: callAPI('tokenPost')(handleTokenInfoResponse), 47 | nativeTokenInfo: callAPI('tokenNativePost')(handleNativeTokenResponse), 48 | stakePositions: callAPI('accountStakesPost')( 49 | handleStakePositionsResponse, 50 | ), 51 | unstakePositions: callAPI('accountUnstakesPost')( 52 | handleUnstakePositionsResponse, 53 | ), 54 | /* 55 | deriveTokenIdentifier: callAPI('tokenDerivePost')( 56 | handleDeriveTokenIdentifierResponse, 57 | ), 58 | */ 59 | accountBalances: callAPI('accountBalancesPost')( 60 | handleAccountBalancesResponse, 61 | ), 62 | accountTransactions: callAPI('accountTransactionsPost')( 63 | handleAccountTransactionsResponse, 64 | ), 65 | validator: callAPI('validatorPost')(handleValidatorResponse), 66 | validators: callAPI('validatorsPost')(handleValidatorsResponse), 67 | /* 68 | transactionRules: callAPI('transactionRulesPost')( 69 | handleTransactionRulesResponse, 70 | ), 71 | */ 72 | buildTransaction: callAPI('transactionBuildPost')( 73 | handleBuildTransactionResponse, 74 | ), 75 | finalizeTransaction: callAPI('transactionFinalizePost')( 76 | handleFinalizeTransactionResponse, 77 | ), 78 | submitTransaction: callAPI('transactionSubmitPost')( 79 | handleSubmitTransactionResponse, 80 | ), 81 | getTransaction: callAPI('transactionStatusPost')( 82 | handleTransactionResponse, 83 | ), 84 | recentTransactions: callAPI('transactionRecentPost')( 85 | handleRecentTransactionResponse, 86 | ), 87 | }), 88 | ) 89 | -------------------------------------------------------------------------------- /packages/application/src/api/utils.ts: -------------------------------------------------------------------------------- 1 | import { log } from '@radixdlt/util' 2 | import { err, Result, ok } from 'neverthrow' 3 | import { Observable, throwError, timer } from 'rxjs' 4 | import { mergeMap } from 'rxjs/operators' 5 | 6 | export const hasRequiredProps = >( 7 | methodName: string, 8 | obj: T, 9 | props: string[], 10 | ): Result => { 11 | for (const prop of props) { 12 | if (obj[prop] === undefined) { 13 | return err([ 14 | Error( 15 | `Prop validation failed for ${methodName} response. ${prop} was undefined.`, 16 | ), 17 | ]) 18 | } 19 | } 20 | return ok(obj) 21 | } 22 | 23 | export const retryOnErrorCode = ({ 24 | maxRetryAttempts = 3, 25 | scalingDuration = 1000, 26 | errorCodes = [], 27 | }: { 28 | maxRetryAttempts?: number 29 | scalingDuration?: number 30 | errorCodes?: number[] 31 | } = {}) => (attempts: Observable<{ error: { code: number } }>) => 32 | attempts.pipe( 33 | mergeMap(({ error }, i) => { 34 | const retryAttempt = i + 1 35 | const foundErrorCode = errorCodes.some(e => e === error.code) 36 | // if maximum number of retries have been met 37 | // or response is a error code we don't wish to retry, throw error 38 | if (retryAttempt > maxRetryAttempts || !foundErrorCode) { 39 | return throwError(() => error) 40 | } 41 | log.debug( 42 | `Attempt ${retryAttempt}: retrying in ${ 43 | retryAttempt * scalingDuration 44 | }ms`, 45 | ) 46 | return timer(retryAttempt * scalingDuration) 47 | }), 48 | ) 49 | -------------------------------------------------------------------------------- /packages/application/src/dto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './_types' 2 | 3 | export * from './transactionIdentifier' 4 | export * from './transactionIntentBuilder' 5 | -------------------------------------------------------------------------------- /packages/application/src/dto/transactionIdentifier.ts: -------------------------------------------------------------------------------- 1 | import { err, ok, Result } from 'neverthrow' 2 | import { TransactionIdentifierT } from './_types' 3 | 4 | const isTransactionIdentifier = ( 5 | something: unknown, 6 | ): something is TransactionIdentifierT => { 7 | const inspection = something as TransactionIdentifierT 8 | return inspection.__hex !== undefined && inspection.__witness === 'isTXId' 9 | } 10 | 11 | const create = ( 12 | bytes: Buffer | string, 13 | ): Result => { 14 | const buffer = typeof bytes === 'string' ? Buffer.from(bytes, 'hex') : bytes 15 | const length = 32 16 | if (buffer.length !== length) { 17 | return err( 18 | new Error(`Expected #${length} bytes, but got #${buffer.length}`), 19 | ) 20 | } 21 | 22 | const asString = buffer.toString('hex') 23 | 24 | return ok({ 25 | __witness: 'isTXId', 26 | __hex: asString, 27 | toString: () => asString, 28 | equals: (other: TransactionIdentifierT) => 29 | other.toString() === asString, 30 | }) 31 | } 32 | 33 | export const TransactionIdentifier = { 34 | is: isTransactionIdentifier, 35 | create, 36 | } 37 | -------------------------------------------------------------------------------- /packages/application/src/global-fetch.d.ts: -------------------------------------------------------------------------------- 1 | declare type GlobalFetch = WindowOrWorkerGlobalScope 2 | -------------------------------------------------------------------------------- /packages/application/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api' 2 | export * from './dto' 3 | export * from './actions' 4 | export * from './mockRadix' 5 | export * from './errors' 6 | export * from './_types' 7 | export * from './wallet' 8 | export * from './account' 9 | export * from './radix' 10 | export * from '@radixdlt/util' 11 | export * from '@radixdlt/account' 12 | export * from '@radixdlt/crypto' 13 | export * from '@radixdlt/primitives' 14 | -------------------------------------------------------------------------------- /packages/application/test/_load-rpc.ts: -------------------------------------------------------------------------------- 1 | // A little hacky solution so that we can load the RPC spec asynchronously 2 | // and generate tests from it, since 'describe()' callback 3 | // isn't allowed to be async in Jest. 💩💩💩 4 | // https://github.com/facebook/jest/issues/2235 5 | 6 | const NodeEnvironment = require('jest-environment-node') 7 | const RPC_SPEC = require('@radixdlt/open-rpc-spec') 8 | const parseOpenRPCDocument = require('@open-rpc/schema-utils-js') 9 | .parseOpenRPCDocument 10 | 11 | class TestEnvironment extends NodeEnvironment { 12 | constructor(config) { 13 | super(config) 14 | } 15 | 16 | async setup() { 17 | await super.setup() 18 | this.global.rpcSpec = await parseOpenRPCDocument( 19 | JSON.stringify(RPC_SPEC), 20 | ) 21 | } 22 | 23 | async teardown() { 24 | this.global.rpcSpec = null 25 | await super.teardown() 26 | } 27 | 28 | runScript(script) { 29 | return super.runScript(script) 30 | } 31 | } 32 | 33 | module.exports = TestEnvironment 34 | -------------------------------------------------------------------------------- /packages/application/test/atomIdentifier.test.ts: -------------------------------------------------------------------------------- 1 | import { TransactionIdentifier } from '../src/dto/transactionIdentifier' 2 | 3 | describe('TransactionIdentifier', () => { 4 | it('can check for equality', () => { 5 | const a0 = TransactionIdentifier.create(buffer0)._unsafeUnwrap() 6 | const b0 = TransactionIdentifier.create(buffer0)._unsafeUnwrap() 7 | const a1 = TransactionIdentifier.create(buffer1)._unsafeUnwrap() 8 | 9 | expect(a0.equals(b0)).toBe(true) 10 | expect(a0.equals(a1)).toBe(false) 11 | }) 12 | 13 | it('can be converted to string', () => { 14 | const txID = TransactionIdentifier.create(buffer0)._unsafeUnwrap() 15 | expect(txID.toString()).toBe(deadbeefString) 16 | }) 17 | 18 | it('can be created from hex string', () => { 19 | const txID = TransactionIdentifier.create( 20 | deadbeefString, 21 | )._unsafeUnwrap() 22 | expect(txID.toString()).toBe(deadbeefString) 23 | }) 24 | 25 | const deadbeefString = 26 | 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef' 27 | const buffer0 = Buffer.from(deadbeefString, 'hex') 28 | const buffer1 = Buffer.from( 29 | 'FadedBeeFadedBeeFadedBeeFadedBeeFadedBeeFadedBeeFadedBeeFadedBee', 30 | 'hex', 31 | ) 32 | }) 33 | -------------------------------------------------------------------------------- /packages/application/test/open-api.test.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import MockAdapter from 'axios-mock-adapter' 3 | 4 | import { openApiClient } from '@radixdlt/networking/src' 5 | import { getAPI } from '../src/api/open-api/interface' 6 | 7 | const BASE_URL = 'https://localhost:9000' 8 | 9 | const api = getAPI(openApiClient(new URL(BASE_URL)).call) 10 | 11 | const mock = new MockAdapter(axios) 12 | 13 | describe('handle error responses', () => { 14 | afterEach(() => { 15 | mock.reset() 16 | }) 17 | 18 | it('should throw if 500 error', async () => { 19 | mock.onPost(`${BASE_URL}/gateway`).reply(500, {}) 20 | try { 21 | await api.gateway({}) 22 | expect(true).toBe(false) 23 | } catch (error) { 24 | expect(error).toBeDefined() 25 | } 26 | }) 27 | 28 | it('should handle 400 error', async done => { 29 | mock.onPost(`${BASE_URL}/gateway`).reply(400, { 30 | code: 400, 31 | message: 'The network selected is not valid.', 32 | details: {}, 33 | }) 34 | 35 | api.gateway({}) 36 | .map(() => { 37 | expect(true).toBe(false) 38 | }) 39 | .mapErr(err => { 40 | expect(err).toEqual([ 41 | { 42 | error: { 43 | code: 400, 44 | message: 'The network selected is not valid.', 45 | details: {}, 46 | }, 47 | }, 48 | ]) 49 | done() 50 | }) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /packages/application/test/responseHandlers.test.ts: -------------------------------------------------------------------------------- 1 | import { handleAccountBalancesResponse } from '../src/api/open-api/responseHandlers' 2 | 3 | const fixtures = [ 4 | { 5 | data: { 6 | ledger_state: { 7 | version: 42905709, 8 | timestamp: '2022-01-06T07:36:32.638Z', 9 | epoch: 4405, 10 | round: 9787, 11 | }, 12 | account_balances: { 13 | staked_and_unstaking_balance: { 14 | value: '593224304698885819581', 15 | token_identifier: { 16 | rri: 'xrd_tr1qyf0x76s', 17 | }, 18 | }, 19 | liquid_balances: [ 20 | { 21 | value: '100000000000000000000000', 22 | token_identifier: { 23 | rri: 24 | 'fire_tr1qvs4gje6qfxmu5wfn9jd5x9ku20ds7fcucn6tzcnyxwq7n02zx', 25 | }, 26 | }, 27 | { 28 | value: '99999894000000100000000', 29 | token_identifier: { 30 | rri: 31 | 'buzzsaw_tr1q0aymplntjgcdsc5fuxcgq9me47yu26qf929cqexduxs7c299n', 32 | }, 33 | }, 34 | { 35 | value: '99971993105302200000000', 36 | token_identifier: { 37 | rri: 38 | 'captainfr33domst0000000000000ken_tr1q09jf8c05v3lfj3tqc04x7nlp0sag8yq5k6qpaexxnrs004s7q', 39 | }, 40 | }, 41 | { 42 | value: '909699413989940263553', 43 | token_identifier: { 44 | rri: 'xrd_tr1qyf0x76s', 45 | }, 46 | }, 47 | { 48 | value: '5999999999999999999', 49 | token_identifier: { 50 | rri: 51 | 'sptve_tr1qvddj7vg004cstsqvu6nar0ssr4x8v2r7rnry2e66n7q0nmaus', 52 | }, 53 | }, 54 | ], 55 | }, 56 | }, 57 | } as any, 58 | ] 59 | 60 | describe('handleAccountBalancesResponse', () => { 61 | it('should correctly transform balances', () => { 62 | const actual = handleAccountBalancesResponse( 63 | fixtures[0], 64 | )._unsafeUnwrap() 65 | 66 | const expectedLiquidBalances = 67 | fixtures[0].data.account_balances.liquid_balances 68 | 69 | const actualLiquidBalances = actual.account_balances.liquid_balances.map( 70 | item => ({ 71 | value: item.value.toString(), 72 | token_identifier: { rri: item.token_identifier.rri.toString() }, 73 | }), 74 | ) 75 | 76 | const expectedStakedBalance = 77 | fixtures[0].data.account_balances.staked_and_unstaking_balance 78 | 79 | const actualStakedBalance = { 80 | value: actual.account_balances.staked_and_unstaking_balance.value.toString(), 81 | token_identifier: { 82 | rri: actual.account_balances.staked_and_unstaking_balance.token_identifier.rri.toString(), 83 | }, 84 | } 85 | 86 | const liquidXrdValue = actual.account_balances.liquid_balances.find( 87 | item => item.token_identifier.rri.toString() === 'xrd_tr1qyf0x76s', 88 | )?.value 89 | const stakedXrdValue = 90 | actual.account_balances.staked_and_unstaking_balance.value 91 | 92 | expect(actualLiquidBalances).toEqual(expectedLiquidBalances) 93 | expect(actualStakedBalance).toEqual(expectedStakedBalance) 94 | expect('1502923718688826083134').toEqual( 95 | liquidXrdValue?.add(stakedXrdValue).toString(), 96 | ) 97 | }) 98 | }) 99 | -------------------------------------------------------------------------------- /packages/application/test/stringifyTypes.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js' 2 | import { 3 | ActionType, 4 | ExecutedAction, 5 | SimpleExecutedTransaction, 6 | SimpleTokenBalance, 7 | SimpleTokenBalances, 8 | SimpleTransactionHistory, 9 | } from '../src' 10 | import { AmountT } from '@radixdlt/primitives' 11 | 12 | export const stringifyAmount = (amount: AmountT) => { 13 | const factor = new BigNumber('1e18') 14 | const bigNumber = new BigNumber(amount.toString()) 15 | const precision = 4 16 | return bigNumber.dividedToIntegerBy(factor).toFormat(precision) 17 | } 18 | 19 | export const stringifyAction = (action: ExecutedAction): string => { 20 | switch (action.type) { 21 | case ActionType.OTHER: 22 | return ` 23 | Other 24 | ` 25 | case ActionType.STAKE_TOKENS: 26 | case ActionType.UNSTAKE_TOKENS: 27 | return ` 28 | type: ${action.type.toString()}, 29 | from: ${action.from.toString()} 30 | validator: ${action.validator.toString()} 31 | amount: ${stringifyAmount(action.amount)} 32 | ` 33 | case ActionType.TOKEN_TRANSFER: 34 | return ` 35 | type: ${action.type.toString()}, 36 | from: ${action.from.toString()} 37 | to: ${action.to.toString()} 38 | amount: ${stringifyAmount(action.amount)} 39 | rri: ${action.rri.toString()} 40 | ` 41 | } 42 | } 43 | 44 | export const stringifySimpleTX = (tx: SimpleExecutedTransaction): string => ` 45 | txID: ${tx.txID.toString()} 46 | fee: ${stringifyAmount(tx.fee)} 47 | sentAt: ${tx.sentAt.toLocaleString()} 48 | message: ${tx.message !== undefined ? tx.message : ''} 49 | actions: [ 50 | ${tx.actions.map(a => stringifyAction(a)).join('\n')} 51 | ] 52 | ` 53 | 54 | export const stringifySimpleTXHistory = ( 55 | simpleTxHist: SimpleTransactionHistory, 56 | ): string => 57 | `transactions: ${simpleTxHist.transactions 58 | .map(t => stringifySimpleTX(t)) 59 | .join('\n')}` 60 | 61 | export const stringifySimpleTokenBalance = (tb: SimpleTokenBalance): string => ` 62 | amount: ${stringifyAmount(tb.amount)} 63 | rri: ${tb.tokenIdentifier.toString()} 64 | ` 65 | 66 | export const stringifySimpleTokenBalances = ( 67 | tokenBalances: SimpleTokenBalances, 68 | ): string => ` 69 | owner: ${tokenBalances.owner.toString()}, 70 | balances: ${tokenBalances.tokenBalances 71 | .map(tb => stringifySimpleTokenBalance(tb)) 72 | .join('\n')} 73 | ` 74 | -------------------------------------------------------------------------------- /packages/application/test/transferTokensAction.deprecatedTest.ts: -------------------------------------------------------------------------------- 1 | /*import { ResourceIdentifier } from '@radixdlt/account' 2 | import { Amount, Network } from '@radixdlt/primitives' 3 | import { IntendedTransferTokens, TransferTokensInput, alice, bob } from '../src' 4 | 5 | describe('TransferTokensActions', () => { 6 | const resourceIdentifier = ResourceIdentifier.fromPublicKeyAndNameAndNetwork( 7 | { 8 | publicKey: alice.publicKey, 9 | name: 'foobar', 10 | network: Network.MAINNET, 11 | }, 12 | )._unsafeUnwrap() 13 | const amount = Amount.fromUnsafe(6)._unsafeUnwrap() 14 | 15 | const input = { 16 | to: bob, 17 | amount: amount, 18 | tokenIdentifier: resourceIdentifier, 19 | } 20 | 21 | it(`should have a 'recipient' equal to 'input.to'.`, () => { 22 | const tokenTransfer = IntendedTransferTokens.create( 23 | input, 24 | alice, 25 | )._unsafeUnwrap() 26 | expect(tokenTransfer.to.equals(bob)).toBe(true) 27 | }) 28 | 29 | it(`should have an 'amount' equal to 'input.amount'.`, () => { 30 | const tokenTransfer = IntendedTransferTokens.create( 31 | input, 32 | alice, 33 | )._unsafeUnwrap() 34 | expect(tokenTransfer.amount.eq(amount)).toBe(true) 35 | }) 36 | 37 | it(`should have a 'sender' equal to 'input.from'.`, () => { 38 | const tokenTransfer = IntendedTransferTokens.create( 39 | input, 40 | alice, 41 | )._unsafeUnwrap() 42 | expect(tokenTransfer.from.equals(alice)).toBe(true) 43 | }) 44 | 45 | it('should be possible to transfer 0 tokens', () => { 46 | const zero = Amount.fromUnsafe(0)._unsafeUnwrap() 47 | const tokenTransfer = IntendedTransferTokens.create( 48 | { ...input, amount: zero }, 49 | alice, 50 | )._unsafeUnwrap() 51 | 52 | expect(tokenTransfer.amount.eq(zero)).toBe(true) 53 | }) 54 | }) 55 | 56 | */ 57 | -------------------------------------------------------------------------------- /packages/application/test/util.ts: -------------------------------------------------------------------------------- 1 | import { Wallet, WalletT } from '../src' 2 | import { Network } from '@radixdlt/primitives' 3 | import { SigningKeychain } from '@radixdlt/account' 4 | import { KeystoreT, Mnemonic } from '@radixdlt/crypto' 5 | import { makeSigningKeyChainWithFunds } from '@radixdlt/account/test/utils' 6 | 7 | export const createWallet = ( 8 | input?: Readonly<{ 9 | network?: Network 10 | startWithInitialSigningKey?: boolean 11 | }>, 12 | ): WalletT => { 13 | const mnemonic = Mnemonic.fromEnglishPhrase( 14 | 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', 15 | )._unsafeUnwrap() 16 | const startWithInitialSigningKey = input?.startWithInitialSigningKey ?? true 17 | const signingKeychain = SigningKeychain.create({ 18 | mnemonic, 19 | startWithInitialSigningKey, 20 | }) 21 | 22 | const network = input?.network ?? Network.MAINNET 23 | 24 | return Wallet.create({ 25 | signingKeychain, 26 | network, 27 | }) 28 | } 29 | 30 | export const makeWalletWithFunds = (network: Network): WalletT => { 31 | return Wallet.create({ 32 | signingKeychain: makeSigningKeyChainWithFunds(), 33 | network, 34 | }) 35 | } 36 | 37 | export type KeystoreForTest = { 38 | keystore: KeystoreT 39 | password: string 40 | expectedSecret: string 41 | expectedMnemonicPhrase: string 42 | publicKeysCompressed: string[] 43 | } 44 | 45 | export const keystoreForTest: KeystoreForTest = { 46 | password: 'my super strong passaword', 47 | expectedMnemonicPhrase: 48 | 'legal winner thank year wave sausage worth useful legal winner thank yellow', 49 | expectedSecret: '7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f', 50 | keystore: { 51 | crypto: { 52 | cipher: 'AES-GCM', 53 | cipherparams: { 54 | nonce: 'd82fd275598b9b288b8c376d', 55 | }, 56 | ciphertext: '208e520802bd17d7a569333df41dfd2d', 57 | kdf: 'scrypt', 58 | kdfparams: { 59 | costParameterN: 8192, 60 | costParameterC: 262144, 61 | blockSize: 8, 62 | parallelizationParameter: 1, 63 | lengthOfDerivedKey: 32, 64 | salt: 65 | 'cb2227c6782493df3e822c9f6cd1131dea14e135751215d66f48227383b80acd', 66 | }, 67 | mac: '68bc72c6a6a89c7fe4eb5fda4f4163e0', 68 | }, 69 | id: 'b34999409a491037', 70 | version: 1, 71 | }, 72 | // 1. input seed at https://iancoleman.io/bip39/ 73 | // 2. change to BIP32 and enter derivation path: m/44'/1022'/0'/0 74 | // 3. Check 'use hardened addresses' checkbox 75 | // 4. Copy Public Key from table 76 | publicKeysCompressed: [ 77 | '036d39bd3894fa2193f1ffc62236bfadf3d3c051e8fe9ca5cc02677ea5e1ad34e8', 78 | '020eb0759d87beb9f97056dc8b3aee12c4b02ad37dd4d259e163a87b273cea8b54', 79 | '0319ed42cc998f7cfa60e568b3c9f631b47582051affc478f68ea3727a977012e0', 80 | '028d31597419a690f369a079dfc54276b643836189a375b56d8c1983bffbb53c36', 81 | '0269f0794113243f60cf8a0ceceffcce93220d7e8531883eac24054d95998dd942', 82 | '025974fa70072cba176a89afeb81b2a93ec8cde196014ff97f4d0a9da8c11ceca1', 83 | ], 84 | } 85 | -------------------------------------------------------------------------------- /packages/application/test/wallet.test.ts: -------------------------------------------------------------------------------- 1 | import { createWallet } from './util' 2 | import { map, take, toArray } from 'rxjs/operators' 3 | import { Subscription } from 'rxjs' 4 | 5 | describe('wallet', () => { 6 | it('can observeActiveAccount', done => { 7 | const subs = new Subscription() 8 | const wallet = createWallet() 9 | 10 | const expectedValues = [0, 1, 2] 11 | 12 | subs.add( 13 | wallet 14 | .observeActiveAccount() 15 | .pipe( 16 | map(account => account.hdPath!.addressIndex.value()), 17 | take(expectedValues.length), 18 | toArray(), 19 | ) 20 | .subscribe( 21 | values => { 22 | expect(values).toStrictEqual(expectedValues) 23 | done() 24 | }, 25 | error => done(error), 26 | ), 27 | ) 28 | 29 | subs.add( 30 | wallet 31 | .deriveNextLocalHDAccount({ alsoSwitchTo: true }) 32 | .subscribe(_ => { 33 | subs.add( 34 | wallet 35 | .deriveNextLocalHDAccount({ alsoSwitchTo: true }) 36 | .subscribe(), 37 | ) 38 | }), 39 | ) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /packages/application/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | }, 7 | "include": [ 8 | "src" 9 | ], 10 | "references": [ 11 | {"path": "../crypto"}, 12 | {"path": "../primitives"}, 13 | {"path": "../util"}, 14 | {"path": "../networking"}, 15 | {"path": "../account"}, 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /packages/application/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.test.json", 3 | "include": [ 4 | "src", 5 | "test" 6 | ], 7 | "references": [ 8 | {"path": "../crypto"}, 9 | {"path": "../primitives"}, 10 | {"path": "../util"}, 11 | {"path": "../networking"}, 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/crypto/README.md: -------------------------------------------------------------------------------- 1 | # `@radixdlt/crypto` 2 | 3 | > TODO: description 4 | 5 | ## Usage 6 | 7 | ``` 8 | const radixCrypto = require('@radixdlt/crypto') 9 | 10 | // TODO: DEMONSTRATE API 11 | ``` 12 | -------------------------------------------------------------------------------- /packages/crypto/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@radixdlt/crypto", 3 | "version": "2.1.17", 4 | "description": "Cryptographic primitives such as digests (hashing) and Elliptic Curve Cryptography (ECC) methods such as key generation, signing and verify.", 5 | "keywords": [ 6 | "Crypto", 7 | "ECC", 8 | "Elliptic Curve Cryptography", 9 | "Hashing", 10 | "SHA256", 11 | "Digests", 12 | "secp256k1", 13 | "Sign", 14 | "Verify", 15 | "KeyGen" 16 | ], 17 | "author": "Alexander Cyon ", 18 | "homepage": "https://github.com/radixdlt/radixdlt-javascript/tree/master/packages/crypto#readme", 19 | "license": "Apache-2.0", 20 | "main": "dist/index.js", 21 | "publishConfig": { 22 | "access": "public" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/radixdlt/radixdlt-javascript.git" 27 | }, 28 | "scripts": { 29 | "test": "echo \"Error: run tests from root\" && exit 1" 30 | }, 31 | "bugs": { 32 | "url": "https://github.com/radixdlt/radixdlt-javascript/issues" 33 | }, 34 | "engines": { 35 | "node": ">=15.5.0 <16" 36 | }, 37 | "dependencies": { 38 | "@radixdlt/data-formats": "1.0.30", 39 | "@radixdlt/primitives": "^3.0.9", 40 | "@radixdlt/uint256": "1.1.0", 41 | "@radixdlt/util": "1.0.29", 42 | "base-x": "^3.0.8", 43 | "elliptic": "^6.5.3", 44 | "neverthrow": "^4.0.1", 45 | "node-forge": "^0.10.0", 46 | "scrypt-js": "^3.0.1", 47 | "uuid": "^8.3.2" 48 | }, 49 | "devDependencies": { 50 | "@radixdlt/radix-engine-toolkit": "^1.0.2", 51 | "@types/elliptic": "^6.4.12", 52 | "@types/node-forge": "^0.9.7", 53 | "@types/scrypt-js": "^2.0.4", 54 | "@types/uuid": "^8.3.0" 55 | }, 56 | "gitHead": "f8026eb0c310402ba395025e6a544f7c6baffd66" 57 | } 58 | -------------------------------------------------------------------------------- /packages/crypto/src/_types.ts: -------------------------------------------------------------------------------- 1 | export type Hasher = (input: Buffer | string) => Buffer 2 | -------------------------------------------------------------------------------- /packages/crypto/src/elliptic-curve/_types.ts: -------------------------------------------------------------------------------- 1 | import { ResultAsync } from 'neverthrow' 2 | import { UInt256 } from '@radixdlt/uint256' 3 | import { Hasher } from '../_types' 4 | 5 | export type DiffieHellman = ( 6 | publicKeyOfOtherParty: PublicKeyT, 7 | ) => ResultAsync 8 | 9 | export type Signer = Readonly<{ 10 | sign: (hashedMessage: Buffer) => ResultAsync 11 | 12 | signUnhashed: ( 13 | input: Readonly<{ 14 | msgToHash: Buffer | string 15 | hasher?: Hasher 16 | }>, 17 | ) => ResultAsync 18 | }> 19 | 20 | export type SignatureT = Readonly<{ 21 | r: UInt256 22 | s: UInt256 23 | toDER: () => string 24 | equals: (other: SignatureT) => boolean 25 | }> 26 | 27 | // A non-infinity point on the EC curve (e.g. `secp256k1`) 28 | export type ECPointOnCurveT = Readonly<{ 29 | x: UInt256 30 | y: UInt256 31 | toBuffer: (includePrefixByte?: boolean) => Buffer 32 | toString: (includePrefixByte?: boolean) => string 33 | equals: (other: ECPointOnCurveT) => boolean 34 | add: (other: ECPointOnCurveT) => ECPointOnCurveT 35 | multiply: (by: UInt256) => ECPointOnCurveT 36 | multiplyWithPrivateKey: (privateKey: PrivateKeyT) => ECPointOnCurveT 37 | }> 38 | 39 | export type PublicKeyT = Readonly<{ 40 | __hex: string // debug print 41 | asData: (input: { readonly compressed: boolean }) => Buffer 42 | toString: (compressed?: boolean) => string 43 | isValidSignature: ( 44 | input: Readonly<{ 45 | signature: SignatureT 46 | hashedMessage: Buffer 47 | }>, 48 | ) => boolean 49 | decodeToPointOnCurve: () => ECPointOnCurveT 50 | equals: (other: PublicKeyT) => boolean 51 | }> 52 | 53 | export type PrivateKeyT = Signer & 54 | Readonly<{ 55 | diffieHellman: DiffieHellman 56 | scalar: UInt256 57 | publicKey: () => PublicKeyT 58 | toString: () => string 59 | }> 60 | 61 | export type KeyPairT = Readonly<{ 62 | publicKey: PublicKeyT 63 | privateKey: PrivateKeyT 64 | }> 65 | -------------------------------------------------------------------------------- /packages/crypto/src/elliptic-curve/hd/bip32/_types.ts: -------------------------------------------------------------------------------- 1 | export type Int32 = number 2 | 3 | export type BIP32T = Readonly<{ 4 | pathComponents: BIP32PathComponentT[] 5 | toString: () => string 6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 7 | equals: (other: any) => boolean 8 | }> 9 | 10 | export type BIP32PathSimpleT = Readonly<{ 11 | index: Int32 12 | isHardened: boolean 13 | }> 14 | 15 | export type BIP32PathComponentT = BIP32PathSimpleT & 16 | Readonly<{ 17 | toString: () => string 18 | 19 | // Not to be confused with the 'index', this is the position of this path component 20 | // inside a BIP32 path, e.g. `5/3/1` the component '5' has level 0 and '1' has level 2. 21 | level: number 22 | 23 | // E.g. 'purpose', 'coinType' 'account', 'change', 'address_index' 24 | name?: string 25 | 26 | // For `0'` the value 0 is returned, even though it is hardened. 27 | value: () => Int32 28 | }> 29 | -------------------------------------------------------------------------------- /packages/crypto/src/elliptic-curve/hd/bip32/bip32.ts: -------------------------------------------------------------------------------- 1 | import { combine, err, ok, Result } from 'neverthrow' 2 | import { BIP32PathComponent } from './bip32PathComponent' 3 | import { BIP32T, BIP32PathComponentT, Int32 } from './_types' 4 | import { ValidationWitness } from '@radixdlt/util' 5 | 6 | const pathSeparator = '/' 7 | const hardener = `'` 8 | 9 | const isBIP32 = (something: unknown): something is BIP32T => { 10 | const inspection = something as BIP32T 11 | return ( 12 | inspection.pathComponents !== undefined && 13 | inspection.toString !== undefined 14 | ) 15 | } 16 | 17 | export const unsafeCreate = (pathComponents: BIP32PathComponentT[]): BIP32T => { 18 | const toString = (): string => 19 | 'm' + 20 | pathSeparator + 21 | pathComponents.map(pc => pc.toString()).join(pathSeparator) 22 | 23 | return { 24 | pathComponents, 25 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 26 | equals: (other: any): boolean => { 27 | if (!isBIP32(other)) return false 28 | return other.toString() === toString() 29 | }, 30 | toString, 31 | } 32 | } 33 | 34 | const validateLevels = ( 35 | pathComponents: BIP32PathComponentT[], 36 | ): Result => 37 | combine( 38 | pathComponents.map>( 39 | (component, i, components) => 40 | component.level !== (i > 0 ? components[i - 1].level + 1 : 1) 41 | ? err( 42 | new Error( 43 | `Expected components with strictly increasing level with an increment of one.`, 44 | ), 45 | ) 46 | : ok({ witness: 'component valid' }), 47 | ), 48 | ).andThen(_a => ok({ witness: 'all components valid' })) 49 | 50 | const create = (pathComponents: BIP32PathComponentT[]): Result => 51 | validateLevels(pathComponents).map(() => unsafeCreate(pathComponents)) 52 | 53 | const fromString = (path: string): Result => { 54 | let bip32Path = path.trim() 55 | if (bip32Path === '' || bip32Path === 'm' || bip32Path === pathSeparator) { 56 | return ok({ 57 | pathComponents: [], 58 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 59 | equals: (other: any): boolean => { 60 | if (!isBIP32(other)) return false 61 | return other.toString() === 'm' 62 | }, 63 | toString: (): string => 'm', 64 | }) 65 | } 66 | 67 | if (bip32Path.startsWith('M/') || bip32Path.startsWith('m/')) { 68 | bip32Path = bip32Path.slice(2) 69 | if (bip32Path.length === 0) { 70 | return err(new Error(`Must start with just 'm/' or 'M/'`)) 71 | } 72 | } 73 | if (bip32Path.length === 0) { 74 | return err(new Error('Must not be empty')) 75 | } 76 | 77 | if (bip32Path.includes('//')) { 78 | return err(new Error(`Must not contain '//'`)) 79 | } 80 | 81 | const components = bip32Path.split(pathSeparator) 82 | const pathComponents: BIP32PathComponentT[] = [] 83 | for (const { index, value } of components.map((value, index) => ({ 84 | index, 85 | value, 86 | }))) { 87 | const pathComponentResult = BIP32PathComponent.fromString( 88 | value, 89 | index + 1, 90 | ) 91 | if (pathComponentResult.isErr()) return err(pathComponentResult.error) 92 | pathComponents.push(pathComponentResult.value) 93 | } 94 | return create(pathComponents) 95 | } 96 | const unsafeFromSimpleComponents = ( 97 | pathComponents: Readonly<{ 98 | index: Int32 99 | isHardened: boolean 100 | }>[], 101 | ): Result => 102 | combine( 103 | pathComponents.map((e, i) => 104 | BIP32PathComponent.create({ 105 | ...e, 106 | level: i, 107 | }), 108 | ), 109 | ).map(unsafeCreate) 110 | 111 | export const BIP32 = { 112 | create, 113 | unsafeCreate, 114 | fromString, 115 | unsafeFromSimpleComponents, 116 | hardener, 117 | pathSeparator, 118 | } 119 | -------------------------------------------------------------------------------- /packages/crypto/src/elliptic-curve/hd/bip32/bip32PathComponent.ts: -------------------------------------------------------------------------------- 1 | import { err, ok, Result } from 'neverthrow' 2 | import { BIP32PathComponentT, BIP32PathSimpleT, Int32 } from './_types' 3 | import { BIP32 } from './bip32' 4 | 5 | export const hardenedIncrement: number = 0x80000000 6 | 7 | const assertNotHardened = ( 8 | simplePath: BIP32PathSimpleT, 9 | ): Result => { 10 | const { index, isHardened } = simplePath 11 | if (index >= hardenedIncrement) { 12 | return err( 13 | new Error( 14 | `Incorrect implementation, expected value of index to be less than 'hardenedIncrement' for path components which are hardended. This function will add 'hardenedIncrement' to the value of index passed in, if 'isHardened' flag is set to true. But got value of index: ${index}, 'isHardened': ${ 15 | isHardened ? 'true' : 'false' 16 | }`, 17 | ), 18 | ) 19 | } 20 | return ok(index) 21 | } 22 | 23 | const create = ( 24 | input: Readonly<{ 25 | index: Int32 26 | isHardened: boolean 27 | level: number 28 | }>, 29 | ): Result => { 30 | const { isHardened } = input 31 | return assertNotHardened({ ...input }).map(index => ({ 32 | ...input, 33 | index: isHardened ? index + hardenedIncrement : index, 34 | value: () => index, 35 | toString: (): string => `${index}` + (isHardened ? `'` : ''), 36 | })) 37 | } 38 | 39 | export const isBIP32PathSimpleT = ( 40 | something: unknown, 41 | ): something is BIP32PathSimpleT => { 42 | const inspection = something as BIP32PathSimpleT 43 | return inspection.index !== undefined && inspection.isHardened !== undefined 44 | } 45 | 46 | const fromString = ( 47 | componentString: string, 48 | level: number, 49 | ): Result => { 50 | if (componentString.includes(BIP32.pathSeparator)) { 51 | return err(new Error('Path component contains separator')) 52 | } 53 | let component = componentString 54 | let isHardened = false 55 | if (component.endsWith(BIP32.hardener)) { 56 | isHardened = true 57 | component = component.replace(BIP32.hardener, '') 58 | } 59 | 60 | let parsedInt: number = 0 61 | try { 62 | parsedInt = parseInt(component, 10) 63 | } catch (e) { 64 | return err(new Error('Failed to parse integer')) 65 | } 66 | if (!Number.isInteger(parsedInt)) { 67 | return err(new Error('Found no integer')) 68 | } 69 | 70 | return BIP32PathComponent.create({ 71 | index: parsedInt, 72 | isHardened, 73 | level, 74 | }) 75 | } 76 | 77 | export const BIP32PathComponent = { 78 | create, 79 | fromString, 80 | } 81 | -------------------------------------------------------------------------------- /packages/crypto/src/elliptic-curve/hd/bip32/bip44/_types.ts: -------------------------------------------------------------------------------- 1 | import { BIP32T, BIP32PathComponentT } from '../_types' 2 | 3 | export type BIP44T = BIP32T & 4 | Readonly<{ 5 | purpose: BIP32PathComponentT 6 | coinType: BIP32PathComponentT 7 | account: BIP32PathComponentT 8 | change: BIP32PathComponentT 9 | addressIndex: BIP32PathComponentT 10 | }> 11 | 12 | export type BIP44ChangeIndex = 0 | 1 13 | 14 | export type HDPathRadixT = BIP44T & 15 | Readonly<{ 16 | purpose: { 17 | index: 0x8000002c 18 | isHardened: true 19 | toString: () => `44'` 20 | level: 1 21 | name: 'purpose' 22 | } 23 | coinType: { 24 | index: 0x80000218 // <=> 1022' dec 25 | isHardened: true 26 | toString: () => `1022'` 27 | level: 2 28 | name: 'coin type' 29 | } 30 | account: { 31 | isHardened: true 32 | level: 3 33 | name: 'account' 34 | } 35 | change: BIP32PathComponentT & { 36 | isHardened: true 37 | level: 4 38 | name: 'change' 39 | } 40 | addressIndex: BIP32PathComponentT & { 41 | level: 5 42 | name: 'address index' 43 | } 44 | }> 45 | -------------------------------------------------------------------------------- /packages/crypto/src/elliptic-curve/hd/bip32/bip44/index.ts: -------------------------------------------------------------------------------- 1 | export * from './_types' 2 | 3 | export * from './bip44' 4 | -------------------------------------------------------------------------------- /packages/crypto/src/elliptic-curve/hd/bip32/index.ts: -------------------------------------------------------------------------------- 1 | export * from './bip44' 2 | 3 | export * from './_types' 4 | 5 | export * from './bip32' 6 | export * from './bip32PathComponent' 7 | -------------------------------------------------------------------------------- /packages/crypto/src/elliptic-curve/hd/bip39/_types.ts: -------------------------------------------------------------------------------- 1 | import { BIP32T } from '../bip32' 2 | import { PrivateKeyT, PublicKeyT } from '../../_types' 3 | 4 | export type HDNodeT = Readonly<{ 5 | publicKey: PublicKeyT 6 | privateKey: PrivateKeyT 7 | chainCode: Buffer 8 | derive: (path: BIP32T) => HDNodeT 9 | toJSON: () => Readonly<{ 10 | // privateExtendedKey 11 | xpriv: string 12 | // publicExtendedKey 13 | xpub: string 14 | }> 15 | }> 16 | 17 | export type HDMasterSeedT = Readonly<{ 18 | seed: Buffer 19 | masterNode: () => HDNodeT 20 | }> 21 | 22 | export enum StrengthT { 23 | /// Entropy of 128 bits 24 | WORD_COUNT_12 = 128, 25 | /// Entropy of 160 bits 26 | WORD_COUNT_15 = 160, 27 | /// Entropy of 192 bits 28 | WORD_COUNT_18 = 192, 29 | /// Entropy of 224 bits 30 | WORD_COUNT_21 = 224, 31 | /// Entropy of 256 bits 32 | WORD_COUNT_24 = 256, 33 | } 34 | 35 | export enum LanguageT { 36 | CZECH, 37 | CHINESE_SIMPLIFIED, 38 | CHINESE_TRADITIONAL, 39 | KOREAN, 40 | FRENCH, 41 | ITALIAN, 42 | SPANISH, 43 | JAPANESE, 44 | PORTUGUESE, 45 | ENGLISH, 46 | } 47 | 48 | export type MnemonicProps = Readonly<{ 49 | strength: StrengthT 50 | entropy: Buffer 51 | words: string[] 52 | phrase: string 53 | language: LanguageT 54 | }> 55 | 56 | export type MnemomicT = MnemonicProps & 57 | Readonly<{ 58 | toString: () => string 59 | equals: (other: MnemomicT) => boolean 60 | }> 61 | -------------------------------------------------------------------------------- /packages/crypto/src/elliptic-curve/hd/bip39/hdMasterSeed.ts: -------------------------------------------------------------------------------- 1 | import { HDMasterSeedT, HDNodeT, MnemomicT } from './_types' 2 | import { mnemonicToSeedSync } from 'bip39' 3 | import HDNodeThirdParty from 'hdkey' 4 | import { BIP32T } from '../bip32' 5 | import { Result, err, ok } from 'neverthrow' 6 | import { PrivateKey } from '../../privateKey' 7 | 8 | const hdNodeFromHDNodeThirdParty = ( 9 | hdNodeThirdParty: HDNodeThirdParty, 10 | ): HDNodeT => { 11 | const privateKeyResult = PrivateKey.fromBuffer(hdNodeThirdParty.privateKey) 12 | if (privateKeyResult.isErr()) 13 | throw new Error( 14 | `Incorrect implementation, failed to get private key from HDNode, third party lib 'hdkey' might be buggy?`, 15 | ) 16 | const privateKey = privateKeyResult.value 17 | 18 | return { 19 | privateKey, 20 | publicKey: privateKey.publicKey(), 21 | chainCode: hdNodeThirdParty.chainCode, 22 | derive: (path: BIP32T): HDNodeT => 23 | hdNodeFromHDNodeThirdParty( 24 | hdNodeThirdParty.derive(path.toString()), 25 | ), 26 | toJSON: () => hdNodeThirdParty.toJSON(), 27 | } 28 | } 29 | 30 | const fromMnemonic = ( 31 | input: Readonly<{ 32 | mnemonic: MnemomicT 33 | passphrase?: string 34 | }>, 35 | ): HDMasterSeedT => { 36 | const seed = mnemonicToSeedSync(input.mnemonic.phrase, input.passphrase) 37 | return fromSeed(seed) 38 | } 39 | 40 | const fromSeed = (seed: Buffer): HDMasterSeedT => { 41 | const hdNodeMaster = HDNodeThirdParty.fromMasterSeed(seed) 42 | 43 | return { 44 | seed, 45 | masterNode: (): HDNodeT => hdNodeFromHDNodeThirdParty(hdNodeMaster), 46 | } 47 | } 48 | 49 | export const HDMasterSeed = { 50 | fromMnemonic, 51 | fromSeed, 52 | } 53 | 54 | const fromExtendedPrivateKey = (xpriv: string): Result => { 55 | try { 56 | const hdKey = HDNodeThirdParty.fromJSON({ xpriv, xpub: 'not used' }) 57 | return ok(hdNodeFromHDNodeThirdParty(hdKey)) 58 | } catch { 59 | return err( 60 | new Error('Failed to create HDNode from extended private key'), 61 | ) 62 | } 63 | } 64 | 65 | export const HDNode = { 66 | fromExtendedPrivateKey, 67 | } 68 | -------------------------------------------------------------------------------- /packages/crypto/src/elliptic-curve/hd/bip39/index.ts: -------------------------------------------------------------------------------- 1 | export * from './_types' 2 | 3 | export * from './mnemonic' 4 | export * from './hdMasterSeed' 5 | -------------------------------------------------------------------------------- /packages/crypto/src/elliptic-curve/hd/index.ts: -------------------------------------------------------------------------------- 1 | export * from './bip32' 2 | export * from './bip39' 3 | -------------------------------------------------------------------------------- /packages/crypto/src/elliptic-curve/index.ts: -------------------------------------------------------------------------------- 1 | export * from './_types' 2 | 3 | export * from './hd' 4 | export * from './secp256k1' 5 | export * from './ecPointOnCurve' 6 | export * from './privateKey' 7 | export * from './publicKey' 8 | export * from './signature' 9 | export * from './keyPair' 10 | -------------------------------------------------------------------------------- /packages/crypto/src/elliptic-curve/keyPair.ts: -------------------------------------------------------------------------------- 1 | import { SecureRandom, secureRandomGenerator } from '@radixdlt/util' 2 | import { PrivateKey } from './privateKey' 3 | import { KeyPairT, PrivateKeyT } from './_types' 4 | 5 | const fromPrivateKey = (privateKey: PrivateKeyT): KeyPairT => ({ 6 | privateKey, 7 | publicKey: privateKey.publicKey(), 8 | }) 9 | 10 | const generateNew = ( 11 | secureRandom: SecureRandom = secureRandomGenerator, 12 | ): KeyPairT => { 13 | const privateKey = PrivateKey.generateNew(secureRandom) 14 | return fromPrivateKey(privateKey) 15 | } 16 | 17 | export const KeyPair = { 18 | generateNew, 19 | fromPrivateKey, 20 | } 21 | -------------------------------------------------------------------------------- /packages/crypto/src/elliptic-curve/secp256k1.ts: -------------------------------------------------------------------------------- 1 | import { UInt256 } from '@radixdlt/uint256' 2 | import { ECPointOnCurve } from './ecPointOnCurve' 3 | import { ECPointOnCurveT } from './_types' 4 | 5 | const generator = ECPointOnCurve.fromXY({ 6 | x: new UInt256( 7 | '79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798', 8 | 16, 9 | ), 10 | y: new UInt256( 11 | '483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8', 12 | 16, 13 | ), 14 | })._unsafeUnwrap() 15 | 16 | const order = new UInt256( 17 | 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141', 18 | 16, 19 | ) 20 | 21 | const fieldSize = new UInt256( 22 | 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F', 23 | 16, 24 | ) 25 | 26 | export enum CurveForm { 27 | /// Short Weierstrass (Weierstraß) form (`𝑆`), commonly used by `secp256k1` 28 | SHORT_WEIERSTRASS = 'ShortWeierstrass', 29 | } 30 | 31 | export type Curve = Readonly<{ 32 | name: string 33 | /// Form, ShortWeierstrass, Edwards, TwistedEdwards or Hessian. 34 | form: CurveForm 35 | /// a.k.a. `n` 36 | order: UInt256 37 | /// a.k.a. `P` or `mod` 38 | fieldSize: UInt256 39 | /// a.k.a. `G` 40 | generator: ECPointOnCurveT 41 | }> 42 | 43 | /// The curve E: `y² = x³ + ax + b` over Fp 44 | /// `secp256k1` Also known as the `Bitcoin curve` (though used by us at Radix, Ethereum, Zilliqa) 45 | export const Secp256k1: Curve = { 46 | name: 'secp256k1', 47 | form: CurveForm.SHORT_WEIERSTRASS, 48 | order: order, 49 | fieldSize: fieldSize, 50 | generator: generator, 51 | } 52 | -------------------------------------------------------------------------------- /packages/crypto/src/elliptic-curve/signature.ts: -------------------------------------------------------------------------------- 1 | import { SignatureT } from './_types' 2 | import { combine, err, ok, Result } from 'neverthrow' 3 | import { UInt256 } from '@radixdlt/uint256' 4 | import { ec } from 'elliptic' 5 | import { uint256FromBN } from '@radixdlt/primitives' 6 | import BN from 'bn.js' 7 | 8 | // @ts-ignore 9 | // eslint-disable-next-line @typescript-eslint/no-var-requires 10 | const __js_DER = require('./indutnyEllipticImportDER') 11 | 12 | const __fromRSAndDER = ( 13 | input: Readonly<{ 14 | r: UInt256 15 | s: UInt256 16 | der: string 17 | }>, 18 | ): SignatureT => { 19 | const { r, s, der } = input 20 | return { 21 | r, 22 | s, 23 | toDER: () => der, 24 | equals: (other: SignatureT): boolean => other.toDER() === der, 25 | } 26 | } 27 | 28 | const fromIndutnyElliptic = ( 29 | ellipticSignature: ec.Signature, 30 | ): Result => { 31 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 32 | const derUnknown = ellipticSignature.toDER('hex') 33 | if (!derUnknown || typeof derUnknown !== 'string') { 34 | throw new Error( 35 | 'Incorrect implementation, should always be able to format DER from signature.', 36 | ) 37 | } 38 | const der: string = derUnknown 39 | 40 | return combine([ 41 | uint256FromBN(ellipticSignature.r), 42 | uint256FromBN(ellipticSignature.s), 43 | ]).map(resultList => { 44 | const r = resultList[0] 45 | const s = resultList[1] 46 | return __fromRSAndDER({ r, s, der }) 47 | }) 48 | } 49 | 50 | const fromRSBuffer = (buffer: Buffer): Result => { 51 | const expectedLength = 64 52 | if (buffer.length !== expectedLength) { 53 | return err( 54 | new Error( 55 | `Incorrect length of signature buffer (R||S), expected #${expectedLength} bytes, but got #${buffer.length}.`, 56 | ), 57 | ) 58 | } 59 | 60 | const rHex = buffer.slice(0, 32).toString('hex') 61 | const r = new UInt256(rHex, 16) 62 | const sHex = buffer.slice(32, 64).toString('hex') 63 | const s = new UInt256(sHex, 16) 64 | /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */ 65 | const der = __js_DER.__js_toDER(new BN(rHex, 16), new BN(sHex, 16), 'hex') 66 | return ok( 67 | __fromRSAndDER({ 68 | r, 69 | s, 70 | der, 71 | }), 72 | ) 73 | /* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */ 74 | } 75 | 76 | const fromDER = (buffer: Buffer | string): Result => { 77 | const dataHex = typeof buffer === 'string' ? buffer : buffer.toString('hex') 78 | /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */ 79 | const importedDER = __js_DER.__js_importDER(dataHex, 'hex') 80 | if (!importedDER) { 81 | return err(new Error('Failed to import DER')) 82 | } 83 | return ok( 84 | __fromRSAndDER({ 85 | r: importedDER.r, 86 | s: importedDER.s, 87 | der: buffer.toString('hex'), 88 | }), 89 | ) 90 | /* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */ 91 | } 92 | 93 | export const Signature = { 94 | fromDER, 95 | fromRSBuffer, 96 | fromIndutnyElliptic, 97 | } 98 | -------------------------------------------------------------------------------- /packages/crypto/src/encryption/_types.ts: -------------------------------------------------------------------------------- 1 | import { Byte, SecureRandom } from '@radixdlt/util' 2 | import { ResultAsync } from 'neverthrow' 3 | import { ECPointOnCurveT, PublicKeyT } from '../elliptic-curve' 4 | 5 | export type MessageEncryptionInput = Readonly<{ 6 | plaintext: Buffer | string 7 | diffieHellmanPoint: () => ResultAsync 8 | secureRandom?: SecureRandom 9 | }> 10 | 11 | export type MessageDecryptionInput = Readonly<{ 12 | encryptedMessage: Buffer | EncryptedMessageT 13 | diffieHellmanPoint: () => ResultAsync 14 | }> 15 | 16 | export const ENCRYPTION_SCHEME_BYTES = 1 17 | 18 | export const MESSAGE_TYPE_BYTES = 1 19 | 20 | export enum MessageType { 21 | PLAINTEXT = 0x00, 22 | ENCRYPTED = 0x01, 23 | HEX = 0x1e, 24 | } 25 | 26 | export enum EncryptionScheme { 27 | NONE = 0x00, 28 | DH_ADD_EPH_AESGCM256_SCRYPT_000 = 0xff, 29 | } 30 | 31 | export type SealedMessageT = Readonly<{ 32 | /* The public key of the ephemeral key pair. 33 bytes */ 33 | ephemeralPublicKey: PublicKeyT 34 | 35 | /* The nonce used to encrypt the data. 12 bytes. AKA "IV". */ 36 | nonce: Buffer 37 | 38 | /* An authentication tag. 16 bytes, e.g. AES GCM tag. */ 39 | authTag: Buffer 40 | 41 | /* The encrypted data. Max 162 bytes. */ 42 | ciphertext: Buffer 43 | 44 | combined: () => Buffer 45 | }> 46 | 47 | type Message = { 48 | kind: Kind 49 | } 50 | 51 | // Max 255 bytes 52 | export type EncryptedMessageT = Message<'ENCRYPTED'> & { 53 | encryptionScheme: EncryptionScheme 54 | 55 | /* Encrypted message with metadata containing about how it can be decrypted. Max 223 bytes. */ 56 | sealedMessage: SealedMessageT 57 | 58 | combined: () => Buffer 59 | } 60 | 61 | export type PlaintextMessageT = Message<'PLAINTEXT'> & { 62 | plaintext: string 63 | bytes: Buffer 64 | } 65 | -------------------------------------------------------------------------------- /packages/crypto/src/encryption/index.ts: -------------------------------------------------------------------------------- 1 | export * from './_types' 2 | 3 | export * from './messageEncryption' 4 | export * from './message' 5 | export * from './sealedMessage' 6 | -------------------------------------------------------------------------------- /packages/crypto/src/encryption/sealedMessage.ts: -------------------------------------------------------------------------------- 1 | import { combine, err, Result } from 'neverthrow' 2 | import { readBuffer } from '@radixdlt/util' 3 | import { AES_GCM, AES_GCM_SealedBoxT } from '../symmetric-encryption' 4 | import { SealedMessageT } from './_types' 5 | import { validateLength } from '../utils' 6 | import { PublicKey, PublicKeyT } from '../elliptic-curve' 7 | 8 | const create = ( 9 | input: Readonly<{ 10 | ephemeralPublicKey: PublicKeyT 11 | nonce: Buffer 12 | authTag: Buffer 13 | ciphertext: Buffer 14 | }>, 15 | ): Result => 16 | combine([__validateNonce(input.nonce), __validateTag(input.authTag)]).map( 17 | _ => ({ 18 | ...input, 19 | combined: (): Buffer => 20 | Buffer.concat([ 21 | input.ephemeralPublicKey.asData({ compressed: true }), 22 | input.nonce, 23 | input.authTag, 24 | input.ciphertext, 25 | ]), 26 | }), 27 | ) 28 | 29 | const sealedMessageNonceLength = AES_GCM.nonceLength 30 | const sealedMessageAuthTagLength = AES_GCM.tagLength 31 | 32 | export const __validateTag: ( 33 | buffer: Buffer, 34 | ) => Result = validateLength.bind( 35 | null, 36 | sealedMessageAuthTagLength, 37 | 'auth tag', 38 | ) 39 | 40 | export const __validateNonce: ( 41 | buffer: Buffer, 42 | ) => Result = validateLength.bind( 43 | null, 44 | sealedMessageNonceLength, 45 | 'nonce', 46 | ) 47 | 48 | const sealedMessageFromBuffer = ( 49 | buffer: Buffer, 50 | ): Result => { 51 | const sealedMessageLength = buffer.length 52 | const lengthOfCiphertext = 53 | sealedMessageLength - 54 | PublicKey.compressedByteCount - 55 | sealedMessageNonceLength - 56 | sealedMessageAuthTagLength 57 | 58 | if (lengthOfCiphertext <= 0) 59 | return err(new Error('Ciphertext cannot be empty')) 60 | 61 | const readNextBuffer = readBuffer.bind(null, buffer)() 62 | 63 | return combine([ 64 | readNextBuffer(PublicKey.compressedByteCount).andThen( 65 | PublicKey.fromBuffer, 66 | ), 67 | readNextBuffer(sealedMessageNonceLength), 68 | readNextBuffer(sealedMessageAuthTagLength), 69 | readNextBuffer(lengthOfCiphertext), 70 | ]).andThen(resultList => { 71 | const ephemeralPublicKey = resultList[0] as PublicKeyT 72 | const nonce = resultList[1] as Buffer 73 | const authTag = resultList[2] as Buffer 74 | const ciphertext = resultList[3] as Buffer 75 | 76 | return create({ 77 | ephemeralPublicKey, 78 | nonce, 79 | authTag, 80 | ciphertext, 81 | }) 82 | }) 83 | } 84 | 85 | const sealedMsgFromAESSealedBox = ( 86 | aesSealedBox: AES_GCM_SealedBoxT, 87 | ephemeralPublicKey: PublicKeyT, 88 | ): Result => 89 | create({ ...aesSealedBox, ephemeralPublicKey }) 90 | 91 | export const SealedMessage = { 92 | nonceByteCount: sealedMessageNonceLength, 93 | authTagByteCount: sealedMessageAuthTagLength, 94 | create, 95 | fromAESSealedBox: sealedMsgFromAESSealedBox, 96 | fromBuffer: sealedMessageFromBuffer, 97 | } 98 | -------------------------------------------------------------------------------- /packages/crypto/src/hash/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sha' 2 | -------------------------------------------------------------------------------- /packages/crypto/src/hash/sha.ts: -------------------------------------------------------------------------------- 1 | import { sha256 as SHA256 } from 'hash.js' 2 | import { Hasher } from '../_types' 3 | 4 | const toBuffer = (input: Buffer | string): Buffer => 5 | typeof input === 'string' ? Buffer.from(input, 'utf-8') : input 6 | 7 | export const sha256: Hasher = (input: Buffer | string): Buffer => 8 | Buffer.from(SHA256().update(toBuffer(input)).digest()) 9 | 10 | export const sha256Twice: Hasher = (input: Buffer | string): Buffer => 11 | sha256(sha256(toBuffer(input))) 12 | -------------------------------------------------------------------------------- /packages/crypto/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hash' 2 | export * from './keystore' 3 | export * from './key-derivation-functions' 4 | export * from './symmetric-encryption' 5 | export * from './elliptic-curve' 6 | export * from './encryption' 7 | 8 | export * from './_types' 9 | -------------------------------------------------------------------------------- /packages/crypto/src/key-derivation-functions/_types.ts: -------------------------------------------------------------------------------- 1 | export type ScryptParamsT = Readonly<{ 2 | // "N", CPU/memory cost parameter, must be power of 2. 3 | costParameterN: number 4 | costParameterC: number 5 | 6 | // "r", blocksize 7 | blockSize: number 8 | 9 | // "p" 10 | parallelizationParameter: number 11 | 12 | // "dklen" 13 | lengthOfDerivedKey: number 14 | 15 | salt: string 16 | }> 17 | -------------------------------------------------------------------------------- /packages/crypto/src/key-derivation-functions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './_types' 2 | 3 | export * from './scrypt' 4 | -------------------------------------------------------------------------------- /packages/crypto/src/key-derivation-functions/scrypt.ts: -------------------------------------------------------------------------------- 1 | import { scrypt } from 'scrypt-js' 2 | import { ResultAsync, errAsync } from 'neverthrow' 3 | import { ScryptParamsT } from './_types' 4 | import { 5 | msgFromError, 6 | SecureRandom, 7 | secureRandomGenerator, 8 | } from '@radixdlt/util' 9 | 10 | const deriveKey = ( 11 | input: Readonly<{ 12 | password: Buffer 13 | kdf: string 14 | params: ScryptParamsT 15 | }>, 16 | ): ResultAsync => { 17 | if (input.kdf !== 'scrypt') 18 | return errAsync(new Error('Wrong KDF, expected scrypt')) 19 | const { params, password: key } = input 20 | const { 21 | lengthOfDerivedKey: dklen, 22 | costParameterN: n, 23 | blockSize: r, 24 | parallelizationParameter: p, 25 | } = params 26 | const salt = Buffer.from(params.salt, 'hex') 27 | 28 | return ResultAsync.fromPromise( 29 | scrypt(key, salt, n, r, p, dklen).then(uint8array => 30 | Buffer.from(uint8array), 31 | ), 32 | (e: unknown) => { 33 | const underlyingErrorMessage = msgFromError(e) 34 | return new Error( 35 | `Failed to derive data using scrypt, underlying error: '${underlyingErrorMessage}'`, 36 | ) 37 | }, 38 | ) 39 | } 40 | 41 | export const Scrypt = { 42 | deriveKey, 43 | } 44 | 45 | const create = ( 46 | input: Readonly<{ 47 | salt?: Buffer 48 | secureRandom?: SecureRandom 49 | }>, 50 | ): ScryptParamsT => { 51 | const secureRandom = input.secureRandom ?? secureRandomGenerator 52 | if (input.salt && input.salt.length !== 32) 53 | throw new Error('Incorrect implementatin expected 32 bytes salt') 54 | const salt = 55 | input.salt?.toString('hex') ?? secureRandom.randomSecureBytes(32) 56 | 57 | return { 58 | costParameterN: 8192, 59 | costParameterC: 262144, 60 | blockSize: 8, 61 | parallelizationParameter: 1, 62 | lengthOfDerivedKey: 32, 63 | salt: salt, 64 | } 65 | } 66 | 67 | export const ScryptParams = { 68 | create, 69 | } 70 | -------------------------------------------------------------------------------- /packages/crypto/src/keystore/_types.ts: -------------------------------------------------------------------------------- 1 | import { ScryptParamsT } from '../key-derivation-functions' 2 | 3 | export type KeystoreCryptoCipherParamsT = Readonly<{ 4 | nonce: string 5 | }> 6 | 7 | export type KeystoreCryptoT = Readonly<{ 8 | cipher: string 9 | cipherparams: KeystoreCryptoCipherParamsT 10 | ciphertext: string 11 | kdf: string 12 | kdfparams: ScryptParamsT 13 | mac: string 14 | }> 15 | 16 | export type KeystoreT = Readonly<{ 17 | memo?: string 18 | crypto: KeystoreCryptoT 19 | id: string 20 | version: number 21 | }> 22 | -------------------------------------------------------------------------------- /packages/crypto/src/keystore/index.ts: -------------------------------------------------------------------------------- 1 | export * from './_types' 2 | 3 | export * from './keystore' 4 | -------------------------------------------------------------------------------- /packages/crypto/src/symmetric-encryption/aes/_types.ts: -------------------------------------------------------------------------------- 1 | import { SecureRandom } from '@radixdlt/util' 2 | 3 | export type AES_GCM_SealedBoxProps = Readonly<{ 4 | authTag: Buffer 5 | ciphertext: Buffer 6 | nonce: Buffer 7 | }> 8 | 9 | export type AES_GCM_SealedBoxT = AES_GCM_SealedBoxProps & 10 | Readonly<{ 11 | combined: () => Buffer 12 | equals: (other: AES_GCM_SealedBoxT) => boolean 13 | }> 14 | 15 | export type AES_GCM_OPEN_Input = AES_GCM_SealedBoxProps & 16 | Readonly<{ 17 | symmetricKey: Buffer 18 | additionalAuthenticationData?: Buffer 19 | }> 20 | 21 | export type AES_GCM_SEAL_Input = Readonly<{ 22 | plaintext: Buffer 23 | symmetricKey: Buffer 24 | additionalAuthenticationData?: Buffer 25 | nonce?: Buffer 26 | secureRandom?: SecureRandom 27 | }> 28 | -------------------------------------------------------------------------------- /packages/crypto/src/symmetric-encryption/aes/aesGCM.ts: -------------------------------------------------------------------------------- 1 | import { cipher as forgeCipher, util as forgeUtil } from 'node-forge' 2 | import { 3 | AES_GCM_OPEN_Input, 4 | AES_GCM_SEAL_Input, 5 | AES_GCM_SealedBoxT, 6 | } from './_types' 7 | import { err, ok, Result } from 'neverthrow' 8 | import { secureRandomGenerator } from '@radixdlt/util' 9 | import { AES_GCM_SealedBox } from './aesGCMSealedBox' 10 | 11 | const AES_GCM_256_ALGORITHM = 'AES-GCM' 12 | 13 | export const aesGCMSealDeterministic = ( 14 | input: Omit & { 15 | readonly nonce: Buffer 16 | }, 17 | ): Result => { 18 | const { nonce } = input 19 | 20 | const aesCipher = forgeCipher.createCipher( 21 | AES_GCM_256_ALGORITHM, 22 | forgeUtil.createBuffer(input.symmetricKey), 23 | ) 24 | 25 | const iv = forgeUtil.createBuffer(nonce) 26 | const startOptions = { iv } 27 | 28 | if (input.additionalAuthenticationData) { 29 | aesCipher.start({ 30 | ...startOptions, 31 | additionalData: forgeUtil.hexToBytes( 32 | input.additionalAuthenticationData.toString('hex'), 33 | ), 34 | }) 35 | } else { 36 | aesCipher.start(startOptions) 37 | } 38 | 39 | aesCipher.update(forgeUtil.createBuffer(input.plaintext)) 40 | 41 | if (!aesCipher.finish()) { 42 | throw new Error(`AES encryption failed, error unknown...`) 43 | } 44 | 45 | const ciphertext = Buffer.from(aesCipher.output.toHex(), 'hex') 46 | const authTag = Buffer.from(aesCipher.mode.tag.toHex(), 'hex') 47 | 48 | return AES_GCM_SealedBox.create({ 49 | ciphertext, 50 | authTag, 51 | nonce, 52 | }) 53 | } 54 | 55 | const seal = (input: AES_GCM_SEAL_Input): Result => { 56 | const secureRandom = input.secureRandom ?? secureRandomGenerator 57 | const nonce = 58 | input.nonce ?? 59 | Buffer.from( 60 | secureRandom.randomSecureBytes(AES_GCM_SealedBox.nonceLength), 61 | 'hex', 62 | ) 63 | return aesGCMSealDeterministic({ ...input, nonce }) 64 | } 65 | 66 | const open = (input: AES_GCM_OPEN_Input): Result => { 67 | const { ciphertext, additionalAuthenticationData, symmetricKey } = input 68 | 69 | return AES_GCM_SealedBox.create(input).andThen( 70 | (box: AES_GCM_SealedBoxT) => { 71 | const nonce = box.nonce 72 | const authTag = box.authTag 73 | 74 | const decipher = forgeCipher.createDecipher( 75 | AES_GCM_256_ALGORITHM, 76 | forgeUtil.createBuffer(symmetricKey), 77 | ) 78 | 79 | const iv = forgeUtil.createBuffer(nonce) 80 | const tag = forgeUtil.createBuffer(authTag) 81 | 82 | const startOptions = { 83 | iv, 84 | tag, 85 | } 86 | 87 | if (additionalAuthenticationData) { 88 | const additionalData = forgeUtil.hexToBytes( 89 | additionalAuthenticationData.toString('hex'), 90 | ) 91 | decipher.start({ 92 | ...startOptions, 93 | additionalData, 94 | }) 95 | } else { 96 | decipher.start(startOptions) 97 | } 98 | 99 | decipher.update(forgeUtil.createBuffer(ciphertext)) 100 | 101 | if (!decipher.finish()) { 102 | return err(new Error(`AES decryption failed.`)) 103 | } 104 | 105 | return ok(Buffer.from(decipher.output.toHex(), 'hex')) 106 | }, 107 | ) 108 | } 109 | 110 | export const AES_GCM = { 111 | seal, 112 | open, 113 | tagLength: AES_GCM_SealedBox.tagLength, 114 | nonceLength: AES_GCM_SealedBox.nonceLength, 115 | algorithm: AES_GCM_256_ALGORITHM, 116 | } 117 | -------------------------------------------------------------------------------- /packages/crypto/src/symmetric-encryption/aes/aesGCMSealedBox.ts: -------------------------------------------------------------------------------- 1 | import { combine, Result } from 'neverthrow' 2 | import { AES_GCM_SealedBoxProps, AES_GCM_SealedBoxT } from './_types' 3 | import { buffersEquals, readBuffer } from '@radixdlt/util' 4 | import { validateLength, validateMinLength } from '../../utils' 5 | 6 | const tagLength = 16 7 | const nonceLength = 12 8 | 9 | const cipherMinLength = 1 10 | 11 | const __validateNonce: ( 12 | buffer: Buffer, 13 | ) => Result = validateLength.bind( 14 | null, 15 | nonceLength, 16 | 'nonce (IV)', 17 | ) 18 | 19 | const __validateTag: ( 20 | buffer: Buffer, 21 | ) => Result = validateLength.bind(null, tagLength, 'auth tag') 22 | 23 | const __validateAESSealedBoxCiphertext: ( 24 | buffer: Buffer, 25 | ) => Result = validateMinLength.bind( 26 | null, 27 | cipherMinLength, 28 | 'ciphertext', 29 | ) 30 | 31 | /* 32 | * returns combined buffers: `nonce || tag || cipher` 33 | * */ 34 | const combineSealedBoxProps = (input: AES_GCM_SealedBoxProps): Buffer => 35 | Buffer.concat([input.nonce, input.authTag, input.ciphertext]) 36 | 37 | const create = ( 38 | input: AES_GCM_SealedBoxProps, 39 | ): Result => 40 | combine([ 41 | __validateNonce(input.nonce), 42 | __validateTag(input.authTag), 43 | __validateAESSealedBoxCiphertext(input.ciphertext), 44 | ]).map(_ => ({ 45 | ...input, 46 | combined: (): Buffer => combineSealedBoxProps(input), 47 | equals: (other: AES_GCM_SealedBoxT): boolean => 48 | buffersEquals(other.nonce, input.nonce) && 49 | buffersEquals(other.authTag, input.authTag) && 50 | buffersEquals(other.ciphertext, input.ciphertext), 51 | })) 52 | 53 | /* Buffer is: `nonce || tag || cipher` */ 54 | const aesSealedBoxFromBuffer = ( 55 | buffer: Buffer, 56 | ): Result => { 57 | const readNextBuffer = readBuffer.bind(null, buffer)() 58 | return combine([ 59 | readNextBuffer(nonceLength), 60 | readNextBuffer(tagLength), 61 | readNextBuffer(buffer.length - nonceLength - tagLength), 62 | ]) 63 | .map((parsed: Buffer[]) => { 64 | const nonce = parsed[0] 65 | const authTag = parsed[1] 66 | const ciphertext = parsed[2] 67 | 68 | return { 69 | nonce, 70 | authTag, 71 | ciphertext, 72 | } 73 | }) 74 | .andThen(create) 75 | } 76 | 77 | export const AES_GCM_SealedBox = { 78 | fromCombinedBuffer: aesSealedBoxFromBuffer, 79 | create, 80 | nonceLength, 81 | tagLength, 82 | } 83 | -------------------------------------------------------------------------------- /packages/crypto/src/symmetric-encryption/aes/index.ts: -------------------------------------------------------------------------------- 1 | export * from './_types' 2 | 3 | export * from './aesGCM' 4 | export * from './aesGCMSealedBox' 5 | -------------------------------------------------------------------------------- /packages/crypto/src/symmetric-encryption/index.ts: -------------------------------------------------------------------------------- 1 | export * from './aes/index' 2 | -------------------------------------------------------------------------------- /packages/crypto/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { err, ok, Result } from 'neverthrow' 2 | import { log } from '@radixdlt/util' 3 | import { UInt256 } from '@radixdlt/uint256' 4 | 5 | const ensureNum = (num: number): void => { 6 | if (!num || Number.isNaN(num)) { 7 | log.error(`Expected number but got none or got NaN: ${num}`) 8 | throw new Error('Incorrect implementation, must get a number') 9 | } 10 | } 11 | 12 | export const validateMaxLength = ( 13 | expectedMaxLength: number, 14 | name: string, 15 | buffer: Buffer, 16 | ): Result => { 17 | ensureNum(expectedMaxLength) 18 | 19 | return buffer.length > expectedMaxLength 20 | ? err( 21 | new Error( 22 | `Incorrect length of ${name}, expected max: #${expectedMaxLength} bytes, but got: #${buffer.length}.`, 23 | ), 24 | ) 25 | : ok(buffer) 26 | } 27 | 28 | export const validateMinLength = ( 29 | expectedMinLength: number, 30 | name: string, 31 | buffer: Buffer, 32 | ): Result => { 33 | ensureNum(expectedMinLength) 34 | return buffer.length < expectedMinLength 35 | ? err( 36 | new Error( 37 | `Incorrect length of ${name}, expected min: #${expectedMinLength} bytes, but got: #${buffer.length}.`, 38 | ), 39 | ) 40 | : ok(buffer) 41 | } 42 | 43 | export const validateLength = ( 44 | expectedLength: number, 45 | name: string, 46 | buffer: Buffer, 47 | ): Result => { 48 | ensureNum(expectedLength) 49 | return buffer.length !== expectedLength 50 | ? err( 51 | new Error( 52 | `Incorrect length of ${name}, expected: #${expectedLength} bytes, but got: #${buffer.length}.`, 53 | ), 54 | ) 55 | : ok(buffer) 56 | } 57 | 58 | export const toPrivateKeyHex = function (scalar: UInt256) { 59 | return [...new Uint8Array(scalar.buffer!)] 60 | .reverse() 61 | .map(x => x.toString(16).padStart(2, '0')) 62 | .join(''); 63 | } 64 | -------------------------------------------------------------------------------- /packages/crypto/test/aesGCM.test.ts: -------------------------------------------------------------------------------- 1 | import { secureRandomGenerator } from '@radixdlt/util' 2 | import { AES_GCM } from '../src' 3 | 4 | describe('aes gcm', () => { 5 | it('should decrypt message from Swift lib', () => { 6 | const decrypted = AES_GCM.open({ 7 | authTag: Buffer.from('baa17313cf02d4f34d135c34643426c8', 'hex'), 8 | ciphertext: Buffer.from( 9 | 'd2d309e9d7c9c5f5ca692b3e51e4f84670e8ee3dfce4d183389e6fcea2f46a707101e38a6aff1754e57c533b6bea0f620d5bac85ec9a2f86352111e2fc2879c839b6ae0d931c30364d5245bcad69', 10 | 'hex', 11 | ), 12 | nonce: Buffer.from('bf9e592c50183db30afff24d', 'hex'), 13 | symmetricKey: Buffer.from( 14 | 'fbe8c9f0dcbdbf52ee3038b18ca378255c35583bae12eb50151d7458d43dc3e1', 15 | 'hex', 16 | ), 17 | additionalAuthenticationData: Buffer.from( 18 | '03B7F98F3FB16527A8F779C326A7A57261F1341EAC191011F7A916D01D668F4549', 19 | 'hex', 20 | ), 21 | })._unsafeUnwrap() 22 | 23 | const plaintext = decrypted.toString('utf-8') 24 | expect(plaintext).toBe( 25 | `Guten tag Joe! My nukes are 100 miles south west of Münich, don't tell anyone`, 26 | ) 27 | }) 28 | 29 | it('should be able to decrypt what was just encrypted', () => { 30 | const plaintext = 'Hey TypeScript lib, this is TypeScript lib!' 31 | const symmetricKey = Buffer.from( 32 | secureRandomGenerator.randomSecureBytes(32), 33 | 'hex', 34 | ) 35 | const sealedBox = AES_GCM.seal({ 36 | plaintext: Buffer.from(plaintext), 37 | symmetricKey, 38 | })._unsafeUnwrap() 39 | 40 | const decrypted = AES_GCM.open({ 41 | ...sealedBox, 42 | symmetricKey, 43 | })._unsafeUnwrap() 44 | 45 | expect(decrypted.toString('utf-8')).toBe(plaintext) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /packages/crypto/test/bip44.test.ts: -------------------------------------------------------------------------------- 1 | import { BIP44, BIP44T, HDPathRadix, HDPathRadixT } from '../src' 2 | 3 | describe('BIP44', () => { 4 | it('bip44 can create one for radix', () => { 5 | const hdPath = BIP44.create({ address: { index: 1337 } }) 6 | expect(hdPath.toString()).toBe(`m/44'/1022'/0'/0/1337'`) 7 | }) 8 | 9 | it('bip44 can be created from a string', () => { 10 | const doTestBIP44Path = (hdPath: BIP44T | HDPathRadixT): void => { 11 | expect(hdPath.toString()).toBe(path) 12 | 13 | // Check 'purpose' component 14 | const purpose = hdPath.purpose 15 | expect(purpose.name).toBe('purpose') 16 | expect(purpose.level).toBe(1) 17 | expect(purpose.isHardened).toBe(true) 18 | expect(purpose.index.toString(16)).toBe('8000002c') // 0x2c = 44 dec 19 | 20 | // Check 'coin type' component 21 | const coinType = hdPath.coinType 22 | expect(coinType.name).toBe('coin type') 23 | expect(coinType.level).toBe(2) 24 | expect(coinType.isHardened).toBe(true) 25 | expect(coinType.index.toString(16)).toBe('800003fe') // 0x218 = 1022 dec 26 | 27 | // Check 'account' component 28 | const account = hdPath.account 29 | expect(account.name).toBe('account') 30 | expect(account.level).toBe(3) 31 | expect(account.isHardened).toBe(true) 32 | expect(account.index.toString(16)).toBe('80000000') 33 | 34 | // Check 'change' component 35 | const change = hdPath.change 36 | expect(change.name).toBe('change') 37 | expect(change.level).toBe(4) 38 | expect(change.isHardened).toBe(false) 39 | expect(change.index.toString(16)).toBe('1') 40 | 41 | // Check 'address' component 42 | const addressIndex = hdPath.addressIndex 43 | expect(addressIndex.name).toBe('address index') 44 | expect(addressIndex.level).toBe(5) 45 | expect(addressIndex.isHardened).toBe(false) 46 | expect(addressIndex.index.toString(16)).toBe('0') 47 | } 48 | 49 | const path = `m/44'/1022'/0'/1/0` 50 | doTestBIP44Path(BIP44.fromString(path)._unsafeUnwrap()) 51 | doTestBIP44Path(HDPathRadix.fromString(path)._unsafeUnwrap()) 52 | }) 53 | 54 | it('bip44 path with non zeros', done => { 55 | const pathString = `m/44'/1022'/2'/1/3'` 56 | HDPathRadix.fromString(pathString).match( 57 | path => { 58 | expect(path.toString()).toBe(pathString) 59 | done() 60 | }, 61 | error => { 62 | done(error) 63 | }, 64 | ) 65 | }) 66 | 67 | it('should not be able to specify wrong coin type for radix path', () => { 68 | HDPathRadix.fromString(`m/44'/123'/0'/1/0`).match( 69 | () => { 70 | throw Error('expected error, but got none') 71 | }, 72 | f => 73 | expect(f.message).toBe( 74 | 'Incorrect coin type, expected Radix coin type: 1022, but got: 123', 75 | ), 76 | ) 77 | }) 78 | }) 79 | -------------------------------------------------------------------------------- /packages/crypto/test/diffiehellman.test.ts: -------------------------------------------------------------------------------- 1 | import { KeyPair } from '../src' 2 | 3 | describe('diffiehellman', () => { 4 | it('works between two', async () => { 5 | const alice = KeyPair.generateNew() 6 | const bob = KeyPair.generateNew() 7 | 8 | const dhAB = ( 9 | await alice.privateKey.diffieHellman(bob.publicKey) 10 | )._unsafeUnwrap() 11 | const dhBA = ( 12 | await bob.privateKey.diffieHellman(alice.publicKey) 13 | )._unsafeUnwrap() 14 | 15 | expect(dhAB.equals(dhBA)).toBe(true) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /packages/crypto/test/hash.test.ts: -------------------------------------------------------------------------------- 1 | import { Hasher, sha256 } from '../src' 2 | import { sha256Twice } from '../src/hash/sha' 3 | 4 | const testHash = (testVector: { 5 | hasher: Hasher 6 | input: Buffer 7 | expected: string 8 | }): void => { 9 | const digest = testVector.hasher(testVector.input) 10 | const calculated = digest.toString('hex') 11 | const expected = testVector.expected 12 | expect(calculated).toBe(expected) 13 | } 14 | 15 | const testHashText = (testVector: { 16 | hasher: Hasher 17 | plainText: string 18 | expected: string 19 | }): void => { 20 | const plainText = testVector.plainText 21 | const input = Buffer.from(plainText, 'utf-8') 22 | testHash({ 23 | ...testVector, 24 | input, 25 | }) 26 | } 27 | 28 | describe('hashing', () => { 29 | // https://www.di-mgt.com.au/sha_testvectors.html 30 | it('can produce sha256 digests', () => { 31 | testHashText({ 32 | hasher: sha256, 33 | plainText: 'abc', 34 | expected: 35 | 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad', 36 | }) 37 | }) 38 | 39 | it('can produce sha256 twice digests', () => { 40 | testHashText({ 41 | hasher: sha256Twice, 42 | plainText: 'hello', 43 | expected: 44 | '9595c9df90075148eb06860365df33584b75bff782a510c6cd4883a419833d50', 45 | }) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /packages/crypto/test/keystore.test.ts: -------------------------------------------------------------------------------- 1 | import { msgFromError, secureRandomGenerator } from '@radixdlt/util' 2 | import { Keystore, KeystoreT } from '../src' 3 | 4 | describe('keystore', () => { 5 | it('should be able to decrypt recently encrypted', async () => { 6 | const secret = 'my super secret phrase' 7 | const password = secureRandomGenerator.randomSecureBytes(20) 8 | const keystoreResult = await Keystore.encryptSecret({ 9 | secret: Buffer.from(secret), 10 | password, 11 | }) 12 | const keystore = keystoreResult._unsafeUnwrap() 13 | 14 | const decryptedResult = await Keystore.decrypt({ 15 | keystore, 16 | password, 17 | }) 18 | 19 | const decrypted = decryptedResult._unsafeUnwrap() 20 | expect(decrypted.toString('utf-8')).toBe(secret) 21 | }) 22 | 23 | it('should be able to decrypt saved keystore', async () => { 24 | const keystore: KeystoreT = { 25 | crypto: { 26 | cipher: 'aes-256-gcm', 27 | cipherparams: { 28 | nonce: '196932fcfa7c0b8061a698b3', 29 | }, 30 | ciphertext: 31 | '49ce243c72077f8b7cbfbb878b2d3f78d192c93adf0e2d07772cbd32dc1b5cd44fad93d4dacdae2ccf4685', 32 | kdf: 'scrypt', 33 | kdfparams: { 34 | costParameterN: 8192, 35 | costParameterC: 262144, 36 | blockSize: 8, 37 | parallelizationParameter: 1, 38 | lengthOfDerivedKey: 32, 39 | salt: 40 | '250dd310370eb0b571d6abce37cce6996edcedee9790e0e864132bce9c4174d1', 41 | }, 42 | mac: 'a0d3461c9ef61d2df0af1bbbf18d994e', 43 | }, 44 | id: 'bfdb15cd-c0e9-4fd0-8dc8-c488068dba79', 45 | version: 1, 46 | } 47 | 48 | const decryptedResult = await Keystore.decrypt({ 49 | keystore, 50 | password: 'strong random generated password', 51 | }) 52 | 53 | const decrypted = decryptedResult._unsafeUnwrap() 54 | expect(decrypted.toString('utf-8')).toBe( 55 | 'My Bitcoins are burried underneath the oak.', 56 | ) 57 | }) 58 | 59 | describe('keystore_fail_scenarios', () => { 60 | beforeAll(() => { 61 | jest.spyOn(console, 'error').mockImplementation(() => {}) 62 | }) 63 | 64 | afterAll(() => { 65 | jest.clearAllMocks() 66 | }) 67 | 68 | it('wrong password, no crash', async () => { 69 | const mnemonicPhrase = 70 | 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about' 71 | 72 | const password = 'my_strong_unique_password' 73 | const keystoreResult = await Keystore.encryptSecret({ 74 | secret: Buffer.from(mnemonicPhrase, 'utf-8'), 75 | password, 76 | }) 77 | const keystore = keystoreResult._unsafeUnwrap() 78 | 79 | const wrongPassword = `${password}1234` 80 | const decryptedResult = await Keystore.decrypt({ 81 | keystore, 82 | password: wrongPassword, 83 | }) 84 | 85 | decryptedResult.match( 86 | _ => { 87 | throw new Error('Decrypted keystore, but expected error.') 88 | }, 89 | e => { 90 | const errMsg = msgFromError(e) 91 | expect(errMsg).toBe( 92 | `Failed to decrypt keystore, wrong password? Underlying error: 'AES decryption failed.'.`, 93 | ) 94 | }, 95 | ) 96 | }) 97 | }) 98 | }) 99 | -------------------------------------------------------------------------------- /packages/crypto/test/scrypt.test.ts: -------------------------------------------------------------------------------- 1 | import { Scrypt, ScryptParams } from '../src/key-derivation-functions/scrypt' 2 | import { ScryptParamsT } from '../src/key-derivation-functions/_types' 3 | 4 | describe('scrypt', () => { 5 | it('returns underlying error', async done => { 6 | const scryptParams: ScryptParamsT = ScryptParams.create({}) 7 | const passwordString = 'my super secret password' 8 | const passwordBuffer = Buffer.from(passwordString) 9 | 10 | await Scrypt.deriveKey({ 11 | password: passwordBuffer, 12 | kdf: 'scrypt', 13 | params: { 14 | ...scryptParams, 15 | costParameterN: 3, // not a multiple of 2, will cause error 16 | }, 17 | }).match( 18 | k => done(new Error('Expected error but got none.')), 19 | e => { 20 | const expectedError = `Failed to derive data using scrypt, underlying error: 'N must be power of 2'` 21 | expect(e.message).toBe(expectedError) 22 | done() 23 | }, 24 | ) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /packages/crypto/test/utils.ts: -------------------------------------------------------------------------------- 1 | import { UInt256 } from '@radixdlt/uint256' 2 | import { SignatureT } from '../' 3 | 4 | export const signatureFromHexStrings = (input: { 5 | r: string 6 | s: string 7 | }): SignatureT => { 8 | const r = new UInt256(input.r, 16) 9 | const s = new UInt256(input.s, 16) 10 | return { 11 | r, 12 | s, 13 | toDER: () => 'not_impl', 14 | equals: (other: SignatureT): boolean => r.eq(other.r) && s.eq(other.s), 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/crypto/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "allowJs": true, 7 | }, 8 | "include": [ 9 | "src" 10 | ], 11 | "references": [ 12 | {"path": "../util"}, 13 | {"path": "../primitives"}, 14 | {"path": "../data-formats"}, 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/crypto/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.test.json", 3 | "include": [ 4 | "src", 5 | "test" 6 | ], 7 | "references": [ 8 | {"path": "../primitives"}, 9 | {"path": "../util"}, 10 | {"path": "../data-formats"} 11 | ] 12 | } -------------------------------------------------------------------------------- /packages/data-formats/README.md: -------------------------------------------------------------------------------- 1 | # `@radixdlt/data-formats` 2 | 3 | ## Usage 4 | ### JSON Decoding 5 | 6 | 7 | #### Examples 8 | 9 | Without dependencies, using provided taggedStringDecoder: 10 | 11 | ```typescript 12 | import { JSONDecoding, taggedStringDecoder } from '@radixdlt/data-formats' 13 | 14 | const strTagDecoder = taggedStringDecoder(':str:')((value) => ok(value)) 15 | 16 | const { fromJSON } = JSONDecoding.withDecoders(strTagDecoder).create() 17 | 18 | fromJSON(':str:xyz') // Ok('xyz') 19 | ``` 20 | 21 | An object with dependencies: 22 | 23 | ```typescript 24 | import { JSONDecoding, taggedStringDecoder } from '@radixdlt/data-formats' 25 | import { ok } from 'neverthrow' 26 | 27 | const strTagDecoder = taggedStringDecoder(':str:')((value) => ok(value)) 28 | 29 | const Object1 = { 30 | ...JSONDecoding.withDecoders(strTagDecoder).create() 31 | } 32 | 33 | const tstTagDecoder = taggedStringDecoder(':tst:')((value) => ok(value)) 34 | 35 | const { fromJSON } = JSONDecoding 36 | .withDependencies(Object1) 37 | .withDecoders(testTagDecoder) 38 | .create() 39 | 40 | fromJSON({ 41 | a: ':str:foo', 42 | b: ':tst:bar' 43 | }) // ok({ a: 'foo', b: 'bar' }) 44 | ``` 45 | 46 | JSON decoding takes an object and applies `decoder`s to each key-value pair. `taggedObjectDecoder` and `taggedStringDecoder` are provided, but you can easily define a new decoder. Here is how `taggedStringDecoder` is defined: 47 | 48 | ```typescript 49 | import { decoder } from '@radixdlt/data-formats' 50 | 51 | export const taggedStringDecoder = (tag: string) => ( 52 | algorithm: (value: string) => Result, 53 | ): Decoder => 54 | decoder((value) => 55 | isString(value) && `:${value.split(':')[1]}:` === tag 56 | ? algorithm(value.slice(tag.length)) 57 | : undefined, 58 | ) 59 | ``` 60 | 61 | A `decoder` should supply a function that defines how the decoding should be applied. First it should do some validation logic (does this decoder apply to this value?), in this case checking if the value is a string and if has a matching tag. Then, apply some `algorithm` function, which is the actual decoding (create an instance of some object). If the validation fails, the decoder has to return `undefined`. 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /packages/data-formats/customTypes/cbor.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'cbor' { 2 | import { CBOREncodableObject, CBOREncodablePrimitive } from '../src/_types' 3 | 4 | type Encoder = { 5 | new (options: EncoderOptions): CBOREncoder 6 | } 7 | type EncoderOptions = { 8 | highWaterMark: number 9 | collapseBigIntegers: boolean 10 | } 11 | type CBOREncoder = { 12 | _encodeAll: ( 13 | data: (CBOREncodablePrimitive | CBOREncodableObject)[], 14 | ) => Buffer 15 | /* eslint-disable @typescript-eslint/no-explicit-any */ 16 | addSemanticType: ( 17 | type: any, 18 | fn: (encoder: CBOREncoder, obj: any) => boolean, 19 | ) => undefined 20 | /* eslint-enable @typescript-eslint/no-explicit-any */ 21 | pushAny: ( 22 | any: 23 | | CBOREncodablePrimitive 24 | | CBOREncodableObject 25 | | CBOREncodableObject[], 26 | ) => boolean 27 | push: (chunk: Buffer) => boolean 28 | } 29 | export const Encoder: Encoder 30 | } 31 | -------------------------------------------------------------------------------- /packages/data-formats/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@radixdlt/data-formats", 3 | "version": "2.1.8", 4 | "description": "Data formats used for serialization.", 5 | "keywords": [ 6 | "Data" 7 | ], 8 | "author": "Alexander Cyon ", 9 | "homepage": "https://github.com/radixdlt/radixdlt-javascript/tree/master/packages/data-formats#readme", 10 | "license": "Apache-2.0", 11 | "main": "dist/index.js", 12 | "publishConfig": { 13 | "access": "public" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/radixdlt/radixdlt-javascript.git" 18 | }, 19 | "scripts": { 20 | "test": "echo \"Error: run tests from root\" && exit 1" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/radixdlt/radixdlt-javascript/issues" 24 | }, 25 | "engines": { 26 | "node": ">=15.5.0 <16" 27 | }, 28 | "dependencies": { 29 | "@radixdlt/util": "1.0.29", 30 | "neverthrow": "^4.0.1", 31 | "ramda": "^0.27.1" 32 | }, 33 | "devDependencies": { 34 | "@types/ramda": "^0.27.38" 35 | }, 36 | "gitHead": "f8026eb0c310402ba395025e6a544f7c6baffd66" 37 | } 38 | -------------------------------------------------------------------------------- /packages/data-formats/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './json' 2 | -------------------------------------------------------------------------------- /packages/data-formats/src/json/_types.ts: -------------------------------------------------------------------------------- 1 | import { Result } from 'neverthrow' 2 | 3 | export type Decoder = ( 4 | value: unknown, 5 | key?: string, 6 | ) => Result | undefined 7 | -------------------------------------------------------------------------------- /packages/data-formats/src/json/decoding.ts: -------------------------------------------------------------------------------- 1 | import { combine, err, ok, Result } from 'neverthrow' 2 | import { mapObjIndexed, pipe } from 'ramda' 3 | import { 4 | isObject, 5 | isString, 6 | flattenResultsObject, 7 | isArray, 8 | isBoolean, 9 | isNumber, 10 | isResult, 11 | } from '../../../util' 12 | import { Decoder } from './_types' 13 | 14 | /** 15 | * Creates a new decoder. A decoder defines a way to transform a key-value pair through a 16 | * supplied algorithm. 17 | */ 18 | export const decoder = ( 19 | algorithm: (value: unknown, key?: string) => Result | undefined, 20 | ): Decoder => (value: unknown, key?: string) => algorithm(value, key) 21 | 22 | const applyDecoders = ( 23 | decoders: Decoder[], 24 | value: unknown, 25 | key?: string, 26 | ): Result => { 27 | let unwrappedValue: unknown 28 | 29 | if (isResult(value)) { 30 | if (value.isOk()) { 31 | unwrappedValue = value.value 32 | } else { 33 | return value 34 | } 35 | } else { 36 | unwrappedValue = value 37 | } 38 | 39 | const results = decoders 40 | .map(decoder => decoder(unwrappedValue, key)) 41 | .filter(result => result !== undefined) 42 | 43 | return results.length > 1 44 | ? err( 45 | Error( 46 | `JSON decoding failed. Several decoders were valid for key/value pair. 47 | This can lead to unexpected behavior.`, 48 | ), 49 | ) 50 | : results[0] 51 | ? results[0] 52 | : ok(unwrappedValue) 53 | } 54 | 55 | const JSONDecode = (...decoders: Decoder[]) => ( 56 | json: Input, 57 | ): Result => { 58 | const decode = JSONDecodeUnflattened(...decoders) 59 | 60 | return pipe( 61 | //applyDecoders.bind(null, decoders), 62 | flattenResultsObject, 63 | )(decode(json)) as Result 64 | } 65 | 66 | /** 67 | * Main decoding logic. Uses the registered decoders and applies matching decoders to 68 | * all key-value pairs in the supplied JSON. 69 | */ 70 | const JSONDecodeUnflattened = (...decoders: Decoder[]) => ( 71 | json: unknown, 72 | ): Result => 73 | isObject(json) 74 | ? flattenResultsObject( 75 | ok( 76 | mapObjIndexed( 77 | (value, key) => 78 | applyDecoders( 79 | decoders, 80 | JSONDecodeUnflattened(...decoders)(value), 81 | key, 82 | ), 83 | json, 84 | ), 85 | ), 86 | ) 87 | : isString(json) || isBoolean(json) || isNumber(json) 88 | ? applyDecoders(decoders, json).mapErr(err => [err]) 89 | : isArray(json) 90 | ? combine( 91 | json.map(item => JSONDecodeUnflattened(...decoders)(item)), 92 | ).mapErr(err => err) 93 | : err([Error('JSON decoding failed. Unknown data type.')]) 94 | 95 | const withDecoders = (...decoders: Decoder[]) => ({ 96 | create: () => JSONDecode(...decoders), 97 | }) 98 | 99 | export const JSONDecoding = { 100 | withDecoders, 101 | create: () => JSONDecode(...[]), 102 | } 103 | -------------------------------------------------------------------------------- /packages/data-formats/src/json/index.ts: -------------------------------------------------------------------------------- 1 | export * from './_types' 2 | export * from './decoding' 3 | -------------------------------------------------------------------------------- /packages/data-formats/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "typeRoots": ["customTypes"] 7 | }, 8 | "include": [ 9 | "src/", 10 | "customTypes" 11 | ], 12 | "references": [ 13 | {"path": "../util"}, 14 | ] 15 | } -------------------------------------------------------------------------------- /packages/data-formats/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.test.json", 3 | "include": [ 4 | "src", 5 | "test" 6 | ], 7 | "references": [ 8 | {"path": "../util"}, 9 | ] 10 | } -------------------------------------------------------------------------------- /packages/hardware-ledger/README.md: -------------------------------------------------------------------------------- 1 | # `@radixdlt/hardware-wallet` 2 | 3 | > TODO: description 4 | 5 | ## Usage 6 | 7 | ``` 8 | const radixHardwareWallet = require('@radixdlt/hardware-wallet') 9 | 10 | // TODO: DEMONSTRATE API 11 | ``` 12 | -------------------------------------------------------------------------------- /packages/hardware-ledger/jest.config.ledger.js: -------------------------------------------------------------------------------- 1 | const sharedConfig = require('../../jest.config.base') 2 | module.exports = { 3 | ...sharedConfig, 4 | testMatch: [ 5 | '/packages/hardware-ledger/test/physical-devices/?(*.)+(spec|test).integration.ts', 6 | ], 7 | } 8 | -------------------------------------------------------------------------------- /packages/hardware-ledger/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@radixdlt/hardware-ledger", 3 | "version": "2.1.40", 4 | "description": "Ledger Nano hardware wallet connection", 5 | "keywords": [ 6 | "Ledger", 7 | "Hardware", 8 | "Wallet", 9 | "Nano" 10 | ], 11 | "author": "Alexander Cyon ", 12 | "homepage": "https://github.com/radixdlt/radixdlt-javascript/tree/master/packages/hardware-ledger#readme", 13 | "license": "Apache-2.0", 14 | "main": "dist/index.js", 15 | "publishConfig": { 16 | "access": "public" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/radixdlt/radixdlt-javascript.git" 21 | }, 22 | "scripts": { 23 | "test": "echo \"Error: run tests from root\" && exit 1" 24 | }, 25 | "bugs": { 26 | "url": "https://github.com/radixdlt/radixdlt-javascript/issues" 27 | }, 28 | "engines": { 29 | "node": ">=15.5.0 <16" 30 | }, 31 | "dependencies": { 32 | "@ledgerhq/hw-transport-node-hid": "^6.2.0", 33 | "@radixdlt/crypto": "^2.1.17", 34 | "@radixdlt/hardware-wallet": "^2.1.20", 35 | "@radixdlt/primitives": "^3.0.9", 36 | "@radixdlt/tx-parser": "^2.1.23", 37 | "@radixdlt/util": "1.0.29", 38 | "neverthrow": "4.0.1", 39 | "prelude-ts": "^1.0.2", 40 | "rxjs": "7.0.0", 41 | "uuid": "^8.3.2" 42 | }, 43 | "devDependencies": { 44 | "@radixdlt/account": "^3.0.14", 45 | "@radixdlt/application": "^3.0.7", 46 | "@types/uuid": "^8.3.0" 47 | }, 48 | "gitHead": "f8026eb0c310402ba395025e6a544f7c6baffd66" 49 | } 50 | -------------------------------------------------------------------------------- /packages/hardware-ledger/src/_types.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs' 2 | 3 | export enum LedgerInstruction { 4 | GET_VERSION = 0x03, 5 | GET_APP_NAME = 0x04, 6 | GET_PUBLIC_KEY = 0x05, 7 | DO_SIGN_TX = 0x06, 8 | DO_SIGN_HASH = 0x07, 9 | DO_KEY_EXCHANGE = 0x08, 10 | } 11 | 12 | /// Keep in sync with: https://github.com/radixdlt/app-radix/blob/main/src/sw.h 13 | export enum LedgerResponseCodes { 14 | ERR_CMD_SIGN_TX_UNSUPPORTED_INSTRUCTION_TYPE = 0xc608, 15 | 16 | ERR_COMMON_BAD_STATE = 0xe001, 17 | ERR_ASSERTION_FAILED = 0xe002, 18 | ERR_FATAL_ERROR = 0xe003, 19 | 20 | SW_DENY = 0x6985, 21 | SW_WRONG_P1P2 = 0x6a86, 22 | SW_WRONG_DATA_LENGTH = 0x6a87, 23 | SW_INS_NOT_SUPPORTED = 0x6d00, 24 | SW_CLA_NOT_SUPPORTED = 0x6e00, 25 | 26 | SW_OK = 0x9000, 27 | } 28 | 29 | export const prettifyLedgerResponseCode = (code: LedgerResponseCodes): string => 30 | `${code === LedgerResponseCodes.SW_OK ? '✅' : '❌'} code: '${ 31 | LedgerResponseCodes[code] 32 | }' 0x${code.toString(16)} (0d${code.toString(10)})` 33 | 34 | export type CreateLedgerNanoTransportInput = Readonly<{ 35 | openTimeout?: number 36 | listenTimeout?: number 37 | }> 38 | 39 | export const radixCLA: number = 0xaa 40 | 41 | export type APDUT = Readonly<{ 42 | // (type: 'number') Always to '0xAA' 43 | cla: number 44 | ins: number 45 | 46 | // Will default to `0` if undefined 47 | p1: number 48 | 49 | // Should not be present if `p1` is 'undefined'. Will default to `0` if undefined 50 | p2: number 51 | 52 | // defaults to zero length buffer 53 | data?: Buffer 54 | 55 | // defaults to: `[SW_OK]` 56 | requiredResponseStatusCodeFromDevice: LedgerResponseCodes[] 57 | }> 58 | 59 | export type PartialAPDUT = Omit< 60 | APDUT, 61 | 'p1' | 'p2' | 'requiredResponseStatusCodeFromDevice' 62 | > & 63 | Readonly<{ 64 | p1?: number 65 | 66 | // Should not be present if `p1` is 'undefined'. Will default to `0` if undefined 67 | p2?: number 68 | 69 | // defaults to: `[SW_OK]` 70 | requiredResponseStatusCodeFromDevice?: LedgerResponseCodes[] 71 | }> 72 | 73 | export type RadixAPDUT = APDUT & 74 | Readonly<{ 75 | cla: typeof radixCLA 76 | ins: LedgerInstruction 77 | }> 78 | 79 | export type LedgerNanoT = Readonly<{ 80 | sendAPDUToDevice: (apdu: RadixAPDUT) => Observable 81 | }> 82 | 83 | export type Device = { 84 | send: ( 85 | cla: number, 86 | ins: number, 87 | p1: number, 88 | p2: number, 89 | data?: Buffer, 90 | statusList?: ReadonlyArray, 91 | ) => Promise 92 | device: { 93 | getDeviceInfo: () => void 94 | } 95 | } 96 | 97 | export type ConnectionEvent = { 98 | type: 'add' | 'remove' 99 | descriptor: string 100 | deviceModel: string 101 | device: any 102 | } 103 | -------------------------------------------------------------------------------- /packages/hardware-ledger/src/device-connection.ts: -------------------------------------------------------------------------------- 1 | import { log } from '@radixdlt/util' 2 | import { LedgerResponseCodes, RadixAPDUT } from './_types' 3 | 4 | export type BasicLedgerTransport = Readonly<{ 5 | send: ( 6 | cla: number, 7 | ins: number, 8 | p1: number, 9 | p2: number, 10 | data?: Buffer, 11 | statusList?: ReadonlyArray, 12 | ) => Promise 13 | }> 14 | 15 | export const send = async ( 16 | input: Readonly<{ 17 | apdu: RadixAPDUT 18 | with: BasicLedgerTransport 19 | }>, 20 | ): Promise => { 21 | const { apdu, with: connectedLedgerTransport } = input 22 | 23 | const acceptableStatusCodes = apdu.requiredResponseStatusCodeFromDevice ?? [ 24 | LedgerResponseCodes.SW_OK, 25 | ] 26 | 27 | const statusList = [...acceptableStatusCodes.map(s => s.valueOf())] 28 | 29 | log.debug(`📦📲 sending APDU to Ledger device: 30 | instruction: ${apdu.ins}, 31 | p1: ${apdu.p1}, 32 | p2: ${apdu.p2}, 33 | data: ${apdu.data !== undefined ? apdu.data.toString('hex') : ''}, 34 | `) 35 | 36 | return connectedLedgerTransport.send( 37 | apdu.cla, 38 | apdu.ins, 39 | apdu.p1, 40 | apdu.p2, 41 | apdu.data, 42 | statusList, 43 | ) 44 | } 45 | 46 | export type OpenLedgerConnectionInput = Readonly<{ 47 | deviceConnectionTimeout?: number 48 | radixAppToOpenWaitPolicy?: Readonly<{ 49 | retryCount: number 50 | delayBetweenRetries: number 51 | }> 52 | }> 53 | -------------------------------------------------------------------------------- /packages/hardware-ledger/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './_types' 2 | 3 | export * from './apdu' 4 | export * from './device-connection' 5 | export * from './hardwareWalletFromLedger' 6 | export * from './ledgerNano' 7 | -------------------------------------------------------------------------------- /packages/hardware-ledger/test/physical-devices/signingKeychain.test.integration.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @group integration 3 | */ 4 | 5 | /* eslint-disable */ 6 | 7 | import { log } from '@radixdlt/util' 8 | import { Observable, Subscription } from 'rxjs' 9 | import { SigningKeychain, SigningKeychainT } from '@radixdlt/account' 10 | import { Mnemonic } from '@radixdlt/crypto' 11 | import { DeriveHWSigningKeyInput } from '@radixdlt/account' 12 | import { HardwareWalletT } from '@radixdlt/hardware-wallet' 13 | import { HWSigningKeyDerivation } from '@radixdlt/account' 14 | import { HardwareWalletLedger } from '../../src' 15 | import { sendAPDU } from './utils' 16 | 17 | describe('signingKeychain_hw_ledger', () => { 18 | beforeAll(() => { 19 | log.setLevel('debug') 20 | }) 21 | 22 | const makeKeychain = (): SigningKeychainT => { 23 | return SigningKeychain.create({ 24 | mnemonic: Mnemonic.generateNew(), // not used 25 | }) 26 | } 27 | 28 | let keychain: SigningKeychainT 29 | 30 | beforeEach(() => { 31 | keychain = makeKeychain() 32 | }) 33 | 34 | afterAll(() => { 35 | log.setLevel('warn') 36 | }) 37 | 38 | it('deriveHWSigningKey', async done => { 39 | const subs = new Subscription() 40 | 41 | const keyDerivation: HWSigningKeyDerivation = 'next' 42 | const hardwareWalletConnection: Observable = HardwareWalletLedger.create( 43 | { 44 | send: sendAPDU, 45 | }, 46 | ) 47 | 48 | const input: DeriveHWSigningKeyInput = { 49 | keyDerivation, 50 | hardwareWalletConnection, 51 | alsoSwitchTo: true, 52 | } 53 | 54 | subs.add( 55 | keychain.deriveHWSigningKey(input).subscribe({ 56 | next: sk => { 57 | expect(sk.hdPath!.toString()).toBe(`m/44'/1022'/0'/0/0'`) 58 | 59 | const publicKeyCompressedHex = sk.publicKey.toString(true) 60 | if ( 61 | publicKeyCompressedHex === 62 | '03bc2ec8f3668c869577bf66b7b48f8dee57b833916aa70966fa4a5029b63bb18f' 63 | ) { 64 | done( 65 | new Error( 66 | `Implementation discrepancy between C code for Ledger Nano and this JS Lib. We, here in JS land, expected Ledger Nano app to respect the of hardening the 5th component 'addressIndex' if we explicitly state that (in 'signinKeychain.ts' method: 'deriveHWSigningKey' for input 'next'). But Ledger nano seems to ignore that input, because we got the publickey for the BIP32 path: "m/44'/1022'/0'/0/0" instead of "m/44'/1022'/0'/0/0'", i.e. HARDENING of address index got ignored.`, 67 | ), 68 | ) 69 | return 70 | } 71 | 72 | expect(publicKeyCompressedHex).toBe( 73 | '03f43fba6541031ef2195f5ba96677354d28147e45b40cde4662bec9162c361f55', 74 | ) 75 | done() 76 | }, 77 | error: e => done(e), 78 | }), 79 | ) 80 | }, 20_000) 81 | }) 82 | -------------------------------------------------------------------------------- /packages/hardware-ledger/test/physical-devices/utils.ts: -------------------------------------------------------------------------------- 1 | import TransportNodeHid from '@ledgerhq/hw-transport-node-hid' 2 | 3 | export const sendAPDU = async ( 4 | cla: number, 5 | ins: number, 6 | p1: number, 7 | p2: number, 8 | data?: Buffer, 9 | statusList?: readonly number[], 10 | ) => { 11 | const devices = await TransportNodeHid.list() 12 | if (!devices[0]) { 13 | throw new Error('No device found.') 14 | } 15 | 16 | const transport = await TransportNodeHid.create() 17 | const result = await transport.send(cla, ins, p1, p2, data) 18 | transport.close() 19 | return result 20 | } 21 | -------------------------------------------------------------------------------- /packages/hardware-ledger/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | }, 7 | "include": [ 8 | "src" 9 | ], 10 | "types": ["ledgerhq__hw-transport-node-hid"], 11 | "references": [ 12 | {"path": "../crypto"}, 13 | {"path": "../primitives"}, 14 | {"path": "../hardware-wallet"}, 15 | {"path": "../util"}, 16 | {"path": "../tx-parser"}, 17 | ] 18 | } -------------------------------------------------------------------------------- /packages/hardware-ledger/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.test.json", 3 | "include": [ 4 | "src", 5 | "test" 6 | ], 7 | "references": [ 8 | {"path": "../crypto"}, 9 | {"path": "../primitives"}, 10 | {"path": "../util"}, 11 | {"path": "../hardware-wallet"}, 12 | {"path": "../account"}, 13 | {"path": "../application"}, 14 | {"path": "../tx-parser"}, 15 | ] 16 | } -------------------------------------------------------------------------------- /packages/hardware-wallet/README.md: -------------------------------------------------------------------------------- 1 | # `@radixdlt/hardware-wallet` 2 | 3 | > TODO: description 4 | 5 | ## Usage 6 | 7 | ``` 8 | const radixHardwareWallet = require('@radixdlt/hardware-wallet') 9 | 10 | // TODO: DEMONSTRATE API 11 | ``` 12 | -------------------------------------------------------------------------------- /packages/hardware-wallet/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@radixdlt/hardware-wallet", 3 | "version": "2.1.20", 4 | "description": "Types for hardware wallets", 5 | "keywords": [ 6 | "Hardware", 7 | "Wallet" 8 | ], 9 | "author": "Alexander Cyon ", 10 | "homepage": "https://github.com/radixdlt/radixdlt-javascript/tree/master/packages/hardware-wallet#readme", 11 | "license": "Apache-2.0", 12 | "main": "dist/index.js", 13 | "publishConfig": { 14 | "access": "public" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/radixdlt/radixdlt-javascript.git" 19 | }, 20 | "scripts": { 21 | "test": "echo \"Error: run tests from root\" && exit 1" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/radixdlt/radixdlt-javascript/issues" 25 | }, 26 | "engines": { 27 | "node": ">=15.5.0 <16" 28 | }, 29 | "dependencies": { 30 | "@radixdlt/crypto": "^2.1.17", 31 | "@radixdlt/primitives": "^3.0.9", 32 | "@radixdlt/util": "1.0.29", 33 | "neverthrow": "4.0.1", 34 | "prelude-ts": "^1.0.2", 35 | "rxjs": "7.0.0", 36 | "uuid": "^8.3.2" 37 | }, 38 | "devDependencies": { 39 | "@types/uuid": "^8.3.0" 40 | }, 41 | "gitHead": "f8026eb0c310402ba395025e6a544f7c6baffd66" 42 | } 43 | -------------------------------------------------------------------------------- /packages/hardware-wallet/src/_types.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs' 2 | import { 3 | ECPointOnCurveT, 4 | HDPathRadixT, 5 | PublicKeyT, 6 | SignatureT, 7 | } from '@radixdlt/crypto' 8 | import { BuiltTransactionReadyToSign, Network } from '@radixdlt/primitives' 9 | 10 | // Semantic versioning, e.g. 1.0.5 11 | export type SemVerT = Readonly<{ 12 | major: number 13 | minor: number 14 | patch: number 15 | 16 | equals: (other: SemVerT) => boolean 17 | 18 | // '{major}.{minor}.{patch}' 19 | toString: () => string 20 | }> 21 | 22 | export type AtPath = Readonly<{ 23 | // defaults to: `m/44'/1022'/0'/0/0` 24 | path?: HDPathRadixT 25 | }> 26 | 27 | export type GetPublicKeyInput = AtPath & 28 | Readonly<{ 29 | display?: boolean 30 | /// Only relevant if `display` is true, this skips showing BIP32 Path on display. 31 | verifyAddressOnly?: boolean 32 | }> 33 | 34 | export type SignTXOutput = Readonly<{ 35 | signature: SignatureT 36 | signatureV: number 37 | hashCalculatedByLedger: Buffer 38 | }> 39 | 40 | export type SignHashInput = GetPublicKeyInput & 41 | Readonly<{ 42 | hashToSign: Buffer 43 | }> 44 | 45 | export type KeyExchangeInput = AtPath & 46 | Readonly<{ 47 | publicKeyOfOtherParty: PublicKeyT 48 | display?: 'encrypt' | 'decrypt' 49 | }> 50 | 51 | export type HardwareSigningKeyT = Readonly<{ 52 | keyExchange: ( 53 | publicKeyOfOtherParty: PublicKeyT, 54 | display?: 'encrypt' | 'decrypt', 55 | ) => Observable 56 | publicKey: PublicKeyT 57 | 58 | // Like property `publicKey` but a function and omits BIP32 path on HW display 59 | getPublicKeyDisplayOnlyAddress: () => Observable 60 | 61 | signHash: (hashedMessage: Buffer) => Observable 62 | sign: ( 63 | tx: BuiltTransactionReadyToSign, 64 | nonXrdHRP?: string, 65 | ) => Observable 66 | }> 67 | 68 | export type SignTransactionInput = Readonly<{ 69 | tx: BuiltTransactionReadyToSign 70 | path: HDPathRadixT 71 | nonXrdHRP?: string 72 | }> 73 | 74 | export type HardwareWalletT = Readonly<{ 75 | getVersion: () => Observable 76 | getPublicKey: (input: GetPublicKeyInput) => Observable 77 | doSignHash: (input: SignHashInput) => Observable 78 | doSignTransaction: (input: SignTransactionInput) => Observable 79 | doKeyExchange: (input: KeyExchangeInput) => Observable 80 | 81 | makeSigningKey: ( 82 | path: HDPathRadixT, 83 | verificationPrompt?: boolean, 84 | ) => Observable 85 | }> 86 | -------------------------------------------------------------------------------- /packages/hardware-wallet/src/hardwareWallet.ts: -------------------------------------------------------------------------------- 1 | import { HardwareSigningKeyT, HardwareWalletT, SignHashInput } from './_types' 2 | import { Observable } from 'rxjs' 3 | import { 4 | ECPointOnCurveT, 5 | HDPathRadix, 6 | HDPathRadixT, 7 | PublicKeyT, 8 | SignatureT, 9 | } from '@radixdlt/crypto' 10 | import { map } from 'rxjs/operators' 11 | import { BuiltTransactionReadyToSign } from '@radixdlt/primitives' 12 | 13 | export const path000H = HDPathRadix.create({ 14 | address: { index: 0, isHardened: true }, 15 | }) 16 | 17 | export type HardwareWalletWithoutSK = Omit 18 | 19 | export const signingKeyWithHardWareWallet = ( 20 | hardwareWallet: HardwareWalletWithoutSK, 21 | path: HDPathRadixT, 22 | verificationPrompt: boolean = true, 23 | ): Observable => 24 | hardwareWallet 25 | .getPublicKey({ 26 | path, 27 | display: verificationPrompt, 28 | // display BIP32 path as part of derivation call. 29 | verifyAddressOnly: false, 30 | }) 31 | .pipe( 32 | map((publicKey: PublicKeyT) => ({ 33 | publicKey, 34 | getPublicKeyDisplayOnlyAddress: (): Observable => { 35 | return hardwareWallet.getPublicKey({ 36 | path, 37 | display: true, 38 | // Omits BIP32 path screen on Ledger. 39 | verifyAddressOnly: true, 40 | }) 41 | }, 42 | signHash: (hashedMessage: Buffer): Observable => 43 | hardwareWallet.doSignHash({ 44 | hashToSign: hashedMessage, 45 | path, 46 | }), 47 | sign: ( 48 | tx: BuiltTransactionReadyToSign, 49 | nonXrdHRP?: string, 50 | ): Observable => 51 | hardwareWallet 52 | .doSignTransaction({ tx, path, nonXrdHRP }) 53 | .pipe(map(o => o.signature)), 54 | keyExchange: ( 55 | publicKeyOfOtherParty: PublicKeyT, 56 | display?: 'encrypt' | 'decrypt', 57 | ): Observable => 58 | hardwareWallet.doKeyExchange({ 59 | display, 60 | path, 61 | publicKeyOfOtherParty, 62 | }), 63 | })), 64 | ) 65 | -------------------------------------------------------------------------------- /packages/hardware-wallet/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hardwareWallet' 2 | export * from './_types' 3 | export * from './semVer' 4 | -------------------------------------------------------------------------------- /packages/hardware-wallet/src/semVer.ts: -------------------------------------------------------------------------------- 1 | import { combine, err, ok, Result } from 'neverthrow' 2 | import { SemVerT } from './_types' 3 | 4 | const separator = '.' 5 | 6 | const create = ( 7 | input: Readonly<{ major: number; minor: number; patch: number }>, 8 | ): SemVerT => { 9 | const { major, minor, patch } = input 10 | const toString = (): string => 11 | [major, minor, patch] 12 | .map((n: number): string => n.toString()) 13 | .join(separator) 14 | 15 | const equals = (other: SemVerT): boolean => 16 | other.major === major && other.minor === minor && other.patch === patch 17 | 18 | return { 19 | major, 20 | minor, 21 | patch, 22 | equals, 23 | toString, 24 | } 25 | } 26 | 27 | const fromBuffer = (buf: Buffer): Result => { 28 | const expectedByteCount = 3 29 | if (buf.length !== expectedByteCount) { 30 | return err( 31 | new Error( 32 | `Incorrect length of buffer, expected #${expectedByteCount} bytes, but got: #${buf.length}`, 33 | ), 34 | ) 35 | } 36 | 37 | const major = buf.readUInt8(0) 38 | const minor = buf.readUInt8(1) 39 | const patch = buf.readUInt8(2) 40 | 41 | return ok(create({ major, minor, patch })) 42 | } 43 | 44 | const fromString = (versionString: string): Result => { 45 | const components = versionString.split(separator) 46 | const expectedComponentCount = 3 47 | if (components.length !== expectedComponentCount) { 48 | return err( 49 | new Error( 50 | `Expected semantic version to contain ${expectedComponentCount} components.`, 51 | ), 52 | ) 53 | } 54 | const numAtIndex = (index: number): Result => { 55 | let parsedInt = undefined 56 | try { 57 | parsedInt = parseInt(components[index], 10) 58 | } catch (e) { 59 | return err(new Error('Failed to parse integer')) 60 | } 61 | if (!Number.isInteger(parsedInt)) { 62 | return err(new Error('Found no integer')) 63 | } 64 | return ok(parsedInt) 65 | } 66 | 67 | return combine([numAtIndex(0), numAtIndex(1), numAtIndex(2)]).map( 68 | resultList => { 69 | const major = resultList[0] 70 | const minor = resultList[1] 71 | const patch = resultList[2] 72 | return create({ 73 | major, 74 | minor, 75 | patch, 76 | }) 77 | }, 78 | ) 79 | } 80 | 81 | export const SemVer = { 82 | fromBuffer, 83 | fromString, 84 | create, 85 | } 86 | -------------------------------------------------------------------------------- /packages/hardware-wallet/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | }, 7 | "include": [ 8 | "src" 9 | ], 10 | "references": [ 11 | {"path": "../crypto"}, 12 | {"path": "../primitives"}, 13 | {"path": "../util"}, 14 | ] 15 | } -------------------------------------------------------------------------------- /packages/hardware-wallet/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.test.json", 3 | "include": [ 4 | "src", 5 | "test" 6 | ], 7 | "references": [ 8 | {"path": "../crypto"}, 9 | {"path": "../primitives"}, 10 | {"path": "../util"}, 11 | {"path": "../account"}, 12 | ] 13 | } -------------------------------------------------------------------------------- /packages/networking/README.md: -------------------------------------------------------------------------------- 1 | # `@radixdlt/networking` 2 | 3 | > TODO: description 4 | 5 | ## Usage 6 | 7 | ``` 8 | const radixNetworking = require('@radixdlt/networking') 9 | 10 | // TODO: DEMONSTRATE API 11 | ``` 12 | -------------------------------------------------------------------------------- /packages/networking/extract-api-version.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | const text = fs.readFileSync( 5 | path.join(__dirname, 'src', 'open-api', 'api.ts'), 6 | { encoding: 'utf-8' }, 7 | ) 8 | const version = text.match(/The version of the OpenAPI document: (\d.\d.\d)/)[1] 9 | 10 | fs.writeFileSync( 11 | path.join(__dirname, 'src', 'open-api', 'api-version.ts'), 12 | `export const apiVersion = '${version}'`, 13 | ) 14 | -------------------------------------------------------------------------------- /packages/networking/generate-open-api.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | SCHEMA_PATH=$1 6 | 7 | if [ -z "$1" ] 8 | then 9 | echo "Missing open api schema path" 10 | exit 1 11 | fi 12 | 13 | npx @openapitools/openapi-generator-cli generate \ 14 | -i $SCHEMA_PATH \ 15 | -g typescript-axios \ 16 | -o src/open-api 17 | 18 | node ./extract-api-version.js -------------------------------------------------------------------------------- /packages/networking/openapitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json", 3 | "spaces": 2, 4 | "generator-cli": { 5 | "version": "5.3.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/networking/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@radixdlt/networking", 3 | "version": "2.1.18", 4 | "description": "Requesting and sending data from/to Radix Core RPC API over HTTP(s).", 5 | "keywords": [ 6 | "RPCClient" 7 | ], 8 | "author": "Alexander Cyon ", 9 | "homepage": "https://github.com/radixdlt/radixdlt-javascript/tree/master/packages/networking#readme", 10 | "license": "Apache-2.0", 11 | "main": "dist/index.js", 12 | "publishConfig": { 13 | "access": "public" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/radixdlt/radixdlt-javascript.git" 18 | }, 19 | "scripts": { 20 | "test": "echo \"Error: run tests from root\" && exit 1" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/radixdlt/radixdlt-javascript/issues" 24 | }, 25 | "engines": { 26 | "node": ">=15.5.0 <16" 27 | }, 28 | "dependencies": { 29 | "@open-rpc/client-js": "1.6.2", 30 | "@open-rpc/schema-utils-js": "1.14.3", 31 | "@radixdlt/open-rpc-spec": "^1.0.19", 32 | "@radixdlt/util": "1.0.29", 33 | "@types/uuid": "^8.3.0", 34 | "axios": "^0.24.0", 35 | "isomorphic-fetch": "^3.0.0", 36 | "neverthrow": "^4.0.1", 37 | "open-rpc-utils": "^1.1.1", 38 | "uuid": "^8.3.2" 39 | }, 40 | "devDependencies": { 41 | "@types/isomorphic-fetch": "^0.0.35", 42 | "axios-mock-adapter": "^1.20.0" 43 | }, 44 | "gitHead": "f8026eb0c310402ba395025e6a544f7c6baffd66" 45 | } 46 | -------------------------------------------------------------------------------- /packages/networking/src/_types.ts: -------------------------------------------------------------------------------- 1 | import { ResultAsync } from 'neverthrow' 2 | import { OpenApiClientCall } from './open-api-client' 3 | import { OpenRPCClientCall } from './open-rpc-client' 4 | 5 | type TransportType = 'json-rpc' | 'open-api' 6 | 7 | export type Call = ( 8 | method: Methods, 9 | param: Params, 10 | headers?: Record, 11 | ) => ResultAsync 12 | 13 | export type Transport = { 14 | type: T 15 | call: T extends 'open-api' ? OpenApiClientCall : OpenRPCClientCall 16 | } 17 | 18 | export type Client = (url: URL) => Transport 19 | -------------------------------------------------------------------------------- /packages/networking/src/global-fetch.d.ts: -------------------------------------------------------------------------------- 1 | declare type GlobalFetch = WindowOrWorkerGlobalScope 2 | -------------------------------------------------------------------------------- /packages/networking/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './_types' 2 | export * from './open-rpc-client' 3 | export * from './open-api/api' 4 | export * from './open-api-client' 5 | export * from './open-api/api-version' 6 | -------------------------------------------------------------------------------- /packages/networking/src/open-api-client.ts: -------------------------------------------------------------------------------- 1 | import 'isomorphic-fetch' 2 | import { log } from '../../util' 3 | import { v4 as uuid } from 'uuid' 4 | import { Client } from './_types' 5 | import { err, ok, ResultAsync } from 'neverthrow' 6 | import { pipe } from 'ramda' 7 | import { TransactionBuildResponse } from './open-api/api' 8 | import { 9 | apiVersion, 10 | AccountApiFactory, 11 | ValidatorApiFactory, 12 | TransactionApiFactory, 13 | TokenApiFactory, 14 | StatusApiFactory, 15 | } from '.' 16 | import { AxiosResponse, AxiosError } from 'axios' 17 | import { Configuration } from './open-api' 18 | 19 | const defaultHeaders = [ 20 | 'X-Radixdlt-Method', 21 | 'X-Radixdlt-Correlation-Id', 22 | 'X-Radixdlt-Target-Gw-Api', 23 | ] 24 | 25 | const correlationID = uuid() 26 | 27 | export type ReturnOfAPICall< 28 | Name extends MethodName 29 | > = Name extends 'transactionBuildPost' 30 | ? AxiosResponse 31 | : Awaited> 32 | 33 | export type InputOfAPICall = Parameters< 34 | ClientInterface[Name] 35 | >[0] 36 | 37 | export type ClientInterface = ReturnType & 38 | ReturnType & 39 | ReturnType & 40 | ReturnType & 41 | ReturnType 42 | 43 | export type MethodName = keyof ClientInterface 44 | export type Response = ReturnOfAPICall 45 | 46 | const handleError = (error: AxiosError) => { 47 | log.debug(error) 48 | if (error.isAxiosError && error.response?.data) { 49 | return err({ 50 | code: error.response.data.code ?? error.response.status, 51 | ...(typeof error.response.data === 'object' 52 | ? error.response.data 53 | : { message: error.response.data }), 54 | }) 55 | } else { 56 | return err({ message: error.message }) 57 | } 58 | } 59 | 60 | const call = (client: ClientInterface) => ( 61 | method: M, 62 | params: InputOfAPICall, 63 | headers?: Record, 64 | ): ResultAsync, Error> => 65 | // @ts-ignore 66 | pipe( 67 | () => 68 | log.info( 69 | `Sending api request with method ${method}. ${JSON.stringify( 70 | params, 71 | null, 72 | 2, 73 | )}`, 74 | ), 75 | () => 76 | ResultAsync.fromPromise( 77 | // @ts-ignore 78 | client[method](params, { 79 | headers: { 80 | [defaultHeaders[0]]: method, 81 | [defaultHeaders[1]]: correlationID, 82 | [defaultHeaders[2]]: apiVersion, 83 | ...headers, 84 | }, 85 | }).then(response => { 86 | log.info( 87 | `Response from api with method ${method}`, 88 | JSON.stringify(response.data, null, 2), 89 | ) 90 | 91 | return response 92 | }), 93 | // @ts-ignore 94 | handleError, 95 | ), 96 | )() 97 | 98 | export type OpenApiClientCall = ReturnType 99 | 100 | export const openApiClient: Client<'open-api'> = (url: URL) => { 101 | const configuration = new Configuration({ 102 | basePath: url.toString().slice(0, -1), 103 | }) 104 | const api = [ 105 | AccountApiFactory, 106 | ValidatorApiFactory, 107 | TransactionApiFactory, 108 | TokenApiFactory, 109 | StatusApiFactory, 110 | ].reduce( 111 | (acc, factory) => ({ 112 | ...acc, 113 | ...factory(configuration), 114 | }), 115 | {} as ClientInterface, 116 | ) 117 | 118 | return { 119 | type: 'open-api', 120 | call: call(api), 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /packages/networking/src/open-api/.gitignore: -------------------------------------------------------------------------------- 1 | wwwroot/*.js 2 | node_modules 3 | typings 4 | dist 5 | -------------------------------------------------------------------------------- /packages/networking/src/open-api/.npmignore: -------------------------------------------------------------------------------- 1 | # empty npmignore to ensure all required files (e.g., in the dist folder) are published by npm -------------------------------------------------------------------------------- /packages/networking/src/open-api/.openapi-generator-ignore: -------------------------------------------------------------------------------- 1 | # OpenAPI Generator Ignore 2 | # Generated by openapi-generator https://github.com/openapitools/openapi-generator 3 | 4 | # Use this file to prevent files from being overwritten by the generator. 5 | # The patterns follow closely to .gitignore or .dockerignore. 6 | 7 | # As an example, the C# client generator defines ApiClient.cs. 8 | # You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: 9 | #ApiClient.cs 10 | 11 | # You can match any string of characters against a directory, file or extension with a single asterisk (*): 12 | #foo/*/qux 13 | # The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux 14 | 15 | # You can recursively match patterns against a directory, file or extension with a double asterisk (**): 16 | #foo/**/qux 17 | # This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux 18 | 19 | # You can also negate patterns with an exclamation (!). 20 | # For example, you can ignore all files in a docs folder with the file extension .md: 21 | #docs/*.md 22 | # Then explicitly reverse the ignore rule for a single file: 23 | #!docs/README.md 24 | -------------------------------------------------------------------------------- /packages/networking/src/open-api/.openapi-generator/FILES: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .npmignore 3 | api.ts 4 | base.ts 5 | common.ts 6 | configuration.ts 7 | git_push.sh 8 | index.ts 9 | -------------------------------------------------------------------------------- /packages/networking/src/open-api/.openapi-generator/VERSION: -------------------------------------------------------------------------------- 1 | 5.3.0 -------------------------------------------------------------------------------- /packages/networking/src/open-api/api-version.ts: -------------------------------------------------------------------------------- 1 | export const apiVersion = '1.1.6' 2 | -------------------------------------------------------------------------------- /packages/networking/src/open-api/git_push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ 3 | # 4 | # Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com" 5 | 6 | git_user_id=$1 7 | git_repo_id=$2 8 | release_note=$3 9 | git_host=$4 10 | 11 | if [ "$git_host" = "" ]; then 12 | git_host="github.com" 13 | echo "[INFO] No command line input provided. Set \$git_host to $git_host" 14 | fi 15 | 16 | if [ "$git_user_id" = "" ]; then 17 | git_user_id="GIT_USER_ID" 18 | echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id" 19 | fi 20 | 21 | if [ "$git_repo_id" = "" ]; then 22 | git_repo_id="GIT_REPO_ID" 23 | echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id" 24 | fi 25 | 26 | if [ "$release_note" = "" ]; then 27 | release_note="Minor update" 28 | echo "[INFO] No command line input provided. Set \$release_note to $release_note" 29 | fi 30 | 31 | # Initialize the local directory as a Git repository 32 | git init 33 | 34 | # Adds the files in the local repository and stages them for commit. 35 | git add . 36 | 37 | # Commits the tracked changes and prepares them to be pushed to a remote repository. 38 | git commit -m "$release_note" 39 | 40 | # Sets the new remote 41 | git_remote=$(git remote) 42 | if [ "$git_remote" = "" ]; then # git remote not defined 43 | 44 | if [ "$GIT_TOKEN" = "" ]; then 45 | echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment." 46 | git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git 47 | else 48 | git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git 49 | fi 50 | 51 | fi 52 | 53 | git pull origin master 54 | 55 | # Pushes (Forces) the changes in the local repository up to the remote repository 56 | echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git" 57 | git push origin master 2>&1 | grep -v 'To https' 58 | -------------------------------------------------------------------------------- /packages/networking/src/open-api/index.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * Radix Gateway API 5 | * This API is designed to enable clients to efficiently query information on the RadixDLT ledger, and allow clients to simply build and submit transactions to the network. The API is designed for use by the Radix Foundation\'s [Desktop Wallet](https://wallet.radixdlt.com/) and [Explorer](https://explorer.radixdlt.com/), and replaces the original Olympia \"Archive Node API\". # Gateway API Overview The Gateway API is separated into distinct groupings: * `/gateway` - Information about the Gateway API status * `/account/_*` - To query account-related information * `/token/_*` - To query token-related information * `/validator/_*` and `/validators` - To query validator-related information * `/transaction/_*` - To build, finalize and submit transactions, and to read the status and content of submitted and on-ledger transactions. The Gateway API is implemented by the [Network Gateway](https://github.com/radixdlt/radixdlt-network-gateway), which is configured to read from full node/s to extract and index data from the network. # Gateway API Format The API is designed in a JSON-RPC style, using HTTP as a transport layer, which means that: * All requests are POST requests. * Any error is returned with an HTTP status code of 500, with a returned error object. * The error object contains an HTTP-like `code` * The error object also contains a structured/typed `details` sub-object, with a `type` discriminator, allowing for structured error interpretation in clients. # Comparison to other Radix APIs * [Core API](https://redocly.github.io/redoc/?url=https://raw.githubusercontent.com/radixdlt/radixdlt/main/radixdlt-core/radixdlt/src/main/java/com/radixdlt/api/core/api.yaml) - The Core API is a low level API exposed by full nodes, and designed for use on internal networks. It is primarily designed for network integrations such as exchanges, ledger analytics providers, or hosted ledger data dashboards. The Core API provides endpoints for reading the mempool, constructing transactions and also exposes a stream of committed transactions. * [System API](https://redocly.github.io/redoc/?url=https://raw.githubusercontent.com/radixdlt/radixdlt/main/radixdlt-core/radixdlt/src/main/java/com/radixdlt/api/system/api.yaml) - The System API is a private API exposed by full nodes to read system status. The Gateway API offers a much wider range of query options and is more performant than the Core API. It is built on top of the Core API, ingesting data via the Core API transaction stream into a relational database. The Gateway API transaction/construction endpoints operate with the concept of \"actions\" - these are higher-levels of intent compared with the Core API, which makes it easier for clients to use. The Core API should be used if you require more power/flexiblity for managing UTXOs, or submitting transactions which can\'t be mapped to a Gateway API action. 6 | * 7 | * The version of the OpenAPI document: 1.1.6 8 | * 9 | * 10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 11 | * https://openapi-generator.tech 12 | * Do not edit the class manually. 13 | */ 14 | 15 | export * from './api' 16 | export * from './configuration' 17 | -------------------------------------------------------------------------------- /packages/networking/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "typeRoots": ["./src/global-fetch.d.ts"] 7 | }, 8 | "include": [ 9 | "src", 10 | "src/open-rpc-spec.json" 11 | ], 12 | "references": [] 13 | } -------------------------------------------------------------------------------- /packages/networking/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.test.json", 3 | "include": [ 4 | "src", 5 | "test" 6 | ], 7 | "references": [], 8 | "compilerOptions": {"typeRoots": ["./src/global-fetch.d.ts"]} 9 | } 10 | -------------------------------------------------------------------------------- /packages/primitives/README.md: -------------------------------------------------------------------------------- 1 | # `@radixdlt/primitives` 2 | 3 | > TODO: description 4 | 5 | ## Usage 6 | 7 | ``` 8 | const radixPrimitives = require('@radixdlt/primitives') 9 | 10 | // TODO: DEMONSTRATE API 11 | ``` 12 | -------------------------------------------------------------------------------- /packages/primitives/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@radixdlt/primitives", 3 | "version": "3.0.9", 4 | "description": "Shared common data types", 5 | "keywords": [ 6 | "Integers", 7 | "Nonce", 8 | "Base58", 9 | "Encoding" 10 | ], 11 | "author": "Alexander Cyon ", 12 | "homepage": "https://github.com/radixdlt/radixdlt-javascript/tree/master/packages/primitives#readme", 13 | "license": "Apache-2.0", 14 | "main": "dist/index.js", 15 | "publishConfig": { 16 | "access": "public" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/radixdlt/radixdlt-javascript.git" 21 | }, 22 | "scripts": { 23 | "test": "echo \"Error: run tests from root\" && exit 1" 24 | }, 25 | "bugs": { 26 | "url": "https://github.com/radixdlt/radixdlt-javascript/issues" 27 | }, 28 | "engines": { 29 | "node": ">=15.5.0 <16" 30 | }, 31 | "dependencies": { 32 | "@radixdlt/data-formats": "1.0.30", 33 | "@radixdlt/uint256": "1.1.0", 34 | "@radixdlt/util": "1.0.29", 35 | "bn.js": "^5.1.3", 36 | "long": "^4.0.0", 37 | "neverthrow": "^4.0.1" 38 | }, 39 | "devDependencies": { 40 | "@types/bn.js": "^4.11.6", 41 | "@types/long": "^4.0.1" 42 | }, 43 | "gitHead": "f8026eb0c310402ba395025e6a544f7c6baffd66" 44 | } 45 | -------------------------------------------------------------------------------- /packages/primitives/src/_types.ts: -------------------------------------------------------------------------------- 1 | import { UInt256 } from '@radixdlt/uint256' 2 | 3 | export type AmountT = UInt256 4 | 5 | export enum Network { 6 | MAINNET = 'mainnet', 7 | STOKENET = 'stokenet', 8 | LOCALNET = 'localnet', 9 | MILESTONENET = 'milestonenet', 10 | RELEASENET = 'releasenet', 11 | RCNET = 'rcnet', 12 | TESTNET6 = 'testnet6', 13 | SANDPITNET = 'sandpitnet', 14 | } 15 | 16 | export const NetworkId = { 17 | 1: Network.MAINNET, 18 | 2: Network.STOKENET, 19 | 3: Network.RELEASENET, 20 | 4: Network.RCNET, 21 | 5: Network.MILESTONENET, 22 | 6: Network.TESTNET6, 23 | 7: Network.SANDPITNET, 24 | 99: Network.LOCALNET, 25 | } 26 | 27 | export const hrpFullSuffixLength = 3 28 | 29 | export const HRP = { 30 | [Network.MAINNET]: { 31 | account: 'rdx', 32 | validator: 'rv', 33 | RRI_suffix: '_rr', 34 | }, 35 | [Network.STOKENET]: { 36 | account: 'tdx', 37 | validator: 'tv', 38 | RRI_suffix: '_tr', 39 | }, 40 | [Network.LOCALNET]: { 41 | account: 'ddx', 42 | validator: 'dv', 43 | RRI_suffix: '_dr', 44 | }, 45 | [Network.RELEASENET]: { 46 | account: 'tdx3', 47 | validator: 'tv3', 48 | RRI_suffix: '_tr3', 49 | }, 50 | [Network.RCNET]: { 51 | account: 'tdx4', 52 | validator: 'tv4', 53 | RRI_suffix: '_tr4', 54 | }, 55 | [Network.MILESTONENET]: { 56 | account: 'tdx5', 57 | validator: 'tv5', 58 | RRI_suffix: '_tr5', 59 | }, 60 | [Network.TESTNET6]: { 61 | account: 'tdx6', 62 | validator: 'tv6', 63 | RRI_suffix: '_tr6', 64 | }, 65 | [Network.SANDPITNET]: { 66 | account: 'tdx7', 67 | validator: 'tv7', 68 | RRI_suffix: '_tr7', 69 | }, 70 | } 71 | 72 | export type BuiltTransactionReadyToSign = Readonly<{ 73 | // Bytes on hex format 74 | blob: string 75 | hashOfBlobToSign: string 76 | }> 77 | -------------------------------------------------------------------------------- /packages/primitives/src/amount.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isUInt256, 3 | isUnsafeInputForUInt256, 4 | uint256FromUnsafe, 5 | UInt256InputUnsafe, 6 | } from './uint256-extensions' 7 | import { err, ok, Result } from 'neverthrow' 8 | import { AmountT } from './_types' 9 | import { UInt256 } from '@radixdlt/uint256' 10 | 11 | export type AmountUnsafeInput = UInt256InputUnsafe 12 | export const isAmountUnsafeInput = ( 13 | something: unknown, 14 | ): something is AmountUnsafeInput => isUnsafeInputForUInt256(something) 15 | 16 | export type AmountOrUnsafeInput = AmountT | AmountUnsafeInput 17 | 18 | export const isAmount = (something: unknown): something is AmountT => 19 | isUInt256(something) 20 | 21 | export const isAmountOrUnsafeInput = ( 22 | something: unknown, 23 | ): something is AmountOrUnsafeInput => 24 | isAmount(something) || isAmountUnsafeInput(something) 25 | 26 | const fromUnsafe = (input: AmountOrUnsafeInput): Result => 27 | isAmount(input) 28 | ? ok(input) 29 | : isAmountUnsafeInput(input) 30 | ? uint256FromUnsafe(input) 31 | : err( 32 | new Error( 33 | `Unable to construct 'AmountT' because of bad input: '${JSON.stringify( 34 | input, 35 | null, 36 | 4, 37 | )}'`, 38 | ), 39 | ) 40 | 41 | const isAmountMultipleOf = ( 42 | input: Readonly<{ 43 | amount: AmountT 44 | granularity: AmountT 45 | }>, 46 | ): boolean => { 47 | const { amount, granularity: other } = input 48 | const zero = UInt256.valueOf(0) 49 | return amount.mod(other, false).eq(zero) 50 | } 51 | 52 | export const Amount = { 53 | fromUnsafe, 54 | isAmountMultipleOf, 55 | } 56 | -------------------------------------------------------------------------------- /packages/primitives/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './_types' 2 | 3 | export * from './uint256-extensions' 4 | export * from './resultAsync' 5 | export * from './amount' 6 | -------------------------------------------------------------------------------- /packages/primitives/src/resultAsync.ts: -------------------------------------------------------------------------------- 1 | import { Result, ResultAsync, okAsync } from 'neverthrow' 2 | 3 | export const resultToAsync = (result: Result): ResultAsync => 4 | result.asyncAndThen(value => okAsync(value)) 5 | -------------------------------------------------------------------------------- /packages/primitives/src/uint256-extensions.ts: -------------------------------------------------------------------------------- 1 | import BN from 'bn.js' 2 | import { err, Result, ok } from 'neverthrow' 3 | import { UInt256 } from '@radixdlt/uint256' 4 | import { 5 | isNumberArray, 6 | SecureRandom, 7 | secureRandomGenerator, 8 | } from '@radixdlt/util' 9 | 10 | const bnUInt256Max: BN = new BN(2).pow(new BN(256)).sub(new BN(1)) 11 | 12 | export const fitsInUInt256 = (number: BN | number): boolean => { 13 | const bn = new BN(number) 14 | const isNotTooBig = bn.lte(bnUInt256Max) 15 | const isNonNegative = bn.gte(new BN(0)) 16 | return isNotTooBig && isNonNegative 17 | } 18 | 19 | /** 20 | * Converts a big number (BN) into a UInt256 21 | * 22 | * @param {BN} bn - A big number to be converted into a UInt256. 23 | * @returns {UInt256} A 256 bit wide unsigned integer. 24 | */ 25 | export const uint256FromBN = (bn: BN): Result => { 26 | if (!fitsInUInt256(bn)) { 27 | return err( 28 | new Error( 29 | `BN is either less than 0 or larger than 2^256 - 1, which does not fit in a UInt256.`, 30 | ), 31 | ) 32 | } 33 | return ok(new UInt256(bn.toString('hex'), 16)) 34 | } 35 | 36 | export type UInt256InputUnsafe = 37 | | number 38 | | string 39 | | number[] 40 | | Uint8Array 41 | | Buffer 42 | 43 | // eslint-disable-next-line complexity 44 | export const isUnsafeInputForUInt256 = ( 45 | something: unknown, 46 | ): something is UInt256InputUnsafe => { 47 | if (typeof something === 'number') { 48 | return true 49 | } else if (typeof something === 'string') { 50 | return true 51 | } else if (isNumberArray(something)) { 52 | return true 53 | } else if (something instanceof Uint8Array) { 54 | return true 55 | } else return something instanceof Buffer 56 | } 57 | 58 | export const uint256FromUnsafe = ( 59 | unsafe: UInt256InputUnsafe, 60 | ): Result => { 61 | // eslint-disable-next-line functional/no-try-statement 62 | try { 63 | const bn = new BN(unsafe) 64 | return uint256FromBN(bn) 65 | } catch (e) { 66 | return err(e as Error) 67 | } 68 | } 69 | 70 | export const bnFromUInt256 = (uint256: UInt256): BN => 71 | new BN(uint256.toString(16), 'hex') 72 | 73 | export const uint256Max = uint256FromBN(bnUInt256Max)._unsafeUnwrap() 74 | 75 | export const secureRandomUInt256 = ( 76 | secureRandom: SecureRandom = secureRandomGenerator, 77 | ): UInt256 => { 78 | const randomBytes = secureRandom.randomSecureBytes(32) 79 | return new UInt256(randomBytes, 16) 80 | } 81 | 82 | export const isUInt256 = (something: unknown): something is UInt256 => 83 | something instanceof UInt256 84 | -------------------------------------------------------------------------------- /packages/primitives/test/amount.test.ts: -------------------------------------------------------------------------------- 1 | import { UInt256 } from '@radixdlt/uint256' 2 | import { isUInt256 } from '../src' 3 | import { Amount } from '../src/amount' 4 | 5 | describe('Amount', () => { 6 | it('can check for multiple with granularity', () => { 7 | const granularity = Amount.fromUnsafe(1)._unsafeUnwrap() 8 | const amount = Amount.fromUnsafe(10)._unsafeUnwrap() 9 | 10 | expect(Amount.isAmountMultipleOf({ amount, granularity })).toBe(true) 11 | expect( 12 | Amount.isAmountMultipleOf({ 13 | amount: granularity, 14 | granularity: amount, 15 | }), 16 | ).toBe(false) 17 | }) 18 | 19 | it('toString of 1 amount', () => { 20 | const uOne = UInt256.valueOf(1) 21 | expect(uOne.toString()).toBe('1') 22 | const amount = Amount.fromUnsafe(1)._unsafeUnwrap() 23 | expect(amount.toString()).toBe('1') 24 | }) 25 | 26 | it('addition works as expected', () => { 27 | const one = Amount.fromUnsafe(1)._unsafeUnwrap() 28 | const two = Amount.fromUnsafe(2)._unsafeUnwrap() 29 | const three = Amount.fromUnsafe(3)._unsafeUnwrap() 30 | const res = one.add(two) 31 | expect(res.eq(three)).toBe(true) 32 | expect(res.toString()).toBe('3') 33 | }) 34 | 35 | it('can typeguard UInt256', () => { 36 | const uOne = UInt256.valueOf(1) 37 | expect(isUInt256(uOne)).toBe(true) 38 | expect(isUInt256(1)).toBe(false) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /packages/primitives/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist" 6 | }, 7 | "include": [ 8 | "src" 9 | ], 10 | "references": [ 11 | {"path": "../util"}, 12 | {"path": "../data-formats"} 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /packages/primitives/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.test.json", 3 | "include": [ 4 | "src", 5 | "test" 6 | ], 7 | "references": [ 8 | {"path": "../util"}, 9 | {"path": "../data-formats"} 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/tx-parser/README.md: -------------------------------------------------------------------------------- 1 | # `@radixdlt/tx-parser` 2 | 3 | > TODO: description 4 | 5 | ## Usage 6 | 7 | ``` 8 | const radixtx-parser = require('@radixdlt/tx-parser') 9 | 10 | // TODO: DEMONSTRATE API 11 | ``` 12 | -------------------------------------------------------------------------------- /packages/tx-parser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@radixdlt/tx-parser", 3 | "version": "2.1.23", 4 | "description": "Parsing of Radix Engine transactions and instructions", 5 | "keywords": [ 6 | "Radix Engine", 7 | "Transaction", 8 | "OP" 9 | ], 10 | "author": "Alexander Cyon ", 11 | "homepage": "https://github.com/radixdlt/radixdlt-javascript/tree/master/packages/tx-parser#readme", 12 | "license": "Apache-2.0", 13 | "main": "dist/index.js", 14 | "publishConfig": { 15 | "access": "public" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/radixdlt/radixdlt-javascript.git" 20 | }, 21 | "scripts": { 22 | "test": "echo \"Error: run tests from root\" && exit 1" 23 | }, 24 | "bugs": { 25 | "url": "https://github.com/radixdlt/radixdlt-javascript/issues" 26 | }, 27 | "engines": { 28 | "node": ">=15.5.0 <16" 29 | }, 30 | "dependencies": { 31 | "@radixdlt/account": "^3.0.14", 32 | "@radixdlt/crypto": "^2.1.17", 33 | "@radixdlt/primitives": "^3.0.9", 34 | "@radixdlt/uint256": "1.1.0", 35 | "@radixdlt/util": "1.0.29", 36 | "long": "^4.0.0", 37 | "neverthrow": "^4.0.1" 38 | }, 39 | "devDependencies": { 40 | "@types/bn.js": "^4.11.6", 41 | "@types/long": "^4.0.1" 42 | }, 43 | "gitHead": "f8026eb0c310402ba395025e6a544f7c6baffd66" 44 | } 45 | -------------------------------------------------------------------------------- /packages/tx-parser/src/bytes.ts: -------------------------------------------------------------------------------- 1 | import { Result } from 'neverthrow' 2 | import { BytesT } from './_types' 3 | import { BufferReaderT } from '@radixdlt/util' 4 | 5 | const LENGTH_BYTES = 2 6 | 7 | const fromBufferReader = (bufferReader: BufferReaderT): Result => 8 | bufferReader 9 | .readNextBuffer(LENGTH_BYTES) 10 | .map(b => b.readUInt16BE(0)) 11 | .andThen((length: number) => 12 | bufferReader.readNextBuffer(length).map(data => ({ 13 | length, 14 | data, 15 | })), 16 | ) 17 | .map(partial => { 18 | const lengthBuf = Buffer.alloc(LENGTH_BYTES) 19 | lengthBuf.writeUInt16BE(partial.length) 20 | 21 | const buffer = Buffer.concat([lengthBuf, partial.data]) 22 | 23 | return { 24 | ...partial, 25 | toBuffer: () => buffer, 26 | toString: () => `0x${partial.data.toString('hex')}`, 27 | } 28 | }) 29 | 30 | export const Bytes = { 31 | fromBufferReader, 32 | } 33 | -------------------------------------------------------------------------------- /packages/tx-parser/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './_types' 2 | 3 | export * from './transaction' 4 | export * from './instruction' 5 | export * from './tokens' 6 | -------------------------------------------------------------------------------- /packages/tx-parser/src/preparedStake.ts: -------------------------------------------------------------------------------- 1 | import { combine, Result } from 'neverthrow' 2 | import { 3 | BaseStakingSubstate, 4 | BaseValidatorSubstate, 5 | PreparedStakeT, 6 | REAddressT, 7 | stringifySubstateType, 8 | SubStateType, 9 | } from './_types' 10 | import { REAddress } from './reAddress' 11 | import { UInt256 } from '@radixdlt/uint256' 12 | import { PublicKey, PublicKeyT } from '@radixdlt/crypto' 13 | import { 14 | amountToBuffer, 15 | stringifyUInt256, 16 | uint256FromReadBuffer, 17 | } from './tokens' 18 | import { BufferReaderT, Byte } from '@radixdlt/util' 19 | import { AccountAddress, ValidatorAddress } from '@radixdlt/account' 20 | 21 | export const pubKeyFromReadBuffer = ( 22 | bufferReader: BufferReaderT, 23 | ): Result => 24 | bufferReader.readNextBuffer(33).andThen(b => PublicKey.fromBuffer(b)) 25 | 26 | export const makeBaseValidatorSubstateFromBuffer = ( 27 | substateType: SST, 28 | ) => ( 29 | bufferReader: BufferReaderT, 30 | ): Result, 'toString'>, Error> => 31 | combine([ 32 | bufferReader.readNextBuffer(1).map(b => b.readUInt8(0)), 33 | pubKeyFromReadBuffer(bufferReader), 34 | ]) 35 | .map(resList => ({ 36 | reserved: resList[0] as Byte, 37 | validator: resList[1] as PublicKeyT, 38 | })) 39 | .map( 40 | (partial): BaseValidatorSubstate => { 41 | const { reserved, validator } = partial 42 | const buffer = Buffer.concat([ 43 | Buffer.from([substateType]), 44 | Buffer.from([reserved]), 45 | validator.asData({ compressed: true }), 46 | ]) 47 | return { 48 | ...partial, 49 | substateType, 50 | toBuffer: () => buffer, 51 | } 52 | }, 53 | ) 54 | 55 | export const makeBaseStakeSubstateFromBuffer = ( 56 | substateType: SST, 57 | ) => ( 58 | bufferReader: BufferReaderT, 59 | lengthData: Buffer, 60 | ): Result, Error> => 61 | makeBaseValidatorSubstateFromBuffer(substateType)(bufferReader).andThen( 62 | base => 63 | combine([ 64 | REAddress.fromBufferReader(bufferReader), 65 | uint256FromReadBuffer(bufferReader), 66 | ]).map( 67 | (resList): BaseStakingSubstate => { 68 | const owner = resList[0] as REAddressT 69 | const amount = resList[1] as UInt256 70 | const reserved = base.reserved 71 | const validator = base.validator 72 | return { 73 | ...base, 74 | owner, 75 | amount, 76 | toBuffer: (): Buffer => 77 | Buffer.concat([ 78 | lengthData, 79 | base.toBuffer(), 80 | owner.toBuffer(), 81 | amountToBuffer(amount), 82 | ]), 83 | toString: () => 84 | `${stringifySubstateType( 85 | substateType, 86 | )} { reserved: ${reserved}, validator: 0x${validator.toString()}, owner: 0x${owner 87 | .toBuffer() 88 | .toString( 89 | 'hex', 90 | )}, amount: U256 { raw: ${amount.toString()} } }`, 91 | 92 | toHumanReadableString: () => 93 | `${stringifySubstateType( 94 | substateType, 95 | )} { reserved: ${reserved}, validator: ${ValidatorAddress.fromUnsafe( 96 | validator.asData({ compressed: true }), 97 | ) 98 | ._unsafeUnwrap() 99 | .toString()}, owner: ${AccountAddress.fromUnsafe( 100 | owner.toBuffer(), 101 | ) 102 | ._unsafeUnwrap() 103 | .toString()}, amount: ${stringifyUInt256( 104 | amount, 105 | )} }`, 106 | } 107 | }, 108 | ), 109 | ) 110 | 111 | export const PreparedStake = { 112 | fromBufferReader: ( 113 | bufferReader: BufferReaderT, 114 | lengthData: Buffer, 115 | ): Result => 116 | makeBaseStakeSubstateFromBuffer(SubStateType.PREPARED_STAKE)( 117 | bufferReader, 118 | lengthData, 119 | ), 120 | } 121 | -------------------------------------------------------------------------------- /packages/tx-parser/src/preparedUnstake.ts: -------------------------------------------------------------------------------- 1 | import { Result } from 'neverthrow' 2 | import { PreparedUnstakeT, SubStateType } from './_types' 3 | import { makeBaseStakeSubstateFromBuffer } from './preparedStake' 4 | import { BufferReaderT } from '@radixdlt/util' 5 | 6 | export const PreparedUnstake = { 7 | fromBufferReader: ( 8 | bufferReader: BufferReaderT, 9 | lengthData: Buffer, 10 | ): Result => 11 | makeBaseStakeSubstateFromBuffer(SubStateType.PREPARED_UNSTAKE)( 12 | bufferReader, 13 | lengthData, 14 | ), 15 | } 16 | -------------------------------------------------------------------------------- /packages/tx-parser/src/reAddress.ts: -------------------------------------------------------------------------------- 1 | import { 2 | REAddressHashedKeyNonce, 3 | REAddressNativeToken, 4 | REAddressPublicKey, 5 | REAddressSystem, 6 | REAddressT, 7 | REAddressType, 8 | } from './_types' 9 | import { ok, Result } from 'neverthrow' 10 | import { PublicKey, PublicKeyT } from '@radixdlt/crypto' 11 | import { BufferReaderT } from '@radixdlt/util' 12 | 13 | /* eslint-disable no-case-declarations */ 14 | const fromBufferReader = ( 15 | bufferReader: BufferReaderT, 16 | ): Result => 17 | bufferReader 18 | .readNextBuffer(1) 19 | .map(b => ({ 20 | reAddressTypeBuf: b, 21 | reAddressType: b.readUInt8(0) as REAddressType, 22 | })) 23 | .andThen( 24 | (aa): Result => { 25 | const { reAddressTypeBuf, reAddressType } = aa 26 | switch (reAddressType) { 27 | case REAddressType.SYSTEM: 28 | const systemAddress: REAddressSystem = { 29 | reAddressType, 30 | toBuffer: () => reAddressTypeBuf, 31 | toString: () => 32 | `REAddressType.SYSTEM (Always empty)`, 33 | } 34 | return ok(systemAddress) 35 | case REAddressType.RADIX_NATIVE_TOKEN: 36 | const nativeToken: REAddressNativeToken = { 37 | reAddressType, 38 | toBuffer: () => reAddressTypeBuf, 39 | toString: () => 40 | `REAddressType.RADIX_NATIVE_TOKEN (Always empty)`, 41 | } 42 | return ok(nativeToken) 43 | case REAddressType.HASHED_KEY_NONCE: 44 | return bufferReader.readNextBuffer(26).map( 45 | (lower26Bytes): REAddressHashedKeyNonce => ({ 46 | reAddressType, 47 | lower26Bytes, 48 | toBuffer: () => 49 | Buffer.concat([ 50 | reAddressTypeBuf, 51 | lower26Bytes, 52 | ]), 53 | toString: () => 54 | `REAddressType.HASHED_KEY_NONCE: { lower26Bytes: ${lower26Bytes.toString( 55 | 'hex', 56 | )} }`, 57 | }), 58 | ) 59 | case REAddressType.PUBLIC_KEY: 60 | return bufferReader 61 | .readNextBuffer(33) 62 | .andThen(pubKeyBytes => 63 | PublicKey.fromBuffer(pubKeyBytes), 64 | ) 65 | .map( 66 | ( 67 | publicKey: PublicKeyT, 68 | ): REAddressPublicKey => ({ 69 | reAddressType, 70 | publicKey, 71 | toBuffer: () => 72 | Buffer.concat([ 73 | reAddressTypeBuf, 74 | publicKey.asData({ 75 | compressed: true, 76 | }), 77 | ]), 78 | toString: () => 79 | `REAddressType.PUBLIC_KEY: { publicKey: ${publicKey.toString( 80 | true, 81 | )} }`, 82 | }), 83 | ) 84 | } 85 | }, 86 | ) 87 | /* eslint-enable no-case-declarations */ 88 | 89 | export const REAddress = { 90 | fromBufferReader, 91 | } 92 | -------------------------------------------------------------------------------- /packages/tx-parser/src/removeWhitespace.ts: -------------------------------------------------------------------------------- 1 | /// Removes LEADING tabs only 2 | // export const trimWS = (str: string): string => str.replace(`/^\t+/gm`, '') 3 | 4 | export const noTab = (str: string): string => str.replace(`/\t/g`, '') 5 | -------------------------------------------------------------------------------- /packages/tx-parser/src/stakeOwnership.ts: -------------------------------------------------------------------------------- 1 | import { Result } from 'neverthrow' 2 | import { SubStateType, StakeOwnershipT } from './_types' 3 | import { BufferReaderT } from '@radixdlt/util' 4 | import { makeBaseStakeSubstateFromBuffer } from './preparedStake' 5 | 6 | export const StakeOwnership = { 7 | fromBufferReader: ( 8 | bufferReader: BufferReaderT, 9 | lengthData: Buffer, 10 | ): Result => 11 | makeBaseStakeSubstateFromBuffer(SubStateType.STAKE_OWNERSHIP)( 12 | bufferReader, 13 | lengthData, 14 | ), 15 | } 16 | -------------------------------------------------------------------------------- /packages/tx-parser/src/substate.ts: -------------------------------------------------------------------------------- 1 | import { SubstateT, SubStateType } from './_types' 2 | import { Result } from 'neverthrow' 3 | import { Tokens } from './tokens' 4 | import { PreparedStake } from './preparedStake' 5 | import { PreparedUnstake } from './preparedUnstake' 6 | import { StakeOwnership } from './stakeOwnership' 7 | import { ValidatorAllowDelegationFlag } from './validatorAllowDelegationFlag' 8 | import { BufferReaderT } from '@radixdlt/util' 9 | import { ValidatorOwnerCopy } from './validatorOwnerCopy' 10 | 11 | const LENGTH_BYTES = 2 12 | const TYPE_BYTES = 1 13 | 14 | const parseFromBufferReader = ( 15 | bufferReader: BufferReaderT, 16 | ): Result => 17 | bufferReader 18 | .readNextBuffer(LENGTH_BYTES + TYPE_BYTES) 19 | .map(b => { 20 | const lengthData = Buffer.from(b.slice(0, LENGTH_BYTES)) 21 | return [b.readUInt8(LENGTH_BYTES), lengthData] 22 | }) 23 | .map( 24 | ([type, lengthData]) => 25 | [type, lengthData] as [SubStateType, Buffer], 26 | ) 27 | .andThen( 28 | ([substateType, lengthData]): Result => { 29 | switch (substateType) { 30 | case SubStateType.TOKENS: 31 | return Tokens.fromBufferReader(bufferReader, lengthData) 32 | case SubStateType.PREPARED_STAKE: 33 | return PreparedStake.fromBufferReader( 34 | bufferReader, 35 | lengthData, 36 | ) 37 | case SubStateType.PREPARED_UNSTAKE: 38 | return PreparedUnstake.fromBufferReader( 39 | bufferReader, 40 | lengthData, 41 | ) 42 | case SubStateType.STAKE_OWNERSHIP: 43 | return StakeOwnership.fromBufferReader( 44 | bufferReader, 45 | lengthData, 46 | ) 47 | case SubStateType.VALIDATOR_ALLOW_DELEGATION_FLAG: 48 | return ValidatorAllowDelegationFlag.fromBufferReader( 49 | bufferReader, 50 | lengthData, 51 | ) 52 | case SubStateType.VALIDATOR_OWNER_COPY: 53 | return ValidatorOwnerCopy.fromBufferReader( 54 | bufferReader, 55 | lengthData, 56 | ) 57 | default: 58 | throw new Error( 59 | `Substate ${substateType} of type: ${SubStateType[substateType]} not implemented.`, 60 | ) 61 | } 62 | }, 63 | ) 64 | 65 | export const Substate = { 66 | parseFromBufferReader, 67 | } 68 | -------------------------------------------------------------------------------- /packages/tx-parser/src/substateId.ts: -------------------------------------------------------------------------------- 1 | import { SubstateIdT } from './_types' 2 | import { combine, Result } from 'neverthrow' 3 | import { noTab } from './removeWhitespace' 4 | import { BufferReaderT } from '@radixdlt/util' 5 | 6 | const parseFromBufferReader = ( 7 | bufferReader: BufferReaderT, 8 | ): Result => 9 | combine([ 10 | bufferReader.readNextBuffer(32), 11 | bufferReader.readNextBuffer(4), 12 | ]).map(resList => { 13 | const hash = resList[0] 14 | const index = resList[1].readUInt32BE(0) 15 | 16 | const buffer = Buffer.concat([resList[0], resList[1]]) 17 | 18 | return { 19 | hash, 20 | index, 21 | toBuffer: () => buffer, 22 | toString: () => 23 | noTab( 24 | `SubstateId { hash: 0x${hash.toString( 25 | 'hex', 26 | )}, index: ${index} }`, 27 | ), 28 | } 29 | }) 30 | 31 | export const SubstateId = { 32 | parseFromBufferReader, 33 | } 34 | -------------------------------------------------------------------------------- /packages/tx-parser/src/tokens.ts: -------------------------------------------------------------------------------- 1 | import { combine, Result } from 'neverthrow' 2 | import { REAddressT, SubStateType, TokensT } from './_types' 3 | import { REAddress } from './reAddress' 4 | import { UInt256 } from '@radixdlt/uint256' 5 | import { BufferReaderT, Byte } from '@radixdlt/util' 6 | import { ResourceIdentifier, AccountAddress } from '@radixdlt/account' 7 | import BigNumber from 'bignumber.js' 8 | 9 | const uint256ByteCount = 32 10 | 11 | export const stringifyUInt256 = (uint256: UInt256) => { 12 | const factor = new BigNumber('1e18') 13 | const bigNumber = new BigNumber(uint256.toString()) 14 | const precision = 4 15 | return bigNumber.dividedToIntegerBy(factor).toFormat(precision) 16 | } 17 | 18 | export const uint256FromReadBuffer = ( 19 | bufferReader: BufferReaderT, 20 | ): Result => 21 | bufferReader 22 | .readNextBuffer(uint256ByteCount) 23 | .map(b => new UInt256(b.toString('hex'), 16)) 24 | 25 | export const amountToBuffer = (amount: UInt256): Buffer => 26 | Buffer.from(amount.toByteArray()).reverse() // fix endianess. 27 | 28 | const fromBufferReader = ( 29 | bufferReader: BufferReaderT, 30 | lengthData: Buffer, 31 | ): Result => 32 | combine([ 33 | bufferReader.readNextBuffer(1).map(b => b.readUInt8(0)), 34 | REAddress.fromBufferReader(bufferReader), 35 | REAddress.fromBufferReader(bufferReader), 36 | uint256FromReadBuffer(bufferReader), 37 | ]) 38 | .map(resList => ({ 39 | reserved: resList[0] as Byte, 40 | owner: resList[1] as REAddressT, 41 | resource: resList[2] as REAddressT, 42 | amount: resList[3] as UInt256, 43 | })) 44 | .map( 45 | (partial): TokensT => { 46 | const { resource, owner, reserved, amount } = partial 47 | const buffer = Buffer.concat([ 48 | lengthData, 49 | Buffer.from([SubStateType.TOKENS]), 50 | Buffer.from([reserved]), 51 | owner.toBuffer(), 52 | resource.toBuffer(), 53 | amountToBuffer(amount), 54 | ]) 55 | return { 56 | ...partial, 57 | substateType: SubStateType.TOKENS, 58 | toBuffer: () => buffer, 59 | toString: () => 60 | `Tokens { reserved: ${reserved}, owner: 0x${owner 61 | .toBuffer() 62 | .toString( 63 | 'hex', 64 | )}, resource: 0x${resource 65 | .toBuffer() 66 | .toString( 67 | 'hex', 68 | )}, amount: U256 { raw: ${amount.toString()} } }`, 69 | toHumanReadableString: () => 70 | `Tokens { 71 | reserved: ${reserved}, 72 | owner: ${AccountAddress.fromUnsafe(owner.toBuffer().slice(1)) 73 | ._unsafeUnwrap() 74 | .toString()} 75 | resource: ${ 76 | resource.toBuffer().length === 1 77 | ? ResourceIdentifier.fromUnsafe( 78 | resource.toBuffer(), 79 | ) 80 | ._unsafeUnwrap() 81 | .toString() 82 | : resource.toBuffer().toString('hex') 83 | }, 84 | , amount: ${stringifyUInt256(amount)} }`, 85 | } 86 | }, 87 | ) 88 | 89 | export const Tokens = { 90 | fromBufferReader, 91 | } 92 | -------------------------------------------------------------------------------- /packages/tx-parser/src/transaction.ts: -------------------------------------------------------------------------------- 1 | import { err, ok, Result } from 'neverthrow' 2 | import { InstructionT, TransactionT } from './_types' 3 | import { Instruction } from './instruction' 4 | import { sha256Twice } from '@radixdlt/crypto' 5 | import { BufferReader } from '@radixdlt/util' 6 | const fromBuffer = (blob: Buffer): Result => { 7 | const instructions: InstructionT[] = [] 8 | 9 | const bufferReader = BufferReader.create(blob) 10 | 11 | while (!bufferReader.finishedParsing()) { 12 | const instructionRes = Instruction.parseFromBufferReader(bufferReader) 13 | if (instructionRes.isErr()) { 14 | return err(instructionRes.error) 15 | } 16 | const instruction = instructionRes.value 17 | instructions.push(instruction) 18 | } 19 | 20 | const reassembleBlob = (): Buffer => 21 | instructions 22 | .map(i => i.toBuffer()) 23 | .reduce((a, i) => Buffer.concat([a, i]), Buffer.alloc(0)) 24 | 25 | const reassembledTxBlob = reassembleBlob() 26 | 27 | if (reassembledTxBlob.toString('hex') !== blob.toString('hex')) { 28 | throw new Error( 29 | `Incorrect implementation, reasambled blob NEQ original blob.`, 30 | ) 31 | } 32 | 33 | const txID = (): string => sha256Twice(reassembledTxBlob).toString('hex') 34 | 35 | const toString = (): string => 36 | [ 37 | 'Instructions:', 38 | instructions 39 | .map(i => `|- ${i.toString()}`) 40 | // .map(s => s.trimStart()) 41 | .join('\n'), 42 | ].join('\n') 43 | 44 | return ok({ 45 | instructions, 46 | txID, 47 | toBuffer: () => reassembledTxBlob, 48 | toString, 49 | }) 50 | } 51 | 52 | export const Transaction = { 53 | fromBuffer, 54 | } 55 | -------------------------------------------------------------------------------- /packages/tx-parser/src/txSignature.ts: -------------------------------------------------------------------------------- 1 | import { combine, Result } from 'neverthrow' 2 | import { TXSig } from './_types' 3 | import { BufferReaderT, Byte } from '@radixdlt/util' 4 | import { UInt256 } from '@radixdlt/uint256' 5 | import { amountToBuffer } from './tokens' 6 | 7 | const fromBufferReader = (bufferReader: BufferReaderT): Result => 8 | combine([ 9 | bufferReader.readNextBuffer(1), 10 | bufferReader.readNextBuffer(32), 11 | bufferReader.readNextBuffer(32), 12 | ]) 13 | .map(resList => { 14 | const v = resList[0].readUInt8(0) as Byte 15 | 16 | const uint256FromBuffer = (b: unknown): UInt256 => { 17 | const hex = (b as Buffer).toString('hex') 18 | return new UInt256(hex, 16) 19 | } 20 | 21 | const r = uint256FromBuffer(resList[1]) 22 | const s = uint256FromBuffer(resList[2]) 23 | 24 | return { 25 | v, 26 | r, 27 | s, 28 | } 29 | }) 30 | .map(partial => { 31 | const buffer = Buffer.concat([ 32 | Buffer.from([partial.v]), 33 | amountToBuffer(partial.r), 34 | amountToBuffer(partial.s), 35 | ]) 36 | return { 37 | ...partial, 38 | toBuffer: () => buffer, 39 | toString: () => 40 | `Signature: { 41 | v: ${partial.v}, 42 | r: ${partial.r.toString(16)}, 43 | s: ${partial.s.toString(16)} 44 | }`, 45 | } 46 | }) 47 | 48 | export const TxSignature = { 49 | fromBufferReader, 50 | } 51 | -------------------------------------------------------------------------------- /packages/tx-parser/src/validatorAllowDelegationFlag.ts: -------------------------------------------------------------------------------- 1 | import { 2 | stringifySubstateType, 3 | SubStateType, 4 | ValidatorAllowDelegationFlagT, 5 | } from './_types' 6 | import { BufferReaderT } from '@radixdlt/util' 7 | import { err, ok, Result } from 'neverthrow' 8 | import { makeBaseValidatorSubstateFromBuffer } from './preparedStake' 9 | 10 | export const boolFromBufferReader = ( 11 | bufferReader: BufferReaderT, 12 | ): Result => 13 | bufferReader 14 | .readNextBuffer(1) 15 | .map(b => b.readUInt8(0)) 16 | .andThen(i => 17 | i === 1 18 | ? ok(true) 19 | : i === 0 20 | ? ok(false) 21 | : err(new Error('Expected bool.')), 22 | ) 23 | 24 | export const boolToBuf = (bool: boolean): Buffer => Buffer.from([bool ? 1 : 0]) 25 | 26 | const makeValidatorAllowDelegationFlagFromBuffer = ( 27 | bufferReader: BufferReaderT, 28 | lengthData: Buffer, 29 | ): Result => 30 | makeBaseValidatorSubstateFromBuffer( 31 | SubStateType.VALIDATOR_ALLOW_DELEGATION_FLAG, 32 | )(bufferReader).andThen( 33 | (base): Result => 34 | boolFromBufferReader(bufferReader).map( 35 | (isDelegationAllowed): ValidatorAllowDelegationFlagT => { 36 | const reserved = base.reserved 37 | const validator = base.validator 38 | return { 39 | ...base, 40 | isDelegationAllowed, 41 | toBuffer: (): Buffer => 42 | Buffer.concat([ 43 | lengthData, 44 | base.toBuffer(), 45 | boolToBuf(isDelegationAllowed), 46 | ]), 47 | toString: () => 48 | `${stringifySubstateType( 49 | SubStateType.VALIDATOR_ALLOW_DELEGATION_FLAG, 50 | )} { reserved: ${reserved}, validator: 0x${validator.toString()}, is_delegation_allowed: ${isDelegationAllowed} }`, 51 | } 52 | }, 53 | ), 54 | ) 55 | 56 | export const ValidatorAllowDelegationFlag = { 57 | fromBufferReader: ( 58 | bufferReader: BufferReaderT, 59 | lengthData: Buffer, 60 | ): Result => 61 | makeValidatorAllowDelegationFlagFromBuffer(bufferReader, lengthData), 62 | } 63 | -------------------------------------------------------------------------------- /packages/tx-parser/src/validatorOwnerCopy.ts: -------------------------------------------------------------------------------- 1 | import { BufferReaderT } from '@radixdlt/util' 2 | import { Result } from 'neverthrow' 3 | import { 4 | stringifySubstateType, 5 | SubStateType, 6 | ValidatorOwnerCopyT, 7 | } from './_types' 8 | import { makeBaseValidatorSubstateFromBuffer } from './preparedStake' 9 | import { REAddress } from './reAddress' 10 | 11 | const makeValidatorValidatorOwnerCopy = ( 12 | bufferReader: BufferReaderT, 13 | lengthData: Buffer, 14 | ): Result => 15 | makeBaseValidatorSubstateFromBuffer(SubStateType.VALIDATOR_OWNER_COPY)( 16 | bufferReader, 17 | ).andThen( 18 | (base): Result => 19 | REAddress.fromBufferReader(bufferReader).map( 20 | (owner): ValidatorOwnerCopyT => { 21 | const reserved = base.reserved 22 | const validator = base.validator 23 | return { 24 | ...base, 25 | owner, 26 | toBuffer: (): Buffer => 27 | Buffer.concat([ 28 | lengthData, 29 | base.toBuffer(), 30 | owner.toBuffer(), 31 | ]), 32 | toString: () => 33 | `${stringifySubstateType( 34 | SubStateType.VALIDATOR_OWNER_COPY, 35 | )} { reserved: ${reserved}, validator: 0x${validator.toString()}, owner: 0x${owner 36 | .toBuffer() 37 | .toString('hex')} }`, 38 | } 39 | }, 40 | ), 41 | ) 42 | 43 | export const ValidatorOwnerCopy = { 44 | fromBufferReader: ( 45 | bufferReader: BufferReaderT, 46 | lengthData: Buffer, 47 | ): Result => 48 | makeValidatorValidatorOwnerCopy(bufferReader, lengthData), 49 | } 50 | -------------------------------------------------------------------------------- /packages/tx-parser/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist" 6 | }, 7 | "include": [ 8 | "src" 9 | ], 10 | "references": [ 11 | {"path": "../util"}, 12 | {"path": "../primitives"}, 13 | {"path": "../account"}, 14 | {"path": "../crypto"}, 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/tx-parser/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.test.json", 3 | "include": [ 4 | "src", 5 | "test" 6 | ], 7 | "references": [ 8 | {"path": "../util"}, 9 | {"path": "../primitives"}, 10 | {"path": "../account"}, 11 | {"path": "../crypto"}, 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/util/README.md: -------------------------------------------------------------------------------- 1 | # `util` 2 | 3 | > TODO: description 4 | 5 | ## Usage 6 | 7 | ``` 8 | // TODO: DEMONSTRATE API 9 | ``` 10 | -------------------------------------------------------------------------------- /packages/util/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@radixdlt/util", 3 | "version": "2.1.6", 4 | "description": "Shared utility methods", 5 | "keywords": [ 6 | "Utility" 7 | ], 8 | "author": "Alexander Cyon ", 9 | "homepage": "https://github.com/radixdlt/radixdlt-javascript/tree/master/packages/util#readme", 10 | "license": "Apache-2.0", 11 | "main": "dist/index.js", 12 | "publishConfig": { 13 | "access": "public" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/radixdlt/radixdlt-javascript.git" 18 | }, 19 | "scripts": { 20 | "test": "echo \"Error: run tests from root\" && exit 1" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/radixdlt/radixdlt-javascript/issues" 24 | }, 25 | "engines": { 26 | "node": ">=15.5.0 <16" 27 | }, 28 | "dependencies": { 29 | "loglevel": "^1.7.1", 30 | "loglevel-plugin-prefix": "^0.8.4", 31 | "neverthrow": "^4.0.1", 32 | "ramda": "^0.27.1", 33 | "randombytes": "^2.1.0", 34 | "sodium-native": "^3.2.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/util/src/_types.ts: -------------------------------------------------------------------------------- 1 | export type SecureRandom = { 2 | randomSecureBytes: (byteCount: number) => string 3 | } 4 | 5 | export type ValidationWitness = Readonly<{ witness: string }> 6 | 7 | // prettier-ignore 8 | export type Byte = | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 9 | -------------------------------------------------------------------------------- /packages/util/src/arrays.ts: -------------------------------------------------------------------------------- 1 | export const isNumberArray = (test: unknown): boolean => 2 | Array.isArray(test) && test.every(value => typeof value === 'number') 3 | 4 | export const arraysEqual = (a: T[], b: T[]): boolean => { 5 | if (a === b) return true 6 | if (a == null || b == null) return false 7 | if (a.length !== b.length) return false 8 | 9 | for (let i = 0; i < a.length; ++i) { 10 | if (a[i] !== b[i]) return false 11 | } 12 | return true 13 | } 14 | -------------------------------------------------------------------------------- /packages/util/src/bufferReader.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { err, ok, Result } from 'neverthrow' 3 | 4 | export type BufferReaderT = Readonly<{ 5 | finishedParsing: () => boolean 6 | readNextBuffer: (byteCount: number) => Result 7 | remainingBytes: () => Buffer 8 | }> 9 | 10 | const createBufferReader = (buf: Buffer): BufferReaderT => { 11 | if (!Buffer.isBuffer(buf)) { 12 | buf = Buffer.from(buf) // Convert Uint8Array to Buffer for Electron renderer compatibility 💩 13 | } 14 | 15 | let offset = 0 16 | let bytesLeftToRead = buf.length 17 | 18 | const readNextBuffer = (byteCount: number): Result => { 19 | if (byteCount < 0) 20 | return err(new Error(`'byteCount' must be no negative`)) 21 | if (byteCount === 0) { 22 | return ok(Buffer.alloc(0)) 23 | } 24 | if (offset + byteCount > buf.length) 25 | return err(new Error(`Out of buffer's boundary`)) 26 | const bufToReturn = Buffer.alloc(byteCount) 27 | buf.copy(bufToReturn, 0, offset, offset + byteCount) 28 | 29 | if (bufToReturn.length !== byteCount) { 30 | throw new Error(`Incorrect length of newly read buffer...`) 31 | } 32 | 33 | offset += byteCount 34 | bytesLeftToRead -= byteCount 35 | 36 | // console.log(` 37 | // 🧵🧵🧵 38 | // read: #${byteCount} bytes, 39 | // read buffer: '0x${bufToReturn.toString('hex')}', 40 | // offset: ${offset}, 41 | // source buffer: '0x${buf.toString('hex')}', 42 | // length of source buffer: #${buf.length} bytes. 43 | // bytesLeftToRead: #${bytesLeftToRead} 44 | // 🧵🧵🧵 45 | // `) 46 | 47 | return ok(bufToReturn) 48 | } 49 | 50 | const finishedParsing = (): boolean => { 51 | if (bytesLeftToRead < 0) { 52 | throw new Error(`Incorrect implementation, read too many bytes.`) 53 | } 54 | return bytesLeftToRead === 0 55 | } 56 | 57 | return { 58 | readNextBuffer, 59 | finishedParsing, 60 | remainingBytes: (): Buffer => { 61 | if (finishedParsing()) return Buffer.alloc(0) 62 | const leftBuf = Buffer.alloc(bytesLeftToRead) 63 | buf.copy(leftBuf, 0, offset) 64 | return leftBuf 65 | }, 66 | } 67 | } 68 | export const BufferReader = { 69 | create: createBufferReader, 70 | } 71 | 72 | export const readBuffer = ( 73 | buffer: Buffer, 74 | ): ((byteCount: number) => Result) => { 75 | const bufferReader: BufferReaderT = createBufferReader(buffer) 76 | return bufferReader.readNextBuffer 77 | } 78 | 79 | /* eslint-enable */ 80 | -------------------------------------------------------------------------------- /packages/util/src/buffers.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line max-params 2 | export const buffersEquals = (lhs: Buffer, rhs: Buffer): boolean => 3 | Buffer.compare(lhs, rhs) === 0 4 | -------------------------------------------------------------------------------- /packages/util/src/byte.ts: -------------------------------------------------------------------------------- 1 | import { Byte } from './_types' 2 | import { Result, err, ok } from 'neverthrow' 3 | 4 | export const fitsInUInt8 = (number: number): boolean => { 5 | const isNotTooBig = number <= 255 6 | const isNonNegative = number >= 0 7 | return isNotTooBig && isNonNegative 8 | } 9 | 10 | export const firstByteOfNumber = (n: number): Byte => 11 | firstByteFromBuffer(Buffer.from([n])) 12 | 13 | export const firstByteFromBuffer = (buffer: Buffer): Byte => { 14 | const firstByte = Uint8Array.from(buffer)[0] 15 | return byteFromNumber(firstByte)._unsafeUnwrap() 16 | } 17 | 18 | const byteFromNumber = (n: number): Result => { 19 | if (!Number.isInteger(n) || !fitsInUInt8(n)) { 20 | return err(new RangeError('Number is out of Uint8 range')) 21 | } 22 | const byte = n as Byte 23 | return ok(byte) 24 | } 25 | 26 | export const byteToBuffer = (byte: Byte): Buffer => 27 | Buffer.from([byteToNumber(byte)]) 28 | 29 | const byteToNumber = (byte: Byte): number => byte as number 30 | -------------------------------------------------------------------------------- /packages/util/src/enumTypeGuard.ts: -------------------------------------------------------------------------------- 1 | export const isSomeEnum = (e: T) => (token: unknown): token is T[keyof T] => 2 | Object.values(e).includes(token as T[keyof T]) 3 | -------------------------------------------------------------------------------- /packages/util/src/extractErrorMessage.ts: -------------------------------------------------------------------------------- 1 | import { isArray } from './' 2 | 3 | type MessageOwner = { 4 | message: string 5 | } 6 | 7 | type FailureOwner = { 8 | failure: string 9 | } 10 | 11 | type ErrorMessageOwner = { 12 | error: string 13 | } 14 | 15 | type ErrorCodeOwner = { 16 | code: string 17 | } 18 | 19 | type ErrorIsh = MessageOwner | FailureOwner | ErrorMessageOwner | ErrorCodeOwner 20 | 21 | type ErrorsOwner = { 22 | errors: ErrorIsh[] 23 | } 24 | 25 | type NestedErrorOwner = { 26 | error: 27 | | MessageOwner 28 | | FailureOwner 29 | | ErrorMessageOwner 30 | | ErrorCodeOwner 31 | | ErrorsOwner 32 | } 33 | 34 | // type ErrorNotificationIsh = { 35 | // errors: Error[] 36 | // } 37 | 38 | const isString = (something: unknown): something is string => 39 | typeof something === 'string' 40 | 41 | const isNonEmptyString = (something: unknown): boolean => 42 | isString(something) && something.length > 0 43 | 44 | const isMessageOwner = (something: unknown): something is MessageOwner => { 45 | const inspection = something as MessageOwner 46 | return ( 47 | inspection.message !== undefined && isNonEmptyString(inspection.message) 48 | ) 49 | } 50 | 51 | const isErrorsOwner = (something: unknown): something is ErrorsOwner => { 52 | const inspection = something as ErrorsOwner 53 | return inspection.errors !== undefined && isArray(inspection.errors) 54 | } 55 | 56 | const isFailureOwner = (something: unknown): something is FailureOwner => { 57 | const inspection = something as FailureOwner 58 | return ( 59 | inspection.failure !== undefined && isNonEmptyString(inspection.failure) 60 | ) 61 | } 62 | 63 | const isErrorMessageOwner = ( 64 | something: unknown, 65 | ): something is ErrorMessageOwner => { 66 | const inspection = something as ErrorMessageOwner 67 | return inspection.error !== undefined && isNonEmptyString(inspection.error) 68 | } 69 | 70 | const isErrorCodeOwner = (something: unknown): something is ErrorCodeOwner => { 71 | const inspection = something as ErrorCodeOwner 72 | return inspection.code !== undefined && isNonEmptyString(inspection.code) 73 | } 74 | 75 | const isNestedErrorOwner = ( 76 | something: unknown, 77 | ): something is NestedErrorOwner => { 78 | const inspection = something as NestedErrorOwner 79 | if (!inspection.error) { 80 | return false 81 | } 82 | const err = inspection.error 83 | return ( 84 | isMessageOwner(err) || 85 | isFailureOwner(err) || 86 | isErrorMessageOwner(err) || 87 | isErrorCodeOwner(err) || 88 | isErrorsOwner(err) 89 | ) 90 | } 91 | 92 | export const msgFromError = (e: unknown, dumpJSON: boolean = true): string => { 93 | if (isNonEmptyString(e)) return e as string 94 | if (isMessageOwner(e)) return e.message 95 | if (isFailureOwner(e)) return e.failure 96 | if (isErrorMessageOwner(e)) return e.error 97 | if (isErrorCodeOwner(e)) return e.code 98 | if (isErrorsOwner(e)) { 99 | return e.errors.map(inner => msgFromError(inner)).join(`, `) 100 | } 101 | if (isNestedErrorOwner(e)) { 102 | const inner = e.error 103 | return msgFromError(inner) 104 | } 105 | if (isArray(e)) { 106 | return e.map(inner => msgFromError(inner)).join(`, `) 107 | } else { 108 | if (dumpJSON) { 109 | const dump = JSON.stringify(e, null, 4) 110 | return `Unknown (json: ${dump})` 111 | } else { 112 | return 'Unknown (maybe not an error?)' 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /packages/util/src/functional.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | export const pipe = (...fns: Function[]) => (x: any) => 3 | // eslint-disable-next-line 4 | fns.reduce((y, f) => f(y), x) 5 | -------------------------------------------------------------------------------- /packages/util/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './_types' 2 | 3 | export * from './secureRandomGenerator' 4 | export * from './buffers' 5 | export * from './arrays' 6 | export * from './objectEquals' 7 | export * from './enumTypeGuard' 8 | export * from './bufferReader' 9 | export * from './byte' 10 | export * from './functional' 11 | export * from './typeGuards' 12 | export * from './resultHelpers' 13 | export * from './logging' 14 | export * from './isNode' 15 | export * from './extractErrorMessage' 16 | export * from './resultAsync_observable' 17 | -------------------------------------------------------------------------------- /packages/util/src/isNode.ts: -------------------------------------------------------------------------------- 1 | export const isNode = 2 | typeof process !== 'undefined' && 3 | process.versions != null && 4 | process.versions.node != null 5 | -------------------------------------------------------------------------------- /packages/util/src/logging.ts: -------------------------------------------------------------------------------- 1 | import log from 'loglevel' 2 | import chalk from 'chalk' 3 | import prefix from 'loglevel-plugin-prefix' 4 | 5 | enum LogLevel { 6 | SILENT = 'silent', 7 | TRACE = 'trace', 8 | DEBUG = 'debug', 9 | INFO = 'info', 10 | WARN = 'warn', 11 | ERROR = 'error', 12 | } 13 | 14 | const defaultLogLevel = LogLevel.WARN 15 | 16 | const restoreDefaultLogLevel = (): void => { 17 | log.setLevel(defaultLogLevel) 18 | } 19 | 20 | restoreDefaultLogLevel() 21 | 22 | const logDecorations = { 23 | trace: { 24 | color: chalk.italic.cyan, 25 | emoji: '💜', 26 | }, 27 | debug: { 28 | color: chalk.italic.cyan, 29 | emoji: '💚', 30 | }, 31 | info: { 32 | color: chalk.blue, 33 | emoji: '💙', 34 | }, 35 | warn: { 36 | color: chalk.yellow, 37 | emoji: '💛', 38 | }, 39 | error: { 40 | color: chalk.red, 41 | emoji: '❤️', 42 | }, 43 | } 44 | 45 | prefix.reg(log) 46 | 47 | prefix.apply(log, { 48 | format: (level, name, timestamp) => 49 | `${chalk.gray(`[${timestamp.toString()}]`)} ${ 50 | logDecorations[level.toLowerCase() as Exclude] 51 | .emoji 52 | } ${logDecorations[ 53 | level.toLowerCase() as Exclude 54 | ].color(level)}`, 55 | }) 56 | 57 | export { log, restoreDefaultLogLevel, LogLevel } 58 | -------------------------------------------------------------------------------- /packages/util/src/objectEquals.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | /* eslint-disable */ 3 | export const objectEquals = ( 4 | lhs: Readonly<{ [key in K]: V }>, 5 | rhs: Readonly<{ [key in K]: V }>, 6 | ): boolean => { 7 | if (Object.keys(lhs).length !== Object.keys(rhs).length) return false 8 | return ( 9 | Object.keys(lhs).filter( 10 | key => 11 | rhs[key] !== lhs[key] || 12 | (rhs[key] === undefined && !(key in rhs)), 13 | ).length === 0 14 | ) 15 | } 16 | 17 | export const autoConvertMapToObject = map => { 18 | const obj = {} 19 | for (const item of [...map]) { 20 | const [key, value] = item 21 | obj[key] = value 22 | } 23 | return obj 24 | } 25 | 26 | export const mapEquals = (lhs: Map, rhs: Map): boolean => 27 | objectEquals(autoConvertMapToObject(lhs), autoConvertMapToObject(rhs)) 28 | 29 | /* eslint-enable */ 30 | -------------------------------------------------------------------------------- /packages/util/src/resultAsync_observable.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs' 2 | import { Result, ResultAsync } from 'neverthrow' 3 | 4 | export const toObservable = ( 5 | asyncResult: ResultAsync, 6 | ): Observable => 7 | new Observable(subscriber => { 8 | void asyncResult.then((res: Result) => { 9 | res.match( 10 | (value: T) => { 11 | subscriber.next(value) 12 | subscriber.complete() 13 | }, 14 | (e: E | E[]) => { 15 | subscriber.error(e) 16 | }, 17 | ) 18 | }) 19 | }) 20 | 21 | export const toObservableFromResult = ( 22 | result: Result, 23 | ): Observable => 24 | new Observable(subscriber => { 25 | result.match( 26 | (value: T) => { 27 | subscriber.next(value) 28 | subscriber.complete() 29 | }, 30 | (e: E) => subscriber.error(e), 31 | ) 32 | }) 33 | -------------------------------------------------------------------------------- /packages/util/src/resultHelpers.ts: -------------------------------------------------------------------------------- 1 | import { err, Result } from 'neverthrow' 2 | import { isObject, isResult } from './typeGuards' 3 | 4 | const unwrap = (maybeResult: unknown, errors: (Error | Error[])[]): unknown => { 5 | if (isResult(maybeResult)) { 6 | if (maybeResult.isOk()) { 7 | const value = maybeResult.value 8 | if (isResult(value)) { 9 | return unwrap(value, errors) 10 | } 11 | return value 12 | } else { 13 | errors.push(maybeResult.error) 14 | return maybeResult 15 | } 16 | } 17 | return maybeResult 18 | } 19 | 20 | export const flattenResultsObject = ( 21 | json: Result, 22 | ): Result => { 23 | const errors: (Error | Error[])[] = [] 24 | 25 | const flattened = json 26 | .map(value => { 27 | if (!isObject(value)) return value 28 | for (const item in value) { 29 | const objValue = value[item] 30 | 31 | if (objValue && isResult(objValue)) { 32 | const res = flattenResultsObject(objValue) 33 | value[item] = unwrap(res, errors) 34 | } 35 | } 36 | return value 37 | }) 38 | .mapErr(err => { 39 | errors.push(err) 40 | return errors.flat() 41 | }) 42 | 43 | return errors.length > 0 ? err(errors.flat()) : flattened 44 | } 45 | -------------------------------------------------------------------------------- /packages/util/src/secureRandomGenerator.ts: -------------------------------------------------------------------------------- 1 | import { SecureRandom } from './_types' 2 | import { Result, ok, err } from 'neverthrow' 3 | 4 | /* eslint-disable */ 5 | 6 | /** 7 | * randomBytes 8 | * 9 | * Uses JS-native CSPRNG to generate a specified number of bytes. 10 | * 11 | * @param {number} byteCount number of bytes to generate 12 | * @returns {Result} result of random byte generation. 13 | */ 14 | const randomBytes = (byteCount: number): Result => { 15 | let buffer: Buffer = Buffer.alloc(byteCount) 16 | if ( 17 | typeof window !== 'undefined' && 18 | window.crypto && 19 | window.crypto.getRandomValues 20 | ) { 21 | const bytes = window.crypto.getRandomValues(new Uint8Array(byteCount)) 22 | buffer = Buffer.from(bytes) 23 | } else if (typeof require !== 'undefined') { 24 | const sodium = require('sodium-native') 25 | sodium.randombytes_buf(buffer) 26 | } else { 27 | return err(new Error('Unable to generate safe random numbers.')) 28 | } 29 | 30 | const byteArray = new Uint8Array( 31 | buffer.buffer, 32 | buffer.byteOffset, 33 | buffer.byteLength / Uint8Array.BYTES_PER_ELEMENT, 34 | ) 35 | let byteString = '' 36 | for (let i = 0; i < byteCount; i++) { 37 | byteString += ('00' + byteArray[i].toString(16)).slice(-2) 38 | } 39 | 40 | return ok(byteString) 41 | } 42 | 43 | export const secureRandomGenerator: SecureRandom = { 44 | randomSecureBytes: (byteCount: number) => 45 | randomBytes(byteCount)._unsafeUnwrap(), 46 | } 47 | -------------------------------------------------------------------------------- /packages/util/src/typeGuards.ts: -------------------------------------------------------------------------------- 1 | import { Result } from 'neverthrow' 2 | 3 | const isT = (validate: (value: unknown) => boolean) => ( 4 | value: unknown, 5 | ): value is T => validate(value) 6 | 7 | export const isString = isT(value => typeof value === 'string') 8 | 9 | export const isObject = isT>( 10 | value => 11 | typeof value === 'object' && !Array.isArray(value) && !isResult(value), 12 | ) 13 | 14 | export const isArray = isT>(value => Array.isArray(value)) 15 | 16 | export const isBoolean = isT(value => typeof value === 'boolean') 17 | 18 | export const isNumber = isT(value => typeof value === 'number') 19 | 20 | export const isResult = isT>( 21 | value => 22 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any 23 | !!(value as any)._unsafeUnwrap, 24 | ) 25 | -------------------------------------------------------------------------------- /packages/util/test/arrays.test.ts: -------------------------------------------------------------------------------- 1 | import { isNumberArray } from '../src/arrays' 2 | 3 | describe('arrays', () => { 4 | it('should be able to type check unknown against number array', () => { 5 | expect(isNumberArray([1, 2, 3])).toBe(true) 6 | expect(isNumberArray(['foo', 'bar'])).toBe(false) 7 | expect(isNumberArray([1, 'bar'])).toBe(false) 8 | expect(isNumberArray('just a string')).toBe(false) 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /packages/util/test/mapEquals.test.ts: -------------------------------------------------------------------------------- 1 | import { mapEquals, objectEquals } from '../src/objectEquals' 2 | 3 | describe('Map Equals', () => { 4 | it('can check for equality of non empty types', () => { 5 | const lhs = { foo: 1 } 6 | const rhs = { foo: 1 } 7 | 8 | expect(objectEquals(lhs, rhs)).toBe(true) 9 | }) 10 | 11 | it('can check for equality of empty types', () => { 12 | const lhs = {} 13 | const rhs = {} 14 | 15 | expect(objectEquals(lhs, rhs)).toBe(true) 16 | }) 17 | 18 | it('can check for inequality of non matching values', () => { 19 | const lhs = { foo: 1 } 20 | const rhs = { foo: 2 } 21 | 22 | expect(objectEquals(lhs, rhs)).toBe(false) 23 | }) 24 | 25 | it('can check for inequality of overlapping values', () => { 26 | const lhs = { foo: 1 } 27 | const rhs = { foo: 1, buz: 2 } 28 | 29 | expect(objectEquals(lhs, rhs)).toBe(false) 30 | }) 31 | 32 | it('can check for equality between maps', () => { 33 | const a = new Map([ 34 | ['foo', 42], 35 | ['bar', 1337], 36 | ]) 37 | 38 | const b = new Map([ 39 | ['foo', 42], 40 | ['bar', 1337], 41 | ]) 42 | 43 | expect(mapEquals(a, b)).toBe(true) 44 | }) 45 | 46 | it('can check for inequality between maps', () => { 47 | const a = new Map([ 48 | ['foo', 42], 49 | ['bar', 1337], 50 | ]) 51 | 52 | const b = new Map([ 53 | ['foo', 42], 54 | ['bar', 1337], 55 | ['buz', 237], 56 | ]) 57 | 58 | expect(mapEquals(a, b)).toBe(false) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /packages/util/test/neverthrow-extensions.test.ts: -------------------------------------------------------------------------------- 1 | import { ok, combine } from 'neverthrow' 2 | 3 | describe('result', () => { 4 | it('can use variadic generic combine', () => { 5 | const foo = ok('Foo') 6 | const bar = ok(1337) 7 | const buz = ok(false) 8 | const biz = ok(3.1415) 9 | 10 | const combined = combine([foo, bar, buz, biz]) 11 | 12 | const result = combined.map(resultList => ({ 13 | foo: resultList[0], 14 | bar: resultList[1], 15 | buz: resultList[2], 16 | pi: resultList[3], 17 | })) 18 | 19 | const expected = { 20 | foo: 'Foo', 21 | bar: 1337, 22 | buz: false, 23 | pi: 3.1415, 24 | } 25 | 26 | expect(result._unsafeUnwrap()).toStrictEqual(expected) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /packages/util/test/util.test.ts: -------------------------------------------------------------------------------- 1 | import { secureRandomGenerator } from '../src' 2 | 3 | const testGenerateBytes = (byteCount: number): string => { 4 | const bytes = secureRandomGenerator.randomSecureBytes(byteCount) 5 | expect(bytes.length).toBe(2 * byteCount) 6 | return bytes 7 | } 8 | 9 | describe('util', () => { 10 | it('can securely generate random bytes', () => { 11 | const byteCount = 8 12 | const byteStrings = [...Array(1024)].map((_, i) => { 13 | return testGenerateBytes(byteCount) 14 | }) 15 | const uniqueByteStrings = new Set(byteStrings) 16 | // Probability of collision is: 2^10/2^64 <=> 1/2^54 = 5e-17 <=> VERY low probability. 17 | expect(uniqueByteStrings.size).toBe(byteStrings.length) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /packages/util/test/util.ts: -------------------------------------------------------------------------------- 1 | export const mockErrorMsg = (msg: string): string => { 2 | const testFilePath = expect.getState().testPath 3 | const testFileName = testFilePath.split('/').reverse()[0] 4 | const testName = expect.getState().currentTestName.replaceAll(' ', '_') 5 | return `MOCKED_ERROR_${msg}_${testName}_in_${testFileName}` 6 | } 7 | -------------------------------------------------------------------------------- /packages/util/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist" 6 | }, 7 | "include": [ 8 | "src" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /packages/util/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.test.json", 3 | "include": [ 4 | "src", 5 | "test" 6 | ], 7 | "references": [] 8 | } -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "baseUrl": ".", 5 | "composite": true, 6 | "declaration": true, 7 | "declarationMap": true, 8 | "downlevelIteration": true, 9 | "emitDecoratorMetadata": true, 10 | "esModuleInterop": true, 11 | "experimentalDecorators": true, 12 | "preserveConstEnums": true, 13 | "importHelpers": true, 14 | "lib": ["ESNext", "dom"], 15 | "module": "commonjs", 16 | "moduleResolution": "node", 17 | "sourceMap": true, 18 | "resolveJsonModule": true, 19 | "noUnusedLocals": false, 20 | "strict": true, 21 | "target": "ES6", 22 | "skipLibCheck": true, 23 | "types": [ 24 | "node", 25 | "jest" 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "packages/util" }, 5 | { "path": "packages/primitives" }, 6 | { "path": "packages/crypto" }, 7 | { "path": "packages/data-formats" }, 8 | { "path": "packages/hardware-wallet" }, 9 | { "path": "packages/account" }, 10 | { "path": "packages/networking" }, 11 | { "path": "packages/application" }, 12 | { "path": "packages/hardware-ledger" }, 13 | { "path": "packages/tx-parser" }, 14 | ] 15 | } -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "noUnusedLocals": false, 5 | }, 6 | "files": [], 7 | "include": [ 8 | ], 9 | "references": [ 10 | { "path": "packages/util" }, 11 | { "path": "packages/primitives" }, 12 | { "path": "packages/crypto" }, 13 | { "path": "packages/data-formats" }, 14 | { "path": "packages/hardware-wallet" }, 15 | { "path": "packages/account" }, 16 | { "path": "packages/networking" }, 17 | { "path": "packages/application" }, 18 | { "path": "packages/hardware-ledger" }, 19 | { "path": "packages/tx-parser" } 20 | ] 21 | } 22 | --------------------------------------------------------------------------------