diff --git a/__tests__/__snapshots__/ain.test.ts.snap b/__tests__/__snapshots__/ain.test.ts.snap index b476fa9..1ef7e02 100644 --- a/__tests__/__snapshots__/ain.test.ts.snap +++ b/__tests__/__snapshots__/ain.test.ts.snap @@ -169,6 +169,20 @@ Object { } `; +exports[`ain-js Database getProofHash 1`] = `"0x88496dfee3566db91f487aa4cbf69a0c42a3e2a5d0a65bfd4897d699e8734785"`; + +exports[`ain-js Database getStateInfo 1`] = ` +Object { + "#num_children": 1, + "#state_ph": "0x985a1f057d5047b1dee392127eb776571fbbe79da7ae6114f8f8f18c4f786135", + "#tree_bytes": 1840, + "#tree_height": 2, + "#tree_max_siblings": 1, + "#tree_size": 3, + "#version": "erased", +} +`; + exports[`ain-js Database matchFunction 1`] = ` Object { "matched_config": Object { diff --git a/__tests__/ain.test.ts b/__tests__/ain.test.ts index 6c91916..f586760 100644 --- a/__tests__/ain.test.ts +++ b/__tests__/ain.test.ts @@ -1,8 +1,9 @@ // @ts-nocheck import Ain from '../src/ain'; +import Wallet from '../src/wallet'; import { TransactionBody, SetOperation, TransactionInput } from '../src/types'; import axios from 'axios'; -import { fail, eraseProtoVer } from './test_util'; +import { fail, eraseProtoVer, eraseStateVersion } from './test_util'; const { test_keystore, test_pw, @@ -72,8 +73,77 @@ describe('ain-js', function() { }); }); + describe('Provider', function() { + it('getAddress', async function() { + const address = await ain.provider.getAddress(); + expect(address).not.toBeNull(); + }); + }); + describe('Wallet', function() { - let addresses = [] + it('countDecimals', function() { + expect(Wallet.countDecimals(0)).toBe(0); // '0' + expect(Wallet.countDecimals(1)).toBe(0); // '1' + expect(Wallet.countDecimals(10)).toBe(0); // '10' + expect(Wallet.countDecimals(100)).toBe(0); // '100' + expect(Wallet.countDecimals(1000)).toBe(0); // '1000' + expect(Wallet.countDecimals(10000)).toBe(0); // '10000' + expect(Wallet.countDecimals(100000)).toBe(0); // '100000' + expect(Wallet.countDecimals(1000000)).toBe(0); // '1000000' + expect(Wallet.countDecimals(10000000)).toBe(0); // '10000000' + expect(Wallet.countDecimals(100000000)).toBe(0); // '100000000' + expect(Wallet.countDecimals(1000000000)).toBe(0); // '1000000000' + expect(Wallet.countDecimals(1234567890)).toBe(0); // '1234567890' + expect(Wallet.countDecimals(-1)).toBe(0); // '-1' + expect(Wallet.countDecimals(-1000000000)).toBe(0); // '-1000000000' + expect(Wallet.countDecimals(11)).toBe(0); // '11' + expect(Wallet.countDecimals(101)).toBe(0); // '101' + expect(Wallet.countDecimals(1001)).toBe(0); // '1001' + expect(Wallet.countDecimals(10001)).toBe(0); // '10001' + expect(Wallet.countDecimals(100001)).toBe(0); // '100001' + expect(Wallet.countDecimals(1000001)).toBe(0); // '1000001' + expect(Wallet.countDecimals(10000001)).toBe(0); // '10000001' + expect(Wallet.countDecimals(100000001)).toBe(0); // '100000001' + expect(Wallet.countDecimals(1000000001)).toBe(0); // '1000000001' + expect(Wallet.countDecimals(-11)).toBe(0); // '-11' + expect(Wallet.countDecimals(-1000000001)).toBe(0); // '-1000000001' + expect(Wallet.countDecimals(0.1)).toBe(1); // '0.1' + expect(Wallet.countDecimals(0.01)).toBe(2); // '0.01' + expect(Wallet.countDecimals(0.001)).toBe(3); // '0.001' + expect(Wallet.countDecimals(0.0001)).toBe(4); // '0.0001' + expect(Wallet.countDecimals(0.00001)).toBe(5); // '0.00001' + expect(Wallet.countDecimals(0.000001)).toBe(6); // '0.000001' + expect(Wallet.countDecimals(0.0000001)).toBe(7); // '1e-7' + expect(Wallet.countDecimals(0.00000001)).toBe(8); // '1e-8' + expect(Wallet.countDecimals(0.000000001)).toBe(9); // '1e-9' + expect(Wallet.countDecimals(0.0000000001)).toBe(10); // '1e-10' + expect(Wallet.countDecimals(-0.1)).toBe(1); // '-0.1' + expect(Wallet.countDecimals(-0.0000000001)).toBe(10); // '-1e-10' + expect(Wallet.countDecimals(1.2)).toBe(1); // '1.2' + expect(Wallet.countDecimals(0.12)).toBe(2); // '0.12' + expect(Wallet.countDecimals(0.012)).toBe(3); // '0.012' + expect(Wallet.countDecimals(0.0012)).toBe(4); // '0.0012' + expect(Wallet.countDecimals(0.00012)).toBe(5); // '0.00012' + expect(Wallet.countDecimals(0.000012)).toBe(6); // '0.000012' + expect(Wallet.countDecimals(0.0000012)).toBe(7); // '0.0000012' + expect(Wallet.countDecimals(0.00000012)).toBe(8); // '1.2e-7' + expect(Wallet.countDecimals(0.000000012)).toBe(9); // '1.2e-8' + expect(Wallet.countDecimals(0.0000000012)).toBe(10); // '1.2e-9' + expect(Wallet.countDecimals(-1.2)).toBe(1); // '-1.2' + expect(Wallet.countDecimals(-0.0000000012)).toBe(10); // '-1.2e-9' + expect(Wallet.countDecimals(1.03)).toBe(2); // '1.03' + expect(Wallet.countDecimals(1.003)).toBe(3); // '1.003' + expect(Wallet.countDecimals(1.0003)).toBe(4); // '1.0003' + expect(Wallet.countDecimals(1.00003)).toBe(5); // '1.00003' + expect(Wallet.countDecimals(1.000003)).toBe(6); // '1.000003' + expect(Wallet.countDecimals(1.0000003)).toBe(7); // '1.0000003' + expect(Wallet.countDecimals(1.00000003)).toBe(8); // '1.00000003' + expect(Wallet.countDecimals(1.000000003)).toBe(9); // '1.000000003' + expect(Wallet.countDecimals(1.0000000003)).toBe(10); // '1.0000000003' + expect(Wallet.countDecimals(-1.03)).toBe(2); // '-1.03' + expect(Wallet.countDecimals(-1.0000000003)).toBe(10); // '-1.0000000003' + }); + it('create', function() { const beforeLength = ain.wallet.length; ain.wallet.create(2); @@ -204,6 +274,16 @@ describe('ain-js', function() { expect(balance).toBeGreaterThan(0); }); + it('getNonce', async function() { + const nonce = await ain.wallet.getNonce(); + expect(nonce).toBe(0); + }); + + it('getTimestamp', async function() { + const timestamp = await ain.wallet.getTimestamp(); + expect(timestamp).toBe(0); + }); + it('transfer with isDryrun = true', async function() { const balanceBefore = await ain.wallet.getBalance(); const response = await ain.wallet.transfer({ @@ -222,6 +302,64 @@ describe('ain-js', function() { expect(balanceAfter).toBe(balanceBefore - 100); }); + it('transfer with a zero value', async function() { + const balanceBefore = await ain.wallet.getBalance(); + try { + const response = await ain.wallet.transfer({ + to: '0xbA58D93edD8343C001eC5f43E620712Ba8C10813', + value: 0, // a zero value + nonce: -1 }); + fail('should not happen'); + } catch(e) { + expect(e.message).toBe('Non-positive transfer value.'); + } finally { + const balanceAfter = await ain.wallet.getBalance(); + expect(balanceAfter).toBe(balanceBefore); + } + }); + + it('transfer with a negative value', async function() { + const balanceBefore = await ain.wallet.getBalance(); + try { + const response = await ain.wallet.transfer({ + to: '0xbA58D93edD8343C001eC5f43E620712Ba8C10813', + value: -0.1, // a negative value + nonce: -1 }); + fail('should not happen'); + } catch(e) { + expect(e.message).toBe('Non-positive transfer value.'); + } finally { + const balanceAfter = await ain.wallet.getBalance(); + expect(balanceAfter).toBe(balanceBefore); + } + }); + + it('transfer with a value of up to 6 decimals', async function() { + const balanceBefore = await ain.wallet.getBalance(); + const response = await ain.wallet.transfer({ + to: '0xbA58D93edD8343C001eC5f43E620712Ba8C10813', + value: 0.000001, // of 6 decimals + nonce: -1 }); + const balanceAfter = await ain.wallet.getBalance(); + expect(balanceAfter).toBe(balanceBefore - 0.000001); + }); + + it('transfer with a value of more than 6 decimals', async function() { + const balanceBefore = await ain.wallet.getBalance(); + try { + const response = await ain.wallet.transfer({ + to: '0xbA58D93edD8343C001eC5f43E620712Ba8C10813', + value: 0.0000001, // of 7 decimals + nonce: -1 }); + fail('should not happen'); + } catch(e) { + expect(e.message).toBe('Transfer value of more than 6 decimals.'); + } finally { + const balanceAfter = await ain.wallet.getBalance(); + expect(balanceAfter).toBe(balanceBefore); + } + }); + it('chainId', function() { // chainId = 0 ain.setProvider(test_node_2, 0); @@ -340,22 +478,115 @@ describe('ain-js', function() { await waitUntilTxFinalized(createApps.result.tx_hash); }); - it('getBlock', async function () { - const block = await ain.getBlock(3) - const hash = block.hash || ""; - expect(await ain.getBlock(hash)).toStrictEqual(block); + it('getLastBlock', async function () { + const block = await ain.getLastBlock() + expect(block).not.toBeNull(); + expect(block.hash).not.toBeNull(); + expect(block.number).toBeGreaterThan(0); + }); + + it('getLastBlockNumber', async function () { + const number = await ain.getLastBlockNumber() + expect(number).not.toBeNull(); + expect(number).toBeGreaterThan(0); + }); + + it('getBlockByNumber', async function () { + const lastBlock = await ain.getLastBlock(); + expect(lastBlock).not.toBeNull(); + expect(lastBlock.number).not.toBeNull(); + const block = await ain.getBlockByNumber(lastBlock.number) + expect(block).not.toBeNull(); + expect(block.number).toBe(lastBlock.number); + expect(block.hash).toBe(lastBlock.hash); + }); + + it('getBlockByHash', async function () { + const lastBlock = await ain.getLastBlock(); + expect(lastBlock).not.toBeNull(); + expect(lastBlock.hash).not.toBeNull(); + const block = await ain.getBlockByHash(lastBlock.hash) + expect(block).not.toBeNull(); + expect(block.number).toBe(lastBlock.number); + expect(block.hash).toBe(lastBlock.hash); + }); + + it('getBlockList', async function () { + const lastBlockNumber = await ain.getLastBlockNumber(); + expect(lastBlockNumber).not.toBeNull(); + expect(lastBlockNumber).toBeGreaterThanOrEqual(0); + const blockList = await ain.getBlockList(lastBlockNumber - 1, lastBlockNumber + 1) + expect(blockList).not.toBeNull(); + expect(blockList.length).toBe(2); + expect(blockList[0].number).toBe(lastBlockNumber - 1); + expect(blockList[1].number).toBe(lastBlockNumber); + }); + + it('getBlockHeadersList', async function () { + const lastBlockNumber = await ain.getLastBlockNumber(); + expect(lastBlockNumber).not.toBeNull(); + expect(lastBlockNumber).toBeGreaterThanOrEqual(0); + const blockList = await ain.getBlockHeadersList(lastBlockNumber - 1, lastBlockNumber + 1) + expect(blockList).not.toBeNull(); + expect(blockList.length).toBe(2); + expect(blockList[0].number).toBe(lastBlockNumber - 1); + expect(blockList[1].number).toBe(lastBlockNumber); }); - it('getProposer', async function () { - const proposer = await ain.getProposer(1); - const hash = (await ain.getBlock(1)).hash || ""; - expect(await ain.getProposer(hash)).toBe(proposer); + it('getBlockTransactionCountByNumber', async function () { + const lastBlockNumber = await ain.getLastBlockNumber(); + expect(lastBlockNumber).not.toBeNull(); + expect(lastBlockNumber).toBeGreaterThanOrEqual(0); + const txCount = await ain.getBlockTransactionCountByNumber(lastBlockNumber) + expect(txCount).not.toBeNull(); }); - it('getValidators', async function () { - const validators = await ain.getValidators(4); - const hash = (await ain.getBlock(4)).hash || ""; - expect(await ain.getValidators(hash)).toStrictEqual(validators); + it('getBlockTransactionCountByHash', async function () { + const lastBlock= await ain.getLastBlock(); + expect(lastBlock).not.toBeNull(); + expect(lastBlock.hash).not.toBeNull(); + const txCount = await ain.getBlockTransactionCountByHash(lastBlock.hash) + expect(txCount).not.toBeNull(); + }); + + it('getValidatorInfo', async function () { + const lastBlock = await ain.getLastBlock(); + expect(lastBlock).not.toBeNull(); + expect(lastBlock.proposer).not.toBeNull(); + const info = await ain.getValidatorInfo(lastBlock.proposer); + expect(info).not.toBeNull(); + }); + + it('getValidatorsByNumber', async function () { + const lastBlock = await ain.getLastBlock(); + expect(lastBlock).not.toBeNull(); + expect(lastBlock.number).not.toBeNull(); + const validators = await ain.getValidatorsByNumber(lastBlock.number); + expect(validators).toStrictEqual(lastBlock.validators); + }); + + it('getValidatorsByHash', async function () { + const lastBlock = await ain.getLastBlock(); + expect(lastBlock).not.toBeNull(); + expect(lastBlock.number).not.toBeNull(); + const validators = await ain.getValidatorsByHash(lastBlock.hash); + expect(validators).toStrictEqual(lastBlock.validators); + }); + + it('getProposerByNumber', async function () { + const lastBlock = await ain.getLastBlock(); + expect(lastBlock).not.toBeNull(); + expect(lastBlock.number).not.toBeNull(); + const proposer = await ain.getProposerByNumber(lastBlock.number); + expect(proposer).toBe(lastBlock.proposer); + }); + + it('getProposerByHash', async function () { + const lastBlock = await ain.getLastBlock(); + expect(lastBlock).not.toBeNull(); + expect(lastBlock.hash).not.toBeNull(); + const proposer = await ain.getProposerByHash(lastBlock.hash); + expect(proposer).toBe(lastBlock.proposer); }); // TODO(liayoo): add getTransactionResult method and test case for it. @@ -397,8 +628,8 @@ describe('ain-js', function() { }) }); - it('getTransaction for sendTransaction with isDryrun = true', async function () { - const tx = await ain.getTransaction(targetTxHash); + it('getTransactionByHash for sendTransaction with isDryrun = true', async function () { + const tx = await ain.getTransactionByHash(targetTxHash); expect(tx).toStrictEqual(null); // The tx is NOT in the blockchain. }); @@ -415,8 +646,8 @@ describe('ain-js', function() { }) }); - it('getTransaction for sendTransaction', async function () { - const tx = await ain.getTransaction(targetTxHash); + it('getTransactionByHash for sendTransaction', async function () { + const tx = await ain.getTransactionByHash(targetTxHash); expect(tx.transaction.tx_body.operation).toStrictEqual(targetTx); }); @@ -643,6 +874,29 @@ describe('ain-js', function() { expect(thrownError.code).toEqual(30401); expect(thrownError.message).toEqual('Invalid batch transaction format.'); }); + + it('getPendingTransactions', async function () { + const txs = await ain.getPendingTransactions(); + expect(txs).not.toBeNull(); + }); + + it('getTransactionPoolSizeUtilization', async function () { + const txs = await ain.getTransactionPoolSizeUtilization(); + expect(txs).not.toBeNull(); + }); + + it('getTransactionByBlockHashAndIndex', async function () { + const genesisBlockNumber = 0; + const genesisBlock = await ain.getBlockByNumber(genesisBlockNumber); + const tx = await ain.getTransactionByBlockHashAndIndex(genesisBlock.hash, 0); + expect(tx).not.toBeNull(); + }); + + it('getTransactionByBlockNumberAndIndex', async function () { + const genesisBlockNumber = 0; + const tx = await ain.getTransactionByBlockNumberAndIndex(genesisBlockNumber, 0); + expect(tx).not.toBeNull(); + }); }); describe('Database', function() { @@ -1119,6 +1373,39 @@ describe('ain-js', function() { }) }); + it('getStateProof', async function() { + await ain.db.ref('/values/blockchain_params').getStateProof() + .then(res => { + expect(res).not.toBeNull(); + }) + .catch(error => { + console.log("error:", error); + fail('should not happen'); + }) + }); + + it('getProofHash', async function() { + await ain.db.ref('/values/blockchain_params').getProofHash() + .then(res => { + expect(res).toMatchSnapshot(); + }) + .catch(error => { + console.log("error:", error); + fail('should not happen'); + }) + }); + + it('getStateInfo', async function() { + await ain.db.ref('/rules/transfer/$from/$to/$key/value').getStateInfo() + .then(res => { + expect(eraseStateVersion(res)).toMatchSnapshot(); + }) + .catch(error => { + console.log("error:", error); + fail('should not happen'); + }) + }); + /*it('on and off', function(done) { try { ain.db.ref().on('value', (snap:any) => console.log) diff --git a/__tests__/test_util.ts b/__tests__/test_util.ts index 473ad7a..8b9dd47 100644 --- a/__tests__/test_util.ts +++ b/__tests__/test_util.ts @@ -1,6 +1,16 @@ +import { set } from 'lodash'; + export declare function fail(error?: any): never; -export function eraseProtoVer(retVal) { - retVal.protoVer = 'erased'; - return retVal; +export function eraseProtoVer(result) { + const erased = JSON.parse(JSON.stringify(result)); + set(erased, 'protoVer', 'erased'); + return erased; +} + +export function eraseStateVersion(result) { + const erased = JSON.parse(JSON.stringify(result)); + set(erased, '#version', 'erased'); + return erased; } + diff --git a/package.json b/package.json index fbc305e..bb8aa59 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ainblockchain/ain-js", - "version": "1.6.3", + "version": "1.7.0", "description": "", "main": "lib/ain.js", "scripts": { diff --git a/src/ain-db/ref.ts b/src/ain-db/ref.ts index e769764..43ac5d5 100755 --- a/src/ain-db/ref.ts +++ b/src/ain-db/ref.ts @@ -11,6 +11,7 @@ import { EvalRuleInput, EvalOwnerInput, MatchInput, + StateInfoInput, GetOptions, } from '../types'; import Ain from '../ain'; @@ -347,6 +348,42 @@ export default class Reference { return this._ain.provider.send('ain_matchOwner', request); } + /** + * Fetches the state proof of a global blockchain state path. + * @param {StateInfoInput} params The state info input. + * @returns {Promise} The return value of the blockchain API. + */ + getStateProof(params?: StateInfoInput): Promise { + const request = { + ref: Reference.extendPath(this.path, params ? params.ref : undefined) + } + return this._ain.provider.send('ain_getStateProof', request); + } + + /** + * Fetches the proof hash of a global blockchain state path. + * @param {StateInfoInput} params The state info input. + * @returns {Promise} The return value of the blockchain API. + */ + getProofHash(params?: StateInfoInput): Promise { + const request = { + ref: Reference.extendPath(this.path, params ? params.ref : undefined) + } + return this._ain.provider.send('ain_getProofHash', request); + } + + /** + * Fetches the state information of a global blockchain state path. + * @param {StateInfoInput} params The state info input. + * @returns {Promise} The return value of the blockchain API. + */ + getStateInfo(params?: StateInfoInput): Promise { + const request = { + ref: Reference.extendPath(this.path, params ? params.ref : undefined) + } + return this._ain.provider.send('ain_getStateInfo', request); + } + // TODO(liayoo): Add this function. ///** // * Attaches an listener for database events. diff --git a/src/ain.ts b/src/ain.ts index 05b69c2..49c4411 100755 --- a/src/ain.ts +++ b/src/ain.ts @@ -85,43 +85,144 @@ export default class Ain { } /** - * Fetches a block with a block hash or block number. - * @param {string | number} blockHashOrBlockNumber The block hash or block number. + * Fetches the last block. + * @returns {Promise} + */ + getLastBlock(): Promise { + return this.provider.send('ain_getLastBlock', {}); + } + + /** + * Fetches the last block number. + * @returns {Promise} + */ + getLastBlockNumber(): Promise { + return this.provider.send('ain_getLastBlockNumber', {}); + } + + /** + * Fetches a block with a block number. + * @param {number} blockNumber The block number. * @param {boolean} returnTransactionObjects If it's true, returns a block with full transaction objects. * Otherwise, returns a block with only transaction hashes. * @returns {Promise} */ - getBlock(blockHashOrBlockNumber: string | number, returnTransactionObjects?: boolean): Promise { - const byHash = typeof blockHashOrBlockNumber === 'string' - const rpcMethod = byHash ? 'ain_getBlockByHash' : 'ain_getBlockByNumber'; - const data = Object.assign({}, - { getFullTransactions: !!returnTransactionObjects, - [byHash ? 'hash' : 'number']: blockHashOrBlockNumber }); - return this.provider.send(rpcMethod, data); + getBlockByNumber(blockNumber: number, returnTransactionObjects?: boolean): Promise { + const data = + Object.assign({}, { getFullTransactions: !!returnTransactionObjects, number: blockNumber }); + return this.provider.send('ain_getBlockByNumber', data); + } + + /** + * Fetches a block with a block hash. + * @param {string} blockHash The block hash. + * @param {boolean} returnTransactionObjects If it's true, returns a block with full transaction objects. + * Otherwise, returns a block with only transaction hashes. + * @returns {Promise} + */ + getBlockByHash(blockHash: string, returnTransactionObjects?: boolean): Promise { + const data = + Object.assign({}, { getFullTransactions: !!returnTransactionObjects, hash: blockHash }); + return this.provider.send('ain_getBlockByHash', data); + } + + /** + * Fetches blocks with a block number range. + * @param {number} from The begining block number (inclusive). + * @param {number} to The ending block number (exclusive). + * @returns {Promise>} + */ + getBlockList(from: number, to: number): Promise> { + return this.provider.send('ain_getBlockList', { from, to }); } /** - * Fetches the forger's address of a block with a block hash or block number. - * @param {string | number} blockHashOrBlockNumber The block hash or block number. + * Fetches block headers with a block number range. + * @param {number} from The begining block number (inclusive). + * @param {number} to The ending block number (exclusive). + * @returns {Promise>} + */ + getBlockHeadersList(from: number, to: number): Promise> { + return this.provider.send('ain_getBlockHeadersList', { from, to }); + } + + /** + * Fetches block transaction count with a block number. + * @param {number} number The block number. + * @returns {Promise} + */ + getBlockTransactionCountByNumber(number: number): Promise { + return this.provider.send('ain_getBlockTransactionCountByNumber', { number }); + } + + /** + * Fetches block transaction count with a block hash. + * @param {string} hash The block hash. + * @returns {Promise} + */ + getBlockTransactionCountByHash(hash: string): Promise { + return this.provider.send('ain_getBlockTransactionCountByHash', { hash }); + } + + /** + * Fetches the information of the given validator address. + * @param {string} address The validator address. + * @returns {Promise} + */ + getValidatorInfo(address: string): Promise { + return this.provider.send('ain_getValidatorInfo', { address }); + } + + /** + * Fetches the validator list of a block with a block number. + * @param {number} blockNumber The block number. + * @returns {Promise} + */ + getValidatorsByNumber(blockNumber: number): Promise { + return this.provider.send('ain_getValidatorsByNumber', { number: blockNumber }); + } + + /** + * Fetches the validator list of a block with a block hash. + * @param {string} blockHash The block hash. + * @returns {Promise} + */ + getValidatorsByHash(blockHash: string): Promise { + return this.provider.send('ain_getValidatorsByHash', { hash: blockHash }); + } + + /** + * Fetches the block proproser's address of a block with a block number. + * @param {number} blockNumber The block number. * @returns {Promise} */ - getProposer(blockHashOrBlockNumber: string | number): Promise { - const byHash = typeof blockHashOrBlockNumber === 'string' - const rpcMethod = byHash ? 'ain_getProposerByHash' : 'ain_getProposerByNumber'; - return this.provider.send(rpcMethod, - {[byHash ? 'hash' : 'number']: blockHashOrBlockNumber}); + getProposerByNumber(blockNumber: number): Promise { + return this.provider.send('ain_getProposerByNumber', { number: blockNumber }); } /** - * Fetches the validator list of a block with a block hash or block number. - * @param {string | number} blockHashOrBlockNumber The block hash or block number. - * @returns {Promise} + * Fetches the block proproser's address of a block with a block hash. + * @param {string} blockHash The block hash. + * @returns {Promise} */ - getValidators(blockHashOrBlockNumber: string | number): Promise { - const byHash = typeof blockHashOrBlockNumber === 'string' - const rpcMethod = byHash ? 'ain_getValidatorsByHash' : 'ain_getValidatorsByNumber'; - return this.provider.send(rpcMethod, - {[byHash ? 'hash' : 'number']: blockHashOrBlockNumber}); + getProposerByHash(blockHash: string): Promise { + return this.provider.send('ain_getProposerByHash', { hash: blockHash }); + } + + /** + * Fetches pending transaction. + * @returns {Promise} + */ + getPendingTransactions(): Promise { + return this.provider.send('ain_getPendingTransactions', {}); + } + + /** + * Fetches transaction pool size utilization. + * @returns {Promise} + */ + getTransactionPoolSizeUtilization(): Promise { + return this.provider.send('ain_getTransactionPoolSizeUtilization', {}); } /** @@ -129,10 +230,30 @@ export default class Ain { * @param {string} transactionHash The transaction hash. * @returns {Promise} */ - getTransaction(transactionHash: string): Promise { + getTransactionByHash(transactionHash: string): Promise { return this.provider.send('ain_getTransactionByHash', { hash: transactionHash }); } + /** + * Fetches a transaction's information with a block hash and an index. + * @param {string} blockHash The block hash. + * @param {number} index The transaction index in the block + * @returns {Promise} + */ + getTransactionByBlockHashAndIndex(blockHash: string, index: Number): Promise { + return this.provider.send('ain_getTransactionByBlockHashAndIndex', { block_hash: blockHash, index }); + } + + /** + * Fetches a transaction's information with a block hash and an index. + * @param {string} blockNumber The block number. + * @param {number} index The transaction index in the block + * @returns {Promise} + */ + getTransactionByBlockNumberAndIndex(blockNumber: Number, index: Number): Promise { + return this.provider.send('ain_getTransactionByBlockNumberAndIndex', { block_number: blockNumber, index }); + } + /** * Fetches a blockchain app's state usage information with an app name. * @param {string} appName The blockchain app name. diff --git a/src/provider.ts b/src/provider.ts index e1ec746..247a80e 100644 --- a/src/provider.ts +++ b/src/provider.ts @@ -37,6 +37,14 @@ export default class Provider { this.httpClient = axios.create(axiosConfig); } + /** + * Fetches the blockchain node's address. + * @returns {Promise} The address value. + */ + getAddress(): Promise { + return this.send('ain_getAddress', {}) + } + /** * Creates a JSON-RPC payload and sends it to the network. * @param {string} rpcMethod The JSON-RPC method. diff --git a/src/signer/default-signer.ts b/src/signer/default-signer.ts index cfa5a89..014f3bf 100644 --- a/src/signer/default-signer.ts +++ b/src/signer/default-signer.ts @@ -124,31 +124,11 @@ export class DefaultSigner implements Signer { } let nonce = transactionInput.nonce; if (nonce === undefined) { - nonce = await this.getNonce({ address, from: "pending" }); + nonce = await this.wallet.getNonce({ address, from: "pending" }); } const timestamp = transactionInput.timestamp ? transactionInput.timestamp : Date.now(); const gasPrice = transactionInput.gas_price || 0; const billing = transactionInput.billing; return Object.assign(tx, { nonce, timestamp, gas_price: gasPrice, billing }); } - - /** - * Fetches an account's nonce value, which is the current transaction count of the account. - * @param {object} args The ferch options. - * It may contain a string 'address' value and a string 'from' value. - * The 'address' is the address of the account to get the nonce of, - * and the 'from' is the source of the data. - * It could be either the pending transaction pool ("pending") or - * the committed blocks ("committed"). The default value is "committed". - * @returns {Promise} The nonce value. - */ - getNonce(args: { address?: string, from?: string }): Promise { - if (!args) { args = {}; } - const address = args.address ? Ain.utils.toChecksumAddress(args.address) - : this.getAddress(args.address); - if (args.from !== undefined && args.from !== 'pending' && args.from !== 'committed') { - throw Error("'from' should be either 'pending' or 'committed'"); - } - return this.provider.send('ain_getNonce', { address, from: args.from }) - } -} \ No newline at end of file + }; diff --git a/src/types.ts b/src/types.ts index d8c8452..15fa14e 100755 --- a/src/types.ts +++ b/src/types.ts @@ -291,6 +291,13 @@ export interface MatchInput { is_global?: boolean; } +/** + * An interface for state information input. + */ +export interface StateInfoInput { + ref?: string; +} + /** * An interface for state usage information. */ diff --git a/src/wallet.ts b/src/wallet.ts index fef037b..12512e2 100755 --- a/src/wallet.ts +++ b/src/wallet.ts @@ -2,7 +2,9 @@ import { Accounts, Account, TransactionBody, V3Keystore, V3KeystoreOptions } fro import Ain from './ain'; import { validateMnemonic, mnemonicToSeedSync } from 'bip39'; import Reference from './ain-db/ref'; -const AIN_HD_DERIVATION_PATH = "m/44'/412'/0'/0/"; /* default wallet address for AIN */ +// TODO(platfowner): Migrate to Ethereum HD derivation path 'm/44'/60'/0'/0/'. +const AIN_HD_DERIVATION_PATH = "m/44'/412'/0'/0/"; // The hardware wallet derivation path of AIN +const MAX_TRANSFERABLE_DECIMALS = 6; // The maximum decimals of transferable values /** * A class for AI Network wallets. @@ -58,6 +60,25 @@ export default class Wallet { return this.accounts[checksummed].public_key; } + /** + * Counts the given number's decimals. + * @param {number} value The number. + * @returns {number} The decimal count. + */ + static countDecimals(value: number): number { + const decimalExponentRegex = /^-{0,1}(\d*\.{0,1}\d*)e-(\d+)$/gm; + + if (Math.floor(value) === value) { + return 0; + } + const valueString = value.toString(); + const matches = decimalExponentRegex.exec(valueString); + if (matches) { + return Number(matches[2]) + Wallet.countDecimals(Number(matches[1])); + } + return valueString.split('.')[1].length || 0; + } + /** * Creates new accounts and adds them to the wallet. * @param {number} numberOfAccounts The number of accounts to create. @@ -227,6 +248,46 @@ export default class Wallet { return this.ain.db.ref(`/accounts/${addr}/balance`).getValue(); } + /** + * Fetches an account's nonce value, which is the current transaction count of the account. + * @param {object} args The ferch options. + * It may contain a string 'address' value and a string 'from' value. + * The 'address' is the address of the account to get the nonce of, + * and the 'from' is the source of the data. + * It could be either the pending transaction pool ("pending") or + * the committed blocks ("committed"). The default value is "committed". + * @returns {Promise} The nonce value. + */ + getNonce(args: { address?: string, from?: string }): Promise { + if (!args) { args = {}; } + const address = args.address ? Ain.utils.toChecksumAddress(args.address) + : this.getImpliedAddress(args.address); + if (args.from !== undefined && args.from !== 'pending' && args.from !== 'committed') { + throw Error("'from' should be either 'pending' or 'committed'"); + } + return this.ain.provider.send('ain_getNonce', { address, from: args.from }) + } + + /** + * Fetches an account's timestamp value, which is the current transaction count of the account. + * @param {object} args The ferch options. + * It may contain a string 'address' value and a string 'from' value. + * The 'address' is the address of the account to get the timestamp of, + * and the 'from' is the source of the data. + * It could be either the pending transaction pool ("pending") or + * the committed blocks ("committed"). The default value is "committed". + * @returns {Promise} The timestamp value. + */ + getTimestamp(args: { address?: string, from?: string }): Promise { + if (!args) { args = {}; } + const address = args.address ? Ain.utils.toChecksumAddress(args.address) + : this.getImpliedAddress(args.address); + if (args.from !== undefined && args.from !== 'pending' && args.from !== 'committed') { + throw Error("'from' should be either 'pending' or 'committed'"); + } + return this.ain.provider.send('ain_getTimestamp', { address, from: args.from }) + } + /** * Sends a transfer transaction to the network. * @param {{to: string, value: number, from?: string, nonce?: number, gas_price?: number}} input The input parameters of the transaction. @@ -236,6 +297,13 @@ export default class Wallet { transfer(input: {to: string, value: number, from?: string, nonce?: number, gas_price?: number}, isDryrun: boolean = false): Promise { const address = this.getImpliedAddress(input.from); const toAddress = Ain.utils.toChecksumAddress(input.to); + if (!(input.value > 0)) { + throw Error(`Non-positive transfer value.`); + } + const decimalCount = Wallet.countDecimals(input.value); + if (decimalCount > MAX_TRANSFERABLE_DECIMALS) { + throw Error(`Transfer value of more than ${MAX_TRANSFERABLE_DECIMALS} decimals.`); + } const transferRef = this.ain.db.ref(`/transfer/${address}/${toAddress}`).push() as Reference; return transferRef.setValue({ ref: '/value', address, value: input.value, nonce: input.nonce, gas_price: input.gas_price }, isDryrun);