tgerboui / nona-lib

TypeScript library to simplify interactions with the Nano currency node.
MIT License
6 stars 1 forks source link
browser javascript library nano nanocurrency nodejs rpc typescript websocket

Nona Lib

test coverage GitHub License

Nona Lib is a powerful and user-friendly TypeScript library designed to simplify interactions with the Nano cryptocurrency network.

Whether you're developing wallet software, integrating Nano payments into your application, or just exploring the possibilities of Nano's block lattice structure, Nona Lib provides a comprehensive set of tools to manage accounts, perform transactions, and query blockchain data efficiently and securely through your Nano node.

Keys Features

Table of contents

Installation

[!NOTE] Before you begin using Nona Lib, ensure that your Nano node's enable_control is set to true, as the library requires this permission to perform certain operations.
More information on how to configure your node can be found here.

To install Nona Lib, run the following command in your project directory:

npm install nona-lib

Getting Started

If your node runs on localhost with the default port:

import { Nona } from 'nona-lib';

const nona = new Nona();

If your node runs on a custom port or on a remote server, you can specify the URLs:

import { Nona } from 'nona-lib';

const nona = new Nona({ 
  url: 'http://localhost:7076',
  websocketUrl: 'ws://localhost:7078',
});

Basic Usage

Create an account

You can simply create a new account with the following code:

const { privateKey, publicKey, address } = await nona.key.create();

Or from a seed:

// Use KeyService to generate a seed or provide your own
const seed = await KeyService.generateSeed();
const { privateKey, publicKey, address } = await nona.key.create(seed);

These keys are generated locally using the nanocurrency-js package.

Open an account

Before using an account, you need to open it.
To open an account, you must have sent some funds to it from another account.

Then call the open method:

// You must provide a representative address to open the account
const reprensentative = 'nano_3rep...';

const wallet = nona.wallet(privateKey);
await wallet.open(reprensentative);

If you don't know how to choose a representative, check out this blog post: How to choose a representative.

Send a transaction

To send a transaction, you must have an opened account (see Open an account).

const receveiver = 'nano_1rece...';
const amount = 2;

const wallet = nona.wallet(privateKey);
await wallet.send(receveiver, amount);

Receive a transaction

To receive a transaction, you must have an opened account (see Open an account).

To receive a single transaction:

const wallet = nona.wallet(privateKey);
await wallet.receive();

If you want to receive all pending transactions:

const wallet = nona.wallet(privateKey);
await wallet.receiveAll();

You can also listen and receive transactions in real time:

const wallet = nona.wallet(privateKey);
// This will create a websocket connection, listen for incoming transactions, and automatically receive them.
const subscription = await wallet.listenAndReceive({
  // (Optional) next will be called each time a transaction is received
  next: (transactionBlock) => console.log('Received transaction', transactionBlock),
});

// Don't forget to unsubscribe when you don't need it anymore
subscription.unsubscribe();

For more details about websocket, see Websocket.

Nona API

Wallet

[!NOTE] All methods in the account API are also available via the wallet object.

[!WARNING]
This wallet API does not interact with the wallet RPCs commands this naming is only for convenience.

The wallet is the main object to interact with your account.

const wallet = nona.wallet(privateKey);

Open

open(representative: NanoTarget): Promise<string>

Opens the account with the provided representative represented as a NanoTarget string.

The first transaction of an account is crafted in a slightly different way. To open an account, you must have sent some funds to it from another account.
Returns the hash of the transaction.

const representative = 'nano_3rep...';
await wallet.open(representative);

Send

send(target: NanoTarget, amount: number | string): Promise<string>

Sends a transaction to the specified target. The amount is in nano unit.
Returns the hash of the transaction.

const address = 'nano_1rece...';
const amount = 2;

await wallet.send(address, amount);

[!NOTE]
The work is generated by the node, the options to provide or generate the work locally are not yet implemented.

Receive

receive(hash?: string): Promise<string | null>

Receives a pending transaction.
The hash of the transaction to receive can be provided. If not, a receivable hash will be used.
Returns the hash of the receive block. If no hash is provided and no transaction is pending, null is returned.

await wallet.receive();

