SPDX-License-Identifier: GPL-3.0-or-later
-->
+## v0.3.0
+
+### Notable Changes
+
+#### Reorganization
+
+It is easy to get lost when scrolling through code in monolithic long files, and
+it is also equally easy to get lost in too many layers of project file
+hierarchy. This update aims to find a balance between the two and has separated
+key behavior into smaller modules while resisting a deeply-nested directory
+structure. Basically, this update moved files around.
+
+#### `import` methods are now `load` methods
+
+To avoid confusion with the `import` JavaScript keyword, `import()` methods like
+`Wallet.import()` and `Account.import()` have been renamed to `load()`.
+
+#### Expanded account info
+
+The `wallet.refresh()` method now uses batch endpoints to fetch basic account
+data like balance, frontier, and representative. The `account.refresh()` method
+now fetches all available data from the relevant endpoint, including confirmed
+block information. In pratical use, the wallet method should be called when
+viewing multiple accounts and basic data should be presented to the end user,
+and the account method should be called when viewing an individual account or
+creating and processing transactions.
+
+#### Ledger aligned with other wallet types
+
+The various functions of the Ledger wallet have been abstracted away under the
+hood of the overarching `Wallet` class. Account derivation, secret verification,
+and block signing can all be done with the same methods for a 'Ledger'-type
+wallet is `libnemo` as its 'BIP-44' and 'BLAKE2b' counterparts.
+
+
+
## v0.2.1
### Notable Changes
# libnemo
-`libnemo` is a fork of the nanocurrency-web toolkit. It is used for client-side
-implementations of Nano cryptocurrency wallets and enables building web-based
-applications that can work even while offline. `libnemo` supports managing
-wallets, deriving accounts, signing blocks, and more.
+`libnemo` started as a fork of the `nanocurrency-web` toolkit and has evolved
+into its own distinct package. It is used for client-side implementations of
+Nano cryptocurrency wallets and enables building web-based applications that can
+work even while offline. `libnemo` supports managing wallets, deriving accounts,
+ signing blocks, and more.
It utilizes the Web Crypto API which is native to all modern browsers. Private
keys are encrypted in storage with a user password as soon as they are derived,
* Get account info and process blocks on the network while online.
* Manage known addresses with a rolodex.
* Sign and verify arbitrary strings with relevant keys.
-* Validate entropy, seeds, mnemonic phrases, and Nano addresses.
+* Validate seeds, mnemonic phrases, and Nano addresses.
* Convert Nano unit denominations.
## Installation
allow a single wallet to store multiple currencies
`libnemo` is able to generate and import HD and BLAKE2b wallets, and it can
-derive accounts for both. An HD wallet seed is 128 characters while a BLAKE2b
-wallet seed is 64 characters. For enhanced security, `libnemo` requires a
-password to create or import wallets, and wallets are initialized in a locked
-state. Implementations can provide their own Uint8Array bytes instead of a
-password. Refer to the documentation on each class factory method for specific
-usage.
+derive accounts for both. An HD wallet seed is 64 bytes (128 hexadecimal
+characters), and a BLAKE2b wallet seed is 32 bytes (64 hexadecimal characters).
+
+For enhanced security, `libnemo` requires a password to create or load wallets,
+and wallets are initialized in a locked state. When importing an existing
+wallet, the seed and, if included, the mnemonic phrase (collectively referenced
+herein as "wallet secrets") are inaccessible; since the user provided them, the
+user should already have a copy of them. For convenience, a verification method
+can be used to compare a user-provided value to the wallet value and return true
+if they are equal. When creating a new wallet, the wallet secrets are each
+accessible **once** and self-destruct after the first access of each of their
+values.
```javascript
import { Wallet } from 'libnemo'
```javascript
try {
- const unlockResult = await wallet.unlock(password)
-} catch(err) {
- console.log(err)
+ await wallet.unlock(password)
+} catch (err) {
+ console.error(err)
}
-console.log(unlockResult) // true if successfully unlocked
-const { mnemonic, seed } = wallet
const firstAccount = await wallet.account()
const secondAccount = await wallet.account(1)
const multipleAccounts = await wallet.accounts(2, 3)
-const thirdAccount = multipleAccounts[3]
+const thirdAccount = multipleAccounts[2]
const { address, publicKey, index } = firstAccount
const nodeUrl = 'https://nano-node.example.com/'
All blocks are 'state' types, but they are interpreted as one of three different
subtypes based on the data they contain: send, receive, or change
-representative. `libnemo` implements them as the following classes:
+representative. `libnemo` implements three methods to handle them appropriately:
-* SendBlock: the Nano balance of the account decreases
-* ReceivBlock: the Nano balance of the account increases and requires a matching
-SendBlock
-* ChangeBlock: the representative for the account changes while the Nano balance
-does not
+* `send(recipient, amount)`: the Nano balance of the account decreases
+* `receive(hash, amount)`: the Nano balance of the account increases and
+requires a matching send block hash
+* `change(representative)`: the representative for the account changes while the
+Nano balance does not
_Nano protocol allows changing the representative at the same time as a balance
-change. `libnemo` does not implement this for purposes of clarity; all
-ChangeBlock objects will maintain the same Nano balance._
+change. `libnemo` does not implement this for purposes of clarity; all change
+block objects will maintain the same Nano balance._
Always fetch the most up to date information for the account from the network
using the
[account_info RPC command](https://docs.nano.org/commands/rpc-protocol/#account_info)
-which can then be used to populate the block parameters.
+which can then be used to populate the block parameters. This can be done on a
+per-account basis with the `account.refresh()` method.
Blocks require a small proof-of-work that must be calculated for the block to be
accepted by the network. This can be provided when creating the block, generated
-with the `block.pow()` method, or a requested from a public node that allows the
+locally with the `block.pow()` method, or requested from a public node that
+allows the
[work_generate RPC command](https://docs.nano.org/commands/rpc-protocol/#work_generate).
Finally, the block must be signed with the private key of the account. `libnemo`
-accounts can sign blocks offline if desired. After being signed, the block can
-be published to the network with the
+wallets, accounts, and blocks can all create signatures, event offline if
+desired. After being signed, the block can be published to the network with the
+`block.process()` method or by separately calling out to the
[process RPC command](https://docs.nano.org/commands/rpc-protocol/#process).
#### Creating blocks
```javascript
-import { SendBlock, ReceiveBlock, ChangeBlock } from 'libnemo'
+import { Block } from 'libnemo'
-const send = new SendBlock(
+const sendBlock = new Block(
'nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d', // sender
'5618869000000000000000000000000', // current balance
- 'nano_3phqgrqbso99xojkb1bijmfryo7dy1k38ep1o3k3yrhb7rqu1h1k47yu78gz', // recipient
- '2000000000000000000000000000000', // amount to send
- 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou', // representative
'92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', // hash of previous block
- 'fbffed7c73b61367' // PoW nonce (optional at first but required to process)
+ 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou' // representative
+)
+sendBlock.send(
+ 'nano_3phqgrqbso99xojkb1bijmfryo7dy1k38ep1o3k3yrhb7rqu1h1k47yu78gz', // recipient
+ '2000000000000000000000000000000' // amount to send
+)
+await sendBlock.pow(
+ 'fbffed7c73b61367' // PoW nonce (argument is optional)
+)
+await sendBlock.sign(wallet, accountIndex) // signature added to block
+await sendBlock.process(
+ 'https://nano-node.example.com' // must be online
)
-const receive = new ReceiveBlock(
+const receiveBlock = await new Block(
'nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d', // recipient
'18618869000000000000000000000000', // current balance
+ '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', // hash of previous block
+ 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou' // representative
+)
+.receive( // methods can be chained
'CBC911F57B6827649423C92C88C0C56637A4274FF019E77E24D61D12B5338783', // origin (hash of matching send block)
'7000000000000000000000000000000', // amount that was sent
- 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou', // representative
- '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', // hash of previous block
- 'c5cf86de24b24419' // PoW nonce (optional at first but required to process)
)
+.pow(
+ 'c5cf86de24b24419' // PoW nonce (synchronous if value provided)
+)
+.sign(wallet, accountIndex, frontier) // frontier may be necessary when using Ledger devices
-const change = new ChangeBlock(
+const changeBlock = await new Block(
'nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d', // account redelegating vote weight
'3000000000000000000000000000000', // current balance
- 'nano_1anrzcuwe64rwxzcco8dkhpyxpi8kd7zsjc1oeimpc3ppca4mrjtwnqposrs', // new representative
- '128106287002E595F479ACD615C818117FCB3860EC112670557A2467386249D4', // hash of previous block
- '0000000000000000' // PoW nonce (optional at first but required to process)
+ '128106287002E595F479ACD615C818117FCB3860EC112670557A2467386249D4' // hash of previous block
)
+.change(
+ 'nano_1anrzcuwe64rwxzcco8dkhpyxpi8kd7zsjc1oeimpc3ppca4mrjtwnqposrs' // new representative
+)
+sign(
+ '1495F2D49159CC2EAAAA97EBB42346418E1268AFF16D7FCA90E6BAD6D0965520' // sign blocks with literal private keys too
+)
+.pow() // async if calculating locally
```
#### Signing a block with a wallet
```javascript
-const wallet = await Bip44Wallet.create('password123')
+const wallet = await Wallet.create('BIP-44', 'password123')
await wallet.unlock('password123')
try {
await wallet.sign(0, block)
} catch (err) {
- console.log(err)
-}
-```
-
-#### Signing a block with a detached account
-
-```javascript
-const account = await Account.import({privateKey: K, index: 0}, 'password123')
-try {
- await account.sign(block, 'password123')
-} catch (err) {
- console.log(err)
+ console.error(err)
}
```
try {
await block.sign(privateKey)
} catch (err) {
- console.log(err)
+ console.error(err)
}
```
try {
await block.pow()
} catch (err) {
- console.log(err)
+ console.error(err)
}
```
try {
await block.pow('https://nano-node.example.com/')
} catch (err) {
- console.log(err)
+ console.error(err)
}
```
try {
const hash = await block.process('https://nano-node.example.com/')
} catch (err) {
- console.log(err)
+ console.error(err)
}
```
#### Converting Nano denominations
Raw values are the native unit of exchange throughout libnemo and are
-represented by the primitive bigint data type. Other supported denominations
+represented by the primitive `bigint` data type. Other supported denominations
are as follows:
| Unit | Raw |
{
"name": "libnemo",
- "version": "0.2.1",
+ "version": "0.3.0",
"description": "Asynchronous, non-blocking Nano cryptocurrency integration toolkit.",
"keywords": [
"nemo",
* @param {string} [salt=''] - Used when generating the final seed\r
* @returns {Wallet} A newly instantiated Wallet\r
*/\r
- static async create (type: 'BIP-44' | 'BLAKE2b', password?: string, mnemonicSalt?: string): Promise<Wallet>\r
+ static async create (type: 'BIP-44' | 'BLAKE2b', password: string, mnemonicSalt?: string): Promise<Wallet>\r
static async create (type: WalletType, password?: string, mnemonicSalt?: string): Promise<Wallet> {\r
Wallet.#isInternal = true\r
const self = new this(type)\r
*/
export declare class Account {
#private
+ [key: string]: any
+ static get DB_NAME (): 'Account'
get address (): string
get index (): number | undefined
get publicKey (): string
get confirmed_balance (): bigint | undefined
- get confirmed_block_height (): number | undefined
+ get confirmed_height (): number | undefined
get confirmed_frontier (): string | undefined
get confirmed_frontier_block (): Block | undefined
get confirmed_receivable (): bigint | undefined
get confirmed_representative (): Account | undefined
get balance (): bigint | undefined
- get block_height (): number | undefined
+ get block_count (): number | undefined
get frontier (): string | undefined
get frontier_block (): Block | undefined
get open_block (): string | undefined
get representative_block (): string | undefined
get weight (): bigint | undefined
set confirmed_balance (v: bigint | number | string)
- set confirmed_block_height (v: number | undefined)
+ set confirmed_height (v: number | undefined)
set confirmed_frontier (v: string | undefined)
set confirmed_frontier_block (v: Block | undefined)
set confirmed_receivable (v: bigint | number | string)
set confirmed_representative (v: unknown)
set balance (v: bigint | number | string)
- set block_height (v: number | undefined)
+ set block_count (v: number | undefined)
set frontier (v: string | undefined)
set frontier_block (v: Block | undefined)
set open_block (v: string | undefined)
set weight (v: bigint | number | string)
private constructor ()
/**
- * Removes encrypted secrets in storage and releases variable references to
- * allow garbage collection.
+ * Releases variable references to allow garbage collection.
*/
destroy (): Promise<void>
/**
* @param {string} address - Nano address to validate
* @throws Error if address is undefined, not a string, or an invalid format
*/
- static validate (address: unknown): asserts address is string
+ static validate (address: string): asserts address is string
}
export declare class AccountList extends Object {
[index: number]: Account
- get length (): number;
+ get length (): number
[Symbol.iterator] (): Iterator<Account>
}
export declare class Bip39 {
#private
/**
+ * https://github.com/bitcoin/bips/blob/master/bip-0039/english.txt
+ */
+ static wordlist: readonly string[]
+ /**
* Derives a mnemonic phrase from source of entropy or seed.
*
* The entropy must be between 16-32 bytes (32-64 characters) to stay within
* the limit of 128-256 bits defined in BIP-39. Typically, wallets use the
* maximum entropy allowed.
*
- * @param {(string|ArrayBuffer|Uint8Array<ArrayBuffer>)} entropy - Cryptographically secure random value
+ * @param {(ArrayBuffer|Uint8Array<ArrayBuffer>)} entropy - Cryptographically secure random value
* @returns {string} Mnemonic phrase created using the BIP-39 wordlist
*/
- static fromEntropy (entropy: string | ArrayBuffer | Uint8Array<ArrayBuffer>): Promise<Bip39>
+ static fromEntropy (entropy: ArrayBuffer | Uint8Array<ArrayBuffer>): Promise<Bip39>
/**
* Imports and validates an existing mnemonic phrase.
*
* @param {Uint8Array} [personal] - (_optional_) Arbitrary user-specified value
*/
constructor (length: number, key?: Uint8Array<ArrayBuffer>, salt?: Uint8Array<ArrayBuffer>, personal?: Uint8Array<ArrayBuffer>)
- update (input: Uint8Array): Blake2b
+ update (input: ArrayBuffer | Uint8Array): Blake2b
digest (): Uint8Array<ArrayBuffer>
digest (out: 'hex'): string
digest (out: 'binary' | Uint8Array<ArrayBuffer>): Uint8Array<ArrayBuffer>
* saved under one nickname.
*/
export declare class Rolodex {
- #private
+ static get DB_NAME (): 'Rolodex'
/**
* Adds an address to the rolodex under a specific nickname.
*
address: string
message: string
}
+
/**
* Converts a decimal amount of nano from one unit divider to another.
*
*/
export declare class Wallet {
#private
- static DB_NAME: string
+ static get DB_NAME (): 'Wallet'
/**
* Retrieves all encrypted wallets from the database.
*
* @param {string} [salt=''] - Used when generating the final seed
* @returns {Wallet} A newly instantiated Wallet
*/
- static create (type: 'BIP-44' | 'BLAKE2b', password?: string, mnemonicSalt?: string): Promise<Wallet>
+ static create (type: 'BIP-44' | 'BLAKE2b', password: string, mnemonicSalt?: string): Promise<Wallet>
/**
* Imports an existing HD wallet by using an entropy value generated using a
* cryptographically strong pseudorandom number generator.NamedD
* calls. This wallet does not feature any seed nor mnemonic phrase as all
* private keys are held in the secure chip of the device. As such, the user
* is responsible for using Ledger technology to back up these pieces of data.
+*
+* https://github.com/roosmaa/ledger-app-nano/blob/master/doc/nano.md
*/
-export declare class Ledger extends Wallet {
+export declare class Ledger {
#private
static DynamicTransport: typeof TransportBLE | typeof TransportUSB | typeof TransportHID
+ static UsbVendorId: number
static SYMBOL: Symbol
/**
* Check which transport protocols are supported by the browser and return the
*/
static get isUnsupported (): boolean
/**
- * Creates a new Ledger hardware wallet communication layer by dynamically
- * importing the ledger.js service.
- *
- * @returns {Ledger} A wallet containing accounts and a Ledger device communication object
- */
- static create (): Promise<Ledger>
- /**
- * Overrides `import()` from the base Wallet class since Ledger secrets cannot
- * be extracted from the device.
- */
- static import (): Promise<Ledger>
- private constructor ()
- get status (): DeviceStatus
- /**
- * Gets the index and public key for an account from the Ledger device.
+ * Status of the Ledger device connection.
*
- * @param {number} index - Wallet index of the account
- * @returns Promise for the Account at the index specified
+ * DISCONNECTED | BUSY | LOCKED | CONNECTED
*/
- account (index: number): Promise<Account>
+ static get status (): DeviceStatus
/**
- * Retrieves accounts from a Ledger wallet using its internal secure software.
- * Defaults to the first account at index 0.
- *
- * The returned object will have keys corresponding with the requested range
- * of account indexes. The value of each key will be the Account derived for
- * that index in the wallet.
- *
- * ```
- * const accounts = await wallet.accounts(0, 1))
- * // outputs the first and second account of the wallet
- * console.log(accounts)
- * // {
- * // 0: {
- * // address: <...>,
- * // publicKey: <...>,
- * // index: 0,
- * // <etc...>
- * // },
- * // 1: {
- * // address: <...>,
- * // publicKey: <...>,
- * // index: 1,
- * // <etc...>
- * // }
- * // }
- * // individual accounts can be referenced using array index notation
- * console.log(accounts[1])
- * // { address: <...>, publicKey: <...>, index: 1, <etc...> }
- * ```
+ * Request an account at a specific BIP-44 index.
*
- * If `from` is greater than `to`, their values will be swapped.
- * @param {number} from - Start index of accounts. Default: 0
- * @param {number} to - End index of accounts. Default: `from`
- * @returns {AccountList} Promise for a list of Accounts at the specified indexes
+ * @returns Response object containing command status, public key, and address
*/
- accounts (from?: number, to?: number): Promise<AccountList>
+ static account (index?: number, show?: boolean): Promise<LedgerAccountResponse>
/**
* Check if the Nano app is currently open and set device status accordingly.
*
* - LOCKED: Nano app is open but the device locked after a timeout
* - CONNECTED: Nano app is open and listening
*/
- connect (): Promise<DeviceStatus>
- /**
- * Removes encrypted secrets in storage and releases variable references to
- * allow garbage collection.
- */
- destroy (): Promise<void>
- /**
- * Revokes permission granted by the user to access the Ledger device.
- *
- * The 'quit app' ADPU command has not passed testing, so this is the only way
- * to ensure the connection is severed at this time. `setTimeout` is used to
- * expire any lingering transient user activation.
- *
- * Overrides the default wallet `lock()` method since as a hardware wallet it
- * does not need to be encrypted by software.
- */
- lock (): void
+ static connect (): Promise<DeviceStatus>
/**
* Sign a block with the Ledger device.
*
* @param {Block} block - Block data to sign
* @param {Block} [frontier] - Previous block data to cache in the device
*/
- sign (index: number, block: Block, frontier?: Block): Promise<void>
- /**
- * Attempts to connect to the Ledger device.
- *
- * Overrides the default wallet `unlock()` method since as a hardware wallet it
- * does not need to be encrypted by software.
- *
- * @returns True if successfully unlocked
- */
- unlock (): Promise<void>
+ static sign (index: number, block: Block, frontier?: Block): Promise<string>
/**
* Update cache from raw block data. Suitable for offline use.
*
* @param {number} index - Account number
* @param {object} block - JSON-formatted block data
*/
- updateCache (index: number, block: Block): Promise<LedgerResponse>
+ static updateCache (index: number, block: Block): Promise<LedgerResponse>
/**
* Update cache from a block hash by calling out to a node. Suitable for online
* use only.
* @param {string} hash - Hexadecimal block hash
* @param {Rpc} rpc - Rpc class object with a node URL
*/
- updateCache (index: number, hash: string, rpc: Rpc): Promise<LedgerResponse>
+ static updateCache (index: number, hash: string, rpc: Rpc): Promise<LedgerResponse>
/**
* Checks whether a given seed matches the wallet seed. The wallet must be
* unlocked prior to verification.
* @param {string} seed - Hexadecimal seed to be matched against the wallet data
* @returns True if input matches wallet seed
*/
- verify (seed: string): Promise<boolean>
+ static verify (seed: string): Promise<boolean>
/**
* Checks whether a given mnemonic phrase matches the wallet mnemonic. If a
* personal salt was used when generating the mnemonic, it cannot be verified.
* @param {string} mnemonic - Phrase to be matched against the wallet data
* @returns True if input matches wallet mnemonic
*/
- verify (mnemonic: string): Promise<boolean>
- /**
- * Get the version of the current process. If a specific app is running, get
- * the app version. Otherwise, get the Ledger BOLOS version instead.
- *
- * https://developers.ledger.com/docs/connectivity/ledgerJS/open-close-info-on-apps#get-information
- *
- * @returns Status, process name, and version
- */
- version (): Promise<LedgerVersionResponse>
+ static verify (mnemonic: string): Promise<boolean>
}