diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 554a50fb18..ceaeb6a4d3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -10,16 +10,16 @@ /website/ @fuxingloh /packages/jellyfish/ @fuxingloh +/packages/jellyfish-address/ @fuxingloh @ivan-zynesis /packages/jellyfish-api-core/ @fuxingloh @canonbrother @jingyi2811 /packages/jellyfish-api-jsonrpc/ @fuxingloh -/packages/jellyfish-api-whale/ @fuxingloh @canonbrother /packages/jellyfish-crypto/ @fuxingloh /packages/jellyfish-json/ @fuxingloh @canonbrother /packages/jellyfish-network/ @fuxingloh /packages/jellyfish-transaction/ @fuxingloh +/packages/jellyfish-transaction-builder/ @fuxingloh @ivan-zynesis /packages/jellyfish-wallet/ @fuxingloh /packages/jellyfish-wallet-mnemonic/ @fuxingloh -/packages/jellyfish-wallet-whale/ @fuxingloh /packages/testcontainers/ @fuxingloh /packages/testing/ @fuxingloh @canonbrother diff --git a/.github/governance.yml b/.github/governance.yml index 8e779908f0..4c857ca686 100644 --- a/.github/governance.yml +++ b/.github/governance.yml @@ -34,16 +34,16 @@ issue: - workflow - website - jellyfish + - jellyfish-address - jellyfish-api-core - jellyfish-api-jsonrpc - - jellyfish-api-whale - jellyfish-crypto - jellyfish-json - jellyfish-network - jellyfish-transaction + - jellyfish-transaction-builder - jellyfish-wallet - jellyfish-wallet-mnemonic - - jellyfish-wallet-whale - testcontainers - testing multiple: true diff --git a/.github/labeler.yml b/.github/labeler.yml index fb5587e192..29a6d22d8e 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -16,6 +16,11 @@ labels: matcher: files: "packages/jellyfish/**" + - label: area/jellyfish-address + sync: true + matcher: + files: "packages/jellyfish-address/**" + - label: area/jellyfish-api-core sync: true matcher: @@ -26,11 +31,6 @@ labels: matcher: files: "packages/jellyfish-api-jsonrpc/**" - - label: area/jellyfish-api-whale - sync: true - matcher: - files: "packages/jellyfish-api-whale/**" - - label: area/jellyfish-crypto sync: true matcher: @@ -51,21 +51,16 @@ labels: matcher: files: "packages/jellyfish-transaction/**" - - label: area/jellyfish-wallet + - label: area/jellyfish-transaction-builder sync: true matcher: - files: "packages/jellyfish-wallet/**" + files: "packages/jellyfish-transaction-builder/**" - label: area/jellyfish-wallet-mnemonic sync: true matcher: files: "packages/jellyfish-wallet-mnemonic/**" - - label: area/jellyfish-wallet-whale - sync: true - matcher: - files: "packages/jellyfish-wallet-whale/**" - - label: area/testcontainers sync: true matcher: diff --git a/.github/labels.yml b/.github/labels.yml index d8b2ccb79a..ab6c0d09a2 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -58,12 +58,12 @@ name: area/website - color: fbca04 name: area/jellyfish +- color: fbca04 + name: area/jellyfish-address - color: fbca04 name: area/jellyfish-api-core - color: fbca04 name: area/jellyfish-api-jsonrpc -- color: fbca04 - name: area/jellyfish-api-whale - color: fbca04 name: area/jellyfish-crypto - color: fbca04 @@ -73,9 +73,9 @@ - color: fbca04 name: area/jellyfish-transaction - color: fbca04 - name: area/jellyfish-wallet + name: area/jellyfish-transaction-builder - color: fbca04 - name: area/jellyfish-wallet-whale + name: area/jellyfish-wallet - color: fbca04 name: area/jellyfish-wallet-mnemonic - color: fbca04 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 80d7c0969d..f30e25eb33 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,8 +11,8 @@ jobs: name: Test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v2.3.4 + - uses: actions/setup-node@v2.1.5 with: node-version: '15' @@ -26,21 +26,21 @@ jobs: name: Lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v2.3.4 + - uses: actions/setup-node@v2.1.5 with: node-version: '15' - run: npm ci - run: npm run build - - run: npm run standard + - run: npx --no-install ts-standard size: name: Size Limit runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v2.3.4 + - uses: actions/setup-node@v2.1.5 with: node-version: '15' diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 1f3d4c8664..ee5a2877bd 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v2.3.4 - name: Initialize CodeQL uses: github/codeql-action/init@v1 diff --git a/.github/workflows/oss-governance-bot.yml b/.github/workflows/oss-governance-bot.yml index 2d500224c8..13dab3f741 100644 --- a/.github/workflows/oss-governance-bot.yml +++ b/.github/workflows/oss-governance-bot.yml @@ -14,7 +14,7 @@ jobs: name: Governance runs-on: ubuntu-latest steps: - - uses: DeFiCh/oss-governance-bot@v2 + - uses: DeFiCh/oss-governance-bot@v2.0.5 with: github-token: ${{ secrets.DEFICHAIN_BOT_GITHUB_TOKEN }} diff --git a/.github/workflows/oss-governance-labeler.yml b/.github/workflows/oss-governance-labeler.yml index 811d5e3706..9b4726d673 100644 --- a/.github/workflows/oss-governance-labeler.yml +++ b/.github/workflows/oss-governance-labeler.yml @@ -10,7 +10,7 @@ jobs: name: Labeler runs-on: ubuntu-latest steps: - - uses: fuxingloh/multi-labeler@v1 + - uses: fuxingloh/multi-labeler@v1.5.1 with: github-token: ${{ secrets.DEFICHAIN_BOT_GITHUB_TOKEN }} config-path: .github/labeler.yml diff --git a/.github/workflows/oss-governance-project.yml b/.github/workflows/oss-governance-project.yml new file mode 100644 index 0000000000..cbb6ba1962 --- /dev/null +++ b/.github/workflows/oss-governance-project.yml @@ -0,0 +1,19 @@ +name: Projects + +on: + pull_request: + types: + - opened + - ready_for_review + +jobs: + main: + name: DeFi Products + runs-on: ubuntu-latest + steps: + - uses: takanabe/github-actions-automate-projects@5d004c140c65fa8b4ef3b18a38219ce680bce816 + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository + env: + GITHUB_TOKEN: ${{ secrets.DEFICHAIN_BOT_GITHUB_TOKEN }} + GITHUB_PROJECT_URL: https://github.com/orgs/DeFiCh/projects/1 + GITHUB_PROJECT_COLUMN_NAME: In progress diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index 26cb023056..ae3051fcd1 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -10,8 +10,8 @@ jobs: runs-on: ubuntu-latest environment: NPM Release Publishing steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/checkout@v2.3.4 + - uses: actions/setup-node@v2.1.5 with: node-version: '15' diff --git a/.github/workflows/sync-labels.yml b/.github/workflows/sync-labels.yml index b949f774cd..01df26ca82 100644 --- a/.github/workflows/sync-labels.yml +++ b/.github/workflows/sync-labels.yml @@ -9,7 +9,7 @@ jobs: main: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v2.3.4 - uses: micnncim/action-label-syncer@0e9c5104859d0e78219af63791636eba42382b5d with: diff --git a/.gitignore b/.gitignore index e273786d3c..5e06f83584 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,7 @@ node_modules dist *.tgz +# debug log +lerna-debug.log + coverage diff --git a/.idea/dictionaries/fuxing.xml b/.idea/dictionaries/fuxing.xml index 070bc95cfb..189be9e5a6 100644 --- a/.idea/dictionaries/fuxing.xml +++ b/.idea/dictionaries/fuxing.xml @@ -1,8 +1,10 @@ + accounttoaccount accounttoutxos acindex + addpoolliquidity amkheight ancestorcount ancestorfees @@ -29,7 +31,9 @@ clarkequayheight codeseparator createmasternode + createpoolpair createrawtransaction + createtoken createwallet currentblocktx currentblockweight @@ -52,6 +56,7 @@ fullstackninja fuxingloh generatetoaddress + getaccount getaddressinfo getbalance getblock @@ -61,8 +66,11 @@ getmintinginfo getnetworkhashps getnewaddress + getpoolpair getrawmempool + getrawtransaction getreceivedbyaddress + gettoken gettransaction gettxout getunconfirmedbalance @@ -96,6 +104,7 @@ listaccounts listaddressgroupings listpoolpairs + listpoolshares listunspent locktime logtimemicros @@ -126,6 +135,7 @@ paytxfee pooledtx poolpair + poolpairs poolswap previousblockhash prevout @@ -138,6 +148,7 @@ pushdata rawtx regtest + removepoolliquidity rewardaddress ripemd rpcallowip @@ -150,6 +161,7 @@ segwit sendrawtransaction sendtoaddress + sendtokenstoaddress setwalletflag sighash sighashtype diff --git a/README.md b/README.md index ed514701a4..49e33ab7d5 100644 --- a/README.md +++ b/README.md @@ -86,17 +86,18 @@ version tag. Package | Description ---------------------------------------------------|------------- `@defichain/jellyfish` | Library bundled usage entrypoint with conventional defaults for 4 bundles: umd, esm, cjs and d.ts +`@defichain/jellyfish-address` | Provide address builder, parser, validator utility library for DeFi Blockchain. `@defichain/jellyfish-api-core` | A protocol agnostic DeFi Blockchain client interfaces, with a "foreign function interface" design. `@defichain/jellyfish-api-jsonrpc` | Implements the [JSON-RPC 1.0](https://www.jsonrpc.org/specification_v1) specification for api-core. -`@defichain/jellyfish-api-whale` | Implements the DeFi Whale service communication specification for api-core. `@defichain/jellyfish-crypto` | Cryptography operations for jellyfish, includes a simple 'secp256k1' EllipticPair. `@defichain/jellyfish-json` | Allows parsing of JSON with 'lossless', 'bignumber' and 'number' numeric precision. `@defichain/jellyfish-network` | Contains DeFi blockchain various network configuration for mainnet, testnet and regtest. -`@defichain/jellyfish-transaction` | Dead simple modern stateless raw transaction builder for DeFi. +`@defichain/jellyfish-transaction` | Dead simple modern stateless raw transaction composer for the DeFi Blockchain. +`@defichain/jellyfish-transaction-builder` | Provides a high-high level abstraction for constructing transaction ready to be broadcast for DeFi Blockchain. `@defichain/jellyfish-wallet` | Jellyfish wallet is a managed wallet, where account can get discovered from an HD seed. `@defichain/jellyfish-wallet-mnemonic` | MnemonicHdNode implements the WalletHdNode from jellyfish-wallet; a CoinType-agnostic HD Wallet for noncustodial DeFi. -`@defichain/jellyfish-wallet-whale` | WhaleWalletAccount implements the WalletAccount from jellyfish-wallet; a stateless account service for DeFi. `@defichain/testcontainers` | Provides a lightweight, throw away instances for DeFiD node provisioned automatically in a Docker container. +`@defichain/testing` | Provides rich test fixture setup functions for effective and effortless testing. ## Developing & Contributing diff --git a/package-lock.json b/package-lock.json index 1c6e27e990..2e1557c554 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1484,6 +1484,10 @@ "resolved": "packages/jellyfish", "link": true }, + "node_modules/@defichain/jellyfish-address": { + "resolved": "packages/jellyfish-address", + "link": true + }, "node_modules/@defichain/jellyfish-api-core": { "resolved": "packages/jellyfish-api-core", "link": true @@ -1508,6 +1512,10 @@ "resolved": "packages/jellyfish-transaction", "link": true }, + "node_modules/@defichain/jellyfish-transaction-builder": { + "resolved": "packages/jellyfish-transaction-builder", + "link": true + }, "node_modules/@defichain/jellyfish-wallet": { "resolved": "packages/jellyfish-wallet", "link": true @@ -1516,10 +1524,6 @@ "resolved": "packages/jellyfish-wallet-mnemonic", "link": true }, - "node_modules/@defichain/jellyfish-wallet-whale": { - "resolved": "packages/jellyfish-wallet-whale", - "link": true - }, "node_modules/@defichain/testcontainers": { "resolved": "packages/testcontainers", "link": true @@ -13118,6 +13122,15 @@ "@types/node": "*" } }, + "node_modules/@types/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-yfAgiWgVLjFCmRv8zAcOIHywYATEwiTVccTLnRp6UxTNavT55M9d/uhK3T03St/+8/z/wW+CRjGKUNmEqoHHCA==", + "dev": true, + "dependencies": { + "base-x": "^3.0.6" + } + }, "node_modules/@types/create-hash": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@types/create-hash/-/create-hash-1.2.2.tgz", @@ -29311,6 +29324,20 @@ "node": ">=12.x" } }, + "packages/jellyfish-address": { + "name": "@defichain/jellyfish-address", + "version": "0.0.0", + "license": "MIT", + "dependencies": { + "@defichain/jellyfish-crypto": "0.0.0", + "@defichain/jellyfish-network": "0.0.0", + "@defichain/jellyfish-transaction": "0.0.0", + "bs58": "^4.0.1" + }, + "devDependencies": { + "@types/bs58": "^4.0.1" + } + }, "packages/jellyfish-api-core": { "name": "@defichain/jellyfish-api-core", "version": "0.0.0", @@ -29343,12 +29370,14 @@ "dependencies": { "bech32": "^2.0.0", "bip66": "^1.1.5", + "bs58": "^4.0.1", "create-hash": "^1.2.0", "tiny-secp256k1": "^1.1.6", "wif": "^2.0.6" }, "devDependencies": { "@types/bech32": "^1.1.2", + "@types/bs58": "^4.0.1", "@types/create-hash": "^1.2.2", "@types/tiny-secp256k1": "^1.0.0", "@types/wif": "^2.0.2" @@ -29390,11 +29419,28 @@ "bignumber.js": "^9.0.1" } }, + "packages/jellyfish-transaction-builder": { + "name": "@defichain/jellyfish-transaction-builder", + "version": "0.0.0", + "license": "MIT", + "dependencies": { + "@defichain/jellyfish-crypto": "0.0.0", + "@defichain/jellyfish-transaction": "0.0.0" + }, + "devDependencies": { + "@defichain/testcontainers": "0.0.0", + "@defichain/testing": "0.0.0" + }, + "peerDependencies": { + "bignumber.js": "^9.0.1" + } + }, "packages/jellyfish-wallet": { "name": "@defichain/jellyfish-wallet", "version": "0.0.0", "license": "MIT", "dependencies": { + "@defichain/jellyfish-address": "0.0.0", "@defichain/jellyfish-crypto": "0.0.0", "@defichain/jellyfish-transaction": "0.0.0" }, @@ -29413,16 +29459,6 @@ "bip39": "^3.0.3" } }, - "packages/jellyfish-wallet-whale": { - "name": "@defichain/jellyfish-wallet-whale", - "version": "0.0.0", - "license": "MIT", - "dependencies": { - "@defichain/jellyfish-crypto": "0.0.0", - "@defichain/jellyfish-network": "0.0.0", - "@defichain/jellyfish-wallet": "0.0.0" - } - }, "packages/testcontainers": { "name": "@defichain/testcontainers", "version": "0.0.0", @@ -30603,6 +30639,16 @@ "parcel": "2.0.0-beta.1" } }, + "@defichain/jellyfish-address": { + "version": "file:packages/jellyfish-address", + "requires": { + "@defichain/jellyfish-crypto": "0.0.0", + "@defichain/jellyfish-network": "0.0.0", + "@defichain/jellyfish-transaction": "0.0.0", + "@types/bs58": "^4.0.1", + "bs58": "^4.0.1" + } + }, "@defichain/jellyfish-api-core": { "version": "file:packages/jellyfish-api-core", "requires": { @@ -30624,11 +30670,13 @@ "version": "file:packages/jellyfish-crypto", "requires": { "@types/bech32": "^1.1.2", + "@types/bs58": "^4.0.1", "@types/create-hash": "^1.2.2", "@types/tiny-secp256k1": "^1.0.0", "@types/wif": "^2.0.2", "bech32": "^2.0.0", "bip66": "^1.1.5", + "bs58": "^4.0.1", "create-hash": "^1.2.0", "tiny-secp256k1": "^1.1.6", "wif": "^2.0.6" @@ -30654,9 +30702,19 @@ "smart-buffer": "^4.1.0" } }, + "@defichain/jellyfish-transaction-builder": { + "version": "file:packages/jellyfish-transaction-builder", + "requires": { + "@defichain/jellyfish-crypto": "0.0.0", + "@defichain/jellyfish-transaction": "0.0.0", + "@defichain/testcontainers": "0.0.0", + "@defichain/testing": "0.0.0" + } + }, "@defichain/jellyfish-wallet": { "version": "file:packages/jellyfish-wallet", "requires": { + "@defichain/jellyfish-address": "0.0.0", "@defichain/jellyfish-crypto": "0.0.0", "@defichain/jellyfish-transaction": "0.0.0" } @@ -30670,14 +30728,6 @@ "bip39": "^3.0.3" } }, - "@defichain/jellyfish-wallet-whale": { - "version": "file:packages/jellyfish-wallet-whale", - "requires": { - "@defichain/jellyfish-crypto": "0.0.0", - "@defichain/jellyfish-network": "0.0.0", - "@defichain/jellyfish-wallet": "0.0.0" - } - }, "@defichain/testcontainers": { "version": "file:packages/testcontainers", "requires": { @@ -39922,6 +39972,15 @@ "@types/node": "*" } }, + "@types/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-yfAgiWgVLjFCmRv8zAcOIHywYATEwiTVccTLnRp6UxTNavT55M9d/uhK3T03St/+8/z/wW+CRjGKUNmEqoHHCA==", + "dev": true, + "requires": { + "base-x": "^3.0.6" + } + }, "@types/create-hash": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@types/create-hash/-/create-hash-1.2.2.tgz", diff --git a/packages/jellyfish-address/README.md b/packages/jellyfish-address/README.md new file mode 100644 index 0000000000..50ebadef58 --- /dev/null +++ b/packages/jellyfish-address/README.md @@ -0,0 +1,6 @@ +[![npm](https://img.shields.io/npm/v/@defichain/jellyfish-address)](https://www.npmjs.com/package/@defichain/jellyfish-address/v/latest) +[![npm@next](https://img.shields.io/npm/v/@defichain/jellyfish-address/next)](https://www.npmjs.com/package/@defichain/jellyfish-address/v/next) + +# @defichain/jellyfish-address + +DeFi blockchain address type builder, parser, validator library. diff --git a/packages/jellyfish-address/__tests__/base58_address.test.ts b/packages/jellyfish-address/__tests__/base58_address.test.ts new file mode 100644 index 0000000000..61658d43a0 --- /dev/null +++ b/packages/jellyfish-address/__tests__/base58_address.test.ts @@ -0,0 +1,57 @@ +import { Script } from '@defichain/jellyfish-transaction' +import { Network } from '@defichain/jellyfish-network' +import { Base58Address } from '../src' + +class DummyB58Address extends Base58Address { + getScript (): Script { + return { + stack: [] + } + } + + getPrefix (): number { + return this.network.pubKeyHashPrefix // match the fixture p2pkh prefix + } +} + +describe('Base58Address', () => { + const b58Fixture = { + p2sh: 'dFFPENo7FPMJpDV6fUcfo4QfkZrfrV1Uf8', // prefix = 0x12 + p2pkh: '8JBuS81VT8ouPrT6YS55qoS74D13Cw7h1Y' + } + + const dummyNetwork: Network = { + name: 'regtest', + bech32: { + hrp: 'dummy' + }, + bip32: { + publicPrefix: 0x00000000, + privatePrefix: 0x00000000 + }, + wifPrefix: 0x00, + pubKeyHashPrefix: 0x12, + scriptHashPrefix: 0x00, + messagePrefix: '\x00Dummy Msg Prefix:\n' + } as any + + describe('extensible, should work for any defined network protocol', () => { + it('fromAddress() - valid', () => { + const valid = Base58Address.fromAddress(dummyNetwork, b58Fixture.p2pkh, DummyB58Address) + expect(valid.validate()).toBeTruthy() + }) + + it('fromAddress() - invalid character set', () => { + const invalid = Base58Address.fromAddress(dummyNetwork, 'invalid b58 address', DummyB58Address) + expect(invalid.validate()).toBeFalsy() + }) + + it('fromAddress() - invalid prefix', () => { + const invalid = Base58Address.fromAddress(dummyNetwork, b58Fixture.p2sh, DummyB58Address) + expect(invalid.validate()).toBeFalsy() + + const valid = Base58Address.fromAddress(dummyNetwork, b58Fixture.p2pkh, DummyB58Address) + expect(valid.validate()).toBeTruthy() + }) + }) +}) diff --git a/packages/jellyfish-address/__tests__/bech32_address.test.ts b/packages/jellyfish-address/__tests__/bech32_address.test.ts new file mode 100644 index 0000000000..e6c533725b --- /dev/null +++ b/packages/jellyfish-address/__tests__/bech32_address.test.ts @@ -0,0 +1,51 @@ +import { Script } from '@defichain/jellyfish-transaction' +import { Network } from '@defichain/jellyfish-network' +import { Bech32Address } from '../src' + +class DummyBech32Address extends Bech32Address { + getScript (): Script { + return { + stack: [] + } + } +} + +describe('Bech32Address', () => { + const bech32Fixture = { + p2wpkh: 'dummy1qpe7q4vvtxpdunpazvmwqdh3xlnatfdt2xr8mpv', // edited prefix to match test network + invalidPrefix: 'prefix1qpe7q4vvtxpdunpazvmwqdh3xlnatfdt2xr8mpv', // original p2wpkh address sample + invalidCharset: 'dummy1qpe7q4vvtxpdunpazvmwqdh3xlnatfdt2xr8mpo' // character 'o' + } + + const dummyNetwork: Network = { + name: 'regtest', + bech32: { + hrp: 'dummy' + }, + bip32: { + publicPrefix: 0x00000000, + privatePrefix: 0x00000000 + }, + wifPrefix: 0x00, + pubKeyHashPrefix: 0x00, + scriptHashPrefix: 0x00, + messagePrefix: '\x00Dummy Msg Prefix:\n' + } as any + + describe('extensible, should work for any defined network protocol', () => { + it('fromAddress() - valid', () => { + const valid = Bech32Address.fromAddress(dummyNetwork, bech32Fixture.p2wpkh, DummyBech32Address) + expect(valid.validate()).toBeTruthy() + }) + + it('fromAddress() - invalid character set', () => { + const invalid = Bech32Address.fromAddress(dummyNetwork, bech32Fixture.invalidCharset, DummyBech32Address) + expect(invalid.validate()).toBeFalsy() + }) + + it('fromAddress() - invalid prefix', () => { + const invalid = Bech32Address.fromAddress(dummyNetwork, bech32Fixture.invalidPrefix, DummyBech32Address) + expect(invalid.validate()).toBeFalsy() + }) + }) +}) diff --git a/packages/jellyfish-address/__tests__/p2pkh.test.ts b/packages/jellyfish-address/__tests__/p2pkh.test.ts new file mode 100644 index 0000000000..a3d1da6997 --- /dev/null +++ b/packages/jellyfish-address/__tests__/p2pkh.test.ts @@ -0,0 +1,141 @@ +import bs58 from 'bs58' +import { MainNet, RegTest, TestNet } from '@defichain/jellyfish-network' +import { OP_CODES } from '@defichain/jellyfish-transaction' +import { RegTestContainer } from '@defichain/testcontainers' +import { DeFiAddress, P2PKH } from '../src' + +describe('P2PKH', () => { + const container = new RegTestContainer() + const p2pkhFixture = { + mainnet: '8JBuS81VT8ouPrT6YS55qoS74D13Cw7h1Y', + testnet: '7LMorkhKTDjbES6DfRxX2RiNMbeemUkxmp', + regtest: '', + + invalid: 'JBuS81VT8ouPrT6YS55qoS74D13Cw7h1Y', // edited, removed prefix + invalidChecksum: '8JBuS81VT8ouPrT6YS55qoS74D13Cw7h1X' // edited checksum (last char) + } + + beforeAll(async () => { + await container.start() + await container.waitForReady() + p2pkhFixture.regtest = await container.getNewAddress('', 'legacy') + }) + + afterAll(async () => await container.stop()) + + describe('from() - valid address', () => { + it('should get the type precisely', () => { + const p2pkh = DeFiAddress.from('mainnet', p2pkhFixture.mainnet) + expect(p2pkh.valid).toBeTruthy() + expect(p2pkh.type).toBe('P2PKH') + expect(p2pkh.constructor.name).toBe('P2PKH') + expect(p2pkh.network).toBe(MainNet) + }) + + it('should work for all recognized network type', () => { + const testnet = DeFiAddress.from('testnet', p2pkhFixture.testnet) + expect(testnet.valid).toBeTruthy() + expect(testnet.type).toBe('P2PKH') + expect(testnet.constructor.name).toBe('P2PKH') + expect(testnet.network).toBe(TestNet) + + const regtest = DeFiAddress.from('regtest', p2pkhFixture.regtest) + expect(regtest.valid).toBeTruthy() + expect(regtest.type).toBe('P2PKH') + expect(regtest.constructor.name).toBe('P2PKH') + expect(regtest.network).toBe(RegTest) + }) + }) + + describe('from() - invalid address', () => { + it('should be able to validate in address prefix with network', () => { + const invalid = DeFiAddress.from('mainnet', p2pkhFixture.invalid) + expect(invalid.valid).toBeFalsy() + }) + + it('should be able to validate in address prefix with network', () => { + // valid address, used on different network + const p2pkh = DeFiAddress.from('testnet', p2pkhFixture.mainnet) + expect(p2pkh.valid).toBeFalsy() + // expect(p2pkh.type).toBe('P2PKH') // invalid address guessed type is not promising, as p2pkh and p2sh are versy similar + expect(p2pkh.network).toBe(TestNet) + }) + + it('should get the type precisely', () => { + const invalid = DeFiAddress.from('mainnet', p2pkhFixture.invalidChecksum) + expect(invalid.valid).toBeFalsy() + }) + }) + + describe('to()', () => { + it('should be able to build a new address using a public key hash (20 bytes, 40 char hex string)', () => { + const pubKeyHash = '134b0749882c225e8647df3a3417507c6f5b2797' + expect(pubKeyHash.length).toEqual(40) + + const p2pkh = P2PKH.to('regtest', pubKeyHash) + expect(p2pkh.type).toEqual('P2PKH') + expect(p2pkh.valid).toBeTruthy() + + const scriptStack = p2pkh.getScript() + expect(scriptStack.stack.length).toEqual(5) + expect(scriptStack.stack[0]).toEqual(OP_CODES.OP_DUP) + expect(scriptStack.stack[1]).toEqual(OP_CODES.OP_HASH160) + expect(scriptStack.stack[2]).toEqual(OP_CODES.OP_PUSHDATA_HEX_LE(pubKeyHash)) + expect(scriptStack.stack[3]).toEqual(OP_CODES.OP_EQUALVERIFY) + expect(scriptStack.stack[4]).toEqual(OP_CODES.OP_CHECKSIG) + }) + + it('should reject invalid data - not 20 bytes data', () => { + const pubKeyHash = '134b0749882c225e8647df3a3417507c6f5b27' + expect(pubKeyHash.length).toEqual(38) + + expect(() => { + P2PKH.to('regtest', pubKeyHash) + }).toThrow('InvalidDataLength') + }) + }) + + describe('getScript()', () => { + it('should refuse to build ops code stack for invalid address', () => { + const invalid = DeFiAddress.from('testnet', p2pkhFixture.mainnet) + expect(invalid.valid).toBeFalsy() + try { + invalid.getScript() + } catch (e) { + expect(e.message).toBe('InvalidDefiAddress') + } + }) + + it('should be able to build script', async () => { + const p2pkh = DeFiAddress.from('mainnet', p2pkhFixture.mainnet) + const scriptStack = p2pkh.getScript() + + expect(scriptStack.stack.length).toEqual(5) + expect(scriptStack.stack[0]).toEqual(OP_CODES.OP_DUP) + expect(scriptStack.stack[1]).toEqual(OP_CODES.OP_HASH160) + expect(scriptStack.stack[2].type).toEqual('OP_PUSHDATA') // tested in `to()` + expect(scriptStack.stack[3]).toEqual(OP_CODES.OP_EQUALVERIFY) + expect(scriptStack.stack[4]).toEqual(OP_CODES.OP_CHECKSIG) + }) + }) + + it('validate()', () => { + const hex = bs58.decode(p2pkhFixture.mainnet).toString('hex').substring(2, 42) // take 20 bytes data only + const p2pkh = new P2PKH(MainNet, p2pkhFixture.mainnet, hex) + + expect(p2pkh.validatorPassed).toEqual(0) + expect(p2pkh.valid).toBeFalsy() + + const isValid = p2pkh.validate() + expect(p2pkh.validatorPassed).toEqual(5) + expect(isValid).toBeTruthy() + }) + + it('guess()', () => { + const p2pkh = DeFiAddress.guess(p2pkhFixture.mainnet) + expect(p2pkh.valid).toBeTruthy() + expect(p2pkh.type).toBe('P2PKH') + expect(p2pkh.constructor.name).toBe('P2PKH') + expect(p2pkh.network).toBe(MainNet) + }) +}) diff --git a/packages/jellyfish-address/__tests__/p2sh.test.ts b/packages/jellyfish-address/__tests__/p2sh.test.ts new file mode 100644 index 0000000000..283de9f5b0 --- /dev/null +++ b/packages/jellyfish-address/__tests__/p2sh.test.ts @@ -0,0 +1,140 @@ +import bs58 from 'bs58' +import { MainNet, RegTest, TestNet } from '@defichain/jellyfish-network' +import { OP_CODES } from '@defichain/jellyfish-transaction' +import { RegTestContainer } from '@defichain/testcontainers' +import { DeFiAddress, P2SH } from '../src' + +describe('P2SH', () => { + const container = new RegTestContainer() + const p2shFixture = { + mainnet: 'dFFPENo7FPMJpDV6fUcfo4QfkZrfrV1Uf8', + testnet: 'trsUzSh3Qcu1MURY1BKDjttJN6hxtoRxM2', + regtest: '', + + invalid: 'FFPENo7FPMJpDV6fUcfo4QfkZrfrV1Uf8', // edited mainnet address, removed prefix + invalidChecksum: 'dFFPENo7FPMJpDV6fUcfo4QfkZrfrV1Uf' // edited mainnet address, trim checksum + } + + beforeAll(async () => { + await container.start() + await container.waitForReady() + p2shFixture.regtest = await container.getNewAddress('', 'p2sh-segwit') + }) + + afterAll(async () => await container.stop()) + + describe('from() - valid address', () => { + it('should get the type precisely', () => { + const p2sh = DeFiAddress.from('mainnet', p2shFixture.mainnet) + expect(p2sh.valid).toBeTruthy() + expect(p2sh.type).toBe('P2SH') + expect(p2sh.constructor.name).toBe('P2SH') + expect(p2sh.network).toBe(MainNet) + }) + + it('should work for all recognized network type', () => { + const testnet = DeFiAddress.from('testnet', p2shFixture.testnet) + expect(testnet.valid).toBeTruthy() + expect(testnet.type).toBe('P2SH') + expect(testnet.constructor.name).toBe('P2SH') + expect(testnet.network).toBe(TestNet) + + const regtest = DeFiAddress.from('regtest', p2shFixture.regtest) + expect(regtest.valid).toBeTruthy() + expect(regtest.type).toBe('P2SH') + expect(regtest.constructor.name).toBe('P2SH') + expect(regtest.network).toBe(RegTest) + }) + }) + + describe('from() - invalid address', () => { + it('trimmed prefix', () => { + const invalid = DeFiAddress.from('mainnet', p2shFixture.invalid) + expect(invalid.valid).toBeFalsy() + }) + + it('should be able to validate in address prefix with network', () => { + // valid address, used on different network + const p2sh = DeFiAddress.from('testnet', p2shFixture.mainnet) + expect(p2sh.valid).toBeFalsy() + // expect(p2sh.type).toBe('P2SH') // invalid address guessed type is not promising, as p2sh and p2sh are versy similar + expect(p2sh.network).toBe(TestNet) + }) + + it('should get the type precisely', () => { + const invalid = DeFiAddress.from('mainnet', p2shFixture.invalidChecksum) + expect(invalid.valid).toBeFalsy() + }) + }) + + describe('to()', () => { + it('should be able to build a new address using a public key hash (20 bytes, 40 char hex string)', () => { + const scriptHash = '134b0749882c225e8647df3a3417507c6f5b2797' + expect(scriptHash.length).toEqual(40) + + const p2sh = P2SH.to('regtest', scriptHash) + expect(p2sh.type).toEqual('P2SH') + expect(p2sh.valid).toBeTruthy() + + const scriptStack = p2sh.getScript() + expect(scriptStack.stack.length).toEqual(3) + expect(scriptStack.stack[0]).toEqual(OP_CODES.OP_HASH160) + expect(scriptStack.stack[1]).toEqual(OP_CODES.OP_PUSHDATA_HEX_LE(scriptHash)) + expect(scriptStack.stack[2]).toEqual(OP_CODES.OP_EQUAL) + }) + + it('should reject invalid data - not 20 bytes data', () => { + const scriptHash = '134b0749882c225e8647df3a3417507c6f5b27' + expect(scriptHash.length).toEqual(38) + + try { + P2SH.to('regtest', scriptHash) + throw new Error('should had failed') + } catch (e) { + expect(e.message).toEqual('InvalidDataLength') + } + }) + }) + + describe('getScript()', () => { + it('should refuse to build ops code stack for invalid address', () => { + const invalid = DeFiAddress.from('testnet', p2shFixture.mainnet) + expect(invalid.valid).toBeFalsy() + try { + invalid.getScript() + } catch (e) { + expect(e.message).toBe('InvalidDefiAddress') + } + }) + + it('should be able to build script', async () => { + const p2sh = DeFiAddress.from('mainnet', p2shFixture.mainnet) + const scriptStack = p2sh.getScript() + + expect(scriptStack.stack.length).toEqual(3) + expect(scriptStack.stack[0]).toEqual(OP_CODES.OP_HASH160) + expect(scriptStack.stack[1].type).toEqual('OP_PUSHDATA') + expect(scriptStack.stack[2]).toEqual(OP_CODES.OP_EQUAL) + }) + }) + + it('validate()', () => { + const hex = bs58.decode(p2shFixture.mainnet).toString('hex').substring(2, 42) // take 20 bytes data only + const p2sh = new P2SH(MainNet, p2shFixture.mainnet, hex) + + expect(p2sh.validatorPassed).toEqual(0) + expect(p2sh.valid).toBeFalsy() + + const isValid = p2sh.validate() + expect(p2sh.validatorPassed).toEqual(5) + expect(isValid).toBeTruthy() + }) + + it('guess()', () => { + const p2sh = DeFiAddress.guess(p2shFixture.mainnet) + expect(p2sh.valid).toBeTruthy() + expect(p2sh.type).toBe('P2SH') + expect(p2sh.constructor.name).toBe('P2SH') + expect(p2sh.network).toBe(MainNet) + }) +}) diff --git a/packages/jellyfish-address/__tests__/p2wpkh.test.ts b/packages/jellyfish-address/__tests__/p2wpkh.test.ts new file mode 100644 index 0000000000..c79d433269 --- /dev/null +++ b/packages/jellyfish-address/__tests__/p2wpkh.test.ts @@ -0,0 +1,137 @@ +import { MainNet, RegTest, TestNet } from '@defichain/jellyfish-network' +import { OP_CODES } from '@defichain/jellyfish-transaction' +import { RegTestContainer } from '@defichain/testcontainers' +import { DeFiAddress, P2WPKH } from '../src' + +describe('P2WPKH', () => { + const container = new RegTestContainer() + const p2wpkhFixture = { + mainnet: 'df1qpe7q4vvtxpdunpazvmwqdh3xlnatfdt2xr8mpv', + testnet: 'tf1qpe7q4vvtxpdunpazvmwqdh3xlnatfdt24nagpg', + regtest: '', + + trimmedPrefix: 'f1qpe7q4vvtxpdunpazvmwqdh3xlnatfdt2xr8mpv', // edited mainnet address with broken prefix + invalid: 'df1pe7q4vvtxpdunpazvmwqdh3xlnatfdt2ncrpqo' // edited mainnet address, letter 'o' + } + + beforeAll(async () => { + await container.start() + await container.waitForReady() + p2wpkhFixture.regtest = await container.getNewAddress('', 'bech32') + }) + + afterAll(async () => await container.stop()) + + describe('from() - valid address', () => { + it('should get the type precisely', () => { + const p2wpkh = DeFiAddress.from('mainnet', p2wpkhFixture.mainnet) + expect(p2wpkh.valid).toBeTruthy() + expect(p2wpkh.type).toBe('P2WPKH') + expect(p2wpkh.constructor.name).toBe('P2WPKH') + expect(p2wpkh.network).toBe(MainNet) + }) + + it('should work for all recognized network type', () => { + const testnet = DeFiAddress.from('testnet', p2wpkhFixture.testnet) + expect(testnet.valid).toBeTruthy() + expect(testnet.type).toBe('P2WPKH') + expect(testnet.constructor.name).toBe('P2WPKH') + expect(testnet.network).toBe(TestNet) + + const regtest = DeFiAddress.from('regtest', p2wpkhFixture.regtest) + expect(regtest.valid).toBeTruthy() + expect(regtest.type).toBe('P2WPKH') + expect(regtest.constructor.name).toBe('P2WPKH') + expect(regtest.network).toBe(RegTest) + }) + }) + + describe('from() - invalid address', () => { + it('trimmed prefix', () => { + const invalid = DeFiAddress.from('mainnet', p2wpkhFixture.trimmedPrefix) + expect(invalid.valid).toBeFalsy() + }) + + it('invalid character set', () => { + const invalid = DeFiAddress.from('mainnet', p2wpkhFixture.invalid) + expect(invalid.valid).toBeFalsy() + }) + + it('should be able to validate in address prefix with network', () => { + // valid address, used on different network + const p2wpkh = DeFiAddress.from('testnet', p2wpkhFixture.mainnet) + expect(p2wpkh.valid).toBeFalsy() + // expect(p2wpkh.type).toBe('P2WPKH') // invalid address guessed type is not promising, as p2wpkh and p2wpkh are versy similar + expect(p2wpkh.network).toBe(TestNet) + }) + }) + + describe('to()', () => { + it('should be able to build a new address using a public key hash (20 bytes, 40 char hex string)', () => { + const data = '0e7c0ab18b305bc987a266dc06de26fcfab4b56a' // 20 bytes + expect(data.length).toEqual(40) + + const p2wpkh = P2WPKH.to('regtest', data) + expect(p2wpkh.type).toEqual('P2WPKH') + expect(p2wpkh.valid).toBeTruthy() + + const scriptStack = p2wpkh.getScript() + expect(scriptStack.stack.length).toEqual(2) + expect(scriptStack.stack[0]).toEqual(OP_CODES.OP_0) + expect(scriptStack.stack[1]).toEqual(OP_CODES.OP_PUSHDATA_HEX_LE(data)) + }) + + it('should reject invalid data - not 32 bytes data', () => { + const pubKeyHash = '9e1be07558ea5cc8e02ed1d80c0911048afad949affa36d5c3951e3159dbea19' + expect(pubKeyHash.length).toEqual(64) // testing with a p2wpkh data + + try { + P2WPKH.to('regtest', pubKeyHash) + throw new Error('should had failed') + } catch (e) { + expect(e.message).toEqual('InvalidPubKeyHashLength') + } + }) + }) + + describe('getScript()', () => { + it('should refuse to build ops code stack for invalid address', () => { + const invalid = DeFiAddress.from('testnet', p2wpkhFixture.mainnet) + expect(invalid.valid).toBeFalsy() + try { + invalid.getScript() + } catch (e) { + expect(e.message).toBe('InvalidDefiAddress') + } + }) + + it('should be able to build script', async () => { + const p2wpkh = DeFiAddress.from('mainnet', p2wpkhFixture.mainnet) + const scriptStack = p2wpkh.getScript() + + expect(scriptStack.stack.length).toEqual(2) + expect(scriptStack.stack[0]).toEqual(OP_CODES.OP_0) + expect(scriptStack.stack[1].type).toEqual('OP_PUSHDATA') + }) + }) + + it('validate()', () => { + const data = '0e7c0ab18b305bc987a266dc06de26fcfab4b56a' // 20 bytes + + const p2wpkh = new P2WPKH(RegTest, p2wpkhFixture.regtest, data) + expect(p2wpkh.validatorPassed).toEqual(0) + expect(p2wpkh.valid).toBeFalsy() + + const isValid = p2wpkh.validate() + expect(p2wpkh.validatorPassed).toEqual(5) // length, network prefix, data character set + expect(isValid).toBeTruthy() + }) + + it('guess()', () => { + const p2wpkh = DeFiAddress.guess(p2wpkhFixture.mainnet) + expect(p2wpkh.valid).toBeTruthy() + expect(p2wpkh.type).toBe('P2WPKH') + expect(p2wpkh.constructor.name).toBe('P2WPKH') + expect(p2wpkh.network).toBe(MainNet) + }) +}) diff --git a/packages/jellyfish-address/__tests__/p2wsh.test.ts b/packages/jellyfish-address/__tests__/p2wsh.test.ts new file mode 100644 index 0000000000..576c0fe69e --- /dev/null +++ b/packages/jellyfish-address/__tests__/p2wsh.test.ts @@ -0,0 +1,127 @@ +import { MainNet, RegTest, TestNet } from '@defichain/jellyfish-network' +import { OP_CODES } from '@defichain/jellyfish-transaction' +import { DeFiAddress, P2WSH } from '../src' + +describe('P2WSH', () => { + const p2wshFixture = { + mainnet: 'df1qncd7qa2cafwv3cpw68vqczg3qj904k2f4lard4wrj50rzkwmagvsfkkf88', // built using jellyfish-transaction P2PSH test case example, no way to find back raw data using random address + testnet: 'tf1qncd7qa2cafwv3cpw68vqczg3qj904k2f4lard4wrj50rzkwmagvsemeex5', + regtest: 'bcrt1qncd7qa2cafwv3cpw68vqczg3qj904k2f4lard4wrj50rzkwmagvssfsq3t', + + trimmedPrefix: 'f1qncd7qa2cafwv3cpw68vqczg3qj904k2f4lard4wrj50rzkwmagvsfkkf88', // edited mainnet valid address, broken prefix + invalid: 'df1qncd7qa2cafwv3cpw68vqczg3qj904k2f4lard4wrj50rzkwmagvsfkkf8o' // edited mainnet address, letter 'o' + } + + describe('from() - valid address', () => { + it('should get the type precisely', () => { + const p2sh = DeFiAddress.from('mainnet', p2wshFixture.mainnet) + expect(p2sh.valid).toBeTruthy() + expect(p2sh.type).toBe('P2WSH') + expect(p2sh.constructor.name).toBe('P2WSH') + expect(p2sh.network).toBe(MainNet) + }) + + it('should work for all recognized network type', () => { + const testnet = DeFiAddress.from('testnet', p2wshFixture.testnet) + expect(testnet.valid).toBeTruthy() + expect(testnet.type).toBe('P2WSH') + expect(testnet.constructor.name).toBe('P2WSH') + expect(testnet.network).toBe(TestNet) + + const regtest = DeFiAddress.from('regtest', p2wshFixture.regtest) + expect(regtest.valid).toBeTruthy() + expect(regtest.type).toBe('P2WSH') + expect(regtest.constructor.name).toBe('P2WSH') + expect(regtest.network).toBe(RegTest) + }) + }) + + describe('from() - invalid address', () => { + it('trimmed prefix', () => { + const invalid = DeFiAddress.from('mainnet', p2wshFixture.trimmedPrefix) + expect(invalid.valid).toBeFalsy() + }) + + it('invalid character set', () => { + const invalid = DeFiAddress.from('mainnet', p2wshFixture.invalid) + expect(invalid.valid).toBeFalsy() + }) + + it('should be able to validate in address prefix with network', () => { + // valid address, used on different network + const p2sh = DeFiAddress.from('testnet', p2wshFixture.mainnet) + expect(p2sh.valid).toBeFalsy() + // expect(p2sh.type).toBe('P2WSH') // invalid address guessed type is not promising, as p2wsh and p2sh are versy similar + expect(p2sh.network).toBe(TestNet) + }) + }) + + describe('to()', () => { + it('should be able to build a new address using a script hash (32 bytes, 64 char hex string)', () => { + const data = '9e1be07558ea5cc8e02ed1d80c0911048afad949affa36d5c3951e3159dbea19' // 32 bytes + expect(data.length).toEqual(64) + + const p2wsh = P2WSH.to('regtest', data) + expect(p2wsh.type).toEqual('P2WSH') + expect(p2wsh.valid).toBeTruthy() + + const scriptStack = p2wsh.getScript() + expect(scriptStack.stack.length).toEqual(2) + expect(scriptStack.stack[0]).toEqual(OP_CODES.OP_0) + expect(scriptStack.stack[1]).toEqual(OP_CODES.OP_PUSHDATA_HEX_LE(data)) + }) + + it('should reject invalid data - not 32 bytes data', () => { + const pubKeyHash = '134b0749882c225e8647df3a3417507c6f5b27' + expect(pubKeyHash.length).toEqual(38) + + try { + P2WSH.to('regtest', pubKeyHash) + throw new Error('should had failed') + } catch (e) { + expect(e.message).toEqual('InvalidScriptHashLength') + } + }) + }) + + describe('getScript()', () => { + it('should refuse to build ops code stack for invalid address', () => { + const invalid = DeFiAddress.from('testnet', p2wshFixture.mainnet) + expect(invalid.valid).toBeFalsy() + try { + invalid.getScript() + } catch (e) { + expect(e.message).toBe('InvalidDefiAddress') + } + }) + + it('should be able to build script', async () => { + const p2wsh = DeFiAddress.from('mainnet', p2wshFixture.mainnet) + const scriptStack = p2wsh.getScript() + + expect(scriptStack.stack.length).toEqual(2) + expect(scriptStack.stack[0]).toEqual(OP_CODES.OP_0) + expect(scriptStack.stack[1].type).toEqual('OP_PUSHDATA') + }) + }) + + it('validate()', () => { + const data = '9e1be07558ea5cc8e02ed1d80c0911048afad949affa36d5c3951e3159dbea19' // 32 bytes + + const p2wsh = new P2WSH(RegTest, p2wshFixture.regtest, data) + expect(p2wsh.validatorPassed).toEqual(0) + expect(p2wsh.valid).toBeFalsy() + + const isValid = p2wsh.validate() + // expect(p2wsh.validatorPassed).toEqual(4) // length, network prefix, data character set + expect(isValid).toBeTruthy() + }) + + it('guess()', () => { + const p2wsh = DeFiAddress.guess(p2wshFixture.mainnet) + expect(p2wsh.valid).toBeTruthy() + expect(p2wsh.type).toBe('P2WSH') + expect(p2wsh.constructor.name).toBe('P2WSH') + expect(p2wsh.network).toBe(MainNet) + }) +}) diff --git a/packages/jellyfish-wallet-whale/jest.config.js b/packages/jellyfish-address/jest.config.js similarity index 100% rename from packages/jellyfish-wallet-whale/jest.config.js rename to packages/jellyfish-address/jest.config.js diff --git a/packages/jellyfish-wallet-whale/package.json b/packages/jellyfish-address/package.json similarity index 84% rename from packages/jellyfish-wallet-whale/package.json rename to packages/jellyfish-address/package.json index 06c79a112d..b272575e7a 100644 --- a/packages/jellyfish-wallet-whale/package.json +++ b/packages/jellyfish-address/package.json @@ -1,6 +1,6 @@ { "private": false, - "name": "@defichain/jellyfish-wallet-whale", + "name": "@defichain/jellyfish-address", "version": "0.0.0", "description": "A collection of TypeScript + JavaScript tools and libraries for DeFi Blockchain developers to build decentralized finance on Bitcoin", "keywords": [ @@ -37,6 +37,10 @@ "dependencies": { "@defichain/jellyfish-crypto": "0.0.0", "@defichain/jellyfish-network": "0.0.0", - "@defichain/jellyfish-wallet": "0.0.0" + "@defichain/jellyfish-transaction": "0.0.0", + "bs58": "^4.0.1" + }, + "devDependencies": { + "@types/bs58": "^4.0.1" } } diff --git a/packages/jellyfish-address/src/address.ts b/packages/jellyfish-address/src/address.ts new file mode 100644 index 0000000000..d9369e42b2 --- /dev/null +++ b/packages/jellyfish-address/src/address.ts @@ -0,0 +1,58 @@ +import { Network } from '@defichain/jellyfish-network' +import { Script } from '@defichain/jellyfish-transaction' + +export type AddressType = 'Unknown' | 'P2PKH' | 'P2SH' | 'P2WPKH' | 'P2WSH' +export type Validator = () => boolean + +export abstract class Address { + network: Network + utf8String: string + type: AddressType + valid: boolean + validatorPassed: number + + constructor (network: Network, utf8String: string, valid: boolean, type: AddressType) { + this.network = network + this.utf8String = utf8String + this.valid = valid + this.type = type + this.validatorPassed = 0 + } + + abstract validators (): Validator[] + abstract getScript (): Script + + validate (): boolean { + this.valid = true + this.validatorPassed = 0 + this.validators().forEach((validator, index) => { + const passed = validator() + this.valid = this.valid && passed + if (passed) { + this.validatorPassed += 1 + } + }) + return this.valid + } +} + +/** + * Default Address implementation when parsed address do not matched any type + */ +export class UnknownTypeAddress extends Address { + constructor (network: Network, raw: string) { + super(network, raw, false, 'Unknown') + } + + validators (): Validator[] { + return [] + } + + validate (): boolean { + return false + } + + getScript (): Script { + throw new Error('InvalidDeFiAddress') + } +} diff --git a/packages/jellyfish-address/src/base58_address.ts b/packages/jellyfish-address/src/base58_address.ts new file mode 100644 index 0000000000..55a47c4c81 --- /dev/null +++ b/packages/jellyfish-address/src/base58_address.ts @@ -0,0 +1,60 @@ +import { Bs58 } from '@defichain/jellyfish-crypto' +import { Network } from '@defichain/jellyfish-network' +import { Address, AddressType, Validator } from './address' + +export abstract class Base58Address extends Address { + static MIN_LENGTH = 26 + static MAX_LENGTH = 35 + + // 20 bytes data + hex: string + static DATA_HEX_LENGTH = 40 // hex char count + + constructor (network: Network, utf8String: string, hex: string, valid: boolean, type: AddressType) { + super(network, utf8String, valid, type) + this.hex = hex + } + + abstract getPrefix (): number + + validators (): Validator[] { + return [ + () => (this.utf8String.length >= Base58Address.MIN_LENGTH), + () => (this.utf8String.length <= Base58Address.MAX_LENGTH), + () => { + const charset = '[1-9A-HJ-NP-Za-km-z]' + return new RegExp(`${charset}{${this.utf8String.length}}$`).test(this.utf8String) + }, + () => { + try { + const { prefix } = Bs58.toHash160(this.utf8String) // built in checksum check + return prefix === this.getPrefix() + } catch (e) { + return false + } + }, + () => { + try { + const { buffer } = Bs58.toHash160(this.utf8String) // built in checksum check + return buffer.toString('hex') === this.hex + } catch (e) { + return false + } + } + ] + } + + getPrefixString (): string { + return Buffer.from([this.getPrefix()]).toString('hex') + } + + static fromAddress (network: Network, utf8String: string, AddressClass: new (...a: any[]) => T): T { + try { + const { buffer } = Bs58.toHash160(utf8String) + return new AddressClass(network, utf8String, buffer.toString('hex')) + } catch (e) { + // non b58 string, invalid address + return new AddressClass(network, utf8String, '', false, 'Unknown') + } + } +} diff --git a/packages/jellyfish-address/src/bech32_address.ts b/packages/jellyfish-address/src/bech32_address.ts new file mode 100644 index 0000000000..6366f546af --- /dev/null +++ b/packages/jellyfish-address/src/bech32_address.ts @@ -0,0 +1,50 @@ +import { Network } from '@defichain/jellyfish-network' +import { bech32 } from 'bech32' +import { Address, AddressType, Validator } from './address' + +export abstract class Bech32Address extends Address { + static MAX_LENGTH = 90 + static MAX_HUMAN_READABLE_LENGTH = 83 + + constructor (network: Network, utf8String: string, valid: boolean, addressType: AddressType) { + super(network, utf8String.toLowerCase(), valid, addressType) + } + + validators (): Validator[] { + return [ + () => (new RegExp(`^${this.getHrp()}`).test(this.utf8String)), + () => { + const charset = '[02-9ac-hj-np-z]' // 0-9, a-z, and reject: [1, b, i, o] + const arr = this.utf8String.split('1') + const excludeHrp = arr[arr.length - 1] + const regex = new RegExp(`${charset}{${excludeHrp.length}}$`) + return regex.test(excludeHrp) + } + ] + } + + getHrp (): string { + return this.network.bech32.hrp + } + + static fromAddress(network: Network, raw: string, AddressClass: new (...a: any[]) => T): T { + let valid: boolean + let prefix: string + let data: string = '' + try { + const decoded = bech32.decode(raw) + valid = true + prefix = decoded.prefix + const trimmedVersion = decoded.words.slice(1) + data = Buffer.from(bech32.fromWords(trimmedVersion)).toString('hex') + + if (prefix !== network.bech32.hrp) { + valid = false + } + } catch (e) { + valid = false + } + + return new AddressClass(network, raw, data, valid) + } +} diff --git a/packages/jellyfish-address/src/index.ts b/packages/jellyfish-address/src/index.ts new file mode 100644 index 0000000000..70793817ac --- /dev/null +++ b/packages/jellyfish-address/src/index.ts @@ -0,0 +1,81 @@ +import { getNetwork, NetworkName } from '@defichain/jellyfish-network' +import { Address, AddressType, UnknownTypeAddress } from './address' +import { Base58Address } from './base58_address' +import { Bech32Address } from './bech32_address' +import { P2PKH } from './p2pkh' +import { P2SH } from './p2sh' +import { P2WSH } from './p2wsh' +import { P2WPKH } from './p2wpkh' + +export * from './address' +export * from './base58_address' +export * from './bech32_address' +export * from './p2pkh' +export * from './p2sh' +export * from './p2wpkh' +export * from './p2wsh' + +/** + * When insist to use the "network" decoded from raw address, instead of passing one based on running application environment + * @param address raw human readable address (utf-8) + * @returns DefiAddress or a child class + */ +function guess (address: string): Address { + const networks: NetworkName[] = ['mainnet', 'testnet', 'regtest'] + const defaultOne = new UnknownTypeAddress(getNetwork('mainnet'), address) + for (let i = 0; i < networks.length; i += 1) { + const guessed = from(networks[i], address) + if (guessed.valid) { + return guessed + } + } + return defaultOne +} + +/** + * @param net to be validated against the decoded one from the raw address + * @param address raw human readable address (utf-8) + * @returns DefiAddress or a child class + */ +function from (net: NetworkName, address: string): T { + const network = getNetwork(net) + const possible: Map = new Map() + possible.set('Unknown', new UnknownTypeAddress(network, address)) + possible.set('P2PKH', Base58Address.fromAddress(network, address, P2PKH)) + possible.set('P2SH', Base58Address.fromAddress(network, address, P2SH)) + possible.set('P2WPKH', Bech32Address.fromAddress(network, address, P2WPKH)) + possible.set('P2WSH', Bech32Address.fromAddress(network, address, P2WSH)) + + possible.forEach(each => each.validate()) + + let valid + possible.forEach(each => { + if (each.valid) { + valid = each + } + }) + + /* eslint-disable @typescript-eslint/strict-boolean-expressions */ + if (valid) { + // find if any has all validator passed + return valid + } + + // else select the closest guess (most validator passed) + // default, when non have validator passed + let highestKey: AddressType = 'Unknown' + let highestCount = 0 + + possible.forEach((val, key) => { + if (val.validatorPassed > highestCount) { + highestKey = key + highestCount = val.validatorPassed + } + }) + return (possible.get(highestKey) as T) +} + +export const DeFiAddress = { + guess, + from +} diff --git a/packages/jellyfish-address/src/p2pkh.ts b/packages/jellyfish-address/src/p2pkh.ts new file mode 100644 index 0000000000..f8c4a1b0eb --- /dev/null +++ b/packages/jellyfish-address/src/p2pkh.ts @@ -0,0 +1,45 @@ +import { Bs58 } from '@defichain/jellyfish-crypto' +import { getNetwork, Network, NetworkName } from '@defichain/jellyfish-network' +import { Script, OP_CODES, OP_PUSHDATA } from '@defichain/jellyfish-transaction' + +import { Base58Address } from './base58_address' + +export class P2PKH extends Base58Address { + constructor (network: Network, utf8String: string, hex: string, validated: boolean = false) { + super(network, utf8String, hex, validated, 'P2PKH') + } + + getPrefix (): number { + return this.network.pubKeyHashPrefix + } + + getScript (): Script { + if (!this.valid) { + this.validate() + } + + if (!this.valid) { + throw new Error('InvalidDefiAddress') + } + + return { + stack: [ + OP_CODES.OP_DUP, + OP_CODES.OP_HASH160, + new OP_PUSHDATA(Buffer.from(this.hex, 'hex'), 'little'), + OP_CODES.OP_EQUALVERIFY, + OP_CODES.OP_CHECKSIG + ] + } + } + + static to (net: NetworkName | Network, h160: string): P2PKH { + if (h160.length !== Base58Address.DATA_HEX_LENGTH) { + throw new Error('InvalidDataLength') + } + + const network = typeof net === 'string' ? getNetwork(net) : net + const address = Bs58.fromHash160(h160, network.pubKeyHashPrefix) + return new P2PKH(network, address, h160, true) + } +} diff --git a/packages/jellyfish-address/src/p2sh.ts b/packages/jellyfish-address/src/p2sh.ts new file mode 100644 index 0000000000..deaf56842c --- /dev/null +++ b/packages/jellyfish-address/src/p2sh.ts @@ -0,0 +1,45 @@ +import { Bs58 } from '@defichain/jellyfish-crypto' +import { getNetwork, Network, NetworkName } from '@defichain/jellyfish-network' +import { Script, OP_CODES, OP_PUSHDATA } from '@defichain/jellyfish-transaction' + +import { Base58Address } from './base58_address' + +export class P2SH extends Base58Address { + static SCRIPT_HASH_LENGTH = 50 // 25 bytes, 50 char + + constructor (network: Network, utf8String: string, hex: string, validated: boolean = false) { + super(network, utf8String, hex, validated, 'P2SH') + } + + getPrefix (): number { + return this.network.scriptHashPrefix + } + + getScript (): Script { + if (!this.valid) { + this.validate() + } + + if (!this.valid) { + throw new Error('InvalidDefiAddress') + } + + return { + stack: [ + OP_CODES.OP_HASH160, + new OP_PUSHDATA(Buffer.from(this.hex, 'hex'), 'little'), + OP_CODES.OP_EQUAL + ] + } + } + + static to (net: NetworkName | Network, h160: string): P2SH { + if (h160.length !== Base58Address.DATA_HEX_LENGTH) { + throw new Error('InvalidDataLength') + } + + const network = typeof net === 'string' ? getNetwork(net) : net + const address = Bs58.fromHash160(h160, network.scriptHashPrefix) + return new P2SH(network, address, h160, true) + } +} diff --git a/packages/jellyfish-address/src/p2wpkh.ts b/packages/jellyfish-address/src/p2wpkh.ts new file mode 100644 index 0000000000..6b0d893263 --- /dev/null +++ b/packages/jellyfish-address/src/p2wpkh.ts @@ -0,0 +1,71 @@ +import { bech32 } from 'bech32' +import { getNetwork, Network, NetworkName } from '@defichain/jellyfish-network' +import { Script, OP_CODES, OP_PUSHDATA } from '@defichain/jellyfish-transaction' + +import { Bech32Address } from './bech32_address' +import { Validator } from './address' + +export class P2WPKH extends Bech32Address { + static SAMPLE = 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq' + static LENGTH_EXCLUDE_HRP = 39 // exclude hrp and separator + + // 20 bytes, data only, 40 char + pubKeyHash: string + static PUB_KEY_HASH_LENGTH = 40 + + constructor (network: Network, utf8String: string, pubKeyHash: string, validated: boolean = false) { + super(network, utf8String, validated, 'P2WPKH') + this.pubKeyHash = pubKeyHash + } + + validators (): Validator[] { + const rawAdd = this.utf8String + return [ + ...super.validators(), + () => (rawAdd.length <= P2WPKH.LENGTH_EXCLUDE_HRP + this.getHrp().length + 1), + () => (rawAdd.length === P2WPKH.LENGTH_EXCLUDE_HRP + this.getHrp().length + 1), + () => (this.pubKeyHash.length === P2WPKH.PUB_KEY_HASH_LENGTH) + ] + } + + getHrp (): string { + return this.network.bech32.hrp + } + + getScript (): Script { + if (!this.valid) { + this.validate() + } + + if (!this.valid) { + throw new Error('InvalidDefiAddress') + } + + return { + stack: [ + OP_CODES.OP_0, + new OP_PUSHDATA(Buffer.from(this.pubKeyHash, 'hex'), 'little') + ] + } + } + + /** + * @param net network + * @param hex data, public key hash (20 bytes, 40 characters) + * @param witnessVersion default 0 + * @returns + */ + static to (net: Network | NetworkName, h160: string, witnessVersion = 0x00): P2WPKH { + const network: Network = typeof net === 'string' ? getNetwork(net) : net + + if (h160.length !== P2WPKH.PUB_KEY_HASH_LENGTH) { + throw new Error('InvalidPubKeyHashLength') + } + + const numbers = Buffer.from(h160, 'hex') + const fiveBitsWords = bech32.toWords(numbers) + const includeVersion = [witnessVersion, ...fiveBitsWords] + const utf8 = bech32.encode(network.bech32.hrp, includeVersion) + return new P2WPKH(network, utf8, h160, true) + } +} diff --git a/packages/jellyfish-address/src/p2wsh.ts b/packages/jellyfish-address/src/p2wsh.ts new file mode 100644 index 0000000000..984cf9fa12 --- /dev/null +++ b/packages/jellyfish-address/src/p2wsh.ts @@ -0,0 +1,67 @@ +import { bech32 } from 'bech32' +import { getNetwork, Network, NetworkName } from '@defichain/jellyfish-network' +import { Script, OP_CODES, OP_PUSHDATA } from '@defichain/jellyfish-transaction' + +import { Bech32Address } from './bech32_address' +import { Validator } from './address' + +export class P2WSH extends Bech32Address { + // the raw utf8, eg bc1... + // supposed to be 62, regtest prefix is longer + static MAX_LENGTH = 64 + + // 32 bytes, data only, 64 char + data: string + static SCRIPT_HASH_LENGTH = 64 + + constructor (network: Network, utf8String: string, data: string, validated: boolean = false) { + super(network, utf8String, validated, 'P2WSH') + this.data = data + } + + // bcrt1ncd7qa2cafwv3cpw68vqczg3qj904k2f4lard4wrj50rzkwmagvs3ttd5f + validators (): Validator[] { + return [ + ...super.validators(), + () => (this.utf8String.length <= P2WSH.MAX_LENGTH), + () => (this.data.length === P2WSH.SCRIPT_HASH_LENGTH) + ] + } + + getScript (): Script { + if (!this.valid) { + this.validate() + } + + if (!this.valid) { + throw new Error('InvalidDefiAddress') + } + + return { + stack: [ + OP_CODES.OP_0, + new OP_PUSHDATA(Buffer.from(this.data, 'hex'), 'little') + ] + } + } + + /** + * @param net network + * @param hex data, redeem script (32 bytes, 64 characters) + * @param witnessVersion default 0 + * @returns + */ + static to (net: Network | NetworkName, hex: string, witnessVersion = 0x00): P2WSH { + const network: Network = typeof net === 'string' ? getNetwork(net) : net + + if (hex.length !== P2WSH.SCRIPT_HASH_LENGTH) { + throw new Error('InvalidScriptHashLength') + } + + const numbers = Buffer.from(hex, 'hex') + const fiveBitsWords = bech32.toWords(numbers) + const includeVersion = [witnessVersion, ...fiveBitsWords] + const utf8 = bech32.encode(network.bech32.hrp, includeVersion) + return new P2WSH(network, utf8, hex, true) + } +} diff --git a/packages/jellyfish-wallet-whale/tsconfig.json b/packages/jellyfish-address/tsconfig.json similarity index 100% rename from packages/jellyfish-wallet-whale/tsconfig.json rename to packages/jellyfish-address/tsconfig.json diff --git a/packages/jellyfish-api-core/__tests__/category/account.test.ts b/packages/jellyfish-api-core/__tests__/category/account.test.ts index 5bb58dd16f..6014deefc3 100644 --- a/packages/jellyfish-api-core/__tests__/category/account.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/account.test.ts @@ -2,6 +2,7 @@ import { MasterNodeRegTestContainer } from '@defichain/testcontainers' import { ContainerAdapterClient } from '../container_adapter_client' import waitForExpect from 'wait-for-expect' import BigNumber from 'bignumber.js' +import { UtxosToAccountPayload } from '../../src/category/account' describe('masternode', () => { const container = new MasterNodeRegTestContainer() @@ -11,7 +12,6 @@ describe('masternode', () => { await container.start() await container.waitForReady() await container.waitForWalletCoinbaseMaturity() - await container.waitForWalletBalanceGTE(300) await setup() }) @@ -21,8 +21,6 @@ describe('masternode', () => { }) async function setup (): Promise { - await container.generate(100) - const from = await container.call('getnewaddress') await createToken(from, 'DBTC', 200) @@ -42,18 +40,19 @@ describe('masternode', () => { tradeable: true, collateralAddress: address } + await container.waitForWalletBalanceGTE(101) await container.call('createtoken', [metadata]) - await container.generate(25) + await container.generate(1) await container.call('minttokens', [`${amount.toString()}@${symbol}`]) - await container.generate(25) + await container.generate(1) } async function accountToAccount (symbol: string, amount: number, from: string, _to = ''): Promise { const to = _to !== '' ? _to : await container.call('getnewaddress') await container.call('accounttoaccount', [from, { [to]: `${amount.toString()}@${symbol}` }]) - await container.generate(25) + await container.generate(1) return to } @@ -414,8 +413,12 @@ describe('masternode', () => { it('should listAccountHistory with options depth', async () => { await waitForExpect(async () => { const depth = 10 + const height = await container.getBlockCount() const accountHistories = await client.account.listAccountHistory('mine', { depth }) - expect(accountHistories.length).toBe(depth + 1) // plus 1 to include zero index + + for (const accountHistory of accountHistories) { + expect(accountHistory.blockHeight).toBeGreaterThanOrEqual(height - depth) + } }) }) @@ -479,4 +482,38 @@ describe('masternode', () => { }) }) }) + + describe('utxosToAccount', () => { + it('should utxosToAccount', async () => { + const payload: UtxosToAccountPayload = {} + // NOTE(jingyi2811): Only support sending utxos to DFI account. + payload[await container.getNewAddress()] = '5@DFI' + payload[await container.getNewAddress()] = '5@DFI' + + const data = await client.account.utxosToAccount(payload) + + expect(typeof data).toBe('string') + expect(data.length).toBe(64) + }) + + it('should utxosToAccount with utxos', async () => { + const payload: UtxosToAccountPayload = {} + // NOTE(jingyi2811): Only support sending utxos to DFI account. + payload[await container.getNewAddress()] = '5@DFI' + payload[await container.getNewAddress()] = '5@DFI' + + const utxos = await container.call('listunspent') + const inputs = utxos.map((utxo: { txid: string, vout: number }) => { + return { + txid: utxo.txid, + vout: utxo.vout + } + }) + + const data = await client.account.utxosToAccount(payload, inputs) + + expect(typeof data).toBe('string') + expect(data.length).toBe(64) + }) + }) }) diff --git a/packages/jellyfish-api-core/__tests__/category/blockchain.test.ts b/packages/jellyfish-api-core/__tests__/category/blockchain.test.ts index 1e69059553..17239616b8 100644 --- a/packages/jellyfish-api-core/__tests__/category/blockchain.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/blockchain.test.ts @@ -1,4 +1,4 @@ -import { RegTestContainer, MasterNodeRegTestContainer } from '@defichain/testcontainers' +import { MasterNodeRegTestContainer, RegTestContainer } from '@defichain/testcontainers' import { ContainerAdapterClient } from '../container_adapter_client' import waitForExpect from 'wait-for-expect' import { BigNumber, blockchain, wallet } from '../../src' @@ -235,66 +235,83 @@ describe('masternode', () => { }) }) - describe('getRawMempool', () => { - let transactionId = '' + describe('getChainTips', () => { + it('should getChainTips', async () => { + const chainTips: blockchain.ChainTip[] = await client.blockchain.getChainTips() + for (let i = 0; i < chainTips.length; i += 1) { + const data = chainTips[i] + expect(data.height).toBeGreaterThan(0) + expect(typeof data.hash).toBe('string') + expect(data.hash.length).toBe(64) + expect(data.branchlen).toBeGreaterThanOrEqual(0) + expect(['invalid', 'headers-only', 'valid-headers', 'valid-fork', 'active'].includes(data.status)).toBe(true) + } + }) + }) + describe('getRawMempool', () => { beforeAll(async () => { await client.wallet.setWalletFlag(wallet.WalletFlag.AVOID_REUSE) - transactionId = await client.wallet.sendToAddress('mwsZw8nF7pKxWH8eoKL9tPxTpaFkz7QeLU', 0.00001) }) it('should getRawMempool and return array of transaction ids', async () => { - const rawMempool: string[] = await client.blockchain.getRawMempool(false) + await waitForExpect(async () => { + await client.wallet.sendToAddress('mwsZw8nF7pKxWH8eoKL9tPxTpaFkz7QeLU', 0.00001) - expect(rawMempool.length > 0).toBe(true) - expect(typeof rawMempool[0]).toBe('string') + const rawMempool: string[] = await client.blockchain.getRawMempool(false) + expect(rawMempool.length > 0).toBe(true) + expect(typeof rawMempool[0]).toBe('string') + }, 10000) }) it('should getRawMempool and return json object', async () => { - const rawMempool: blockchain.MempoolTx = await client.blockchain.getRawMempool(true) - - const data = rawMempool[transactionId] - expect(data.fees.base instanceof BigNumber).toBe(true) - expect(data.fees.modified instanceof BigNumber).toBe(true) - expect(data.fees.ancestor instanceof BigNumber).toBe(true) - expect(data.fees.descendant instanceof BigNumber).toBe(true) - expect(data.fees.base.isGreaterThan(new BigNumber('0'))).toBe(true) - expect(data.fees.modified.isGreaterThan(new BigNumber('0'))).toBe(true) - expect(data.fees.ancestor.isGreaterThan(new BigNumber('0'))).toBe(true) - expect(data.fees.descendant.isGreaterThan(new BigNumber('0'))).toBe(true) - - expect(data.fee instanceof BigNumber).toBe(true) - expect(data.fee.isGreaterThan(new BigNumber('0'))).toBe(true) - expect(data.modifiedfee instanceof BigNumber).toBe(true) - expect(data.modifiedfee.isGreaterThan(new BigNumber('0'))).toBe(true) - - expect(data.vsize instanceof BigNumber).toBe(true) - expect(data.weight instanceof BigNumber).toBe(true) - expect(data.height instanceof BigNumber).toBe(true) - expect(data.time instanceof BigNumber).toBe(true) - expect(data.vsize.isGreaterThan(new BigNumber('0'))).toBe(true) - expect(data.weight.isGreaterThan(new BigNumber('0'))).toBe(true) - expect(data.height.isGreaterThan(new BigNumber('0'))).toBe(true) - expect(data.time.isGreaterThan(new BigNumber('0'))).toBe(true) - - expect(typeof data.wtxid).toBe('string') - expect(data.depends.length >= 0).toBe(true) - expect(data.spentby.length >= 0).toBe(true) - expect(data['bip125-replaceable']).toBe(false) - - expect(data.descendantcount instanceof BigNumber).toBe(true) - expect(data.descendantsize instanceof BigNumber).toBe(true) - expect(data.descendantfees instanceof BigNumber).toBe(true) - expect(data.descendantcount.isGreaterThan(new BigNumber('0'))).toBe(true) - expect(data.descendantsize.isGreaterThan(new BigNumber('0'))).toBe(true) - expect(data.descendantfees.isGreaterThan(new BigNumber('0'))).toBe(true) - - expect(data.ancestorcount instanceof BigNumber).toBe(true) - expect(data.ancestorsize instanceof BigNumber).toBe(true) - expect(data.ancestorfees instanceof BigNumber).toBe(true) - expect(data.ancestorcount.isGreaterThan(new BigNumber('0'))).toBe(true) - expect(data.ancestorsize.isGreaterThan(new BigNumber('0'))).toBe(true) - expect(data.ancestorfees.isGreaterThan(new BigNumber('0'))).toBe(true) + await waitForExpect(async () => { + const transactionId = await client.wallet.sendToAddress('mwsZw8nF7pKxWH8eoKL9tPxTpaFkz7QeLU', 0.00001) + const rawMempool: blockchain.MempoolTx = await client.blockchain.getRawMempool(true) + + const data = rawMempool[transactionId] + expect(data.fees.base instanceof BigNumber).toBe(true) + expect(data.fees.modified instanceof BigNumber).toBe(true) + expect(data.fees.ancestor instanceof BigNumber).toBe(true) + expect(data.fees.descendant instanceof BigNumber).toBe(true) + expect(data.fees.base.isGreaterThan(new BigNumber('0'))).toBe(true) + expect(data.fees.modified.isGreaterThan(new BigNumber('0'))).toBe(true) + expect(data.fees.ancestor.isGreaterThan(new BigNumber('0'))).toBe(true) + expect(data.fees.descendant.isGreaterThan(new BigNumber('0'))).toBe(true) + + expect(data.fee instanceof BigNumber).toBe(true) + expect(data.fee.isGreaterThan(new BigNumber('0'))).toBe(true) + expect(data.modifiedfee instanceof BigNumber).toBe(true) + expect(data.modifiedfee.isGreaterThan(new BigNumber('0'))).toBe(true) + + expect(data.vsize instanceof BigNumber).toBe(true) + expect(data.weight instanceof BigNumber).toBe(true) + expect(data.height instanceof BigNumber).toBe(true) + expect(data.time instanceof BigNumber).toBe(true) + expect(data.vsize.isGreaterThan(new BigNumber('0'))).toBe(true) + expect(data.weight.isGreaterThan(new BigNumber('0'))).toBe(true) + expect(data.height.isGreaterThan(new BigNumber('0'))).toBe(true) + expect(data.time.isGreaterThan(new BigNumber('0'))).toBe(true) + + expect(typeof data.wtxid).toBe('string') + expect(data.depends.length >= 0).toBe(true) + expect(data.spentby.length >= 0).toBe(true) + expect(data['bip125-replaceable']).toBe(false) + + expect(data.descendantcount instanceof BigNumber).toBe(true) + expect(data.descendantsize instanceof BigNumber).toBe(true) + expect(data.descendantfees instanceof BigNumber).toBe(true) + expect(data.descendantcount.isGreaterThan(new BigNumber('0'))).toBe(true) + expect(data.descendantsize.isGreaterThan(new BigNumber('0'))).toBe(true) + expect(data.descendantfees.isGreaterThan(new BigNumber('0'))).toBe(true) + + expect(data.ancestorcount instanceof BigNumber).toBe(true) + expect(data.ancestorsize instanceof BigNumber).toBe(true) + expect(data.ancestorfees instanceof BigNumber).toBe(true) + expect(data.ancestorcount.isGreaterThan(new BigNumber('0'))).toBe(true) + expect(data.ancestorsize.isGreaterThan(new BigNumber('0'))).toBe(true) + expect(data.ancestorfees.isGreaterThan(new BigNumber('0'))).toBe(true) + }) }) }) }) diff --git a/packages/jellyfish-api-core/__tests__/category/mining.test.ts b/packages/jellyfish-api-core/__tests__/category/mining.test.ts index 307d9fefb9..17471d7d39 100644 --- a/packages/jellyfish-api-core/__tests__/category/mining.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/mining.test.ts @@ -80,4 +80,34 @@ describe('masternode', () => { expect(result).toBeGreaterThan(0) }) }) + + it('should getMiningInfo', async () => { + await waitForExpect(async () => { + const info = await client.mining.getMiningInfo() + await expect(info.blocks).toBeGreaterThan(1) + }) + + const info = await client.mining.getMiningInfo() + const mn1 = info.masternodes[0] + + expect(info.blocks).toBeGreaterThan(0) + + expect(info.currentblockweight).toBeGreaterThan(0) + expect(info.currentblocktx).toBe(0) + + expect(info.difficulty).toBeDefined() + expect(info.isoperator).toBe(true) + + expect(mn1.masternodeid).toBeDefined() + expect(mn1.masternodeoperator).toBeDefined() + expect(mn1.masternodestate).toBe('ENABLED') + expect(mn1.generate).toBe(true) + expect(mn1.mintedblocks).toBe(0) + expect(mn1.lastblockcreationattempt).toBe('0') + + expect(info.networkhashps).toBeGreaterThan(0) + expect(info.pooledtx).toBe(0) + expect(info.chain).toBe('regtest') + expect(info.warnings).toBe('') + }) }) diff --git a/packages/jellyfish-api-core/__tests__/category/poolpair.test.ts b/packages/jellyfish-api-core/__tests__/category/poolpair.test.ts index 9809b8d72f..9fc91bfe79 100644 --- a/packages/jellyfish-api-core/__tests__/category/poolpair.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/poolpair.test.ts @@ -54,7 +54,7 @@ describe('masternode', () => { await container.call('utxostoaccount', [payload]) await container.call('minttokens', [`2000@${symbol}`]) - await container.generate(25) + await container.generate(1) } describe('addPoolLiquidity', () => { @@ -80,7 +80,7 @@ describe('masternode', () => { const tokenBAddress = await container.call('getnewaddress') await container.call('sendtokenstoaddress', [{}, { [tokenAAddress]: ['10@DFI'] }]) await container.call('sendtokenstoaddress', [{}, { [tokenBAddress]: ['200@DDAI'] }]) - await container.generate(25) + await container.generate(1) const shareAddress = await container.call('getnewaddress') const data = await client.poolpair.addPoolLiquidity({ @@ -97,13 +97,13 @@ describe('masternode', () => { const tokenBAddress = await container.call('getnewaddress') await container.call('sendtokenstoaddress', [{}, { [tokenAAddress]: ['10@DFI'] }]) await container.call('sendtokenstoaddress', [{}, { [tokenBAddress]: ['200@DDAI'] }]) - await container.generate(25) + await container.generate(1) const txid = await container.call('sendmany', ['', { [tokenAAddress]: 10, [tokenBAddress]: 20 }]) - await container.generate(2) + await container.generate(1) const utxos = await container.call('listunspent') const inputs = utxos.filter((utxo: any) => utxo.txid === txid).map((utxo: any) => { diff --git a/packages/jellyfish-api-core/__tests__/category/rawtx.test.ts b/packages/jellyfish-api-core/__tests__/category/rawtx.test.ts index fce0102549..539579c0ce 100644 --- a/packages/jellyfish-api-core/__tests__/category/rawtx.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/rawtx.test.ts @@ -115,7 +115,7 @@ describe('signRawTransactionWithKey', () => { expect(signed.complete).toBe(true) expect(signed.hex.substr(0, 14)).toBe('04000000000101') - expect(signed.hex.substr(86, 82)).toBe('00ffffffff010065cd1d000000001600144ab4391ce5a732e36139e72d79a28e01b7b0803400024730') + expect(signed.hex.substr(86, 78)).toBe('00ffffffff010065cd1d000000001600144ab4391ce5a732e36139e72d79a28e01b7b080340002') expect(signed.hex).toContain('012103987aec2e508e124468f0f07a836d185b329026e7aaf75be48cf12be8f18cbe8100000000') }) @@ -135,7 +135,7 @@ describe('signRawTransactionWithKey', () => { expect(signed.complete).toBe(true) expect(signed.hex.substr(0, 14)).toBe('04000000000101') - expect(signed.hex.substr(86, 146)).toBe('00ffffffff020065cd1d000000001600144ab4391ce5a732e36139e72d79a28e01b7b080340080ce341d0000000016001425a544c073cbca4e88d59f95ccd52e584c7e6a8200024730') + expect(signed.hex.substr(86, 142)).toBe('00ffffffff020065cd1d000000001600144ab4391ce5a732e36139e72d79a28e01b7b080340080ce341d0000000016001425a544c073cbca4e88d59f95ccd52e584c7e6a820002') expect(signed.hex).toContain('012103987aec2e508e124468f0f07a836d185b329026e7aaf75be48cf12be8f18cbe8100000000') }) diff --git a/packages/jellyfish-api-core/__tests__/category/token.test.ts b/packages/jellyfish-api-core/__tests__/category/token.test.ts index 4a55733f8a..36bc3013c9 100644 --- a/packages/jellyfish-api-core/__tests__/category/token.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/token.test.ts @@ -259,7 +259,7 @@ describe('masternode', () => { beforeAll(async () => { await createToken('DTEST') await createToken('DABC') - await container.generate(2) + await container.generate(1) const tokens = await client.token.listTokens() for (const k in tokens) { @@ -334,7 +334,7 @@ describe('masternode', () => { await createToken('DBTC', { isDAT: true }) await createToken('DNOTMINT', { mintable: false }) await createToken('DNOTTRAD', { tradeable: false }) - await container.generate(3) + await container.generate(1) }) it('should listTokens', async () => { diff --git a/packages/jellyfish-api-core/src/category/account.ts b/packages/jellyfish-api-core/src/category/account.ts index 3ea3d49567..6bd5fd11b8 100644 --- a/packages/jellyfish-api-core/src/category/account.ts +++ b/packages/jellyfish-api-core/src/category/account.ts @@ -2,10 +2,10 @@ import BigNumber from 'bignumber.js' import { ApiClient } from '../.' /** - * Single account ID (CScript or address) or reserved words, - * 'mine' to list history for all owned accounts or - * 'all' to list the whole DB - */ + * Single account ID (CScript or address) or reserved words, + * - 'mine' to list history for all owned accounts or + * - 'all' to list the whole DB + */ type OwnerType = 'mine' | 'all' | string /** @@ -204,7 +204,7 @@ export class Account { * * @param {OwnerType} [owner='mine'] single account ID (CScript or address) or reserved words 'mine' to list history for all owned accounts or 'all' to list whole DB * @param {AccountHistoryOptions} [options] - * @param {number} [options.maxBlockHeight] Optional height to iterate from (downto genesis block), (default = chaintip). + * @param {number} [options.maxBlockHeight] Optional height to iterate from (down to genesis block), (default = chaintip). * @param {number} [options.depth] Maximum depth, from the genesis block is the default * @param {boolean} [options.no_rewards] Filter out rewards * @param {string} [options.token] Filter by token @@ -220,10 +220,25 @@ export class Account { ): Promise { return await this.client.call('listaccounthistory', [owner, options], 'number') } + + /** + * Creates and submits to a connect node; a transfer transaction from the wallet UTXOs to a specified account. + * Optionally, specific UTXOs to spend to create that transaction. + * + * @param {UtxosToAccountPayload} payload + * @param {string} payload[address] + * @param {UtxosToAccountUTXO[]} [utxos=[]] + * @param {string} [utxos.txid] + * @param {number} [utxos.vout] + * @return {Promise} + */ + async utxosToAccount (payload: UtxosToAccountPayload, utxos: UtxosToAccountUTXO[] = []): Promise { + return await this.client.call('utxostoaccount', [payload, utxos], 'number') + } } export interface AccountPagination { - start?: string | number + start?: number including_start?: boolean limit?: number } @@ -277,3 +292,12 @@ export interface AccountHistoryOptions { txtype?: string limit?: number } + +export interface UtxosToAccountPayload { + [key: string]: string +} + +export interface UtxosToAccountUTXO { + txid: string + vout: number +} diff --git a/packages/jellyfish-api-core/src/category/blockchain.ts b/packages/jellyfish-api-core/src/category/blockchain.ts index ff850dabe4..18fc2df497 100644 --- a/packages/jellyfish-api-core/src/category/blockchain.ts +++ b/packages/jellyfish-api-core/src/category/blockchain.ts @@ -100,6 +100,16 @@ export class Blockchain { return await this.client.call('getblockheader', [hash, verbosity], 'number') } + /** + * Return information about all known tips in the block tree + * including the main chain as well as orphaned branches. + * + * @return {Promise} + */ + async getChainTips (): Promise { + return await this.client.call('getchaintips', [], 'number') + } + /** * Get details of unspent transaction output (UTXO). * @@ -259,6 +269,13 @@ export interface ScriptPubKey { addresses: string[] } +export interface ChainTip { + height: number + hash: string + branchlen: number + status: string +} + export interface MempoolTx { [key: string]: { vsize: BigNumber diff --git a/packages/jellyfish-api-core/src/category/mining.ts b/packages/jellyfish-api-core/src/category/mining.ts index 905ba607b0..0356445d02 100644 --- a/packages/jellyfish-api-core/src/category/mining.ts +++ b/packages/jellyfish-api-core/src/category/mining.ts @@ -24,10 +24,19 @@ export class Mining { /** * Get minting-related information * @return {Promise} + * @deprecated Prefer using getMiningInfo. */ async getMintingInfo (): Promise { return await this.client.call('getmintinginfo', [], 'number') } + + /** + * Get mining-related information, replaces deprecated getMintingInfo + * @return {Promise} + */ + async getMiningInfo (): Promise { + return await this.client.call('getmininginfo', [], 'number') + } } /** @@ -49,3 +58,31 @@ export interface MintingInfo { chain: 'main' | 'test' | 'regtest' | string warnings: string } + +/** + * Minting related information + */ +export interface MiningInfo { + blocks: number + currentblockweight?: number + currentblocktx?: number + difficulty: string + isoperator: boolean + masternodes: MasternodeInfo[] + networkhashps: number + pooledtx: number + chain: 'main' | 'test' | 'regtest' | string + warnings: string +} + +/** + * Masternode related information + */ +export interface MasternodeInfo { + masternodeid?: string + masternodeoperator?: string + masternodestate?: 'PRE_ENABLED' | 'ENABLED' | 'PRE_RESIGNED' | 'RESIGNED' | 'PRE_BANNED' | 'BANNED' + generate?: boolean + mintedblocks?: number + lastblockcreationattempt?: string +} diff --git a/packages/jellyfish-crypto/__tests__/bech32.test.ts b/packages/jellyfish-crypto/__tests__/bech32.test.ts index 472f7095ca..9a182b41a5 100644 --- a/packages/jellyfish-crypto/__tests__/bech32.test.ts +++ b/packages/jellyfish-crypto/__tests__/bech32.test.ts @@ -43,6 +43,13 @@ it('should convert pubkey to bech32', () => { expect(bech32).toBe(keypair.bech32) }) +it('should reject non 33 bytes long input (expected public key)', () => { + expect(() => { + // @ts-expect-error + Bech32.fromPubKey(Buffer.from(keypair.pubKey, 'hex').slice(1), 'bcrt', 0x01) + }).toThrow('InvalidPubKeyLength') +}) + it('should convert pubkey to bech32 with witness version', () => { const pubKey = Buffer.from(keypair.pubKey, 'hex') const bech32 = Bech32.fromPubKey(pubKey, keypair.hrp, 0x00) diff --git a/packages/jellyfish-crypto/__tests__/bs58.test.ts b/packages/jellyfish-crypto/__tests__/bs58.test.ts new file mode 100644 index 0000000000..fa857d47da --- /dev/null +++ b/packages/jellyfish-crypto/__tests__/bs58.test.ts @@ -0,0 +1,80 @@ +import { Bs58, HASH160 } from '../src' + +const fixture = { + base58: 'dGrLbw2nTo7de6tKF6cxCyiymarNaB1jFi', + prefix: 0x5a, + h160: '25a544c073cbca4e88d59f95ccd52e584c7e6a82' +} + +describe('toHash160()', () => { + it('should convert base58 to hash160 + prefix', () => { + const decoded = Bs58.toHash160(fixture.base58) + expect(decoded.prefix).toEqual(fixture.prefix) + expect(decoded.buffer.toString('hex')).toEqual(fixture.h160) + }) + + it('should reject invalid address, invalid charset', async () => { + // edited last char, invalida checksum + expect(() => { + Bs58.toHash160('dGrLbw2nTo7de6tKF6cxCyiymarNaB1jFj') + }).toThrow('InvalidBase58Address') + }) + + it('should reject invalid address, invalid charset', async () => { + expect(() => { + // edited, put 'O' invalid character into a normal valid address + Bs58.toHash160('dGrLbw2nTo7de6tKF6cxCyiymarNaB1jFO') + }).toThrow('Non-base58 character') + }) +}) + +describe('fromHash160()', () => { + it('should convert prefix + hash160 to base58', () => { + const address = Bs58.fromHash160(Buffer.from(fixture.h160, 'hex'), fixture.prefix) + expect(address).toEqual(fixture.base58) + }) + + it('should be able to take in hash160 string', () => { + const address = Bs58.fromHash160(fixture.h160, fixture.prefix) + expect(address).toEqual(fixture.base58) + }) + + it('should reject non 20 bytes long data', () => { + expect(() => { + Bs58.fromHash160(fixture.h160.substring(1), fixture.prefix) + }).toThrow('InvalidDataLength') + }) + + it('should reject version prefix > 255 (1 byte)', () => { + expect(() => { + Bs58.fromHash160(fixture.h160, 256) + }).toThrow('InvalidVersionPrefix') + }) +}) + +describe('fromPubKey()', () => { + const thirdyThreeBytes = '03987aec2e508e124468f0f07a836d185b329026e7aaf75be48cf12be8f18cbe81' + const pubKey = Buffer.from(thirdyThreeBytes, 'hex') + const invalidPubKey = Buffer.from(thirdyThreeBytes.slice(2), 'hex') // less 1 byte + + beforeAll(() => { + expect(HASH160(pubKey).toString('hex')).toEqual(fixture.h160) + }) + + it('should convert prefix + hash160 to base58', () => { + const address = Bs58.fromPubKey(pubKey, fixture.prefix) + expect(address).toEqual(fixture.base58) + }) + + it('should reject non 33 bytes long data', () => { + expect(() => { + Bs58.fromPubKey(invalidPubKey, fixture.prefix) + }).toThrow('InvalidPubKeyLength') + }) + + it('should reject version prefix > 255 (1 byte)', () => { + expect(() => { + Bs58.fromPubKey(pubKey, 256) + }).toThrow('InvalidVersionPrefix') + }) +}) diff --git a/packages/jellyfish-crypto/package.json b/packages/jellyfish-crypto/package.json index 4ae4c4ad96..8014e732d0 100644 --- a/packages/jellyfish-crypto/package.json +++ b/packages/jellyfish-crypto/package.json @@ -37,12 +37,14 @@ "dependencies": { "bech32": "^2.0.0", "bip66": "^1.1.5", + "bs58": "^4.0.1", "create-hash": "^1.2.0", "tiny-secp256k1": "^1.1.6", "wif": "^2.0.6" }, "devDependencies": { "@types/bech32": "^1.1.2", + "@types/bs58": "^4.0.1", "@types/create-hash": "^1.2.2", "@types/tiny-secp256k1": "^1.0.0", "@types/wif": "^2.0.2" diff --git a/packages/jellyfish-crypto/src/bech32.ts b/packages/jellyfish-crypto/src/bech32.ts index 8d21d8f279..2a255171b6 100644 --- a/packages/jellyfish-crypto/src/bech32.ts +++ b/packages/jellyfish-crypto/src/bech32.ts @@ -52,6 +52,9 @@ export const Bech32 = { * @return {string} bech32 encoded address */ fromPubKey (pubKey: Buffer, hrp: HRP, version: 0x00 = 0x00): string { + if (pubKey.length !== 33) { + throw new Error('InvalidPubKeyLength') + } const hash = HASH160(pubKey) return toBech32(hash, hrp, version) }, diff --git a/packages/jellyfish-crypto/src/bs58.ts b/packages/jellyfish-crypto/src/bs58.ts new file mode 100644 index 0000000000..5e56e7c8cf --- /dev/null +++ b/packages/jellyfish-crypto/src/bs58.ts @@ -0,0 +1,88 @@ +import bs58 from 'bs58' +import { SHA256, HASH160 } from './hash' + +export interface DecodedB58 { + buffer: Buffer + prefix: number +} + +function _checksum (twentyOneBytes: Buffer): Buffer { + return SHA256(SHA256(twentyOneBytes)).slice(0, 4) +} + +/** + * Decode a base58 address into 20 bytes data, for p2pkh and p2sh use + * @param {string} base58 33 to 35 characters string (utf8) + * @returns {DecodedB58} 20 bytes data + 1 byte prefix + */ +function toHash160 (base58: string): DecodedB58 { + const buffer = bs58.decode(base58) + if (buffer.length !== 25) { + throw new Error('InvalidBase58Address') + } + + const withPrefix = buffer.slice(0, 21) + const checksum = buffer.slice(21, 25) + + const expectedChecksum = _checksum(withPrefix) + if (checksum.compare(expectedChecksum) !== 0) { + throw new Error('InvalidBase58Address') + } + + return { + prefix: withPrefix[0], + buffer: withPrefix.slice(1, 21) + } +} + +/** + * To create Base58 address using 20 bytes data + prefix, for p2pkh and p2sh use + * @param {Buffer|string} data 20 bytes Buffer or 40 characters string + * @param {number} prefix max = 255 = 1 byte + * @returns Base58 address (in utf8) + */ +function fromHash160 (data: string | Buffer, prefix: number): string { + if (typeof data === 'string') { + // 40 hex char string only + if (data.length !== 40) { + throw new Error('InvalidDataLength') + } + } else if (data.length !== 20) { + // 20 bytes buffer only + throw new Error('InvalidDataLength') + } + + if (prefix > 255) { + throw new Error('InvalidVersionPrefix') + } + + const buffer = typeof data === 'string' ? Buffer.from(data, 'hex') : data + const withPrefix = Buffer.from([prefix, ...buffer]) + const checksum = _checksum(withPrefix) + return bs58.encode(Buffer.from([...withPrefix, ...checksum])) +} + +/** + * To create Base58 address using a raw 33 bytes (compressed) public key, for p2pkh and p2sh use + * @param {Buffer} pubKey 33 bytes long public key + * @param {number} prefix max = 255 = 1 byte + * @returns {string} base58 encoded string + */ +function fromPubKey (pubKey: Buffer, prefix: number): string { + if (pubKey.length !== 33) { + throw new Error('InvalidPubKeyLength') + } + + if (prefix > 255) { + throw new Error('InvalidVersionPrefix') + } + + const hash = HASH160(pubKey) + return fromHash160(hash, prefix) +} + +export const Bs58 = { + toHash160, + fromPubKey, + fromHash160 +} diff --git a/packages/jellyfish-crypto/src/index.ts b/packages/jellyfish-crypto/src/index.ts index d02d470a2b..bf916807d8 100644 --- a/packages/jellyfish-crypto/src/index.ts +++ b/packages/jellyfish-crypto/src/index.ts @@ -1,4 +1,5 @@ export * from './bech32' +export * from './bs58' export * from './der' export * from './elliptic' export * from './hash' diff --git a/packages/jellyfish-network/__tests__/index.test.ts b/packages/jellyfish-network/__tests__/index.test.ts index 82e23f1b50..93407807a8 100644 --- a/packages/jellyfish-network/__tests__/index.test.ts +++ b/packages/jellyfish-network/__tests__/index.test.ts @@ -8,19 +8,23 @@ it('should be exported', () => { describe('getNetwork', () => { it('should get mainnet', () => { + expect(getNetwork('mainnet').name).toBe('mainnet') expect(getNetwork('mainnet').bech32.hrp).toBe('df') }) it('should get testnet', () => { + expect(getNetwork('testnet').name).toBe('testnet') expect(getNetwork('testnet').bech32.hrp).toBe('tf') }) it('should get regtest', () => { + expect(getNetwork('regtest').name).toBe('regtest') expect(getNetwork('regtest').bech32.hrp).toBe('bcrt') }) }) it('should match MainNet network', () => { + expect(MainNet.name).toBe('mainnet') expect(MainNet.bech32.hrp).toBe('df') expect(MainNet.bip32.publicPrefix).toBe(0x0488b21e) expect(MainNet.bip32.privatePrefix).toBe(0x0488ade4) @@ -31,6 +35,7 @@ it('should match MainNet network', () => { }) it('should match TestNet network', () => { + expect(TestNet.name).toBe('testnet') expect(TestNet.bech32.hrp).toBe('tf') expect(TestNet.bip32.publicPrefix).toBe(0x043587cf) expect(TestNet.bip32.privatePrefix).toBe(0x04358394) @@ -41,6 +46,7 @@ it('should match TestNet network', () => { }) it('should match RegTest network', () => { + expect(RegTest.name).toBe('regtest') expect(RegTest.bech32.hrp).toBe('bcrt') expect(RegTest.bip32.publicPrefix).toBe(0x043587cf) expect(RegTest.bip32.privatePrefix).toBe(0x04358394) diff --git a/packages/jellyfish-network/src/index.ts b/packages/jellyfish-network/src/index.ts index 19dca6d4fd..8629199aae 100644 --- a/packages/jellyfish-network/src/index.ts +++ b/packages/jellyfish-network/src/index.ts @@ -1,33 +1,39 @@ +/** + * Networks available in DeFi Blockchain. + */ +export type NetworkName = 'mainnet' | 'testnet' | 'regtest' + /** * Network specific DeFi configuration. * They can be found in DeFiCh/ain project in file chainparams.cpp, under base58Prefixes */ export interface Network { + name: NetworkName bech32: { /** bech32 human readable part */ - hrp: string + hrp: 'df' | 'tf' | 'bcrt' } bip32: { /** base58Prefixes.EXT_PUBLIC_KEY */ - publicPrefix: number + publicPrefix: 0x0488b21e | 0x043587cf /** base58Prefixes.EXT_SECRET_KEY */ - privatePrefix: number + privatePrefix: 0x0488ade4 | 0x04358394 } /** base58Prefixes.SECRET_KEY */ - wifPrefix: number + wifPrefix: 0x80 | 0xef /** base58Prefixes.PUBKEY_ADDRESS */ - pubKeyHashPrefix: number + pubKeyHashPrefix: 0x12 | 0xf | 0x6f /** base58Prefixes.SCRIPT_ADDRESS */ - scriptHashPrefix: number + scriptHashPrefix: 0x5a | 0x80 | 0xc4 /** For message signing. */ - messagePrefix: string + messagePrefix: '\x15Defi Signed Message:\n' } /** * @param network name * @return Network specific DeFi configuration */ -export function getNetwork (network: 'mainnet' | 'testnet' | 'regtest'): Network { +export function getNetwork (network: NetworkName): Network { switch (network) { case 'mainnet': return MainNet @@ -44,6 +50,7 @@ export function getNetwork (network: 'mainnet' | 'testnet' | 'regtest'): Network * MainNet specific DeFi configuration. */ export const MainNet: Network = { + name: 'mainnet', bech32: { hrp: 'df' }, @@ -61,6 +68,7 @@ export const MainNet: Network = { * TestNet specific DeFi configuration. */ export const TestNet: Network = { + name: 'testnet', bech32: { hrp: 'tf' }, @@ -78,6 +86,7 @@ export const TestNet: Network = { * RegTest specific DeFi configuration. */ export const RegTest: Network = { + name: 'regtest', bech32: { hrp: 'bcrt' }, diff --git a/packages/jellyfish-transaction-builder/README.md b/packages/jellyfish-transaction-builder/README.md new file mode 100644 index 0000000000..1bcf63f8d6 --- /dev/null +++ b/packages/jellyfish-transaction-builder/README.md @@ -0,0 +1,22 @@ +[![npm](https://img.shields.io/npm/v/@defichain/jellyfish-transaction-builder)](https://www.npmjs.com/package/@defichain/jellyfish-transaction-builder/v/latest) +[![npm@next](https://img.shields.io/npm/v/@defichain/jellyfish-transaction-builder/next)](https://www.npmjs.com/package/@defichain/jellyfish-transaction-builder/v/next) + +# @defichain/jellyfish-transaction-builder + +While jellyfish-transaction provides a dead simple, modern, stateless raw transaction builder for DeFi. Constructing a +trust-less crypto transaction from scratch still has certain complexity as with the nature of blockchain technologies. +This package `jellyfish-transaction-builder` provides a high-high level abstraction for constructing transaction ready +to be broadcast for DeFi Blockchain. + +What can `jellyfish-transaction-builder` do? + +1. Uses low-level `jellyfish-*` packages for creating transaction. +2. Construct signed segwit transaction ready for broadcasting +3. Construct DeFi custom transaction +4. Lastly, provides a simple developer experience for creating signed transaction. + +## Testing + +For testing accuracy and convenience. All implementations must be e2e tested on `@defichain/testcontainers`. Due to the +complexity of testing, `@defichain/jellyfish-api-jsonrpc` and `@defichain/testing` is included in `devDependencies` for +setting up and tearing down test fixtures. diff --git a/packages/jellyfish-transaction-builder/__tests__/provider.mock.ts b/packages/jellyfish-transaction-builder/__tests__/provider.mock.ts new file mode 100644 index 0000000000..9203fcb51e --- /dev/null +++ b/packages/jellyfish-transaction-builder/__tests__/provider.mock.ts @@ -0,0 +1,130 @@ +import { BigNumber } from 'bignumber.js' +import { Bech32, EllipticPair, HASH160, WIF } from '@defichain/jellyfish-crypto' +import { EllipticPairProvider, FeeRateProvider, Prevout, PrevoutProvider } from '../src' +import { MasterNodeRegTestContainer } from '@defichain/testcontainers' +import { OP_CODES, Script } from '@defichain/jellyfish-transaction' +import { randomEllipticPair } from './test.utils' + +export class MockFeeRateProvider implements FeeRateProvider { + constructor ( + public readonly container: MasterNodeRegTestContainer + ) { + } + + async estimate (): Promise { + const result = await this.container.call('estimatesmartfee', [1]) + if (result.errors !== undefined && result.errors.length > 0) { + return new BigNumber('0.00005000') + } + return new BigNumber(result.feerate) + } +} + +export class MockPrevoutProvider implements PrevoutProvider { + constructor ( + public readonly container: MasterNodeRegTestContainer, + public ellipticPair: EllipticPair + ) { + } + + async all (): Promise { + const pubKey = await this.ellipticPair.publicKey() + const unspent: any[] = await this.container.call('listunspent', [ + 1, 9999999, [Bech32.fromPubKey(pubKey, 'bcrt')] + ]) + + return unspent.map((utxo: any): Prevout => { + return MockPrevoutProvider.mapPrevout(utxo, pubKey) + }) + } + + async collect (minBalance: BigNumber): Promise { + const pubKey = await this.ellipticPair.publicKey() + const address = Bech32.fromPubKey(pubKey, 'bcrt') + + // TODO(fuxingloh): minimumSumAmount behavior is weirdly inconsistent, listunspent will always + // return the correct result without providing options. However, with 'minimumSumAmount', it + // will appear sometimes. Likely due to race conditions in bitcoin code, + // e.g. -reindex when importprivkey. + const unspent: any[] = await this.container.call('listunspent', [ + 1, 9999999, [address], true + ]) + + return unspent.map((utxo: any): Prevout => { + return MockPrevoutProvider.mapPrevout(utxo, pubKey) + }) + } + + static mapPrevout (utxo: any, pubKey: Buffer): Prevout { + return { + txid: utxo.txid, + vout: utxo.vout, + value: new BigNumber(utxo.amount), + script: { + stack: [ + OP_CODES.OP_0, + OP_CODES.OP_PUSHDATA(HASH160(pubKey), 'little') + ] + }, + tokenId: utxo.tokenId + } + } +} + +export class MockEllipticPairProvider implements EllipticPairProvider { + constructor ( + public ellipticPair: EllipticPair + ) { + } + + async script (): Promise