diff --git a/src/packages/core/api/__mocks__/account.ts b/src/packages/core/api/__mocks__/account.ts new file mode 100644 index 0000000..991be01 --- /dev/null +++ b/src/packages/core/api/__mocks__/account.ts @@ -0,0 +1,7 @@ +export const accountApiInstance = { + getBalance: jest.fn() +} + +export const AccountApi = jest.fn().mockImplementation(() => { + return accountApiInstance; +}) \ No newline at end of file diff --git a/src/packages/core/api/account.ts b/src/packages/core/api/account.ts new file mode 100644 index 0000000..c620d03 --- /dev/null +++ b/src/packages/core/api/account.ts @@ -0,0 +1,31 @@ +import * as config from 'config'; +import { APIServerClass, IAPIConfig } from './APIServer'; + +export interface ApiBalance { + total: number; + usable: number; + locked: number; +} + +export class AccountApi extends APIServerClass { + + constructor(conf: IAPIConfig = config.nuls.api.explorer) { + super({ ...config.nuls.api.explorer, ...conf }); + } + + async getBalance(address): Promise { + + const resource: string = this.getResource('balance', address); + + try { + return (await this.api.get(resource)).data; + + } catch (e) { + + throw this.handleErrors(e); + + } + + } + +} diff --git a/src/packages/core/common.ts b/src/packages/core/common.ts index ef84ec4..e46053e 100644 --- a/src/packages/core/common.ts +++ b/src/packages/core/common.ts @@ -1,6 +1,6 @@ export const HASH_LENGTH = 34; export const ADDRESS_LENGTH = 23; -export const P2SH_ADDRESS_TYPE = 3; + export const CONSENSUS_LOCK_TIME = -1; export const BLACK_HOLE_ADDRESS = 'Nse5FeeiYk1opxdc5RqYpEWkiUDGNuLs'; diff --git a/src/packages/core/protocol/__tests__/account.spec.ts b/src/packages/core/protocol/__tests__/account.spec.ts index 5e59658..2886c4a 100644 --- a/src/packages/core/protocol/__tests__/account.spec.ts +++ b/src/packages/core/protocol/__tests__/account.spec.ts @@ -1,9 +1,12 @@ import { Account, AddressType, ChainIdType, CustomAddressPosition } from '../../index'; +//import { accountApiInstance } from '../../api/__mocks__/account'; +import * as accountModule from '../../api/account'; import { randomBytes } from 'crypto'; import { publicKeyCreate, verify } from 'secp256k1'; jest.mock('crypto'); jest.mock('secp256k1'); +jest.mock('../../api/account'); describe('create new accounts', () => { @@ -13,6 +16,40 @@ describe('create new accounts', () => { describe('valid private key', () => { + describe('balance', () => { + + it('getting the balance of an account', async () => { + + // This API (explorer.nuls.services) doesn't currently work for new addresses + // If it would, this is what a test could look like: + + // const account = Account.create(); + + // account.switchChain(ChainIdType.Testnet); + // let balance = await account.getBalance({host: 'https://explorer.nuls.services', base: ''}); + // console.log(balance); + + // expect(balance).toEqual({ + // total: 0, + // locked: 0, + // usable: 0 + // }); + // Using a pre-defined address.. + + const accountApiInstance = new accountModule.AccountApi(); + accountApiInstance.getBalance.mockResolvedValueOnce({total: 9999999999999, locked: 0, usable: 9999999999999}); + + const address = 'TTatyig2SCtmUEsgguKvxQQ421e6NULS'; + const balance = await Account.getBalance(address, {host: 'https://explorer.nuls.services', base: ''}); + + expect(balance.locked + balance.usable).toEqual(balance.total); + expect(balance.total).toEqual(9999999999999); + expect(balance.locked).toEqual(0); + expect(balance.usable).toEqual(9999999999999); + + }); + }); + describe('create', () => { it('creating a new account', () => { diff --git a/src/packages/core/protocol/account.ts b/src/packages/core/protocol/account.ts index 1c16c38..76b9be8 100644 --- a/src/packages/core/protocol/account.ts +++ b/src/packages/core/protocol/account.ts @@ -5,7 +5,11 @@ import Hex from 'crypto-js/enc-hex'; import Base64 from 'crypto-js/enc-base64'; import * as secp256k1 from 'secp256k1'; -import { getPrivateKeyBuffer, getXOR, sha256, ripemd160 } from '../utils'; +import { getPrivateKeyBuffer, getXOR, sha256, ripemd160, isValidAddress } from '../utils'; + + +import { IAPIConfig } from '..'; +import { AccountApi, ApiBalance } from '../api/account'; export interface AccountObject { address: string; @@ -16,7 +20,8 @@ export interface AccountObject { export enum AddressType { Default = 1, - Contract = 2 + Contract = 2, + P2SH = 3 } export enum ChainIdType { @@ -65,6 +70,21 @@ export class Account { /** Public key HEX Buffer */ private publicKeyBuffer: Buffer = Buffer.from([]); + /* Gets the balance of an address (Testnet only for now) */ + public static async getBalance(address: string, iapiConfig: IAPIConfig): Promise { + if(isValidAddress(address)){ + let accountApi = new AccountApi(iapiConfig); + return await accountApi.getBalance(address); + } else { + throw new Error("Invalid Address Used!"); + } + } + + /* Gets the balance of this account (works for test net only) */ + public async getBalance(iapiConfig:IAPIConfig): Promise { + return await Account.getBalance(this.address, iapiConfig); + } + /** * This will loop around until it finds a matching address * @param str The string to look for in addresses created - The larger the string the harder it is to find diff --git a/src/packages/core/utils/__tests__/utils.test.ts b/src/packages/core/utils/__tests__/utils.test.ts new file mode 100644 index 0000000..1281719 --- /dev/null +++ b/src/packages/core/utils/__tests__/utils.test.ts @@ -0,0 +1,26 @@ +import { isValidAddress } from '../crypto'; + +jest.unmock('crypto'); +jest.unmock('secp256k1'); + +describe('Address validation checks', () => { + + beforeEach(() => { + jest.restoreAllMocks(); + }); + + it('check validity of different addresses', () => { + + expect(isValidAddress("TTavFTDgdQNeYgVQBaNTF6SeK54nswH5")).toEqual(true); + expect(isValidAddress("Nsdwnd4auFisFJKU6iDvBxTdPkeg8qkB")).toEqual(true); + expect(isValidAddress("Nse3uLgeCBWP48GCGh8L54gnELfpnSG9")).toEqual(true); + expect(isValidAddress("NseBuUpi4iwbJsj1UrUb4eiAWav9UY4C")).toEqual(true); + expect(isValidAddress("Nse7MZAwVTbdWXxWwgN6vfcPwBYC1izz")).toEqual(true); + + + expect(isValidAddress("avFTDgdQNeYgVQBaNTF6SeK54nswH5")).toEqual(false); + expect(isValidAddress("TTavFTDgdQNeYgVQBaNTF6SeK54nswH5DXXD")).toEqual(false); + expect(isValidAddress("")).toEqual(false); + + }); +}); diff --git a/src/packages/core/utils/crypto.ts b/src/packages/core/utils/crypto.ts index 0a2a3e9..d2c9524 100644 --- a/src/packages/core/utils/crypto.ts +++ b/src/packages/core/utils/crypto.ts @@ -2,7 +2,13 @@ import * as bs58 from 'bs58'; import RIPEMD160 from 'ripemd160'; import * as secp256k1 from 'secp256k1'; import * as shajs from 'sha.js'; -import { HASH_LENGTH } from '../common'; +import { + HASH_LENGTH, + ADDRESS_LENGTH, + } from '../common'; + +import { AddressType, ChainIdType } from '../protocol/account'; + import { isHex } from './serialize'; export const PRIVATE_KEY_LENGTH = 64; @@ -53,9 +59,47 @@ export function isValidPrivateKey(privateKey: string): boolean { } +/* +https://github.com/nuls-io/nuls/blob/b8e490a26eeec7b16d924d8398a67ede24ff86ca/core-module/kernel/src/main/java/io/nuls/kernel/utils/AddressTool.java#L77-L117 +*/ export function isValidAddress(address: string): boolean { - return /^(Ns|TT)([a-zA-Z-0-9]{30})$/.test(address); + if(!/^(Ns|TT)([a-zA-Z-0-9]{30})$/.test(address)) + return false; + + let bytes: Buffer; + + try { + bytes = Buffer.from(bs58.decode(address)); + if(bytes.length != ADDRESS_LENGTH + 1) + return false; + } catch { + return false; + } + + let chainId: Number; + let type: Number; + + try { + chainId = bytes.readInt16LE(0); + type = bytes.readInt8(2); + } catch { + return false; + } + + if (Object.values(ChainIdType).indexOf(chainId) === -1){ + return false; + } + if (Object.values(AddressType).indexOf(type) === -1) { + return false; + } + try { + checkXOR(bytes); + } catch { + return false; + } + + return true; } @@ -89,6 +133,19 @@ export function getXOR(bytes: Buffer): number { } +/* +https://github.com/nuls-io/nuls/blob/b8e490a26eeec7b16d924d8398a67ede24ff86ca/core-module/kernel/src/main/java/io/nuls/kernel/utils/AddressTool.java#L169-L183 +*/ +export function checkXOR(hashs: Buffer) { + + const body: Buffer = hashs.slice(0, ADDRESS_LENGTH); + const xor = getXOR(body); + + if (xor != hashs[ADDRESS_LENGTH]) { + throw new Error("Address XOR doesn't check out."); + } +} + export function addressFromHash(hash: AddressHash): string { return bs58.encode(Buffer.concat([hash, Buffer.from([getXOR(hash)])]));