An hash can also be provided to receive a specific transaction:

const hash = 'D83124BB...';
await wallet.receive(hash);

Receive all

receiveAll(hashes?: string[]): Promise<string[]>

Receives all pending transactions.
Returns an array of hashes of the received blocks.

await wallet.receiveAll();

An array of hashes can also be provided to receive specific transactions:

const hashes = ['D83124BB...', '1208FF64...'];
await wallet.receiveAll(hashes);

Listen and receive

[!NOTE]
All the webscoket related methods use Rxjs Observables and return a Subscription object. For more information about observables and subscriptions, see the Rxjs documentation.

listenAndReceive(params?: WalletListAndReceiveParams): Subscription

Listens for incoming transactions and automatically receives them.
Return a Subscription object.

interface WalletListAndReceiveParams {
  /**
   * A function that will be called each time a transaction is received.
   * @param block The block that was received.
   */
  next?: (block: ConfirmationBlock) => unknown;
  /**
   * A function that will be called when an error occurs.
   * @param error The error that occurred.
   */
  error?: (error: unknown) => unknown;
  /**
   * A function that will be called when the listener completes.
   */
  complete?: () => unknown;
}
const subscription = await wallet.listenAndReceive({
  next: (transactionBlock) => console.log('Received transaction', transactionBlock),
  error: (error) => console.error('An error occurred', error),
  complete: () => console.log('Subscription completed'),
});

// Don't forget to unsubscribe when you don't need it anymore
subscription.unsubscribe();

[!WARNING] Depending on the node setup and sync status, multiple confirmation notifications for the same block hash may be sent by a single tracking mechanism.
Theses block will call the error function with the error Unreceivable.

Account

[!NOTE] Theses commands are also available using the wallet object.

All commands related to public account information.

You can create an account object with the following code:

const address = 'nano_13e2ue...';
const account = await nona.account(address);

You can also use your wallet object:

const wallet = nona.wallet(privateKey);

Receivable

receivable(params?: ReceivableParams): Promise<Receivable>

Returns a list of block hashes which have not yet been received by this account.

interface ReceivableParams {
  /**
   * Specifies the number of blocks to return.
   * Default to 100.
   */
  count?: number;
  /**
   * Specifies whether to sort the response by block amount.
   * Default to false.
   */
  sort?: boolean;
}

Depending on the sort parameter, the blocks will be returned in two different ways.

If sort is false (default), the blocks will be returned as an array of strings:

const receivable = await account.receivable();
console.log(receivable);
// ['D83124BB...', '1208FF64...', ...]

If sort is true, the blocks will be returned as an object with the block hash as the key and the amount as the value:

const receivable = await account.receivable({ sort: true });
console.log(receivable);
// { 'D83124BB...': 2, '1208FF64...': 1, ... }

Account info

info(params?: InfoParams): Promise<AccountInfo>

Returns general information for account.
Only works for accounts that have received their first transaction and have an entry on the ledger, will return "Account not found" otherwise.
To open an account, use open.

interface InfoParams {
  /**
   * Specifies whether to include the representative in the response.
   * Default to false.
   */
  representative?: boolean;
  /**
   * Specifies whether to return the raw balance.
   * Default to false.
   */
  raw?: boolean;
}
const info = await account.info();
console.log(info);
// {
//   frontier: '0D60C42554478A2EDAD18AD5E975422297E62082E612C60ECABBDD4D01B65D46',
//   open_block: '92CBCAC62345F58A58CE513652D22BD6E13CB094BF6F8D825DEE01CE54718868',
//   representative_block: '0D60C42253478E2ADAD18AD5E975426297E62082E612C60ECAACDD4D01B65D46',
//   balance: '2.3',
//   modified_timestamp: '1712846889',
//   block_count: '82',
//   account_version: '2',
//   confirmation_height: '82',
//   confirmation_height_frontier: '0D60C22554478E2EDAD81BD5E975426297E62082E612C60ECAACDD4D01B65D46'
// }

Balance

balance({ raw = false }: { raw?: boolean }): Promise<AccountBalance>

Returns how many nano is owned and how many have not yet been received by account. Set raw to true to return the balance in raw unit.

