nano-wallet-js-test-1
Version:
SDK for developers to create and interact with Nanocurrency wallet easily
309 lines (253 loc) • 6.58 kB
text/typescript
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;
}
}