From 6f591004b1d0c22c76b7c8dff51d7ae1de569a1d Mon Sep 17 00:00:00 2001 From: Yak Jun Xiang Date: Tue, 28 Nov 2017 01:18:17 +0800 Subject: [PATCH] v2.1.0 - Balance as an ES6 class. - Added `doTransferToken` to `api/nep5` - Unit tests for `utils` - Typescript typings fixed fix --- package-lock.json | 22 +- package.json | 2 +- source/changelog.rst | 23 + source/conf.py | 4 +- source/interface.rst | 16 +- source/reference/wallet.rst | 3 + source/wallet.rst | 25 + src/api/index.d.ts | 254 +++--- src/api/index.js | 18 + src/api/neonDB.js | 51 +- src/api/neoscan.js | 18 +- src/consts.d.ts | 83 +- src/index.d.ts | 48 +- src/rpc/index.d.ts | 142 ++-- src/rpc/index.js | 8 + src/sc/index.d.ts | 135 ++-- src/transactions/core.js | 4 +- src/transactions/index.d.ts | 260 +++--- src/utils.d.ts | 53 +- src/utils.js | 82 +- src/wallet/Balance.js | 191 +++++ src/wallet/index.d.ts | 164 ++-- src/wallet/index.js | 3 +- tests/api/core.js | 51 +- tests/api/neonDB.js | 4 +- tests/api/neoscan.js | 23 +- tests/api/nep5.js | 4 +- tests/testData.json | 1178 ++++++++++++++-------------- tests/transactions/createData.json | 106 +-- tests/utils.js | 310 +++++++- tests/wallet/Balance.js | 87 ++ webpack.config.js | 2 +- 32 files changed, 2077 insertions(+), 1297 deletions(-) create mode 100644 src/wallet/Balance.js create mode 100644 tests/wallet/Balance.js diff --git a/package-lock.json b/package-lock.json index aa985e889..07e76cb1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { - "name": "neon-js", - "version": "1.1.1", + "name": "@cityofzion/neon-js", + "version": "2.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -4390,6 +4390,15 @@ "xtend": "4.0.1" } }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", @@ -4401,15 +4410,6 @@ "strip-ansi": "3.0.1" } }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "dev": true, - "requires": { - "safe-buffer": "5.1.1" - } - }, "stringstream": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", diff --git a/package.json b/package.json index e5eecb8c3..e3062f706 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "name": "@cityofzion/neon-js", "description": "Javascript libraries for neo wallet using https://github.com/neochainio/neowallet/blob/master/js/wallet.js as the original source.", - "version": "2.0.0", + "version": "2.1.0", "main": "lib/index.js", "browser": "lib/browser.js", "types": "src/index.d.ts", diff --git a/source/changelog.rst b/source/changelog.rst index 020c9fcc7..f88e64475 100644 --- a/source/changelog.rst +++ b/source/changelog.rst @@ -4,6 +4,29 @@ Changelog This details the changes made from the previous recorded version. +2.1.0 +===== + +- Balance as an ES6 class. + + - ``verifyAssets`` to validate unspent coins against a given NEO node. Used to check if balance is fully synced and usable. + - ``applyTx`` to apply a spending of a Transaction to the Balance. Allows a Balance to be used to build another Transaction without waiting for sync. + - Data structure reworked. AssetBalances are now tucked under ``assets``. Use ``assetSymbols`` to discover the keys for lookup. + + :: + + // This array contains all the symbols of the assets available in this Balance + balance.assetSymbols = ['NEO', 'GAS'] + // Lookup assets using their symbols + balance.assets = { + NEO: {balance: 1, unspent: [ Object ], spent: [], unconfirmed: []} + GAS: {balance: 25.1, unspent: [ Object ], spent: [], unconfirmed: []} + } + +- Added ``doTransferToken`` to ``api/nep5`` +- Unit tests for ``utils`` +- Typescript typings fixed + 2.0.0 ====== diff --git a/source/conf.py b/source/conf.py index e13c8ee7f..3b858b1db 100644 --- a/source/conf.py +++ b/source/conf.py @@ -66,9 +66,9 @@ # built documents. # # The short X.Y version. -version = u'2.0.0' +version = u'2.1.0' # The full version, including alpha/beta/rc tags. -release = u'2.0.0' +release = u'2.1.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/source/interface.rst b/source/interface.rst index 69ef6840f..c145ec1ff 100644 --- a/source/interface.rst +++ b/source/interface.rst @@ -13,19 +13,21 @@ A balance describes the assets that an address owns, as well as list the unspent {// Balance address: string, // The address - NEO: { - balance: number, - unspent: Coin[] + assets: { + NEO: { + balance: number, + unspent: Coin[] }, - GAS: { - balance: number, - unspent: Coin[] + GAS: { + balance: number, + unspent: Coin[] } + } } The address property acts as an ID for this object and is used to derive the scriptHash when calculating the change to give back to the account. -All the other properties are named using asset symbols. Currently, there are only 2 assets available (NEO/GAS) and they both have similar names to symbols. Symbols should be in capital letters and be of 3-4 letters long. +The assets are stored in an object and retrieved using their symbols. Currently, there are only 2 assets available (NEO/GAS) and they both have similar names to symbols. Symbols should be in capital letters and be of 3-4 letters long. Each symbol will be contain the ``balance`` and ``unspent`` property. ``balance`` tells us the total amount of this asset available and serves as a simple check if there is enough assets available to fulfil the sending intents. ``unspent`` contains a list of ``Coin`` that are used as Transaction Inputs. diff --git a/source/reference/wallet.rst b/source/reference/wallet.rst index 64b49d407..9c9f28f75 100644 --- a/source/reference/wallet.rst +++ b/source/reference/wallet.rst @@ -7,6 +7,9 @@ Module ``wallet`` .. autoclass:: Account :members: +.. autoclass:: Balance + :members: + .. autofunction:: encrypt .. autofunction:: decrypt diff --git a/source/wallet.rst b/source/wallet.rst index bf21b2418..7c94d7f61 100644 --- a/source/wallet.rst +++ b/source/wallet.rst @@ -59,6 +59,31 @@ You cannot derive a private key from a Account created using a Public Key. (Acco const account2 = Neon.create.account(publicKey) account.privateKey // Throws error +Balance +======= + +The Balance class stores the balance of the account. It is usually retrieved using a 3rd party API as NEO nodes do not have a RPC call to easily retrieve this information with a single call. + +:: + + import Neon from 'neon-js' + Neon.create.balance({net: 'TestNet', address: 'ALq7AWrhAueN6mJNqk6FHJjnsEoPRytLdW'}) + + import {wallet, api} from 'neon-js' + // This form is useless as it is an empty balance. + const balance = new wallet.Balance({net: 'TestNet', address: 'ALq7AWrhAueN6mJNqk6FHJjnsEoPRytLdW'}) + // We get a useful balance that can be used to fill a transaction through api + const filledBalance = api.getBalanceFrom('ALq7AWrhAueN6mJNqk6FHJjnsEoPRytLdW', api.neonDB) + // This contains all symbols of assets available in this balance + const symbols = filledBalance.assetSymbols + // We retrieve the unspent coins from the assets object using the symbol + const neoCoins = filledBalance.assets['NEO'].unspent + // We can verify the information retrieved using verifyAssets + filledBalance.verifyAssets('https://seed1.neo.org:20332') + +The Balance class is used to track the unspent coins available to construct transactions with. ``verifyAssets`` is a handy method to make sure the unspent coins provided by the 3rd party API is really unspent by verifying them against a NEO node. However, this is an expensive operation so use sparingly. + + Core ==== diff --git a/src/api/index.d.ts b/src/api/index.d.ts index 7c3678305..4a408e662 100644 --- a/src/api/index.d.ts +++ b/src/api/index.d.ts @@ -1,130 +1,152 @@ -import { RPCResponse } from "../rpc/index"; - - -interface apiConfig { - net: string, - address: string, - privateKey?: string, - publicKey?: string, - url?: string, - balance?: Balance -} - -interface AssetAmounts { - GAS: number - NEO: number -} +declare module '@cityofzion/neon-js' { + interface apiConfig { + net: string, + address: string, + privateKey?: string, + publicKey?: string, + url?: string, + balance?: Balance + } -interface History { - address: string - history: PastTransaction[] - name: string - net: Net -} + interface AssetAmounts { + GAS: number + NEO: number + } -interface PastTransaction { - GAS: number - NEO: number - block_index: number - gas_sent: boolean - neo_sent: boolean - txid: string -} + interface History { + address: string + history: PastTransaction[] + name: string + net: 'MainNet' | 'TestNet' + } -//coinmarketcap -export namespace cmc { - export function getPrice(coin?: string, currency?: string): Promise -} + interface PastTransaction { + GAS: number + NEO: number + block_index: number + gas_sent: boolean + neo_sent: boolean + txid: string + } + export module api { + //coinmarketcap + export namespace cmc { + export function getPrice(coin?: string, currency?: string): Promise + } -//core -export function getBalanceFrom(config: apiConfig, api: object): apiConfig -export function getClaimsFrom(config: apiConfig, api: object): apiConfig -export function createTx(config: apiConfig, txType: string): apiConfig -export function signTx(config: apiConfig): apiConfig -export function sendTx(config: apiConfig): apiConfig -export function makeIntent(assetAmts: assetAmounts, address: string): TransactionOutput[] -export function sendAsset(config: apiConfig): apiConfig -export function claimGas(config: apiConfig): apiConfig -export function doInvoke(config: apiConfig): apiConfig -//neonDB -export namespace neonDB { - export function getAPIEndpoint(net: string): string - export function getBalance(net: string, address: string): Promise - export function getClaims(net: string, address: string): Promise - export function getRPCEndpoint(net: string): Promise - export function getTransactionHistory(net: string, address: string): Promise - export function getWalletDBHeight(net: string): Promise + //core + export function getBalanceFrom(config: apiConfig, api: object): apiConfig + export function getClaimsFrom(config: apiConfig, api: object): apiConfig + export function createTx(config: apiConfig, txType: string): apiConfig + export function signTx(config: apiConfig): apiConfig + export function sendTx(config: apiConfig): apiConfig + export function makeIntent(assetAmts: AssetAmounts, address: string): TransactionOutput[] + export function sendAsset(config: apiConfig): apiConfig + export function claimGas(config: apiConfig): apiConfig + export function doInvoke(config: apiConfig): apiConfig - export function doClaimAllGas( - net: string, - privateKey: string - ): Promise - export function doClaimAllGas( - net: string, - publicKey: string, - signingFunction: (unsigned: Transaction, publicKey: string) => Transaction - ): Promise + //neonDB + export namespace neonDB { + export function getAPIEndpoint(net: string): string + export function getBalance(net: string, address: string): Promise + export function getClaims(net: string, address: string): Promise + export function getRPCEndpoint(net: string): Promise + export function getTransactionHistory(net: string, address: string): Promise + export function getWalletDBHeight(net: string): Promise - export function doMintTokens( - net: string, - scriptHash: string, - fromWif: string, - neo: number, - gasCost: number - ): Promise - export function doMintTokens( - net: string, - scriptHash: string, - publicKey: string, - neo: number, - gasCost: number, - signingFunction: (unsigned: Transaction, publicKey: string) => Transaction - ): Promise + export function doClaimAllGas( + net: string, + privateKey: string + ): Promise + export function doClaimAllGas( + net: string, + publicKey: string, + signingFunction: (unsigned: Transaction, publicKey: string) => Transaction + ): Promise - export function doSendAsset( - net: string, - toAddress: string, - from: string, - assetAmounts: assetAmounts - ): Promise - export function doSendAsset( - net: string, - toAddress: string, - publicKey: string, - assetAmounts: assetAmounts, - signingFunction: (unsigned: Transaction, publicKey: string) => Transaction - ): Promise -} + export function doMintTokens( + net: string, + scriptHash: string, + fromWif: string, + neo: number, + gasCost: number + ): Promise + export function doMintTokens( + net: string, + scriptHash: string, + publicKey: string, + neo: number, + gasCost: number, + signingFunction: (unsigned: Transaction, publicKey: string) => Transaction + ): Promise -//neoscan -export namespace neoscan { - export function getAPIEndpoint(net: string): string - export function getRPCEndpoint(net: string): Promise - export function getBalance(net: string, address: string): Promise - export function getClaims(net: string, address: string): Promise -} + export function doSendAsset( + net: string, + toAddress: string, + from: string, + assetAmounts: AssetAmounts + ): Promise + export function doSendAsset( + net: string, + toAddress: string, + publicKey: string, + assetAmounts: AssetAmounts, + signingFunction: (unsigned: Transaction, publicKey: string) => Transaction + ): Promise + } -//nep5 -export namespace nep5 { - export function getTokenInfo(net: string, scriptHash: string): Promise<{ name: string, symbol: string, decimals: number, totalSupply: number }> - export function getTokenBalance(net: string, scriptHash: string, address: string): Promise -} + //neoscan + export namespace neoscan { + export function getAPIEndpoint(net: string): string + export function getRPCEndpoint(net: string): Promise + export function getBalance(net: string, address: string): Promise + export function getClaims(net: string, address: string): Promise + } -//index -export default { - get: { - price: cmc.getPrice, - balance: neonDB.getBalance, - claims: neonDB.getClaims, - transactionHistory: neonDB.getTransactionHistory, - tokenBalance: nep5.getTokenBalance, - tokenInfo: nep5.getTokenInfo - }, - do: { - sendAsset: neonDB.doSendAsset, - claimAllGas: neonDB.doClaimAllGas, - mintTokens: neonDB.doMintTokens + //nep5 + export namespace nep5 { + export function getTokenInfo(net: string, scriptHash: string): Promise<{ name: string, symbol: string, decimals: number, totalSupply: number }> + export function getTokenBalance(net: string, scriptHash: string, address: string): Promise + export function doTransferToken( + net: string, + scriptHash: string, + fromWif: string, + toAddress: string, + transferAmount: number, + gasCost?: number, + signingFunction?: (unsigned: Transaction, publicKey: string) => Transaction + ): Promise + } + } + export interface semantic { + get: { + price: (coin?: string, currency?: string) => Promise + balance: (net: string, address: string) => Promise + claims: (net: string, address: string) => Promise + transactionHistory: (net: string, address: string) => Promise + tokenBalance: (net: string, scriptHash: string) => Promise<{ name: string, symbol: string, decimals: number, totalSupply: number }> + tokenInfo: (net: string, scriptHash: string) => Promise<{ name: string, symbol: string, decimals: number, totalSupply: number }> + }, + do: { + sendAsset: ( + net: string, + toAddress: string, + from: string, + assetAmounts: AssetAmounts + ) => Promise + claimAllGas: ( + net: string, + privateKey: string + ) => Promise + mintTokens: ( + net: string, + scriptHash: string, + fromWif: string, + neo: number, + gasCost: number + ) => Promise + } } } diff --git a/src/api/index.js b/src/api/index.js index f72e8b51f..ca58bda87 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -3,6 +3,24 @@ import * as cmc from './coinmarketcap' import * as nep5 from './nep5' import * as neoscan from './neoscan' +/** + * @typedef {object} History + * @property {string} address - Address. + * @property {string} name - API name. + * @property {string} net - 'MainNet' or 'TestNet' + * @property {PastTx[]} history - List of past transactions. + */ + +/** + * @typedef {object} PastTx + * @property {number} GAS - Gas involved. + * @property {number} NEO - NEO involved. + * @property {number} block_index - Block index. + * @property {boolean} gas_sent - Was GAS sent. + * @property {boolean} neo_sent - Was NEO sent. + * @property {string} txid - Transaction ID. + */ + export default { get: { price: cmc.getPrice, diff --git a/src/api/neonDB.js b/src/api/neonDB.js index cd46c4b79..bee162bcf 100644 --- a/src/api/neonDB.js +++ b/src/api/neonDB.js @@ -1,49 +1,9 @@ import axios from 'axios' -import { Account } from '../wallet' +import { Account, Balance } from '../wallet' import { Transaction } from '../transactions' import { Query } from '../rpc' import { ASSET_ID } from '../consts' -/** - * @typedef {object} Coin - * @property {number} index - Index in list. - * @property {string} txid - Transaction ID which produced this coin. - * @property {number} value - Value of this coin. - */ - -/** - * @typedef {object} Balance - * @property {{balance: number, unspent: Coin[]}} NEO Amount of NEO in address - * @property {{balance: number, unspent: Coin[]}} GAS Amount of GAS in address - * @property {string} address - The Address that was queried - * @property {string} net - 'MainNet' or 'TestNet' - */ - -/** - * @typedef {object} History - * @property {string} address - Address. - * @property {string} name - API name. - * @property {string} net - 'MainNet' or 'TestNet' - * @property {PastTx[]} history - List of past transactions. - */ - -/** - * @typedef {object} PastTx - * @property {number} GAS - Gas involved. - * @property {number} NEO - NEO involved. - * @property {number} block_index - Block index. - * @property {boolean} gas_sent - Was GAS sent. - * @property {boolean} neo_sent - Was NEO sent. - * @property {string} txid - Transaction ID. - */ -/** - * @typedef {object} Response - * @property {string} jsonrpc - JSON-RPC Version - * @property {number} id - Unique ID. - * @property {any} result - Result - * @property {string} [txid] - Transaction hash of the successful transaction. Only available when result is true. -*/ - /** * API Switch for MainNet and TestNet * @param {string} net - 'MainNet', 'TestNet', or custom neon-wallet-db URL. @@ -69,7 +29,14 @@ export const getBalance = (net, address) => { const apiEndpoint = getAPIEndpoint(net) return axios.get(apiEndpoint + '/v2/address/balance/' + address) .then((res) => { - return res.data + const bal = new Balance({ net, address: res.data.address }) + Object.keys(res.data).map((key) => { + if (key === 'net' || key === 'address') return + bal.addAsset(key, res.data[key]) + }) + // To be deprecated + Object.assign(bal, res.data) + return bal }) } diff --git a/src/api/neoscan.js b/src/api/neoscan.js index 55ebb2277..497991fef 100644 --- a/src/api/neoscan.js +++ b/src/api/neoscan.js @@ -1,4 +1,5 @@ import axios from 'axios' +import { Balance } from '../wallet' /** * Returns the appropriate NeoScan endpoint. @@ -10,7 +11,7 @@ export const getAPIEndpoint = (net) => { case 'MainNet': return 'https://neoscan.io/api/main_net' case 'TestNet': - throw new Error(`Not Implemented`) + return 'https://neoscan-testnet.io/api/test_net' default: return net } @@ -24,10 +25,10 @@ export const getAPIEndpoint = (net) => { export const getRPCEndpoint = (net) => { const apiEndpoint = getAPIEndpoint(net) return axios.get(apiEndpoint + '/v1/get_all_nodes') - .then((res) => { + .then(({ data }) => { let bestHeight = 0 let nodes = [] - for (const node in res.data) { + for (const node of data) { if (node.height > bestHeight) { bestHeight = node.height nodes = [node] @@ -49,14 +50,19 @@ export const getBalance = (net, address) => { const apiEndpoint = getAPIEndpoint(net) return axios.get(apiEndpoint + '/v1/get_balance/' + address) .then((res) => { - const balances = { address: res.data.address, net } + const bal = new Balance({ address: res.data.address, net }) res.data.balance.map((b) => { - balances[b.asset] = { + bal.addAsset(b.asset, { + balance: b.amount, + unspent: parseUnspent(b.unspent) + }) + // To be deprecated + bal[b.asset] = { balance: b.amount, unspent: parseUnspent(b.unspent) } }) - return balances + return bal }) } diff --git a/src/consts.d.ts b/src/consts.d.ts index f16568e60..3189160d2 100644 --- a/src/consts.d.ts +++ b/src/consts.d.ts @@ -1,44 +1,41 @@ - -export const ADDR_VERISON = '17' - -export const ASSETS = { - NEO: 'NEO', - 'c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b': 'NEO', - GAS: 'GAS', - '602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7': 'GAS' -} - -export const ASSET_ID = { - NEO: 'c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b', - GAS: '602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7' -} - -export const CONTRACTS: { - RPX: 'ecc6b20d3ccac1ee9ef109af5a7cdb85706b1df9', - TEST_RPX: '5b7074e873973a6ed3708862f219a6fbf4d1c411' -} - -export const DEFAULT_RPC: { - MAIN: 'http://seed1.neo.org:10332', - TEST: 'http://seed1.neo.org:20332' -} - -export const DEFAULT_REQ: { - jsonrpc: string, - method: string, - params: [], - id: number -} - -export const NEO_NETWORK = { - MAIN: 'MainNet', - TEST: 'TestNet' -} - -export const RPC_VERSION = '2.3.2' - -export const TX_VERSION = { - CLAIM: number, - CONTRACT: number, - INVOCATION: number +declare module '@cityofzion/neon-js' { + export module CONST { + export const ADDR_VERISON = '17' + + export namespace ASSETS { + export const NEO: string + export const c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b: string + export const GAS: string + } + + export namespace ASSET_ID { + export const NEO: string + export const GAS: string + } + + export namespace CONTRACTS { + export const RPX: string + export const TEST_RPX: string + } + + export namespace DEFAULT_RPC { + export const MAIN: string + export const TEST: string + } + + export const DEFAULT_REQ: object + + export namespace NEO_NETWORK { + export const MAIN: string + export const TEST: string + } + + export const RPC_VERSION: string + + export namespace TX_VERSION { + export const CLAIM: number + export const CONTRACT: number + export const INVOCATION: number + } + } } diff --git a/src/index.d.ts b/src/index.d.ts index e801096b2..a9da8ef50 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,37 +1,13 @@ -import u from './utils' -import CONST from './consts' -import tx from './transactions' -import api from './api' -import sc from './sc' -import rpc from './rpc' -import wallet from './wallet' - -export as namespace Neon; - -declare module 'api' { - export = api -} - -declare module 'CONST' { - export = CONST -} - -declare module 'rpc' { - export = rpc -} - -declare module 'sc' { - export = sc -} - -declare module 'tx' { - export = tx -} - -declare module 'u' { - export = u -} - -declare module 'wallet' { - export = wallet +/// +/// +/// +/// +/// +/// +/// +import { u, wallet } from "@cityofzion/neon-js"; + +declare module '@cityofzion/neon-js' { + const _semantic: semantic + export default _semantic } diff --git a/src/rpc/index.d.ts b/src/rpc/index.d.ts index 9bee7550f..55c7e10e8 100644 --- a/src/rpc/index.d.ts +++ b/src/rpc/index.d.ts @@ -1,76 +1,86 @@ +import { tx } from '@cityofzion/neon-js' +declare module '@cityofzion/neon-js' { + export interface RPCRequest { + method: string, + params: Array, + id: number + } -interface RPCRequest { - method: string, - params: Array, - id: number -} + export interface RPCResponse { + jsonrpc: string, + id: number, + result: any + } -interface RPCResponse { - jsonrpc: string, - id: number, - result: any -} + export module rpc { + //client + export class RPCClient { + constructor(net: string, version: string) -//client -export class RPCClient { - constructor(net: string, version: string) + execute(query: Query): Promise + query(req: RPCRequest): Promise + getAccountState(addr: string): Promise + getAssetState(assetId: string): Promise + getBlock(indexOrHash: string | number, verbose?: number): Promise + getBestBlockHash(): Promise + getBlockCount(): Promise + getBlockSysFee(index: number): Promise + getConnectionCount(): Promise + getContractState(scriptHash: string): Promise + getPeers(): Promise + getRawMemPool(): Promise + getRawTransaction(txid: string, verbose?: number): Promise + getStorage(scriptHash: string, key: string): Promise + getTxOut(txid: string, index: number): Promise + getVersion(): Promise + invoke(scriptHash: string, params: Array): Promise + invokeFunction(scriptHash: string, operation: string, params: Array): Promise + invokeScript(script: string): Promise + sendRawTransaction(transaction: Transaction | string): Promise + submitBlock(block: string): Promise + validateAddress(addr: string): Promise + } - execute(query: Query): Promise - query(req: RPCRequest): Promise - getAccountState(addr: string): Promise - getAssetState(assetId: string): Promise - getBlock(indexOrHash: string | number, verbose?: number): Promise - getBestBlockHash(): Promise - getBlockCount(): Promise - getBlockSysFee(index: number): Promise - getConnectionCount(): Promise - getContractState(scriptHash: string): Promise - getPeers(): Promise - getRawMemPool(): Promise - getRawTransaction(txid: string, verbose?: number): Promise - getStorage(scriptHash: string, key: string): Promise - getTxOut(txid: string, index: number): Promise - getVersion(): Promise - invoke(scriptHash: string, params: Array): Promise - invokeFunction(scriptHash: string, operation: string, params: Array): Promise - invokeScript(script: string): Promise - sendRawTransaction(transaction: Transaction | string): Promise - submitBlock(block: string): Promise - validateAddress(addr: string): Promise -} + //parse + export function VMParser(res: RPCResponse): Array + export function VMExtractor(res: RPCResponse): Array -//parse -export function VMParser(res: RPCResponse): Array -export function VMExtractor(res: RPCResponse): Array + //query + export class Query { + constructor(req: RPCRequest) -//query -export class Query { - constructor(req: RPCRequest) + parseWith(parser: (any) => any): this + execute(url: string): Promise - parseWith(parser: (any) => any): this - execute(url: string): Promise + static getAccountState(addr: string): Query + static getAssetState(assetId: string): Query + static getBlock(indexOrHash: string | number, verbose?: number): Query + static getBestBlockHash(): Query + static getBlockCount(): Query + static getBlockSysFee(index: number): Query + static getConnectionCount(): Query + static getContractState(scriptHash: string): Query + static getPeers(): Query + static getRawMemPool(): Query + static getRawTransaction(txid: string, verbose?: number): Query + static getStorage(scriptHash: string, key: string): Query + static getTxOut(txid: string, index: number): Query + static getVersion(): Query + static invoke(scriptHash: string, params: Array): Query + static invokeFunction(scriptHash: string, operation: string, params: Array): Query + static invokeScript(script: string): Query + static sendRawTransaction(transaction: Transaction | string): Query + static submitBlock(block: string): Query + static validateAddress(addr: string): Query + } - static getAccountState(addr: string): Query - static getAssetState(assetId: string): Query - static getBlock(indexOrHash: string | number, verbose?: number): Query - static getBestBlockHash(): Query - static getBlockCount(): Query - static getBlockSysFee(index: number): Query - static getConnectionCount(): Query - static getContractState(scriptHash: string): Query - static getPeers(): Query - static getRawMemPool(): Query - static getRawTransaction(txid: string, verbose?: number): Query - static getStorage(scriptHash: string, key: string): Query - static getTxOut(txid: string, index: number): Query - static getVersion(): Query - static invoke(scriptHash: string, params: Array): Query - static invokeFunction(scriptHash: string, operation: string, params: Array): Query - static invokeScript(script: string): Query - static sendRawTransaction(transaction: Transaction | string): Query - static submitBlock(block: string): Query - static validateAddress(addr: string): Query + export function queryRPC(urrl: string, req: RPCRequest): Promise + } + export interface semantic { + create: { + rpcClient: (net) => RPCClient + query: (req) => Query + } + } } - -export function queryRPC(urrl: string, req: RPCRequest): Promise diff --git a/src/rpc/index.js b/src/rpc/index.js index 8cf976711..30401175c 100644 --- a/src/rpc/index.js +++ b/src/rpc/index.js @@ -1,6 +1,14 @@ import RPCClient from './client' import Query, { queryRPC } from './query' +/** + * @typedef {object} RPCResponse + * @property {string} jsonrpc - JSON-RPC Version + * @property {number} id - Unique ID. + * @property {any} result - Result + * @property {string} [txid] - Transaction hash of the successful transaction. Only available when result is true. +*/ + export default { create: { rpcClient: (net) => new RPCClient(net), diff --git a/src/sc/index.d.ts b/src/sc/index.d.ts index aac63ee29..97bb5c894 100644 --- a/src/sc/index.d.ts +++ b/src/sc/index.d.ts @@ -1,78 +1,83 @@ -import { StringStream } from '../utils' +/// -interface scriptParams { - scriptHash: string, - operation?: string, - args?: Array | string | number | boolean, - useTailCall?: boolean -} +declare module '@cityofzion/neon-js' { -//ContractParam -export class ContractParam { - constructor(type: string, value: any) + export interface scriptParams { + scriptHash: string, + operation?: string, + args?: Array | string | number | boolean, + useTailCall?: boolean + } - static string(value: string): ContractParam - static boolean(value: any): ContractParam - static integer(value: any): ContractParam - static byteArray(value: any, format: string): ContractParam - static array(param: any[]): ContractParam -} + export module sc { + //ContractParam + export class ContractParam { + constructor(type: string, value: any) -//opCode -export enum OpCode { } + static string(value: string): ContractParam + static boolean(value: any): ContractParam + static integer(value: any): ContractParam + static byteArray(value: any, format: string): ContractParam + static array(param: any[]): ContractParam + } -//ScriptBuilder -export class ScriptBuilder extends StringStream { - constructor() + //opCode + export enum OpCode { } - _emitAppCall(scriptHash: string, useTailCall?: boolean): this - _emitArray(arr: Array): this - _emitString(hexstring: string): this - _emitNum(num: number): this - _emitParam(param: ContractParam): this + //ScriptBuilder + export class ScriptBuilder extends u.StringStream { + constructor() - emit(op: OpCode, args: string): this - emitAppCall( - scriptHash: string, - operation: string, - args: Array | string | number | boolean, - useTailCall?: boolean): this - emitSysCall(api: string): this - emitPush(data: Array | string | number | boolean): this -} + _emitAppCall(scriptHash: string, useTailCall?: boolean): this + _emitArray(arr: Array): this + _emitString(hexstring: string): this + _emitNum(num: number): this + _emitParam(param: ContractParam): this -export function createScript({ scriptHash, operation, args, useTailCall }: scriptParams): string + emit(op: OpCode, args: string): this + emitAppCall( + scriptHash: string, + operation: string, + args: Array | string | number | boolean, + useTailCall?: boolean): this + emitSysCall(api: string): this + emitPush(data: Array | string | number | boolean): this + } -//index -interface DeployScriptConfig { - script: string, - name: string, - version: string, - author: string, - email: string, - description: string, - needsStorage?: boolean, - returnType?: string, - paramaterList?: string -} + export function createScript({ scriptHash, operation, args, useTailCall }: scriptParams): string -export function generateDeployScript({ - script, - name, - version, - author, - email, - description, - needsStorage, - returnType, - paramaterList -}: DeployScriptConfig): string + //index + interface DeployScriptConfig { + script: string, + name: string, + version: string, + author: string, + email: string, + description: string, + needsStorage?: boolean, + returnType?: string, + paramaterList?: string + } + + export function generateDeployScript({ + script, + name, + version, + author, + email, + description, + needsStorage, + returnType, + paramaterList + }: DeployScriptConfig): string + } -export default { - create: { - contractParam: (args: any) => ContractParam, - script: createScript, - scriptBuilder: (args: any) => ScriptBuilder, - deployScript: (any) => string + export interface semantic { + create: { + contractParam: (args: any) => sc.ContractParam + script: ({ scriptHash, operation, args, useTailCall }: sc.scriptParams) => string + scriptBuilder: (args: any) => sc.ScriptBuilder + deployScript: (any) => string + } } } diff --git a/src/transactions/core.js b/src/transactions/core.js index 12af8d34b..da59859df 100644 --- a/src/transactions/core.js +++ b/src/transactions/core.js @@ -28,7 +28,9 @@ export const calculateInputs = (balances, intents, gasCost = 0) => { let change = [] const inputs = Object.keys(requiredAssets).map((assetId) => { const requiredAmt = requiredAssets[assetId] - const assetBalance = balances[ASSETS[assetId]] + const assetSymbol = ASSETS[assetId] + if (balances.assetSymbols.indexOf(assetSymbol) === -1) throw new Error(`This balance does not contain any ${assetSymbol}!`) + const assetBalance = balances.assets[assetSymbol] if (assetBalance.balance * 100000000 < requiredAmt) throw new Error(`Insufficient ${ASSETS[assetId]}! Need ${requiredAmt / 100000000} but only found ${assetBalance.balance}`) // Ascending order sort assetBalance.unspent.sort((a, b) => a.value - b.value) diff --git a/src/transactions/index.d.ts b/src/transactions/index.d.ts index 132029dc1..871afbf3f 100644 --- a/src/transactions/index.d.ts +++ b/src/transactions/index.d.ts @@ -1,118 +1,144 @@ -import { NEO_NETWORK } from '../consts' -import { StringStream } from '../utils' -import { scriptParams } from '../sc' - -interface TransactionAttribute { - data: string - usage: number -} - -interface TransactionInput { - prevHash: string - prevIndex: number -} - -interface TransactionOutput { - assetId: string - scriptHash: string - value: number -} - -interface Witness { - invocationScript: string - verificationScript: string -} - -interface Balance { - GAS: TokenBalance - NEO: TokenBalance - address: string - net: Net -} - -interface Claim { - claim: number - index: number - txid: string -} - -interface ClaimData { - claims: Claim[] -} - -interface Coin { - index: number - txid: string - value: number -} - -interface TokenBalance { - balance: number - unspent: Coin[] -} - -//components -export function serializeTransactionInput(input: TransactionInput): string -export function deserializeTransactionInput(stream: StringStream): TransactionInput -export function serializeTransactionOutput(output: TransactionOutput): string -export function deserializeTransactionOutput(stream: StringStream): TransactionOutput -export function createTransactionOutput(assetSym: string, value: number, address: string): TransactionOutput -export function serializeTransactionAttribute(attr: TransactionAttribute): string -export function deserializeTransactionAttribute(stream: StringStream): TransactionAttribute -export function serializeWitness(witness: Witness): string -export function deserializeWitness(stream: StringStream): Witness - -//core -export function calculateInputs(balances: Balance, intents: TransactionOutput[], gasCost?: number): { inputs: TransactionInput[], change: TransactionOutput[] } -export function serializeTransaction(tx: Transaction, signed?: boolean): string -export function deserializeTransaction(data: string): Transaction -export function signTransaction(transaction: Transaction, privateKey: string): Transaction -export function getTransactionHash(transaction: Transaction): string - -//exclusive -export const serializeExclusive: { - 2: (tx: Transaction) => string - 128: (tx: Transaction) => '' - 209: (tx: Transaction) => string -} - -export const deserializeExclusive: { - 2: (stream: StringStream) => { claims: TransactionInput[] } - 128: (stream: StringStream) => {} - 209: (stream: StringStream) => { gas: number, script: string } -} - -export const getExclusive: { - 2: (tx: Transaction) => { claims: TransactionInput[] } - 128: (tx: Transaction) => {} - 209: (tx: Transaction) => { gas: number, script: string } -} - -//transaction -interface txConfig { - type: number, - version: number, - attributes: TransactionAttribute[] - inputs: TransactionInput[] - outputs: TransactionOutput[] - scripts: Witness[] -} - - -export class Transaction { - public type: number - public version: number - public attributes: TransactionAttribute[] - public inputs: TransactionInput[] - public outputs: TransactionOutput[] - public scripts: Witness[] - - constructor(TxConfig) - - get exclusiveData(): object - get hash(): string - - static createClaimTx(publicKeyOrAddress: string, claimData: Claim, override: object): Transaction - static createContractTx(balances: Balance, intents: TransactionOutput[], override: object): Transaction - static createInvocationTx(balance: Balance, intents: TransactionOutput[], invoke: object | string, gasCost: number, override: object): Transaction +declare module '@cityofzion/neon-js' { + export interface Transaction { + type: number + version: number + attributes: TransactionAttribute[] + inputs: TransactionInput[] + outputs: TransactionOutput[] + scripts: Witness[] + } + + export interface TransactionAttribute { + data: string + usage: number + } + + export interface TransactionInput { + prevHash: string + prevIndex: number + } + + export interface TransactionOutput { + assetId: string + scriptHash: string + value: number + } + + export interface Witness { + invocationScript: string + verificationScript: string + } + + export interface Balance { + GAS: TokenBalance + NEO: TokenBalance + address: string + net: 'MainNet' | 'TestNet' + } + + export interface Claim { + claim: number + index: number + txid: string + } + + interface ClaimData { + claims: Claim[] + } + + interface txConfig { + type: number, + version: number, + attributes: TransactionAttribute[] + inputs: TransactionInput[] + outputs: TransactionOutput[] + scripts: Witness[] + } + + export module tx { + //components + export function serializeTransactionInput(input: TransactionInput): string + export function deserializeTransactionInput(stream: u.StringStream): TransactionInput + export function serializeTransactionOutput(output: TransactionOutput): string + export function deserializeTransactionOutput(stream: u.StringStream): TransactionOutput + export function createTransactionOutput(assetSym: string, value: number, address: string): TransactionOutput + export function serializeTransactionAttribute(attr: TransactionAttribute): string + export function deserializeTransactionAttribute(stream: u.StringStream): TransactionAttribute + export function serializeWitness(witness: Witness): string + export function deserializeWitness(stream: u.StringStream): Witness + + //core + export function calculateInputs(balances: Balance, intents: TransactionOutput[], gasCost?: number): { inputs: TransactionInput[], change: TransactionOutput[] } + export function serializeTransaction(tx: Transaction, signed?: boolean): string + export function deserializeTransaction(data: string): Transaction + export function signTransaction(transaction: Transaction, privateKey: string): Transaction + export function getTransactionHash(transaction: Transaction): string + + //exclusive + export const serializeExclusive: { + 2: (tx: Transaction) => string + 128: (tx: Transaction) => '' + 209: (tx: Transaction) => string + } + + export const deserializeExclusive: { + 2: (stream: u.StringStream) => { claims: TransactionInput[] } + 128: (stream: u.StringStream) => {} + 209: (stream: u.StringStream) => { gas: number, script: string } + } + + export const getExclusive: { + 2: (tx: Transaction) => { claims: TransactionInput[] } + 128: (tx: Transaction) => {} + 209: (tx: Transaction) => { gas: number, script: string } + } + + //transaction + export class Transaction { + public type: number + public version: number + public attributes: TransactionAttribute[] + public inputs: TransactionInput[] + public outputs: TransactionOutput[] + public scripts: Witness[] + + constructor(TxConfig) + + exclusiveData(): object + hash(): string + + static createClaimTx(publicKeyOrAddress: string, claimData: Claim, override: object): Transaction + static createContractTx(balances: Balance, intents: TransactionOutput[], override: object): Transaction + static createInvocationTx(balance: Balance, intents: TransactionOutput[], invoke: object | string, gasCost: number, override: object): Transaction + } + } + + export interface semantic { + create: { + tx: (args: any[]) => Transaction + claimTx: (publicKeyOrAddress: string, claimData: Claim, override: object) => Transaction + contractTx: (balances: Balance, intents: TransactionOutput[], override: object) => Transaction + invocationTx: (balance: Balance, intents: TransactionOutput[], invoke: object | string, gasCost: number, override: object) => Transaction + } + serialize: { + attribute: (attr: TransactionAttribute) => string + input: (input: TransactionInput) => string + output: (output: TransactionOutput) => string + exclusiveData: object + tx: (tx: Transaction) => string + } + deserialize: { + attribute: (stream: StringStream) => TransactionAttribute + input: (stream: StringStream) => TransactionInput + output: (stream: StringStream) => TransactionOutput + exclusiveData: object + tx: (stream: StringStream) => Transaction + } + get: { + transactionHash: (transaction: Transaction) => string + } + sign: { + transaction: (transaction: Transaction, privateKey: string) => Transaction + } + } } diff --git a/src/utils.d.ts b/src/utils.d.ts index d55121783..a6348b18e 100644 --- a/src/utils.d.ts +++ b/src/utils.d.ts @@ -1,30 +1,33 @@ +declare module '@cityofzion/neon-js' { + export module u { + export function ab2str(buf: ArrayBuffer): string + export function str2ab(str: string): ArrayBuffer + export function hexstring2ab(str: string): number[] + export function ab2hexstring(arr: ArrayBuffer): string + export function str2hexstring(str: string): string + export function int2hex(mNumber: number): string + export function num2hexstring(num: number, size: number, littleEndian?: boolean): string + export function num2fixed8(num: number, size?: number): string + export function fixed82num(fixed8: string): number + export function num2VarInt(num: number): string + export function hexXor(str1: string, str2: string): string + export function reverseArray(arr: Array): Uint8Array + export function reverseHex(hex: string): string -export function ab2str(buf: ArrayBuffer): string -export function str2ab(str: string): ArrayBuffer -export function hexstring2ab(str: string): number[] -export function ab2hexstring(arr: ArrayBuffer): string -export function str2hexstring(str: string): string -export function int2hex(mNumber: number): string -export function num2hexstring(num: number, size: number, littleEndian?: boolean): string -export function num2fixed8(num: number): string -export function fixed82num(fixed8: string): number -export function num2VarInt(num: number): string -export function hexXor(str1: string, str2: string): string -export function reverseArray(arr: Array): Uint8Array -export function reverseHex(hex: string): string + export class StringStream { + public pter: 0 + public str: string -export class StringStream { - public pter: 0 - public str: string + constructor(str?: string) - constructor(str?: string) + public isEmpty(): boolean + public read(bytes: number): string + public readVarBytes(): string + public readVarInt(): string + } - public isEmpty(): boolean - public read(bytes: number): string - public readVarBytes(): string - public readVarInt(): string + export function hash160(hex: string): string + export function hash256(hex: string): string + export function sha256(hex: string): string + } } - -export function hash160(hex: string): string -export function hash256(hex: string): string -export function sha256(hex: string): string diff --git a/src/utils.js b/src/utils.js index dca5e42f6..e41fd47cb 100644 --- a/src/utils.js +++ b/src/utils.js @@ -4,18 +4,22 @@ import { SHA256, RIPEMD160, enc } from 'crypto-js' * @param {arrayBuffer} buf * @returns {string} ASCII string */ -export const ab2str = buf => { return String.fromCharCode.apply(null, new Uint8Array(buf)) } +export const ab2str = buf => + String.fromCharCode.apply(null, new Uint8Array(buf)) /** * @param {string} str - ASCII string * @returns {arrayBuffer} */ export const str2ab = str => { - let bufView = new Uint8Array(str.length) + if (typeof str !== 'string') { + throw new Error('str2ab expects a string') + } + const result = new Uint8Array(str.length) for (let i = 0, strLen = str.length; i < strLen; i++) { - bufView[i] = str.charCodeAt(i) + result[i] = str.charCodeAt(i) } - return bufView + return result } /** @@ -23,12 +27,16 @@ export const str2ab = str => { * @returns {number[]} */ export const hexstring2ab = str => { - let result = [] - while (str.length >= 2) { - result.push(parseInt(str.substring(0, 2), 16)) - str = str.substring(2, str.length) + if (typeof str !== 'string') { + throw new Error('hexstring2ab expects a string') + } + if (!str.length) return new Uint8Array() + const iters = str.length / 2 + const result = new Uint8Array(iters) + for (let i = 0; i < iters; i++) { + result[i] = parseInt(str.substring(0, 2), 16) + str = str.substring(2) } - return result } @@ -37,6 +45,9 @@ export const hexstring2ab = str => { * @returns {string} HEX string */ export const ab2hexstring = arr => { + if (typeof arr !== 'object') { + throw new Error('ab2hexstring expects an array') + } let result = '' for (let i = 0; i < arr.length; i++) { let str = arr[i].toString(16) @@ -52,17 +63,18 @@ export const ab2hexstring = arr => { * @param {string} str - ASCII string * @returns {string} HEX string */ -export const str2hexstring = str => { - return ab2hexstring(str2ab(str)) -} +export const str2hexstring = str => ab2hexstring(str2ab(str)) /** - * convert an integer to hex and add leading zeros - * @param {number} mNumber + * convert an integer to big endian hex and add leading zeros + * @param {number} num * @returns {string} */ -export const int2hex = mNumber => { - let h = mNumber.toString(16) +export const int2hex = num => { + if (typeof num !== 'number') { + throw new Error('int2hex expects a number') + } + let h = num.toString(16) return h.length % 2 ? '0' + h : h } @@ -74,6 +86,9 @@ export const int2hex = mNumber => { * @return {string} */ export const num2hexstring = (num, size = 2, littleEndian = false) => { + if (typeof num !== 'number') throw new Error('num must be numeric') + if (num < 0) throw new RangeError('num is unsigned (>= 0)') + if (!Number.isSafeInteger(num)) throw new RangeError(`num (${num}) must be a safe integer`) let hexstring = num.toString(16) hexstring = hexstring.length % size === 0 ? hexstring : ('0'.repeat(size) + hexstring).substring(hexstring.length) if (littleEndian) hexstring = reverseHex(hexstring) @@ -81,22 +96,25 @@ export const num2hexstring = (num, size = 2, littleEndian = false) => { } /** - * Converts a number to a Fixed8 format string + * Converts a number to a Fixed8 format hex string * @param {number} num + * @param {number} size output size in hex chars * @return {string} number in Fixed8 representation. */ -export const num2fixed8 = (num) => { - const hexValue = Math.round(num * 100000000).toString(16) - return reverseHex(('0000000000000000' + hexValue).substring(hexValue.length)) +export const num2fixed8 = (num, size = 16) => { + if (typeof num !== 'number') throw new Error('num must be numeric') + return num2hexstring(Math.round(num * Math.pow(10, 8)), size, true) } /** - * Converts a Fixed8 string to number - * @param {string} fixed8 - number in Fixed8 representation + * Converts a Fixed8 hex string to its original number + * @param {string} fixed8hex - number in Fixed8 representation * @return {number} */ -export const fixed82num = (fixed8) => { - return parseInt(reverseHex(fixed8), 16) / 100000000 +export const fixed82num = (fixed8hex) => { + if (typeof fixed8hex !== 'string') throw new Error('fixed8hex must be a string') + if (!fixed8hex.length || fixed8hex.length % 2 !== 0) throw new Error('fixed8hex must be hex') + return parseInt(reverseHex(fixed8hex), 16) / Math.pow(10, 8) } /** @@ -105,9 +123,6 @@ export const fixed82num = (fixed8) => { * @returns {string} hexstring of the variable Int. */ export const num2VarInt = (num) => { - if (typeof num !== 'number') throw new Error('VarInt must be numeric') - if (num < 0) throw new RangeError('VarInts are unsigned (> 0)') - if (!Number.isSafeInteger(num)) throw new RangeError('VarInt must be a safe integer') if (num < 0xfd) { return num2hexstring(num) } else if (num <= 0xffff) { @@ -129,8 +144,9 @@ export const num2VarInt = (num) => { * @returns {string} XOR output as a HEX string */ export const hexXor = (str1, str2) => { - if (str1.length !== str2.length) throw new Error() - if (str1.length % 2 !== 0) throw new Error() + if (typeof str1 !== 'string' || typeof str2 !== 'string') throw new Error('hexXor expects hex strings') + if (str1.length !== str2.length) throw new Error('strings are disparate lengths') + if (str1.length % 2 !== 0) throw new Error('strings must be hex') const result = [] for (let i = 0; i < str1.length; i += 2) { result.push(parseInt(str1.substr(i, 2), 16) ^ parseInt(str2.substr(i, 2), 16)) @@ -144,6 +160,7 @@ export const hexXor = (str1, str2) => { * @returns {Uint8Array} */ export const reverseArray = arr => { + if (typeof arr !== 'object' || !arr.length) throw new Error('reverseArray expects an array') let result = new Uint8Array(arr.length) for (let i = 0; i < arr.length; i++) { result[i] = arr[arr.length - 1 - i] @@ -160,6 +177,7 @@ export const reverseArray = arr => { * @return {string} HEX string reversed in 2s. */ export const reverseHex = hex => { + if (typeof hex !== 'string') throw new Error('reverseHex expects a string') if (hex.length % 2 !== 0) throw new Error(`Incorrect Length: ${hex}`) let out = '' for (let i = hex.length - 2; i >= 0; i -= 2) { @@ -226,6 +244,8 @@ export class StringStream { * @returns {string} hash output */ export const hash160 = (hex) => { + if (typeof hex !== 'string') throw new Error('reverseHex expects a string') + if (hex.length % 2 !== 0) throw new Error(`Incorrect Length: ${hex}`) let hexEncoded = enc.Hex.parse(hex) let ProgramSha256 = SHA256(hexEncoded) return RIPEMD160(ProgramSha256).toString() @@ -237,6 +257,8 @@ export const hash160 = (hex) => { * @returns {string} hash output */ export const hash256 = (hex) => { + if (typeof hex !== 'string') throw new Error('reverseHex expects a string') + if (hex.length % 2 !== 0) throw new Error(`Incorrect Length: ${hex}`) let hexEncoded = enc.Hex.parse(hex) let ProgramSha256 = SHA256(hexEncoded) return SHA256(ProgramSha256).toString() @@ -248,6 +270,8 @@ export const hash256 = (hex) => { * @returns {string} hash output */ export const sha256 = (hex) => { + if (typeof hex !== 'string') throw new Error('reverseHex expects a string') + if (hex.length % 2 !== 0) throw new Error(`Incorrect Length: ${hex}`) let hexEncoded = enc.Hex.parse(hex) return SHA256(hexEncoded).toString() } diff --git a/src/wallet/Balance.js b/src/wallet/Balance.js new file mode 100644 index 000000000..3389df575 --- /dev/null +++ b/src/wallet/Balance.js @@ -0,0 +1,191 @@ +import { Transaction } from '../transactions' +import { ASSETS } from '../consts' +import { Query } from '../rpc' + +/** + * @typedef AssetBalance + * @property {number} balance - The total balance in this AssetBalance + * @property {Coin[]} unspent - Unspent coins + * @property {Coin[]} spent - Spent coins + * @property {Coin[]} unconfirmed - Unconfirmed coins + */ + +/** +* @typedef Coin +* @property {number} index - Index in list. +* @property {string} txid - Transaction ID which produced this coin. +* @property {number} value - Value of this coin. +*/ + +/** + * @class Balance + * @classdesc object describing the coins found within an Account. Look up various balances through its symbol. For example, NEO or GAS. + * @param {object} bal - Balance object as a JSON. + * @param {string} bal.net - 'MainNet' or 'TestNet' + * @param {string} bal.address - The address of the Account + * @param {string[]} bal.assetSymbols - The symbols of the assets available in this Balance + * @param {object} bal.assets - The collection of assets in this Balance + * @param {string[]} bal.tokenSymbols - The symbols of the tokens available in this Balance + * @param {object} bal.tokens - The collection of tokens in this Balance + */ +class Balance { + constructor (bal) { + this.address = bal.address + this.net = bal.net + this.assetSymbols = bal.assetSymbols ? bal.assetSymbols : [] + this.assets = bal.assets ? bal.assets : {} + if (bal.assets) { + Object.keys(bal).map((key) => { + if (typeof bal[key] === 'object') { + this.addAsset(key, bal[key]) + } + }) + } + this.tokenSymbols = bal.tokenSymbols ? bal.tokenSymbols : [] + this.tokens = bal.tokens ? bal.tokens : {} + } + + /** + * Adds a new asset to this Balance. + * @param {string} sym - The symbol to refer by. This function will force it to upper-case. + * @param {AssetBalance} [assetBalance] - The assetBalance if initialized. Default is a zero balance object. + * @return this + */ + addAsset (sym, assetBalance = { balance: 0, spent: [], unspent: [], unconfirmed: [] }) { + sym = sym.toUpperCase() + this.assetSymbols.push(sym) + const newBalance = Object.assign({}, { balance: 0, spent: [], unspent: [], unconfirmed: [] }, assetBalance) + this.assets[sym] = JSON.parse(JSON.stringify(newBalance)) + return this + } + + /** + * Adds a new NEP-5 Token to this Balance. + * @param {string} sym - The NEP-5 Token Symbol to refer by. + * @param {number} tokenBalance - The amount of tokens this account holds. + * @return this + */ + addToken (sym, tokenBalance = 0) { + sym = sym.toUpperCase() + this.tokenSymbols.push(sym) + this.tokens[sym] = tokenBalance + return this + } + + /** + * Applies a Transaction to a Balance, removing spent coins and adding new coins. This currently applies only to Assets. + * @param {Transaction|string} tx - Transaction that has been sent and accepted by Node. + * @param {boolean} confirmed - If confirmed, new coins will be added to unspent. Else, new coins will be added to unconfirmed property first. + * @return {Balance} this + */ + applyTx (tx, confirmed = false) { + tx = tx instanceof Transaction ? tx : Transaction.deserialize(tx) + const symbols = this.assetSymbols + // Spend coins + for (const input of tx.inputs) { + const findFunc = (el) => el.txid === input.prevHash && el.index === input.prevIndex + for (const sym of symbols) { + let assetBalance = this.assets[sym] + let ind = assetBalance.unspent.findIndex(findFunc) + if (ind >= 0) { + let spentCoin = assetBalance.unspent.splice(ind, 1) + assetBalance.spent = assetBalance.spent.concat(spentCoin) + break + } + } + } + + // Add new coins + const hash = tx.hash + for (let i = 0; i < tx.outputs.length; i++) { + const output = tx.outputs[i] + const sym = ASSETS[output.assetId] + let assetBalance = this.assets[sym] + if (!assetBalance) this.addAsset(sym) + const coin = { index: i, txid: hash, value: output.value } + if (confirmed) { + assetBalance.balance += output.value + if (!assetBalance.unspent) assetBalance.unspent = [] + assetBalance.unspent.push(coin) + } else { + if (!assetBalance.unconfirmed) assetBalance.unconfirmed = [] + assetBalance.unconfirmed.push(coin) + } + this.assets[sym] = assetBalance + } + + return this + } + + /** + * Verifies the coins in balance are unspent. This is an expensive call. + * @param {string} url - NEO Node to check against. + * @return {Promise} Returns this + */ + verifyAssets (url) { + const promises = [] + const symbols = this.assetSymbols + symbols.map((key) => { + const assetBalance = this.assets[key] + promises.push(verifyAssetBalance(url, assetBalance)) + }) + return Promise.all(promises) + .then((newBalances) => { + symbols.map((sym, i) => { + this.assets[sym] = newBalances[i] + }) + return this + }) + } +} + +/** + * Verifies an AssetBalance + * @param {string} url + * @param {AssetBalance} assetBalance + * @return {Promise} Returns a new AssetBalance + */ +export const verifyAssetBalance = (url, assetBalance) => { + let newAssetBalance = { balance: 0, spent: [], unspent: [], unconfirmed: [] } + return verifyCoins(url, assetBalance.unspent) + .then((values) => { + values.map((v, i) => { + let coin = assetBalance.unspent[i] + if (v) { + if (v.value !== coin.value) coin.value = v.value + newAssetBalance.unspent.push(coin) + newAssetBalance.balance += coin.value + } else { + newAssetBalance.spent.push(coin) + } + }) + return newAssetBalance + }) +} + +/** + * Verifies a list of Coins + * @param {string} url + * @param {Coin[]} coinArr + * @return {Promise} + */ +export const verifyCoins = (url, coinArr) => { + const promises = [] + for (const coin of coinArr) { + const promise = Query.getTxOut(coin.txid, coin.index) + .execute(url) + .then(({ result }) => { + if (!result) return null + return { + txid: coin.txid, + index: result.n, + assetId: result.asset, + value: parseInt(result.value, 10) + } + }) + promises.push(promise) + } + return Promise.all(promises) +} + +export default Balance diff --git a/src/wallet/index.d.ts b/src/wallet/index.d.ts index 75258517d..47903d866 100644 --- a/src/wallet/index.d.ts +++ b/src/wallet/index.d.ts @@ -1,70 +1,112 @@ +declare module '@cityofzion/neon-js' { + export interface Account { + WIF: string + privateKey: string + publicKey: string + scriptHash: string + address: string + } -export class Account { - constructor(str: string) + export interface AssetBalance { + balance: string + unspent: Coin[] + spent: Coin[] + uncofirmed: Coin[] + } - WIF: string - privateKey: string - publicKey: string - scriptHash: string - address: string + export interface Coin { + index: number + txid: string + value: number + } - getPublicKey(encoded: boolean): string -} + export module wallet { + //Account + export class Account { + constructor(str: string) + + WIF: string + privateKey: string + publicKey: string + scriptHash: string + address: string -//core -export function getPublicKeyEncoded(publicKey: string): string -export function getPublicKeyUnencoded(publicKey: string): string -export function getPrivateKeyFromWIF(wif: string): string -export function getWIFFromPrivateKey(privateKey: string): string -export function getPublicKeyFromPrivateKey(publicKey: string, encode?: boolean): string -export function getVerificationScriptFromPublicKey(publicKey: string): string -export function getScriptHashFromPublicKey(publicKey: string): string -export function getAddressFromScriptHash(scriptHash: string): string -export function getScriptHashFromAddress(address: string): string -export function generateSignature(tx: string, privateKey: string): string -export function generatePrivateKey(): string -export function generateRandomArray(length: number): string + getPublicKey(encoded: boolean): string + } -//nep2 -export function encryptWifAccount(wif: string, passphrase: string): Promise -export function generateEncryptedWif(passphrase: string): Promise -export function encrypt(wifKey: string, keyphrase: string): string -export function decrypt(encryptedKey: string, keyphrase: string): string -export function encryptWIF(wif: string, passphrase: string): Promise -export function decryptWIF(encrypted: string, passphrase: string): Promise + //Balance + export class Balance { + constructor(bal: object) -//verify -export function isNEP2(nep2: string): boolean -export function isWIF(wif: string): boolean -export function isPrivateKey(key: string): boolean -export function isPublicKey(key: string, encoded?: boolean): boolean -export function isAddress(address: string): boolean + address: string + net: NEO_NETWORK + assetSymbols: string[] + assets: { [index: string]: AssetBalance } + tokenSymbols: string[] + tokens: { [index: string]: number } + + addAsset(sym: string, assetBalance?: AssetBalance): this + addToken(sym: string, tokenBalance?: number): this + applyTx(tx: Transaction, confirmed?: boolean): this + verifyAssets(url: string): Promise + } + + //core + export function getPublicKeyEncoded(publicKey: string): string + export function getPublicKeyUnencoded(publicKey: string): string + export function getPrivateKeyFromWIF(wif: string): string + export function getWIFFromPrivateKey(privateKey: string): string + export function getPublicKeyFromPrivateKey(publicKey: string, encode?: boolean): string + export function getVerificationScriptFromPublicKey(publicKey: string): string + export function getScriptHashFromPublicKey(publicKey: string): string + export function getAddressFromScriptHash(scriptHash: string): string + export function getScriptHashFromAddress(address: string): string + export function generateSignature(tx: string, privateKey: string): string + export function generatePrivateKey(): string + export function generateRandomArray(length: number): string + + //nep2 + export function encryptWifAccount(wif: string, passphrase: string): Promise + export function generateEncryptedWif(passphrase: string): Promise + export function encrypt(wifKey: string, keyphrase: string): string + export function decrypt(encryptedKey: string, keyphrase: string): string + export function encryptWIF(wif: string, passphrase: string): Promise + export function decryptWIF(encrypted: string, passphrase: string): Promise + + //verify + export function isNEP2(nep2: string): boolean + export function isWIF(wif: string): boolean + export function isPrivateKey(key: string): boolean + export function isPublicKey(key: string, encoded?: boolean): boolean + export function isAddress(address: string): boolean + } -export default { - create: { - account: (k: string) => Account, - privateKey: generatePrivateKey, - signature: generateSignature - }, - is: { - address: isAddress, - publicKey: isPublicKey, - encryptedKey: isNEP2, - privateKey: isPrivateKey, - wif: isWIF - }, - encrypt: { - privateKey: encrypt - }, - decrypt: { - privateKey: decrypt - }, - get: { - privateKeyFromWIF: getPrivateKeyFromWIF, - WIFFromPrivateKey: getWIFFromPrivateKey, - publicKeyFromPrivateKey: getPublicKeyFromPrivateKey, - scriptHashFromPublicKey: getScriptHashFromPublicKey, - addressFromScriptHash: getAddressFromScriptHash, - scriptHashFromAddress: getScriptHashFromAddress + export interface semantic { + create: { + account: (k: string) => Account + privateKey: () => string + signature: (tx: string, privateKey: string) => string + } + is: { + address: (address: string) => boolean + publicKey: (key: string, encode?: boolean) => boolean + encryptedKey: (bep2: string) => string + privateKey: (key: string) => string + wif: (wif: string) => string + } + encrypt: { + privateKey: (wifKey: string, keyphrase: string) => string + } + decrypt: { + privateKey: (encryptedKey: string, keyphrase: string) => string + } + get: { + privateKeyFromWIF: (wif: string) => string + WIFFromPrivateKey: (privateKey: string) => string + publicKeyFromPrivateKey: (publicKey: string, encode?: boolean) => string + scriptHashFromPublicKey: (publicKey: string) => string + addressFromScriptHash: (scriptHash: string) => string + scriptHashFromAddress: (address: string) => string + } } } diff --git a/src/wallet/index.js b/src/wallet/index.js index db4bf35bb..96f9d9cf8 100644 --- a/src/wallet/index.js +++ b/src/wallet/index.js @@ -4,6 +4,7 @@ import * as core from './core' import * as verify from './verify' import * as nep2 from './nep2' import Account from './Account' +import Balance from './Balance' export default { create: { @@ -37,4 +38,4 @@ export default { export * from './core' export * from './verify' export * from './nep2' -export { Account } +export { Account, Balance } diff --git a/tests/api/core.js b/tests/api/core.js index c9b43eb16..66da61b98 100644 --- a/tests/api/core.js +++ b/tests/api/core.js @@ -1,5 +1,5 @@ import * as core from '../../src/api/core' -import { neonDB } from '../../src/api' +import { neonDB, neoscan } from '../../src/api' import { Transaction, signTransaction } from '../../src/transactions' import testKeys from '../testKeys.json' import testData from '../testData.json' @@ -8,21 +8,20 @@ import MockAdapter from 'axios-mock-adapter' describe('Core API', function () { let mock - this.timeout(5000) + this.timeout(10000) const baseConfig = { net: 'TestNet', address: testKeys.a.address } describe('getBalanceFrom', function () { - const config = { - net: 'TestNet', - address: testKeys.a.address, - privateKey: testKeys.a.privateKey, - other: 'props', - intents: {} - } - it('neonDB', () => { + const config = { + net: 'TestNet', + address: testKeys.a.address, + privateKey: testKeys.a.privateKey, + other: 'props', + intents: {} + } return core.getBalanceFrom(config, neonDB) .should.eventually.have.keys([ 'net', @@ -35,19 +34,25 @@ describe('Core API', function () { ]) }) - it('neoscan') - // , () => { - // return core.getBalanceFrom(config, neoscan) - // .should.eventually.have.keys([ - // 'net', - // 'address', - // 'privateKey', - // 'other', - // 'intents', - // 'balance', - // 'url' - // ]) - // }) + it('neoscan', () => { + const config = { + net: 'TestNet', + address: testKeys.a.address, + privateKey: testKeys.a.privateKey, + other: 'props', + intents: {} + } + return core.getBalanceFrom(config, neoscan) + .should.eventually.have.keys([ + 'net', + 'address', + 'privateKey', + 'other', + 'intents', + 'balance', + 'url' + ]) + }) }) it('makeIntent', () => { diff --git a/tests/api/neonDB.js b/tests/api/neonDB.js index b7b537247..a853753ee 100644 --- a/tests/api/neonDB.js +++ b/tests/api/neonDB.js @@ -18,8 +18,8 @@ describe('NeonDB', function () { it('should get balance from address', () => { return neonDB.getBalance('TestNet', testKeys.a.address) .then((response) => { - response.NEO.balance.should.be.a('number') - response.GAS.balance.should.be.a('number') + response.assets.NEO.balance.should.be.a('number') + response.assets.GAS.balance.should.be.a('number') }).catch((e) => { console.log(e) throw e diff --git a/tests/api/neoscan.js b/tests/api/neoscan.js index 388179340..4a43c77ff 100644 --- a/tests/api/neoscan.js +++ b/tests/api/neoscan.js @@ -1,32 +1,31 @@ import * as neoscan from '../../src/api/neoscan' -// import testKeys from '../testKeys.json' +import testKeys from '../testKeys.json' describe('Neoscan', function () { this.timeout(15000) - const mainAddr = 'ATsmYxUsJaU1AcZm3RCcTAhrvYUNMPaCvq' it('should get balance from address', () => { - return neoscan.getBalance('MainNet', mainAddr) + return neoscan.getBalance('TestNet', testKeys.a.address) .then((response) => { - response.NEO.balance.should.be.a('number') - response.GAS.balance.should.be.a('number') - response.address.should.equal(mainAddr) + response.assets.NEO.balance.should.be.a('number') + response.assets.GAS.balance.should.be.a('number') + response.address.should.equal(testKeys.a.address) }) }) it('should get unspent transactions', () => { - return neoscan.getBalance('MainNet', mainAddr) + return neoscan.getBalance('TestNet', testKeys.a.address) .then((response) => { - response.NEO.unspent.should.be.an('array') - response.GAS.unspent.should.be.an('array') - response.address.should.equal(mainAddr) + response.assets.NEO.unspent.should.be.an('array') + response.assets.GAS.unspent.should.be.an('array') + response.address.should.equal(testKeys.a.address) }) }) it('should get claimable gas', () => { - return neoscan.getClaims('MainNet', mainAddr) + return neoscan.getClaims('TestNet', testKeys.a.address) .then((response) => { response.claims.should.be.an('array') - response.address.should.equal(mainAddr) + response.address.should.equal(testKeys.a.address) }) }) }) diff --git a/tests/api/nep5.js b/tests/api/nep5.js index 9dc7942da..ca4f4b83a 100644 --- a/tests/api/nep5.js +++ b/tests/api/nep5.js @@ -1,4 +1,4 @@ -import * as NEP5 from '../../src/api/nep5' +import * as NEP5 from '../../src/api/nep5' import testKeys from '../testKeys.json' describe('NEP5', function () { @@ -12,7 +12,7 @@ describe('NEP5', function () { result.name.should.equal('LOCALTOKEN') result.symbol.should.equal('LWTF') result.decimals.should.equal(8) - result.totalSupply.should.equal(1969000) + result.totalSupply.should.least(1969000) }) .catch((e) => { console.log(e) diff --git a/tests/testData.json b/tests/testData.json index d05e0bc6d..109066306 100644 --- a/tests/testData.json +++ b/tests/testData.json @@ -1,31 +1,39 @@ { "a": { "balance": { - "GAS": { - "balance": 1118.96175487, - "unspent": [ - { - "index": 1, - "txid": "8019d2e1aa49adcd0be7dd133f4d6d5687a176e40a5c4eeaebaa32484de6e04b", - "value": 698 - }, - { - "index": 1, - "txid": "4146bc7276635d1cfe4c9a3db18d56b8cbb9599a3272ba53467fe228e5f6ab87", - "value": 420.96175487 - } - ] - }, - "NEO": { - "balance": 259, - "unspent": [ - { - "index": 1, - "txid": "f8aa4c8d29529a2424426a29e51f646398beba59c6b7bcd344b66ba72784b22c", - "value": 259 - } - ] + "assets": { + "GAS": { + "balance": 1118.96175487, + "unspent": [ + { + "index": 1, + "txid": "8019d2e1aa49adcd0be7dd133f4d6d5687a176e40a5c4eeaebaa32484de6e04b", + "value": 698 + }, + { + "index": 1, + "txid": "4146bc7276635d1cfe4c9a3db18d56b8cbb9599a3272ba53467fe228e5f6ab87", + "value": 420.96175487 + } + ] + }, + "NEO": { + "balance": 259, + "unspent": [ + { + "index": 1, + "txid": "f8aa4c8d29529a2424426a29e51f646398beba59c6b7bcd344b66ba72784b22c", + "value": 259 + } + ] + } }, + "assetSymbols": [ + "NEO", + "GAS" + ], + "tokenSymbols": [], + "tokens": {}, "address": "ALq7AWrhAueN6mJNqk6FHJjnsEoPRytLdW", "net": "TestNet" }, @@ -230,501 +238,509 @@ }, "b": { "balance": { - "GAS": { - "balance": 51.84397576, - "unspent": [ - { - "index": 0, - "txid": "2f6eb9291a2fb54cdcb4900edebf7e46717614863849a42a35a49a70da48afac", - "value": 1 - }, - { - "index": 0, - "txid": "52b6b3f39b9a579e5260a70b592641f0b89a414d96db94813f62b0f6f15c4d4c", - "value": 1 - }, - { - "index": 0, - "txid": "a7c85cbccd44b9d835874cc426091bd5524c21fd5a82c2a07804e1c7f10f917a", - "value": 1 - }, - { - "index": 0, - "txid": "3ecc7086344ecc0d5043317ae1b30f425dd678e47ce0c7683317242689858a1c", - "value": 1 - }, - { - "index": 0, - "txid": "693f922ea5661f3b9f03ce3cf9b497f7f51df2a3e80850a5c39250554e59175c", - "value": 1 - }, - { - "index": 0, - "txid": "92537f466e479fbeb02dbdabdddc101031cdb7623d11986a6a0f2f11864c1c7d", - "value": 1 - }, - { - "index": 0, - "txid": "25f47c9beba9e5d6fe4fea0cf21e63f3ce7dd5c83b8156d281750ab91dc76b45", - "value": 1 - }, - { - "index": 0, - "txid": "61f05c1b5e84a667563a471bda4e04e919652d3943610aee3447d40c86704210", - "value": 1 - }, - { - "index": 0, - "txid": "fb1999ece1ec25c81a156de7017799802ad29282eae86ebfe716d482b8a35f1c", - "value": 1 - }, - { - "index": 0, - "txid": "fd04110ba5779afcc5ccdbfcf2ac37baac57c780209f5521a6891c9de69afe98", - "value": 1 - }, - { - "index": 0, - "txid": "1cc192f43fc7fc31e9fa9483d492ea7fbb1b86611bee692968ee05f66893a7d3", - "value": 1 - }, - { - "index": 0, - "txid": "cbf71b6fd772a1a55cbbdff12c04c76c3426f3f7972bc2fc4f82dca0412eea56", - "value": 1 - }, - { - "index": 0, - "txid": "ce5df7d4899fa18bc60103b7f207db2dd69c572a95c7f0d7ae0bf176faa14517", - "value": 1 - }, - { - "index": 0, - "txid": "4e744fbd7dc9a984646d8564617227c8c7bbb930d1a9a7747bea172eccffbec4", - "value": 1 - }, - { - "index": 0, - "txid": "7e57b647f1d62d0313b3258a42a5d4979e23898a95b2b9e44553fee6230a6d8f", - "value": 1 - }, - { - "index": 0, - "txid": "048bcfd748a85d806f8300e183b112502da0adac69340f9069419205145a5c10", - "value": 1 - }, - { - "index": 0, - "txid": "f47f7ef68d5911d3b0f54c755fc0e970af7cc702fc34101338f4b3bee5fb9b7f", - "value": 1 - }, - { - "index": 0, - "txid": "4ad9e0c885e93841a6d05456848963230372cc8285addd42b96d1b0bda67a9c2", - "value": 1 - }, - { - "index": 0, - "txid": "fb3a0a9a5ecebb2d38db47e08be0aa5fb22fbc3e6f7e62c54f70aec486e5d5dc", - "value": 1 - }, - { - "index": 0, - "txid": "ed413b52744b1a9b2524ae021c7590b0322ee2d8981af60b18efceee0729f858", - "value": 1 - }, - { - "index": 0, - "txid": "07093803a9a4202c9f692e7b5e4af2d732cc0e4741d48947582fcb9cde57968e", - "value": 1 - }, - { - "index": 0, - "txid": "c21414c034dbf4b782dac84367e80ca2e9a7b39b1128fff4e53b77f6a7b28954", - "value": 1 - }, - { - "index": 0, - "txid": "7e79aed42fd03b353e55a6c5c305172b0d12dfe9ab3ce930c3538e8f613dabb0", - "value": 1 - }, - { - "index": 0, - "txid": "8a569624fd80c394dd0313714a147256478a1ce1075b108ac92fa8a1ea65925d", - "value": 1 - }, - { - "index": 0, - "txid": "f41a07fed881b9b24604cc122414f9689969bb6622707dc523c4b13f83f9703b", - "value": 1 - }, - { - "index": 0, - "txid": "06667c00b09feed36e205a86a99db926e924cdd278d32425f628dbc61a135797", - "value": 1 - }, - { - "index": 0, - "txid": "8f15378235aa9306a72190ab55dbabb5456ff9197e5fe06e9301d498bb5cad9f", - "value": 1 - }, - { - "index": 0, - "txid": "e79c35ffd4d3e8bedbd6676c88d49674ebff137b617cbd44cbf60e590f36821b", - "value": 1 - }, - { - "index": 0, - "txid": "9c11085084d04a60ea501efe10ed21e44f07146534efa4f568b81c04a9e5ab38", - "value": 1 - }, - { - "index": 0, - "txid": "737d17c3abf10e2d7ca4489d63acc85bc3ebd767ba30de0babfa439152acfd58", - "value": 1 - }, - { - "index": 0, - "txid": "7772761db659270d8859a9d5084ec69d49669bba574881eb4c67d7035792d1d3", - "value": 1 - }, - { - "index": 0, - "txid": "76a2b7d2c5193d24b289ee98d923f2f4eeddf707e4503420f2a1fd6a183baf5f", - "value": 1 - }, - { - "index": 0, - "txid": "abddbc754b63d855271012d835437240f0f60e33e5c7de4391b926cfb9d67ce1", - "value": 1 - }, - { - "index": 0, - "txid": "7f1a2c262b51fc9fa51a798370653f80797026d0033fcf14bd0883ca403c3ea0", - "value": 1 - }, - { - "index": 0, - "txid": "fd6e7e6d0501f5727b2ca53ac3d0abcd3aa6a2a2fcd9480449d4e3d225f81732", - "value": 1 - }, - { - "index": 0, - "txid": "8cf3ca6be574b8acda815c59be5f08625b2eb0bc5bba843ec458db7720a3ea1f", - "value": 1 - }, - { - "index": 0, - "txid": "3907a8e9dae246d2774e44c38e11aacaefc2d1d1c3007e7990122c170f8f5052", - "value": 1 - }, - { - "index": 0, - "txid": "988638300ba39a849dd29d151eb3db1f80a4d7208484607f834e406ff887def6", - "value": 1 - }, - { - "index": 0, - "txid": "d7100f35c371c0223b137566247cf5469ff3b2dc958d78e99be9dfb482abd4f2", - "value": 1 - }, - { - "index": 0, - "txid": "67dd1afff4b50aeae53854e55723947604525845b90f02aed081585d4b93f24e", - "value": 1 - }, - { - "index": 0, - "txid": "e7d4e779d7d076908d86d28667dd00df1705a7985e557511de715ab1223e323c", - "value": 1 - }, - { - "index": 0, - "txid": "f954510b737b85ea1951fd1800de423a1c28ddb10c3740d95d95260452b38259", - "value": 1 - }, - { - "index": 0, - "txid": "6ccdc4f1d2f2dde89fcc7aedd8f3374b9c3c647845aad86982847ba8fa98de8e", - "value": 1 - }, - { - "index": 0, - "txid": "1839462f2e170c6585c0c6e701349d3305d874d2b5562d31a779f48eb2676005", - "value": 1 - }, - { - "index": 0, - "txid": "7755d3e5a8eb7fed70b11457a38c499ac780d74d753447ff980b741b20de578a", - "value": 1 - }, - { - "index": 0, - "txid": "1e2118b1a91d7cf34ec46514930bfe0912f4858d04687d4b99aef187063504a8", - "value": 1 - }, - { - "index": 0, - "txid": "f450fd4ebc1dc97c11e127734f00f0e09d782ba07126ae59cf9bb73917adb349", - "value": 1 - }, - { - "index": 0, - "txid": "6427191dcb0df5776c4edc6418f266cf6b7a87a949b2433ad7f688f0f5d2a6b3", - "value": 1 - }, - { - "index": 0, - "txid": "eb3fa13fd0b9bdbe03a2f91e7c015432672eacb2cf729e493a2d7021c48745fa", - "value": 1 - }, - { - "index": 0, - "txid": "df0f7f0fa79b8f01d200677dc59da0f4bd523cd5326442529fda4c953e88a2f9", - "value": 1 - }, - { - "index": 0, - "txid": "df25810d89b3a5a3d34be8dae06fd6ba906e8cb269b243deb5cd549d3cc0f2a0", - "value": 1 - }, - { - "index": 1, - "txid": "8be684b055a10c1c0921febfc9096e8a46cf51d3650ed532327ebe20e64a7164", - "value": 0.75262519 - }, - { - "index": 0, - "txid": "79cee764bb81ab8a41f5d39d9232f0afdfbeef94ca3b144110a69ecb1cebe835", - "value": 0.08999746 - }, - { - "index": 0, - "txid": "45f28aa5669de08a210dc44eac39523956786f8b7dc9160ba677a2b89cf330e6", - "value": 0.00135311 - } - ] - }, - "NEO": { - "balance": 62, - "unspent": [ - { - "index": 0, - "txid": "be570f80881e39c755b31eebf6d6895dadafc41b2205256735a3ecf8228bf54c", - "value": 1 - }, - { - "index": 0, - "txid": "aa5c3eb337342a21bfb00bf506d22b301564949592548e9d30e9127df6a322d2", - "value": 1 - }, - { - "index": 0, - "txid": "ecb33f588822bf2810df0114c10b290302a73939e1a5013780d50821c7f93396", - "value": 1 - }, - { - "index": 0, - "txid": "1681e193c0ca0c60a2b8be1113a74fef9c9f9125475e2b524a667cd361c14fe4", - "value": 1 - }, - { - "index": 0, - "txid": "7379a9b47c20301d3983d589f2527c3c95a2b47de22b43cf6483079d5db4d497", - "value": 1 - }, - { - "index": 0, - "txid": "e6c50f14b570dac4c1a917446174519bd67e1d864fe4bd67566d96197a591e57", - "value": 1 - }, - { - "index": 0, - "txid": "ae1bd54c00474aba23a95af93263daf8b5ca450b24e5177f33fe1b49ec90b15a", - "value": 1 - }, - { - "index": 0, - "txid": "061c21a632784dc21576672b6aec4b12f11e1830fa4182fe8bed18ce044ad2ae", - "value": 1 - }, - { - "index": 0, - "txid": "f01de93b20cdc8a61428f7c24e45ad3d0767efb8b3a8ed6d6c83817ea7548f69", - "value": 1 - }, - { - "index": 0, - "txid": "595068216243086d897d6efacaeea4dcf0b601bfbd49fea0f46eff3bb4ad28a5", - "value": 1 - }, - { - "index": 0, - "txid": "610095b50e7b1aef190e380063aa5048095fced03d99011f075b2ba12e4327a1", - "value": 1 - }, - { - "index": 0, - "txid": "e6e0a261b95c4edb7756d6f2361927accfa9f93a519a1247ef8f73792b619ff2", - "value": 1 - }, - { - "index": 0, - "txid": "04fa805c5f89c37f9cf021ee35c6a4e37392534dc6a0c43a5a0763ec315e7402", - "value": 1 - }, - { - "index": 0, - "txid": "76a89db3d75c1d1ab59d0b4bc72418c5d1aed5a08c4fdafb49a1ccadc0096d3e", - "value": 1 - }, - { - "index": 0, - "txid": "ec488aeb257d2079637ea4e3ee61f228953972c30dbab320a5be4b7a984b03d5", - "value": 1 - }, - { - "index": 0, - "txid": "ab96dd10056f9b50aa2f394dab1f0708e5ef06ef8022bd71fcb468cd20dbc195", - "value": 1 - }, - { - "index": 0, - "txid": "98bb9c6778e3ae58b42121edac9b6d1818ee3dec78115806969862f46aa9c991", - "value": 1 - }, - { - "index": 0, - "txid": "a8a8f8c1b4f3da1c1e813c93038c413cfe0193a4ad3fd0baaaca184d41535d21", - "value": 1 - }, - { - "index": 0, - "txid": "cfc2e4ea3b79b073b5b9cc063b594ce1d07ea6c9de1a9a49f13b021073ccc23a", - "value": 1 - }, - { - "index": 0, - "txid": "005be75b34318e7efe3c44e831c26c8c6c9e9aebf9d440c63e9b5bad29ca1699", - "value": 1 - }, - { - "index": 1, - "txid": "c0a70e68b4dc31d7e6befbb9998984a5d739ea2e1a39f075562a42e9ebbdb18e", - "value": 20 - }, - { - "index": 0, - "txid": "6fe0a91966c0f52205cd9d6fc4648b0754e4f562b9f677c623171092e73af573", - "value": 1 - }, - { - "index": 0, - "txid": "615f6afe8b22cbfcfec12b0ebde0eac53403a3651eae64eaf3fb1295eb203220", - "value": 1 - }, - { - "index": 0, - "txid": "78de8738c5e902f18e29dfd0a03c3c02fdbc834d412a5de63d4237dcaec2f694", - "value": 1 - }, - { - "index": 0, - "txid": "e9143feef1647c9d13f38a5125ba448d8ec0f0ac283f023a0b652d9856c8b65b", - "value": 1 - }, - { - "index": 0, - "txid": "8897e0c8ea74cb02ef6f2cfc585619ddcfb0c494510449db04f85592661b5d51", - "value": 1 - }, - { - "index": 0, - "txid": "75ad7bf67b9f5b74fe7555873b19fc3e750662751513d65e13434a0ac2d78a14", - "value": 1 - }, - { - "index": 0, - "txid": "d26559df32ee883b3dc4ea9f3ac5d136b69af4bbf370dfc85420cb0477b1b8ed", - "value": 1 - }, - { - "index": 0, - "txid": "d86e20b69852b6e0c1fe88252249966e5a8262ee7cd8153a472d31b894160886", - "value": 1 - }, - { - "index": 0, - "txid": "ec64eab96ed55f9c665ed1201eabe811e7ca91959db0336ed18201055691d431", - "value": 1 - }, - { - "index": 0, - "txid": "6e4280e14b09487f4e9f3f9e1a1a11091c0318cbcfaaa0e3e0e376ed3e0821bb", - "value": 1 - }, - { - "index": 0, - "txid": "77d5fec3a2bf7cd989ab7d7153d192023d5e7eab290f473025ec71041f35eeb6", - "value": 1 - }, - { - "index": 0, - "txid": "a5d018f41beb805d240c2a214830650ea8edcc69c5181790d13271ca53637041", - "value": 1 - }, - { - "index": 0, - "txid": "1d7948bbf0c9ebb59180ccc174851efd3127157b595c97552f55a58bb045ba36", - "value": 1 - }, - { - "index": 0, - "txid": "3ff78abf6054be55f325c18148fc42abfefbd495fb99d8e2c5f7879cccf3e1f9", - "value": 1 - }, - { - "index": 0, - "txid": "cce2fd5709800cec0f5709bdf46f68a76d6577a87322136a9bc86454fd84aaac", - "value": 1 - }, - { - "index": 0, - "txid": "8ccf2544314b1bac8ab439ec65d439c6b385f0e31b3759095674328896dab546", - "value": 1 - }, - { - "index": 0, - "txid": "869eed73ea1671bc66291bf2f5fb1f06c9d2d2e50d79a986da4cccb8bde6ccbf", - "value": 1 - }, - { - "index": 0, - "txid": "c51f3228a4c8b3d0636261d3eee9413d93671a8982aee0a10e56dff9c2ae1ca3", - "value": 1 - }, - { - "index": 0, - "txid": "d5e7a259feff6cfe17ed54bd9cf542f7ef57d04df11cfcba712318a95151544b", - "value": 1 - }, - { - "index": 0, - "txid": "86d3497fb22c290c0386c32c9db5e781a366a674116d189683e9afe9075fc1cf", - "value": 1 - }, - { - "index": 0, - "txid": "2075dacc35fee78b84a75db8f18fe75e34e61dd4383acf1afe79be327e559aff", - "value": 1 - }, - { - "index": 0, - "txid": "b3d4d9cd5f5c02a06fa4075f3dbbb453d82ffe9adbaae79f404085c9f820adb7", - "value": 1 - } - ] + "assets": { + "GAS": { + "balance": 51.84397576, + "unspent": [ + { + "index": 0, + "txid": "2f6eb9291a2fb54cdcb4900edebf7e46717614863849a42a35a49a70da48afac", + "value": 1 + }, + { + "index": 0, + "txid": "52b6b3f39b9a579e5260a70b592641f0b89a414d96db94813f62b0f6f15c4d4c", + "value": 1 + }, + { + "index": 0, + "txid": "a7c85cbccd44b9d835874cc426091bd5524c21fd5a82c2a07804e1c7f10f917a", + "value": 1 + }, + { + "index": 0, + "txid": "3ecc7086344ecc0d5043317ae1b30f425dd678e47ce0c7683317242689858a1c", + "value": 1 + }, + { + "index": 0, + "txid": "693f922ea5661f3b9f03ce3cf9b497f7f51df2a3e80850a5c39250554e59175c", + "value": 1 + }, + { + "index": 0, + "txid": "92537f466e479fbeb02dbdabdddc101031cdb7623d11986a6a0f2f11864c1c7d", + "value": 1 + }, + { + "index": 0, + "txid": "25f47c9beba9e5d6fe4fea0cf21e63f3ce7dd5c83b8156d281750ab91dc76b45", + "value": 1 + }, + { + "index": 0, + "txid": "61f05c1b5e84a667563a471bda4e04e919652d3943610aee3447d40c86704210", + "value": 1 + }, + { + "index": 0, + "txid": "fb1999ece1ec25c81a156de7017799802ad29282eae86ebfe716d482b8a35f1c", + "value": 1 + }, + { + "index": 0, + "txid": "fd04110ba5779afcc5ccdbfcf2ac37baac57c780209f5521a6891c9de69afe98", + "value": 1 + }, + { + "index": 0, + "txid": "1cc192f43fc7fc31e9fa9483d492ea7fbb1b86611bee692968ee05f66893a7d3", + "value": 1 + }, + { + "index": 0, + "txid": "cbf71b6fd772a1a55cbbdff12c04c76c3426f3f7972bc2fc4f82dca0412eea56", + "value": 1 + }, + { + "index": 0, + "txid": "ce5df7d4899fa18bc60103b7f207db2dd69c572a95c7f0d7ae0bf176faa14517", + "value": 1 + }, + { + "index": 0, + "txid": "4e744fbd7dc9a984646d8564617227c8c7bbb930d1a9a7747bea172eccffbec4", + "value": 1 + }, + { + "index": 0, + "txid": "7e57b647f1d62d0313b3258a42a5d4979e23898a95b2b9e44553fee6230a6d8f", + "value": 1 + }, + { + "index": 0, + "txid": "048bcfd748a85d806f8300e183b112502da0adac69340f9069419205145a5c10", + "value": 1 + }, + { + "index": 0, + "txid": "f47f7ef68d5911d3b0f54c755fc0e970af7cc702fc34101338f4b3bee5fb9b7f", + "value": 1 + }, + { + "index": 0, + "txid": "4ad9e0c885e93841a6d05456848963230372cc8285addd42b96d1b0bda67a9c2", + "value": 1 + }, + { + "index": 0, + "txid": "fb3a0a9a5ecebb2d38db47e08be0aa5fb22fbc3e6f7e62c54f70aec486e5d5dc", + "value": 1 + }, + { + "index": 0, + "txid": "ed413b52744b1a9b2524ae021c7590b0322ee2d8981af60b18efceee0729f858", + "value": 1 + }, + { + "index": 0, + "txid": "07093803a9a4202c9f692e7b5e4af2d732cc0e4741d48947582fcb9cde57968e", + "value": 1 + }, + { + "index": 0, + "txid": "c21414c034dbf4b782dac84367e80ca2e9a7b39b1128fff4e53b77f6a7b28954", + "value": 1 + }, + { + "index": 0, + "txid": "7e79aed42fd03b353e55a6c5c305172b0d12dfe9ab3ce930c3538e8f613dabb0", + "value": 1 + }, + { + "index": 0, + "txid": "8a569624fd80c394dd0313714a147256478a1ce1075b108ac92fa8a1ea65925d", + "value": 1 + }, + { + "index": 0, + "txid": "f41a07fed881b9b24604cc122414f9689969bb6622707dc523c4b13f83f9703b", + "value": 1 + }, + { + "index": 0, + "txid": "06667c00b09feed36e205a86a99db926e924cdd278d32425f628dbc61a135797", + "value": 1 + }, + { + "index": 0, + "txid": "8f15378235aa9306a72190ab55dbabb5456ff9197e5fe06e9301d498bb5cad9f", + "value": 1 + }, + { + "index": 0, + "txid": "e79c35ffd4d3e8bedbd6676c88d49674ebff137b617cbd44cbf60e590f36821b", + "value": 1 + }, + { + "index": 0, + "txid": "9c11085084d04a60ea501efe10ed21e44f07146534efa4f568b81c04a9e5ab38", + "value": 1 + }, + { + "index": 0, + "txid": "737d17c3abf10e2d7ca4489d63acc85bc3ebd767ba30de0babfa439152acfd58", + "value": 1 + }, + { + "index": 0, + "txid": "7772761db659270d8859a9d5084ec69d49669bba574881eb4c67d7035792d1d3", + "value": 1 + }, + { + "index": 0, + "txid": "76a2b7d2c5193d24b289ee98d923f2f4eeddf707e4503420f2a1fd6a183baf5f", + "value": 1 + }, + { + "index": 0, + "txid": "abddbc754b63d855271012d835437240f0f60e33e5c7de4391b926cfb9d67ce1", + "value": 1 + }, + { + "index": 0, + "txid": "7f1a2c262b51fc9fa51a798370653f80797026d0033fcf14bd0883ca403c3ea0", + "value": 1 + }, + { + "index": 0, + "txid": "fd6e7e6d0501f5727b2ca53ac3d0abcd3aa6a2a2fcd9480449d4e3d225f81732", + "value": 1 + }, + { + "index": 0, + "txid": "8cf3ca6be574b8acda815c59be5f08625b2eb0bc5bba843ec458db7720a3ea1f", + "value": 1 + }, + { + "index": 0, + "txid": "3907a8e9dae246d2774e44c38e11aacaefc2d1d1c3007e7990122c170f8f5052", + "value": 1 + }, + { + "index": 0, + "txid": "988638300ba39a849dd29d151eb3db1f80a4d7208484607f834e406ff887def6", + "value": 1 + }, + { + "index": 0, + "txid": "d7100f35c371c0223b137566247cf5469ff3b2dc958d78e99be9dfb482abd4f2", + "value": 1 + }, + { + "index": 0, + "txid": "67dd1afff4b50aeae53854e55723947604525845b90f02aed081585d4b93f24e", + "value": 1 + }, + { + "index": 0, + "txid": "e7d4e779d7d076908d86d28667dd00df1705a7985e557511de715ab1223e323c", + "value": 1 + }, + { + "index": 0, + "txid": "f954510b737b85ea1951fd1800de423a1c28ddb10c3740d95d95260452b38259", + "value": 1 + }, + { + "index": 0, + "txid": "6ccdc4f1d2f2dde89fcc7aedd8f3374b9c3c647845aad86982847ba8fa98de8e", + "value": 1 + }, + { + "index": 0, + "txid": "1839462f2e170c6585c0c6e701349d3305d874d2b5562d31a779f48eb2676005", + "value": 1 + }, + { + "index": 0, + "txid": "7755d3e5a8eb7fed70b11457a38c499ac780d74d753447ff980b741b20de578a", + "value": 1 + }, + { + "index": 0, + "txid": "1e2118b1a91d7cf34ec46514930bfe0912f4858d04687d4b99aef187063504a8", + "value": 1 + }, + { + "index": 0, + "txid": "f450fd4ebc1dc97c11e127734f00f0e09d782ba07126ae59cf9bb73917adb349", + "value": 1 + }, + { + "index": 0, + "txid": "6427191dcb0df5776c4edc6418f266cf6b7a87a949b2433ad7f688f0f5d2a6b3", + "value": 1 + }, + { + "index": 0, + "txid": "eb3fa13fd0b9bdbe03a2f91e7c015432672eacb2cf729e493a2d7021c48745fa", + "value": 1 + }, + { + "index": 0, + "txid": "df0f7f0fa79b8f01d200677dc59da0f4bd523cd5326442529fda4c953e88a2f9", + "value": 1 + }, + { + "index": 0, + "txid": "df25810d89b3a5a3d34be8dae06fd6ba906e8cb269b243deb5cd549d3cc0f2a0", + "value": 1 + }, + { + "index": 1, + "txid": "8be684b055a10c1c0921febfc9096e8a46cf51d3650ed532327ebe20e64a7164", + "value": 0.75262519 + }, + { + "index": 0, + "txid": "79cee764bb81ab8a41f5d39d9232f0afdfbeef94ca3b144110a69ecb1cebe835", + "value": 0.08999746 + }, + { + "index": 0, + "txid": "45f28aa5669de08a210dc44eac39523956786f8b7dc9160ba677a2b89cf330e6", + "value": 0.00135311 + } + ] + }, + "NEO": { + "balance": 62, + "unspent": [ + { + "index": 0, + "txid": "be570f80881e39c755b31eebf6d6895dadafc41b2205256735a3ecf8228bf54c", + "value": 1 + }, + { + "index": 0, + "txid": "aa5c3eb337342a21bfb00bf506d22b301564949592548e9d30e9127df6a322d2", + "value": 1 + }, + { + "index": 0, + "txid": "ecb33f588822bf2810df0114c10b290302a73939e1a5013780d50821c7f93396", + "value": 1 + }, + { + "index": 0, + "txid": "1681e193c0ca0c60a2b8be1113a74fef9c9f9125475e2b524a667cd361c14fe4", + "value": 1 + }, + { + "index": 0, + "txid": "7379a9b47c20301d3983d589f2527c3c95a2b47de22b43cf6483079d5db4d497", + "value": 1 + }, + { + "index": 0, + "txid": "e6c50f14b570dac4c1a917446174519bd67e1d864fe4bd67566d96197a591e57", + "value": 1 + }, + { + "index": 0, + "txid": "ae1bd54c00474aba23a95af93263daf8b5ca450b24e5177f33fe1b49ec90b15a", + "value": 1 + }, + { + "index": 0, + "txid": "061c21a632784dc21576672b6aec4b12f11e1830fa4182fe8bed18ce044ad2ae", + "value": 1 + }, + { + "index": 0, + "txid": "f01de93b20cdc8a61428f7c24e45ad3d0767efb8b3a8ed6d6c83817ea7548f69", + "value": 1 + }, + { + "index": 0, + "txid": "595068216243086d897d6efacaeea4dcf0b601bfbd49fea0f46eff3bb4ad28a5", + "value": 1 + }, + { + "index": 0, + "txid": "610095b50e7b1aef190e380063aa5048095fced03d99011f075b2ba12e4327a1", + "value": 1 + }, + { + "index": 0, + "txid": "e6e0a261b95c4edb7756d6f2361927accfa9f93a519a1247ef8f73792b619ff2", + "value": 1 + }, + { + "index": 0, + "txid": "04fa805c5f89c37f9cf021ee35c6a4e37392534dc6a0c43a5a0763ec315e7402", + "value": 1 + }, + { + "index": 0, + "txid": "76a89db3d75c1d1ab59d0b4bc72418c5d1aed5a08c4fdafb49a1ccadc0096d3e", + "value": 1 + }, + { + "index": 0, + "txid": "ec488aeb257d2079637ea4e3ee61f228953972c30dbab320a5be4b7a984b03d5", + "value": 1 + }, + { + "index": 0, + "txid": "ab96dd10056f9b50aa2f394dab1f0708e5ef06ef8022bd71fcb468cd20dbc195", + "value": 1 + }, + { + "index": 0, + "txid": "98bb9c6778e3ae58b42121edac9b6d1818ee3dec78115806969862f46aa9c991", + "value": 1 + }, + { + "index": 0, + "txid": "a8a8f8c1b4f3da1c1e813c93038c413cfe0193a4ad3fd0baaaca184d41535d21", + "value": 1 + }, + { + "index": 0, + "txid": "cfc2e4ea3b79b073b5b9cc063b594ce1d07ea6c9de1a9a49f13b021073ccc23a", + "value": 1 + }, + { + "index": 0, + "txid": "005be75b34318e7efe3c44e831c26c8c6c9e9aebf9d440c63e9b5bad29ca1699", + "value": 1 + }, + { + "index": 1, + "txid": "c0a70e68b4dc31d7e6befbb9998984a5d739ea2e1a39f075562a42e9ebbdb18e", + "value": 20 + }, + { + "index": 0, + "txid": "6fe0a91966c0f52205cd9d6fc4648b0754e4f562b9f677c623171092e73af573", + "value": 1 + }, + { + "index": 0, + "txid": "615f6afe8b22cbfcfec12b0ebde0eac53403a3651eae64eaf3fb1295eb203220", + "value": 1 + }, + { + "index": 0, + "txid": "78de8738c5e902f18e29dfd0a03c3c02fdbc834d412a5de63d4237dcaec2f694", + "value": 1 + }, + { + "index": 0, + "txid": "e9143feef1647c9d13f38a5125ba448d8ec0f0ac283f023a0b652d9856c8b65b", + "value": 1 + }, + { + "index": 0, + "txid": "8897e0c8ea74cb02ef6f2cfc585619ddcfb0c494510449db04f85592661b5d51", + "value": 1 + }, + { + "index": 0, + "txid": "75ad7bf67b9f5b74fe7555873b19fc3e750662751513d65e13434a0ac2d78a14", + "value": 1 + }, + { + "index": 0, + "txid": "d26559df32ee883b3dc4ea9f3ac5d136b69af4bbf370dfc85420cb0477b1b8ed", + "value": 1 + }, + { + "index": 0, + "txid": "d86e20b69852b6e0c1fe88252249966e5a8262ee7cd8153a472d31b894160886", + "value": 1 + }, + { + "index": 0, + "txid": "ec64eab96ed55f9c665ed1201eabe811e7ca91959db0336ed18201055691d431", + "value": 1 + }, + { + "index": 0, + "txid": "6e4280e14b09487f4e9f3f9e1a1a11091c0318cbcfaaa0e3e0e376ed3e0821bb", + "value": 1 + }, + { + "index": 0, + "txid": "77d5fec3a2bf7cd989ab7d7153d192023d5e7eab290f473025ec71041f35eeb6", + "value": 1 + }, + { + "index": 0, + "txid": "a5d018f41beb805d240c2a214830650ea8edcc69c5181790d13271ca53637041", + "value": 1 + }, + { + "index": 0, + "txid": "1d7948bbf0c9ebb59180ccc174851efd3127157b595c97552f55a58bb045ba36", + "value": 1 + }, + { + "index": 0, + "txid": "3ff78abf6054be55f325c18148fc42abfefbd495fb99d8e2c5f7879cccf3e1f9", + "value": 1 + }, + { + "index": 0, + "txid": "cce2fd5709800cec0f5709bdf46f68a76d6577a87322136a9bc86454fd84aaac", + "value": 1 + }, + { + "index": 0, + "txid": "8ccf2544314b1bac8ab439ec65d439c6b385f0e31b3759095674328896dab546", + "value": 1 + }, + { + "index": 0, + "txid": "869eed73ea1671bc66291bf2f5fb1f06c9d2d2e50d79a986da4cccb8bde6ccbf", + "value": 1 + }, + { + "index": 0, + "txid": "c51f3228a4c8b3d0636261d3eee9413d93671a8982aee0a10e56dff9c2ae1ca3", + "value": 1 + }, + { + "index": 0, + "txid": "d5e7a259feff6cfe17ed54bd9cf542f7ef57d04df11cfcba712318a95151544b", + "value": 1 + }, + { + "index": 0, + "txid": "86d3497fb22c290c0386c32c9db5e781a366a674116d189683e9afe9075fc1cf", + "value": 1 + }, + { + "index": 0, + "txid": "2075dacc35fee78b84a75db8f18fe75e34e61dd4383acf1afe79be327e559aff", + "value": 1 + }, + { + "index": 0, + "txid": "b3d4d9cd5f5c02a06fa4075f3dbbb453d82ffe9adbaae79f404085c9f820adb7", + "value": 1 + } + ] + } }, + "assetSymbols": [ + "NEO", + "GAS" + ], + "tokenSymbols": [], + "tokens": {}, "address": "ALfnhLg7rUyL6Jr98bzzoxz5J7m64fbR4s", "net": "TestNet" }, @@ -748,66 +764,74 @@ }, "c": { "balance": { - "GAS": { - "balance": 8.38907573, - "unspent": [ - { - "index": 0, - "txid": "0f85bd3b0f422a7a448b812e9fc47a06d47767510a1637ccb1244e09dd4608a8", - "value": 1 - }, - { - "index": 0, - "txid": "e016a9ff19161792d89d0ae0127e6ea9ba800c5fdad749f3a236144b6a0d0c9a", - "value": 1 - }, - { - "index": 0, - "txid": "0a6da0fbd8a3d282627a7de360aa5758bb46395927c5c9bfcbfc39b6938c63a6", - "value": 0.54251778 - }, - { - "index": 0, - "txid": "1790b4e2c9a3e1ea66943d75196925d54dd0de5ae13d7de51d72f4fc2e13d72b", - "value": 2.1053989 - }, - { - "index": 1, - "txid": "72ef38e9d1e30ffead84502249950ad0119b704a8cdc756e16a8644a4ebe0968", - "value": 2 - }, - { - "index": 0, - "txid": "b580438c64ce093b10997df60d97fc824bd798272136aa6aba2420cdf27b9093", - "value": 0.59725874 - }, - { - "index": 0, - "txid": "029631bb693d445a9312474d92fad883a3fed78083bb07714fc10f62b1a206db", - "value": 0.60742564 - }, - { - "index": 0, - "txid": "fc4a06259483fd6e976c5851011ed905f5fef15c2a282c91ff43307396bb0f60", - "value": 0.1 - }, - { - "index": 1, - "txid": "fc4a06259483fd6e976c5851011ed905f5fef15c2a282c91ff43307396bb0f60", - "value": 0.43647467 - } - ] - }, - "NEO": { - "balance": 130, - "unspent": [ - { - "index": 0, - "txid": "ed93b787273c2d897344db43b3afa55d5295ca4a0890044b3723b4f0de0acc39", - "value": 130 - } - ] + "assets": { + "GAS": { + "balance": 8.38907573, + "unspent": [ + { + "index": 0, + "txid": "0f85bd3b0f422a7a448b812e9fc47a06d47767510a1637ccb1244e09dd4608a8", + "value": 1 + }, + { + "index": 0, + "txid": "e016a9ff19161792d89d0ae0127e6ea9ba800c5fdad749f3a236144b6a0d0c9a", + "value": 1 + }, + { + "index": 0, + "txid": "0a6da0fbd8a3d282627a7de360aa5758bb46395927c5c9bfcbfc39b6938c63a6", + "value": 0.54251778 + }, + { + "index": 0, + "txid": "1790b4e2c9a3e1ea66943d75196925d54dd0de5ae13d7de51d72f4fc2e13d72b", + "value": 2.1053989 + }, + { + "index": 1, + "txid": "72ef38e9d1e30ffead84502249950ad0119b704a8cdc756e16a8644a4ebe0968", + "value": 2 + }, + { + "index": 0, + "txid": "b580438c64ce093b10997df60d97fc824bd798272136aa6aba2420cdf27b9093", + "value": 0.59725874 + }, + { + "index": 0, + "txid": "029631bb693d445a9312474d92fad883a3fed78083bb07714fc10f62b1a206db", + "value": 0.60742564 + }, + { + "index": 0, + "txid": "fc4a06259483fd6e976c5851011ed905f5fef15c2a282c91ff43307396bb0f60", + "value": 0.1 + }, + { + "index": 1, + "txid": "fc4a06259483fd6e976c5851011ed905f5fef15c2a282c91ff43307396bb0f60", + "value": 0.43647467 + } + ] + }, + "NEO": { + "balance": 130, + "unspent": [ + { + "index": 0, + "txid": "ed93b787273c2d897344db43b3afa55d5295ca4a0890044b3723b4f0de0acc39", + "value": 130 + } + ] + } }, + "assetSymbols": [ + "NEO", + "GAS" + ], + "tokenSymbols": [], + "tokens": {}, "address": "AVf4UGKevVrMR1j3UkPsuoYKSC4ocoAkKx", "net": "TestNet" }, diff --git a/tests/transactions/createData.json b/tests/transactions/createData.json index 28618fc20..99c09311f 100644 --- a/tests/transactions/createData.json +++ b/tests/transactions/createData.json @@ -44,56 +44,64 @@ "total_unspent_claim": 32392540 }, "balance": { - "GAS": { - "balance": 20, - "unspent": [ - { - "index": 0, - "txid": "4f25c371699794a576608092fadfffc4543e4a945a34c9c415f709e4d767421f", - "value": 1 - }, - { - "index": 0, - "txid": "890fa70ec14e55fead6e0d0f571282a136709827d310761fe2872818e0f20ba9", - "value": 5 - }, - { - "index": 0, - "txid": "2895c439879bf552ba787e7acff84703afaba064fd8fc2d7df7537f9ed000919", - "value": 8.32557442 - }, - { - "index": 1, - "txid": "4787b4d5f02765b5d81c3b381e591aab6ea190b561ba4d89ab64c794c3d946f9", - "value": 5.67442558 - } - ] - }, - "NEO": { - "balance": 10, - "unspent": [ - { - "index": 0, - "txid": "e874a294158ace545697fb5e45a5fbeae984c5bae757d2d8d864fe4e7552833d", - "value": 1 - }, - { - "index": 0, - "txid": "d75b19095e6c461fc3ebb824417ac53fd70fc933591673a3e4c8be839fd91df0", - "value": 2 - }, - { - "index": 0, - "txid": "61c0839c2941839c95452e0022df7f0c095e0f749ee72a10de6f22a140c9b53c", - "value": 3 - }, - { - "index": 0, - "txid": "991ac3c895b34c55d59b62f383a5e49b8ac4a8d5568b892092a7c482c7bbff6b", - "value": 4 - } - ] + "assets": { + "GAS": { + "balance": 20, + "unspent": [ + { + "index": 0, + "txid": "4f25c371699794a576608092fadfffc4543e4a945a34c9c415f709e4d767421f", + "value": 1 + }, + { + "index": 0, + "txid": "890fa70ec14e55fead6e0d0f571282a136709827d310761fe2872818e0f20ba9", + "value": 5 + }, + { + "index": 0, + "txid": "2895c439879bf552ba787e7acff84703afaba064fd8fc2d7df7537f9ed000919", + "value": 8.32557442 + }, + { + "index": 1, + "txid": "4787b4d5f02765b5d81c3b381e591aab6ea190b561ba4d89ab64c794c3d946f9", + "value": 5.67442558 + } + ] + }, + "NEO": { + "balance": 10, + "unspent": [ + { + "index": 0, + "txid": "e874a294158ace545697fb5e45a5fbeae984c5bae757d2d8d864fe4e7552833d", + "value": 1 + }, + { + "index": 0, + "txid": "d75b19095e6c461fc3ebb824417ac53fd70fc933591673a3e4c8be839fd91df0", + "value": 2 + }, + { + "index": 0, + "txid": "61c0839c2941839c95452e0022df7f0c095e0f749ee72a10de6f22a140c9b53c", + "value": 3 + }, + { + "index": 0, + "txid": "991ac3c895b34c55d59b62f383a5e49b8ac4a8d5568b892092a7c482c7bbff6b", + "value": 4 + } + ] + } }, + "assetSymbols": [ + "NEO", + "GAS" + ], + "tokenSymbols": [], + "tokens": {}, "address": "ALq7AWrhAueN6mJNqk6FHJjnsEoPRytLdW", "net": "TestNet" } diff --git a/tests/utils.js b/tests/utils.js index dc43a49db..58b9f6b9c 100644 --- a/tests/utils.js +++ b/tests/utils.js @@ -1,7 +1,224 @@ -import { num2VarInt, reverseHex } from '../src/utils' +import { + ab2hexstring, + ab2str, + fixed82num, + hash160, + hash256, + hexstring2ab, + hexXor, + int2hex, + num2fixed8, + num2hexstring, + num2VarInt, + reverseArray, + reverseHex, + sha256, + str2ab, + str2hexstring +} from '../src/utils' describe('Utils', () => { - describe('num2VarInt', () => { + describe('ab2str', () => { + const testingab = [84, 101, 115, 116] + it('converts an empty array to an empty string', () => { + const actual = ab2str([]) + const expected = '' + actual.should.eql(expected) + }) + it('converts an array of ascii bytes to string', () => { + const actual = ab2str(testingab) + const expected = 'Test' + actual.should.eql(expected) + }) + it('converts an arraybuffer of ascii bytes to string', () => { + const buffer = new Uint8Array(testingab).buffer + const actual = ab2str(buffer) + const expected = 'Test' + actual.should.eql(expected) + }) + }) + + describe('str2ab', () => { + it('throws if non-string', () => { + (() => str2ab([0])).should.throw() + }) + it('converts an empty string to an empty array', () => { + const actual = str2ab('') + actual.length.should.eql(0) + }) + it('converts a string to an array of ascii bytes', () => { + const actual = str2ab('Test') + const expected = new Uint8Array([84, 101, 115, 116]) + actual.should.eql(expected) + }) + }) + + describe('hexstring2ab', () => { + it('throws if non-string', () => { + (() => hexstring2ab([0])).should.throw() + }) + it('converts an empty string to an empty array', () => { + const actual = hexstring2ab('') + actual.length.should.eql(0) + }) + it('converts a hex string to an array of ascii bytes', () => { + const actual = hexstring2ab('54657374') + actual.should.eql(new Uint8Array([84, 101, 115, 116])) + }) + }) + + describe('ab2hexstring', () => { + it('throws if non-array', () => { + (() => ab2hexstring('Test')).should.throw() + }) + it('converts an empty array to an empty string', () => { + const actual = ab2hexstring([]) + actual.should.eql('') + }) + it('converts a an array of ascii bytes to a hex string', () => { + const actual = ab2hexstring(new Uint8Array([84, 101, 115, 116])) + actual.should.eql('54657374') + }) + }) + + describe('str2hexstring', () => { + it('throws if non-string', () => { + (() => str2hexstring([0])).should.throw() + }) + it('converts an empty string to an empty string', () => { + const actual = str2hexstring('') + actual.should.eql('') + }) + it('converts a string to a hex string', () => { + const actual = str2hexstring('Test') + actual.should.eql('54657374') + }) + }) + + describe('int2hex (BE)', () => { + it('throws if non-number', () => { + (() => int2hex('01')).should.throw() + }) + it('converts a number to its hex form', () => { + const actual = int2hex(0x00) + actual.should.eql('00') + }) + it('converts a number to its hex form (big)', () => { + const actual = int2hex(0xFFF000F) + actual.should.eql('0fff000f') + }) + }) + + describe('num2hexstring (BE)', () => { + it('throws if negative', () => { + (() => num2hexstring(-1)).should.throw() + }) + it('throws if not a number', () => { + (() => num2hexstring('1')).should.throw() + }) + it('throws if unsafe', () => { + (() => num2hexstring(Number.MAX_SAFE_INTEGER + 1)).should.throw() + }) + it('converts a byte size int', () => { + const actual = num2hexstring(1) + const expected = '01' + actual.should.eql(expected) + }) + it('converts a byte size int (max)', () => { + const actual = num2hexstring(0xff) + const expected = 'ff' + actual.should.eql(expected) + }) + it('converts a uint16', () => { + const num = 0xff + const actual = num2hexstring(num, 4) + const expected = '00ff' + Number.parseInt(expected, 16).should.eql(num) + actual.should.eql(expected) + }) + it('converts a uint16 (max)', () => { + const num = 0xffff + const actual = num2hexstring(num, 4) + const expected = 'ffff' + Number.parseInt(expected, 16).should.eql(num) + actual.should.eql(expected) + }) + it('converts a uint32', () => { + const num = 0x010000 + const actual = num2hexstring(num, 8) + const expected = '00010000' + Number.parseInt(expected, 16).should.eql(num) + actual.should.eql(expected) + }) + it('converts a uint32 (max)', () => { + const num = 0xffffffff + const actual = num2hexstring(num, 8) + const expected = 'ffffffff' + Number.parseInt(expected, 16).should.eql(num) + actual.should.eql(expected) + }) + it('converts a uint64', () => { + const num = 0x0100000000 + const actual = num2hexstring(num, 16) + const expected = '0000000100000000' + Number.parseInt(expected, 16).should.eql(num) + actual.should.eql(expected) + }) + it('converts a uint64 (max)', () => { + const num = Number.MAX_SAFE_INTEGER + const actual = num2hexstring(num, 16) // 0x1fffffffffffff + const expected = '001fffffffffffff' + Number.parseInt(expected, 16).should.eql(num) + actual.should.eql(expected) + }) + it('converts a uint64 (max) (little endian)', () => { + const num = Number.MAX_SAFE_INTEGER + const actual = num2hexstring(num, 16, true) // 0x1fffffffffffff + const expected = 'ffffffffffff1f00' + Number.parseInt(reverseHex(expected), 16).should.eql(num) + actual.should.eql(expected) + }) + }) + + describe('num2fixed8', () => { + it('throws if not a number', () => { + (() => num2fixed8('1')).should.throw() + }) + it('converts a number to fixed8 hex', () => { + const actual = num2fixed8(1) + const expected = '00e1f50500000000' + actual.should.eql(expected) + }) + it('converts a big number with decimal places', () => { + const actual = num2fixed8(123456.12345678) + const expected = '4ea1d66f3a0b0000' + actual.should.eql(expected) + }) + }) + + describe('fixed82num', () => { + it('throws if not a string', () => { + (() => fixed82num(1)).should.throw() + }) + it('throws if empty string', () => { + (() => fixed82num('')).should.throw() + }) + it('throws if non-hex string', () => { + (() => fixed82num('xxx')).should.throw() + }) + it('converts a number to fixed8 hex', () => { + const actual = fixed82num('00e1f50500000000') + const expected = 1 + actual.should.eql(expected) + }) + it('converts a big number with decimal places', () => { + const actual = fixed82num('4ea1d66f3a0b0000') + const expected = 123456.12345678 + actual.should.eql(expected) + }) + }) + + describe('num2VarInt (LE)', () => { it('throws if negative', () => { (() => num2VarInt(-1)).should.throw() }) @@ -64,4 +281,93 @@ describe('Utils', () => { actual.should.eql(expected) }) }) + + describe('hexXor', () => { + it('throws if not a string', () => { + (() => hexXor(0x0101, 0x0000)).should.throw() + }) + it('throws if disparate lengths', () => { + (() => hexXor('0101', '000')).should.throw() + }) + it('xors a hex string', () => { + const actual = hexXor('0101', '0001') + const expected = '0100' + actual.should.eql(expected) + }) + }) + + describe('reverseArray', () => { + it('throws if not an array', () => { + (() => reverseArray('123')).should.throw() + }) + it('reverses an array of ints', () => { + const actual = reverseArray([1, 2, 3]) + const expected = new Uint8Array([3, 2, 1]) + actual.should.eql(expected) + }) + it('reverses a Uint8Array', () => { + const actual = reverseArray(new Uint8Array([1, 2, 3])) + const expected = new Uint8Array([3, 2, 1]) + actual.should.eql(expected) + }) + }) + + describe('reverseHex', () => { + it('throws if not a string', () => { + (() => reverseHex(0x01)).should.throw() + }) + it('throws if not hex', () => { + (() => reverseHex('fff')).should.throw() + }) + it('reverses hex', () => { + const actual = reverseHex('feff') + const expected = 'fffe' + actual.should.eql(expected) + }) + }) + + describe('hash160', () => { + it('throws if not a string', () => { + (() => hash160(0x01)).should.throw() + }) + it('throws if not hex', () => { + (() => hash160('fff')).should.throw() + }) + it('produces a ripemd160 + sha256 from the given hex', () => { + const actual = hash160('54657374') + const expected = '0d6d086ce847371e069db7f67c5de45ed9ef1e54' + actual.should.eql(expected) + }) + }) + + describe('hash256', () => { + it('throws if not a string', () => { + (() => hash256(0x01)).should.throw() + }) + it('throws if not hex', () => { + (() => hash256('fff')).should.throw() + }) + it('produces a double sha256 from the given hex', () => { + const actual = hash256('54657374') + const expected = 'c60907e990745f7d91c4423713764d2724571269d3db4856d37c6302792c59a6' + actual.should.eql(expected) + }) + }) + + describe('sha256', () => { + it('throws if not a string', () => { + (() => sha256(0x01)).should.throw() + }) + it('throws if not hex', () => { + (() => sha256('fff')).should.throw() + }) + it('produces a sha256 from the given hex', () => { + const input = '54657374' + const actual = sha256(input) + const expected = '532eaabd9574880dbf76b9b8cc00832c20a6ec113d682299550d7a6e0f345e25' + const again = sha256(expected) + actual.should.eql(expected) + again.should.eql(hash256(input)) + }) + }) }) diff --git a/tests/wallet/Balance.js b/tests/wallet/Balance.js new file mode 100644 index 000000000..967337445 --- /dev/null +++ b/tests/wallet/Balance.js @@ -0,0 +1,87 @@ +import Balance from '../../src/wallet/Balance' +import testData from '../testData.json' +import { Transaction } from '../../src/transactions' + +describe('Balance', function () { + let bal + beforeEach('refresh balance', () => { + bal = new Balance({ net: 'TestNet', address: testData.a.address }) + bal.addAsset('NEO', testData.a.balance.assets.NEO) + bal.addAsset('GAS', testData.a.balance.assets.GAS) + }) + + describe('addAsset', function () { + it('empty balance', () => { + bal.addAsset('new1') + bal.assetSymbols.length.should.equal(3) + bal.assetSymbols[2].should.equal('NEW1') + }) + + it('filled balance', () => { + const coin = { + value: 1, + assetId: 'abc', + scriptHash: 'def' + } + bal.addAsset('new2', { balance: 1, unspent: [coin] }) + bal.assetSymbols.length.should.equal(3) + bal.assetSymbols[2].should.equal('NEW2') + bal.assets.NEW2.should.have.keys(['balance', 'spent', 'unspent', 'unconfirmed']) + bal.assets.NEW2.balance.should.equal(1) + }) + }) + + describe('applyTx', function () { + const intents = [ + { + assetId: 'c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b', + value: 200, + scriptHash: 'cef0c0fdcfe7838eff6ff104f9cdec2922297537' + }, + { + assetId: 'c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b', + value: 59, + scriptHash: 'cef0c0fdcfe7838eff6ff104f9cdec2922297537' + }, + { + assetId: '602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7', + value: 400, + scriptHash: 'cef0c0fdcfe7838eff6ff104f9cdec2922297537' + }, + { + assetId: '602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7', + value: 20.96175487, + scriptHash: 'cef0c0fdcfe7838eff6ff104f9cdec2922297537' + } + ] + const tx = Transaction.createContractTx(testData.a.balance, intents) + + it('unconfirmed', () => { + bal.applyTx(tx, false) + bal.assets.GAS.spent.length.should.equal(1) + bal.assets.GAS.unspent.length.should.equal(1) + bal.assets.GAS.unconfirmed.length.should.equal(2) + bal.assets.NEO.spent.length.should.equal(1) + bal.assets.NEO.unspent.length.should.equal(0) + bal.assets.NEO.unconfirmed.length.should.equal(2) + }) + + it('confirmed', () => { + bal.applyTx(tx, true) + bal.assets.GAS.spent.length.should.equal(1) + bal.assets.GAS.unspent.length.should.equal(3) + if (bal.assets.GAS.unconfirmed) bal.assets.GAS.unconfirmed.length.should.equal(0) + bal.assets.NEO.spent.length.should.equal(1) + bal.assets.NEO.unspent.length.should.equal(2) + if (bal.assets.NEO.unconfirmed) bal.assets.NEO.unconfirmed.length.should.equal(0) + }) + }) + + it('verifyAssets', () => { + return bal.verifyAssets('http://seed1.neo.org:20332') + .then((bal) => { + bal.assets.GAS.spent.length.should.least(1) + bal.assets.NEO.spent.length.should.least(1) + }) + }) +}) diff --git a/webpack.config.js b/webpack.config.js index c628d9f7b..b34c791f8 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -66,7 +66,7 @@ module.exports = function (env) { output: { path: __dirname, filename: './lib/browser.js', - libraryTarget: 'amd', + libraryTarget: 'umd', library: 'Neon' // This is the var name in browser } })