const balance = await account.balance();
console.log(balance);
// { balance: '2.3', receivable: '5', pending: '0' }

balance is the total amount of nano owned by the account.
receivable is the amount of nano that has not yet been received by the account.

[!NOTE] pending was deprecated in favor of receivable. For compatibility reasons both terms are still available for many calls and in responses.
For more details see: https://docs.nano.org/releases/release-v24-0/#pendingreceivable-term-rpc-updates.

Listen confirmation

listenConfirmation(params: ListenConfirmationParams): Subscription

Listen for all confirmed blocks for the related account.

export interface ListenConfirmationParams {
  /**
   * A function that will be called each time a transaction is received.
   * @param block The block that was received.
   */
  next: (block: ConfirmationBlock) => unknown;
  /**
   * A function that will be called when an error occurs.
   * @param error The error that occurred.
   */
  error?: (error: unknown) => unknown;
  /**
   * A function that will be called when the listener completes.
   */
  complete?: () => unknown;
  /**
   * A filter that will be used to filter the confirmation blocks.
   */
  filter?: ConfirmationFilter;
}

export interface ConfirmationFilter {
  /** List of block subtypes to filter the confirmation blocks. */
  subtype?: string[];
  /** Account addresses that received the transaction. */
  to?: NanoAddress[];
}

Listen all sent confirmation blocks for the account from a specific address:

const subscription = await account.listenConfirmation({
  next: (transactionBlock) => console.log('Received confirmation', transactionBlock),
  filter: {
    accounts: ['nano_1send...'],
    subtype: ['send']
  },
});

[!WARNING] Depending on the node setup and sync status, multiple confirmation notifications for the same block hash may be sent by a single tracking mechanism.
In order to prevent potential issues, integrations must track these block hashes externally to the node and prevent any unwanted actions based on multiple notifications.

History

history(params?: AccountHistoryParams): Promise<AccountHistory>

Retrieves the account history.
Returns only send & receive blocks by default (unless raw is set to true - see optional parameters below): change, state change & state epoch blocks are skipped, open & state open blocks will appear as receive, state receive/send blocks will appear as receive/send entries.
Response will start with the latest block for the account (the frontier), and will list all blocks back to the open block of this account when "count" is set to "-1".

export interface AccountHistoryParams {
  /** Number of blocks to return. Default to 100. */
  count?: number;
  /** Hash of the block to start from. */
  head?: string;
  /** Number of blocks to skip. */
  offset?: number;
  /** Reverse order */
  reverse?: boolean;
  /** Results will be filtered to only show sends/receives connected to the provided account(s). */
  accounts?: NanoAddress[];
  /**
   * if set to true instead of the default false, returns all blocks history and output all parameters of the block itself.
   */
  raw?: boolean;
}
const history = await account.history();
console.log(history);
// {
//   history: [
//     {
//       type: 'send',
//       account: 'nano_13dtu...',
//       amount: '1.23',
//       hash: 'C43ED22C09...',
//       local_timestamp: '1712846889',
//       height: '82',
//     },
//     [...]
//   ],
//   previous: 'D83124BB...',
// }

To paginate the history, you can use the head parameter with the previous value from the previous call:

let hasNext = true;
let head: string | undefined;
while (hasNext) {
  const { history, previous } = await account.history({ head });
  console.log(history);

  if (previous) {
    head = previous;
  } else {
    hasNext = false;
  }
}

If reverse is true, use next instead of previous.

Block count

blockCount(): Promise<number>

Returns the number of blocks for this account.

const blockCount = await account.blockCount();
console.log(blockCount);
// 42

Representative

representative(): Promise<string>

Returns the representative for this account.

const representative = await account.representative();
console.log(representative);
// nano_3rep...

Weight

weight(): Promise<string>

Returns the voting weight for this account in nano unit (default).

const weight = await account.weight();
console.log(weight);
// 123.456789

Set raw to true to return the weight in raw unit.

const weight = await account.weight({ raw: true });
console.log(weight);
// 123456789000000000000000000000000

Websocket

[!NOTE]
All the webscoket related methods use Rxjs Observables and return a Subscription object. For more information about observables and subscriptions, see the Rxjs documentation.

