nano-wallet-js
Version:
SDK for developers to create and interact with Nanocurrency wallet easily
341 lines (340 loc) • 12.1 kB
JavaScript
import { Unit, convert, createBlock, deriveAddress, derivePublicKey, validateWork, } from 'nanocurrency';
import NanoRPC from '../rpc/RpcController';
import { DEFAULT_TIMEOUT, MIN_AMOUNT, RECEIVE_DIFFICULTY, SEND_DIFFICULTY, } from '../Constants';
import BaseController from '../BaseController';
import { TunedBigNumber } from '../utils';
import Logger from '../logger/Logger';
import Locker from './Locker';
export default class NanoWallet extends BaseController {
rpc;
publicKey;
account;
defaultConfig = {
rpcUrls: [],
workerUrls: [],
privateKey: null,
representative: '',
precomputeWork: true,
minAmountRaw: convert(MIN_AMOUNT.toString(), {
from: Unit.NANO,
to: Unit.raw,
}),
timeout: DEFAULT_TIMEOUT,
debug: false,
};
defaultState = {
balance: '0',
receivable: '0',
receivableBlocks: [],
frontier: null,
representative: null,
work: null,
};
logger;
locker = new Locker();
constructor(config, state) {
super(config, state || undefined);
this.publicKey = derivePublicKey(config.privateKey);
this.account = deriveAddress(this.publicKey, { useNanoPrefix: true });
this.logger = new Logger('NANO_WALLET', config.debug);
this.logger.info(`Imported account: ${this.account}`);
this.initialize();
this.rpc = new NanoRPC({
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, 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: ${convert(balance, {
from: Unit.raw,
to: Unit.NANO,
})} NANO. Receivable: ${convert(receivable, {
from: Unit.raw,
to: 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 = 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 = TunedBigNumber(receivable)
.plus(blocks[blockHash])
.toString();
}
this.logger.info(`${convert(receivable, {
from: Unit.raw,
to: 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 = TunedBigNumber(this.state.balance)
.plus(amount)
.toString();
this.logger.info(`Receiving ${convert(amount, {
from: Unit.raw,
to: Unit.NANO,
})} NANO from block ${link}`);
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.getWork(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();
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 = TunedBigNumber(this.state.balance)
.minus(amount)
.toString();
this.logger.info(`Sending ${convert(amount, {
from: Unit.raw,
to: Unit.NANO,
})} NANO to ${to}`);
const { block, hash } = 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, 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 } = 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, 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 } = 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, 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;
}
}