UNPKG

nano-wallet-js

Version:

SDK for developers to create and interact with Nanocurrency wallet easily

347 lines (346 loc) 12.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const nanocurrency_1 = require("nanocurrency"); const RpcController_1 = __importDefault(require("../rpc/RpcController")); const Constants_1 = require("../Constants"); const BaseController_1 = __importDefault(require("../BaseController")); const utils_1 = require("../utils"); const Logger_1 = __importDefault(require("../logger/Logger")); const Locker_1 = __importDefault(require("./Locker")); class NanoWallet extends BaseController_1.default { rpc; publicKey; account; defaultConfig = { rpcUrls: [], workerUrls: [], privateKey: null, representative: '', precomputeWork: true, minAmountRaw: (0, nanocurrency_1.convert)(Constants_1.MIN_AMOUNT.toString(), { from: nanocurrency_1.Unit.NANO, to: nanocurrency_1.Unit.raw, }), timeout: Constants_1.DEFAULT_TIMEOUT, debug: false, }; defaultState = { balance: '0', receivable: '0', receivableBlocks: [], frontier: null, representative: null, work: null, }; logger; locker = new Locker_1.default(); constructor(config, state) { super(config, state || undefined); this.publicKey = (0, nanocurrency_1.derivePublicKey)(config.privateKey); this.account = (0, nanocurrency_1.deriveAddress)(this.publicKey, { useNanoPrefix: true }); this.logger = new Logger_1.default('NANO_WALLET', config.debug); this.logger.info(`Imported account: ${this.account}`); this.initialize(); this.rpc = new RpcController_1.default({ rpcUrls: this.config.rpcUrls, workerUrls: this.config.workerUrls, timeout: this.config.timeout, debug: this.config.debug, }); let previousFrontier = this.state.frontier; if (this.config.precomputeWork) { this.subscribe(state => { if (state.work && state.work.hash !== state.frontier) { this.update({ work: null }); } if (state.frontier && state.frontier !== previousFrontier && (!state.work || state.work.hash !== state.frontier)) { this.logger.info('Precomputing Work for', state.frontier); this.getWork(state.frontier, Constants_1.SEND_DIFFICULTY); } previousFrontier = state.frontier; }); } } async sync() { try { const { balance, frontier, receivable, representative } = await this.rpc.accountInfo(this.account); this.logger.info(`Wallet Sync! Balance: ${(0, nanocurrency_1.convert)(balance, { from: nanocurrency_1.Unit.raw, to: nanocurrency_1.Unit.NANO, })} NANO. Receivable: ${(0, nanocurrency_1.convert)(receivable, { from: nanocurrency_1.Unit.raw, to: nanocurrency_1.Unit.NANO, })}`); this.update({ balance, frontier, receivable, representative }); } catch (error) { if (error.message !== 'Account not found') { this.logger.error('sync:', error instanceof Error ? error.message : error); throw error; } } await this.getReceivable(); } async workGenerate(hash, threshold) { try { const startedAt = Date.now(); const { work } = await this.rpc.workGenerate(hash, threshold); if (!work) { throw new Error('No work'); } const isValidWork = (0, nanocurrency_1.validateWork)({ work, blockHash: hash, threshold, }); if (!isValidWork) { throw new Error('Invalid work'); } this.logger.info(`Generated Work for ${hash} in ${Date.now() - startedAt} ms`); return work; } catch (error) { this.logger.error('workGenerate:', error instanceof Error ? error.message : error); throw error; } } async getWork(hash, threshold) { if (this.state.work?.hash === hash && parseInt(this.state.work.threshold, 16) >= parseInt(threshold, 16)) { this.logger.info(`Using precomputed Work for ${hash}`); return this.state.work.work; } const work = await this.workGenerate(hash, threshold); // TODO: Store the generated threshold, instead the requested one this.update({ work: { hash, threshold, work, }, }); return work; } async getReceivable() { try { const { blocks = {} } = await this.rpc.receivable(this.account, { threshold: this.config.minAmountRaw, }); let receivableBlocks = []; let receivable = '0'; for (const blockHash in blocks) { receivableBlocks.push({ blockHash, amount: blocks[blockHash], }); receivable = (0, utils_1.TunedBigNumber)(receivable) .plus(blocks[blockHash]) .toString(); } this.logger.info(`${(0, nanocurrency_1.convert)(receivable, { from: nanocurrency_1.Unit.raw, to: nanocurrency_1.Unit.NANO, })} NANO to receive from ${receivableBlocks.length} blocks`); this.update({ receivableBlocks, receivable }); return { receivableBlocks, receivable }; } catch (error) { this.logger.error('getReceivable', error instanceof Error ? error.message : error); throw error; } } async receive(link) { await this.locker.acquire(); try { link = link.toUpperCase(); const amount = this.state.receivableBlocks.find(({ blockHash }) => blockHash === link)?.amount; if (!amount) { throw new Error('No receivable block'); } const balance = (0, utils_1.TunedBigNumber)(this.state.balance) .plus(amount) .toString(); this.logger.info(`Receiving ${(0, nanocurrency_1.convert)(amount, { from: nanocurrency_1.Unit.raw, to: nanocurrency_1.Unit.NANO, })} NANO from block ${link}`); const { block, hash } = (0, nanocurrency_1.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.getWork(frontier, Constants_1.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 = (0, utils_1.TunedBigNumber)(this.state.receivable) .minus(amount) .toString(); this.update({ balance, frontier: hash, receivableBlocks, receivable, }); return { hash }; } catch (error) { this.logger.error('receive:', error instanceof Error ? error.message : error); throw error; } finally { await this.locker.release(); } } async send(to, amount) { await this.locker.acquire(); try { if (this.state.frontier === null) { throw new Error('No frontier'); } const balance = (0, utils_1.TunedBigNumber)(this.state.balance) .minus(amount) .toString(); this.logger.info(`Sending ${(0, nanocurrency_1.convert)(amount, { from: nanocurrency_1.Unit.raw, to: nanocurrency_1.Unit.NANO, })} NANO to ${to}`); const { block, hash } = (0, nanocurrency_1.createBlock)(this.config.privateKey, { previous: this.state.frontier, representative: this.config.representative, balance, link: to, work: null, }); const work = await this.getWork(this.state.frontier, Constants_1.SEND_DIFFICULTY); const processed = await this.rpc.process({ ...block, work, }); if (processed.hash !== hash) { throw new Error('Block hash mismatch'); } this.update({ balance, frontier: hash, }); return { hash }; } catch (error) { this.logger.error('send:', error instanceof Error ? error.message : error); throw error; } finally { this.locker.release(); } } async sweep(to) { await this.locker.acquire(); try { if (this.state.frontier === null) { throw new Error('No frontier'); } this.logger.info(`Sweeping all funds from ${this.account} to ${to}`); const { block, hash } = (0, nanocurrency_1.createBlock)(this.config.privateKey, { previous: this.state.frontier, representative: this.config.representative, balance: '0', link: to, work: null, }); const work = await this.getWork(this.state.frontier, Constants_1.SEND_DIFFICULTY); const processed = await this.rpc.process({ ...block, work, }); if (processed.hash !== hash) { throw new Error('Block hash mismatch'); } this.update({ balance: '0', frontier: hash, }); return { hash }; } catch (error) { this.logger.error('sweep:', error instanceof Error ? error.message : error); throw error; } finally { this.locker.release(); } } async setRepresentative(account) { await this.locker.acquire(); try { if (this.state.frontier === null) { throw new Error('No frontier'); } this.logger.info(`Setting representative: ${account}`); const representative = account || this.config.representative; const { block, hash } = (0, nanocurrency_1.createBlock)(this.config.privateKey, { previous: this.state.frontier, representative, balance: this.state.balance, link: null, work: null, }); const work = await this.getWork(this.state.frontier, Constants_1.SEND_DIFFICULTY); const processed = await this.rpc.process({ ...block, work, }); if (processed.hash !== hash) { throw new Error('Block hash mismatch'); } this.update({ balance: '0', frontier: hash, representative, }); this.configure({ representative, }); return { hash }; } catch (error) { this.logger.error('setRepresentative:', error instanceof Error ? error.message : error); throw error; } finally { this.locker.release(); } } 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; } } exports.default = NanoWallet;