At the first subscription, the websocket connection to the node will be established.
If all subscriptions are unsubscribed, the connection will be closed.

You can access to the websocket object with the following code:

const ws = nona.ws;

Confirmation

confirmation(params: WebSocketConfirmationParams): Subscription

Listens for confirmed blocks.
Return a Subscription object.

interface WebSocketConfirmationParams {
  /**
   * A function that will be called each time a transaction is received.
   * @param block The block that was received.
   */
  next: (block: ConfirmationBlock) => unknown;
  /**
   * A function that will be called when an error occurs.
   * @param error The error that occurred.
   */
  error?: (error: unknown) => unknown;
  /**
   * A function that will be called when the listener completes.
   */
  complete?: () => unknown;
  /**
   * A filter that will be used to filter the confirmation blocks.
   */
  filter?: ConfirmationFilter;
}

interface ConfirmationFilter {
  /** List of account addresses to filter the confirmation blocks. */
  accounts?: NanoAddress[];
  /** List of block subtypes to filter the confirmation blocks. */
  subtype?: string[];
  /** Account addresses that received the transaction. */
  to?: NanoAddress[];
}

Subscribe to all new confirmed blocks on the network:

const subscription = await nona.ws.confirmation({
  // next will be called each time a transaction is received
  next: (confirmationBlock) => console.log('Received confirmation', confirmationBlock),
});

// Don't forget to unsubscribe when you don't need it anymore
subscription.unsubscribe();

Subscribe to all new sent confirmation blocks to a specific account:

const subscription = await nona.ws.confirmation({
  next: (block) => console.log('Received confirmation', block),
  filter: {
    subtype: ['send'],
    to: ['nano_1send...'],
  },
});

Blocks

You can access to the blocks object with the following code:

const blocks = nona.blocks;

Count

count(): Promise<BlockCount>

Reports the number of blocks in the ledger and unchecked synchronizing blocks.
This count represent the node ledger and not the network status.

const count = await blocks.count();
console.log(count);
// { count: '198574599', unchecked: '14', cemented: '198574599' }

count - The total number of blocks in the ledger. This includes all send, receive, open, and change blocks.
unchecked - The number of blocks that have been downloaded but not yet confirmed. These blocks are waiting in the processing queue.
cemented - The number of blocks that have been confirmed and cemented in the ledger. Cemented blocks are confirmed irreversible transactions.

Create block

[!WARNING] This method is for advanced usage, use it if you know what you are doing.

create(params: CreateBlockParams): Promise<string>

Creates a block object based on input data & signed with private key or account in wallet.

Create a send block. Let's say you want to send 1 nano to nano_1send.... You have currently 3 nano in your account.

const wallet = nona.wallet(privateKey);
const info = await wallet.info();
const recipient = 'nano_1send...';
const recipientPublicKey = KeyService.getPublicKey(recipient);

const sendBlock = await this.blocks.create({
  // Current account 
  account: wallet.address,
  // Final balance for account after block creation in raw unit (here: current balance - send amount).
  balance: '2000000000000000000000000000000',
  // The block hash of the previous block on this account's block chain ("0" for first block). 
  previous: info.frontier,
  // The representative account for the account. 
  representative: info.representative,
  // If the block is sending funds, set link to the public key of the destination account.
  // If it is receiving funds, set link to the hash of the block to receive.
  // If the block has no balance change but is updating representative only, set link to 0. 
  link: recipientPublicKey,
  // Private key of the account 
  key: privateKey,
});

Process block

[!WARNING] This method is for advanced usage, use it if you know what you are doing.

process(block: Block, subtype: string): Promise<string>

Publish it to the network. Returns the hash of the published block.

If we want to process the block created in the previous example:

await this.blocks.process(sendBlock, 'send');

Block info

info(hash: string): Promise<BlockInfo>

Retrieves information about a specific block.

const hash = 'D83124BB...';
const info = await blocks.info(hash);

Key

You can access to the key object with the following code:

const blocks = nona.key;

Create

[!NOTE] The keys are generated locally using the nanocurrency-js package.

create(seed?: string): Promise<AccountKeys>

