UNPKG

nano-wallet-js-test-1

Version:

SDK for developers to create and interact with Nanocurrency wallet easily

309 lines (253 loc) 6.58 kB
import { Unit, convert, createBlock, deriveAddress, derivePublicKey, validateWork, } from 'nanocurrency'; import NanoRPC, { NanoRpcConfig } from '../rpc/RpcController'; import { DEFAULT_TIMEOUT, MIN_AMOUNT, RECEIVE_DIFFICULTY, SEND_DIFFICULTY, } from '@/Constants'; import BaseController from '@/BaseController'; import { TunedBigNumber } from '@/utils'; export interface NanoWalletConfig extends NanoRpcConfig { privateKey: string; representative: string; minAmountRaw?: string; } export interface ReceivableBlock { blockHash: string; amount: string; } export interface NanoWalletState { balance: string; receivable: string; receivableBlocks: ReceivableBlock[]; frontier: string | null; representative: string | null; } export default class NanoWallet extends BaseController< NanoWalletConfig, NanoWalletState > { rpc: NanoRPC; publicKey: string; account: string; defaultConfig = { rpcUrls: [], workerUrls: [], privateKey: null, representative: '', minAmountRaw: convert(MIN_AMOUNT.toString(), { from: Unit.NANO, to: Unit.raw, }), timeout: DEFAULT_TIMEOUT, } as any as NanoWalletConfig; defaultState: NanoWalletState = { balance: '0', receivable: '0', receivableBlocks: [], frontier: null, representative: null, }; constructor(config: NanoWalletConfig, state?: NanoWalletState | null) { super(config, state || undefined); this.publicKey = derivePublicKey(config.privateKey); this.account = deriveAddress(this.publicKey, { useNanoPrefix: true }); this.rpc = new NanoRPC({ rpcUrls: config.rpcUrls, workerUrls: config.workerUrls, timeout: config.timeout, }); this.initialize(); } async sync() { try { const { balance, frontier, receivable, representative } = await this.rpc.accountInfo(this.account); await this.update({ balance, frontier, receivable, representative }); } catch (error: any) { if (error.message !== 'Account not found') { throw error; } } await this.getReceivable(); } async workGenerate(hash: string, threshold: string) { const { work } = await this.rpc.workGenerate(hash, threshold); if (!work) { throw new Error('No work'); } const isValidWork = validateWork({ work, blockHash: hash, threshold, }); if (!isValidWork) { throw new Error('Invalid work'); } return work; } async getReceivable() { const { blocks = {} } = await this.rpc.receivable(this.account, { threshold: this.config.minAmountRaw, }); let receivableBlocks: ReceivableBlock[] = []; let receivable = '0'; for (const blockHash in blocks) { receivableBlocks.push({ blockHash, amount: blocks[blockHash], }); receivable = TunedBigNumber(receivable) .plus(blocks[blockHash]) .toString(); } await this.update({ receivableBlocks, receivable }); return { receivableBlocks, receivable }; } async receive(link: string) { link = link.toUpperCase(); const amount = this.state.receivableBlocks.find( ({ blockHash }) => blockHash === link, )?.amount; if (!amount) { throw new Error('No receivable block'); } const balance = TunedBigNumber(this.state.balance).plus(amount).toString(); const { block, hash } = createBlock(this.config.privateKey, { previous: this.state.frontier, representative: this.config.representative, balance, link, work: null, }); const frontier = this.state.frontier || this.publicKey; const work = await this.workGenerate(frontier, RECEIVE_DIFFICULTY); const processed = await this.rpc.process({ ...block, work, }); if (processed.hash !== hash) { throw new Error('Block hash mismatch'); } const receivableBlocks = this.state.receivableBlocks.filter( ({ blockHash }) => blockHash !== link, ); const receivable = TunedBigNumber(this.state.receivable) .minus(amount) .toString(); await this.update({ balance, frontier: hash, receivableBlocks, receivable, }); return { hash }; } async send(to: string, amount: string) { if (this.state.frontier === null) { throw new Error('No frontier'); } const balance = TunedBigNumber(this.state.balance).minus(amount).toString(); const { block, hash } = createBlock(this.config.privateKey, { previous: this.state.frontier, representative: this.config.representative, balance, link: to, work: null, }); const work = await this.workGenerate(this.state.frontier, SEND_DIFFICULTY); const processed = await this.rpc.process({ ...block, work, }); if (processed.hash !== hash) { throw new Error('Block hash mismatch'); } await this.update({ balance, frontier: hash, }); return { hash }; } async sweep(to: string) { if (this.state.frontier === null) { throw new Error('No frontier'); } const { block, hash } = createBlock(this.config.privateKey, { previous: this.state.frontier, representative: this.config.representative, balance: '0', link: to, work: null, }); const work = await this.workGenerate(this.state.frontier, SEND_DIFFICULTY); const processed = await this.rpc.process({ ...block, work, }); if (processed.hash !== hash) { throw new Error('Block hash mismatch'); } await this.update({ balance: '0', frontier: hash, }); return { hash }; } async setRepresentative(account?: string) { if (this.state.frontier === null) { throw new Error('No frontier'); } const representative = account || this.config.representative; const { block, hash } = createBlock(this.config.privateKey, { previous: this.state.frontier, representative, balance: this.state.balance, link: null, work: null, }); const work = await this.workGenerate(this.state.frontier, SEND_DIFFICULTY); const processed = await this.rpc.process({ ...block, work, }); if (processed.hash !== hash) { throw new Error('Block hash mismatch'); } await this.update({ balance: '0', frontier: hash, representative, }); this.configure({ representative, }); return { hash }; } get balance() { return this.state.balance; } get receivable() { return this.state.receivable; } get receivableBlocks() { return this.state.receivableBlocks; } get frontier() { return this.state.frontier; } // Current representative may be different from the initialized representative // if after initialization of instance there has not yet been a new transaction or // .setRepresentative() has not been called yet. get currentRepresentative() { return this.state.representative; } }