Create keys for an account.

const { privateKey, publicKey, address } = await nona.key.create();

Expand

expand(privateKey: string): AccountKeys

Expand a private key into public key and address.

const { publicKey, address } = nona.key.expand(privateKey);

Key Service

Service to generate seeds and keys.

Generate seed

KeyService.generateSeed(): Promise<string>

Generates a random seed.

const seed = await KeyService.generateSeed();

Get private key

KeyService.getPrivateKey(seed: string, index: number): string

Derive a private key from a seed, given an index.

const privateKey = KeyService.getPrivateKey(seed, 0);

Get public key

KeyService.getPublicKey(privateKeyOrAddress: string): string

Derive a public key from a private key or an address.

const publicKey = KeyService.getPublicKey(privateKey);

Get address

KeyService.getAddress(publicKey: string): string

Derive an address from a public key.

const address = KeyService.getAddress(publicKey);

Node

You can access to the node object with the following code:

const node = nona.node;

Telemetry

telemetry(): Promise<Telemetry>

Return metrics from other nodes on the network. Summarized view of the whole network.

const telemetry = await node.telemetry();

Uptime

uptime(): Promise<number>

Return node uptime in seconds.

const uptime = await node.uptime();
console.log(uptime);
// 832870

Version

version(): Promise<Version>

Returns version information for RPC, Store, Protocol (network), Node (Major & Minor version).

const version = await node.version();

Rpc

if you prefer to call RPC directly you can use nona.rpc.

rpc(action: string, body?: object): Promise<unknown>

Call account_info RPC command:

const info = await nona.rpc('account_info', {
  account: 'nano_13e2ue...',
});

Name Service

You can access to the nameService object with the following code:

const nameService = nona.nameService;

Resolve Username

resolveUsername(username: NanoUsername): Promise<NanoAddress>

Resolves a username registered with the Nano Name Service to the registed NanoAddress

const username = '@nona-lib';
const address = await nona.nameService.resolveUsername(username);

Resolve Target

resolveTarget(target: NanoTarget): Promise<NanoAddress>

Takes in a NanoTarget to potentially resolve. It checks if the target is a valid NanoAddress or a NanoUsername. In the case a NanoUsername is passed to the method, it is automatically resolved

const target = '@nona-lib';
const address = await nona.nameService.resolveTarget(target);
const target = 'nano_1rece...';
const address = await nona.nameService.resolveTarget(target);

Datatypes

Nano Address

A valid Nano address according to the offical docs:

nano_1anrzcuwe64rwxzcco8dkhpyxpi8kd7zsjc1oeimpc3ppca4mrjtwnqposrs

Its validity is checked with the nanocurrency-js package.

Nano Username

A Nano username in the form of @name:

@nona-lib

This username is resolved at runtime with the Nano Name Service.

Nano Target

A NanoTarget is either a valid address (e.g. nano_1rece...) or a resolveable username registered with the Nano Name Service (e.g. @nona-lib). Nonalib automatically resolves these usernames to valid adresses for you.

Handling Errors

All handled errors are instances of NonaError.
Each types of errors are extended from NonaError and have a specific instance.

NonaError - Base class for all errors and generic error.
NonaNetworkError - Network error, likely related to the node connection.
NonaRpcError - Error related to the RPC call response. NonaParseError - Error related to the response parsing.
If this occured while using the library, please report it.
NonaUserError - Error related to the user input.
For example, if you try to send a transaction with an insufficient balance:

try {
  await wallet.send('nano_1rece...', 100);
} catch (error) {
  if (error instanceof NonaUserError && error.message === 'Insufficient balance') {
    console.log('You don\'t have enough balance to send this amount');
  } else {
    console.error('An error occurred', error);
  }
}

Disclaimer

Nona Lib is a young and evolving TypeScript library designed to interact with the Nano cryptocurrency network. While we strive to ensure reliability and robustness, the following points need to be considered:

We encourage the community to contribute to the development and testing of Nona Lib to help us improve its functionality and security. Use this library with caution, and ensure you have robust backup and recovery processes in place.

Roadmap

Websocket

Proof of work

External services

Nano.to

Tests