// Your current balance in RAW
walletBalanceRaw: '18618869000000000000000000000000',
- // From the pending transaction
- fromAddress: 'nano_1e5aqegc1jb7qe964u4adzmcezyo6o146zb8hm6dft8tkp79za3sxwjym5rx',
-
// Your address
toAddress: 'nano_3kyb49tqpt39ekc49kbej51ecsjqnimnzw1swxz4boix4ctm93w517umuiw8',
// From wallet info
frontier: '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D',
+ // From the pending transaction
+ transactionHash: 'CBC911F57B6827649423C92C88C0C56637A4274FF019E77E24D61D12B5338783',
+
// From the pending transaction in RAW
amountRaw: '7000000000000000000000000000000',
- // Generate the work server-side or a DPOW service
+ // Generate the work server-side or with a DPOW service
work: 'c5cf86de24b24419',
}
// The amount to send in RAW
amountRaw: '2000000000000000000000000000000',
- // Generate work on server-side or a DPOW service
+ // Generate work on server-side or with a DPOW service
work: 'fbffed7c73b61367',
}
// Previous block, from account info
frontier: '128106287002E595F479ACD615C818117FCB3860EC112670557A2467386249D4',
- // Generate work on the server side or a DPOW service
+ // Generate work on the server side or with a DPOW service
work: '0000000000000000',
}
### In web
```
-<script src="https://unpkg.com/nanocurrency-web@1.0.3" type="text/javascript"></script>
+<script src="https://unpkg.com/nanocurrency-web@1.0.5" type="text/javascript"></script>
<scrypt type="text/javascript">
NanocurrencyWeb.wallet.generate(...);
</script>
import AddressGenerator from './lib/address-generator'
import AddressImporter, { Account, Wallet } from './lib/address-importer'
-import BlockSigner, { TransactionBlock, RepresentativeBlock, SignedBlock } from './lib/block-signer'
+import BlockSigner, { SendBlock, ReceiveBlock, RepresentativeBlock, SignedBlock } from './lib/block-signer'
import BigNumber from 'bignumber.js'
import NanoConverter from './lib/nano-converter'
/**
* Sign a send block with the input parameters
*
- * For a receive block, put your own address to the 'toAddress' property.
+ * For a send block, put your own address to the 'fromAddress' property and
+ * the recipient address to the 'toAddress' property.
* All the NANO amounts should be input in RAW format. The addresses should be
* valid Nano addresses. Fetch the current balance, frontier (previous block) and
* representative address from the blockchain and generate work for the signature.
* @param {SendBlock} data The data for the block
* @param {string} privateKey Private key to sign the block
*/
- sign: (data: TransactionBlock, privateKey: string): SignedBlock => {
- return blockSigner.sign(data, privateKey)
+ send: (data: SendBlock, privateKey: string): SignedBlock => {
+ return blockSigner.send(data, privateKey)
},
+
+ /**
+ * Sign a receive block with the input parameters
+ *
+ * For a receive block, put your own address to the 'toAddress' property.
+ * All the NANO amounts should be input in RAW format. The addresses should be
+ * valid Nano addresses. Fetch the current balance, frontier (previous block) and
+ * representative address from the blockchain and generate work for the signature.
+ * Input the receive amount and transaction hash from the pending block.
+ *
+ * The return value of this function is ready to be published to the blockchain.
+ *
+ * NOTICE: Always fetch up-to-date account info from the blockchain
+ * before signing the block
+ *
+ * @param {ReceiveBlock} data The data for the block
+ * @param {string} privateKey Private key to sign the block
+ */
+ receive: (data: ReceiveBlock, privateKey: string): SignedBlock => {
+ return blockSigner.receive(data, privateKey)
+ },
+
+
/**
* Sign a representative change block with the input parameters
*
*
*/
representative: (data: RepresentativeBlock, privateKey: string): SignedBlock => {
- const block: TransactionBlock = {
+ const block: SendBlock = {
...data,
fromAddress: data.address,
amountRaw: '0',
- toAddress: 'nano_1111111111111111111111111111111111111111111111111111hifc8npp' // Burn address
+ toAddress: 'nano_1111111111111111111111111111111111111111111111111111hifc8npp', // Burn address
}
- return blockSigner.sign(block, privateKey)
+ return blockSigner.send(block, privateKey)
},
}
import BigNumber from 'bignumber.js'
-import Convert from './util/convert'
import Ed25519 from './ed25519'
+import Convert from './util/convert'
import NanoAddress from './nano-address'
import NanoConverter from './nano-converter'
preamble = 0x6.toString().padStart(64, '0')
- sign(data: TransactionBlock, privateKey: string): SignedBlock {
+ receive(data: ReceiveBlock, privateKey: string): SignedBlock {
+ const validateInputRaw = (input: string) => !!input && !isNaN(+input)
+ if (!validateInputRaw(data.walletBalanceRaw)) {
+ throw new Error('Invalid format in wallet balance')
+ }
+
+ if (!validateInputRaw(data.amountRaw)) {
+ throw new Error('Invalid format in send amount')
+ }
+
+ if (!this.nanoAddress.validateNanoAddress(data.toAddress)) {
+ throw new Error('Invalid toAddress')
+ }
+
+ if (!this.nanoAddress.validateNanoAddress(data.representativeAddress)) {
+ throw new Error('Invalid representativeAddress')
+ }
+
+ if (!data.transactionHash) {
+ throw new Error('No transaction hash')
+ }
+
+ if (!data.frontier) {
+ throw new Error('No frontier')
+ }
+
+ if (!data.work) {
+ throw new Error('No work')
+ }
+
+ if (!privateKey) {
+ throw new Error('Please input the private key to sign the block')
+ }
+
+ const balanceNano = NanoConverter.convert(data.walletBalanceRaw, 'RAW', 'NANO')
+ const amountNano = NanoConverter.convert(data.amountRaw, 'RAW', 'NANO')
+ const newBalanceNano = new BigNumber(balanceNano).plus(new BigNumber(amountNano))
+ const newBalanceRaw = NanoConverter.convert(newBalanceNano, 'NANO', 'RAW')
+ const newBalanceHex = Convert.dec2hex(newBalanceRaw, 16).toUpperCase()
+ const account = this.nanoAddressToHexString(data.toAddress)
+ const link = data.transactionHash
+ const representative = this.nanoAddressToHexString(data.representativeAddress)
+
+ const signatureBytes = this.ed25519.sign(
+ this.generateHash(this.preamble, account, data.frontier, representative, newBalanceHex, link),
+ Convert.hex2ab(privateKey))
+
+ return {
+ type: 'state',
+ account: data.toAddress,
+ previous: data.frontier,
+ representative: data.representativeAddress,
+ balance: newBalanceRaw,
+ link: link,
+ signature: Convert.ab2hex(signatureBytes),
+ work: data.work,
+ }
+ }
+
+ send(data: SendBlock, privateKey: string): SignedBlock {
+ const validateInputRaw = (input: string) => !!input && !isNaN(+input)
+ if (!validateInputRaw(data.walletBalanceRaw)) {
+ throw new Error('Invalid format in wallet balance')
+ }
+
+ if (!validateInputRaw(data.amountRaw)) {
+ throw new Error('Invalid format in send amount')
+ }
+
+ if (!this.nanoAddress.validateNanoAddress(data.toAddress)) {
+ throw new Error('Invalid toAddress')
+ }
+
+ if (!this.nanoAddress.validateNanoAddress(data.fromAddress)) {
+ throw new Error('Invalid fromAddress')
+ }
+
+ if (!this.nanoAddress.validateNanoAddress(data.representativeAddress)) {
+ throw new Error('Invalid representativeAddress')
+ }
+
+ if (!data.frontier) {
+ throw new Error('No frontier')
+ }
+
+ if (!data.work) {
+ throw new Error('No work')
+ }
+
if (!privateKey) {
throw new Error('Please input the private key to sign the block')
}
- const balance = NanoConverter.convert(data.walletBalanceRaw, 'RAW', 'NANO')
- const amount = NanoConverter.convert(data.amountRaw, 'RAW', 'NANO')
- const newBalance = new BigNumber(balance).minus(new BigNumber(amount))
- const rawBalance = NanoConverter.convert(newBalance, 'NANO', 'RAW')
- const hexBalance = Convert.dec2hex(rawBalance, 16).toUpperCase()
+ const balanceNano = NanoConverter.convert(data.walletBalanceRaw, 'RAW', 'NANO')
+ const amountNano = NanoConverter.convert(data.amountRaw, 'RAW', 'NANO')
+ const newBalanceNano = new BigNumber(balanceNano).minus(new BigNumber(amountNano))
+ const newBalanceRaw = NanoConverter.convert(newBalanceNano, 'NANO', 'RAW')
+ const newBalanceHex = Convert.dec2hex(newBalanceRaw, 16).toUpperCase()
const account = this.nanoAddressToHexString(data.fromAddress)
const link = this.nanoAddressToHexString(data.toAddress)
const representative = this.nanoAddressToHexString(data.representativeAddress)
const signatureBytes = this.ed25519.sign(
- this.generateHash(this.preamble, account, data.frontier, representative, hexBalance, link),
+ this.generateHash(this.preamble, account, data.frontier, representative, newBalanceHex, link),
Convert.hex2ab(privateKey))
return {
account: data.fromAddress,
previous: data.frontier,
representative: data.representativeAddress,
- balance: rawBalance,
- link,
+ balance: newBalanceRaw,
+ link: link,
signature: Convert.ab2hex(signatureBytes),
work: data.work,
}
}
-export interface TransactionBlock {
+export interface ReceiveBlock {
+ walletBalanceRaw: string
+ toAddress: string
+ transactionHash: string
+ frontier: string
+ representativeAddress: string
+ amountRaw: string
+ work: string
+}
+
+export interface SendBlock {
walletBalanceRaw: string
fromAddress: string
toAddress: string
return output
}
+ /**
+ * Validates a Nano address with 'nano' and 'xrb' prefixes
+ *
+ * Derived from https://github.com/alecrios/nano-address-validator
+ *
+ * @param {string} address Nano address
+ */
+ validateNanoAddress = (address: string): boolean => {
+ /** Ensure the address is provided */
+ if (address === undefined) {
+ throw Error('Address must be defined.')
+ }
+
+ /** Ensure the address is a string */
+ if (typeof address !== 'string') {
+ throw TypeError('Address must be a string.')
+ }
+
+ /** The array of allowed prefixes */
+ const allowedPrefixes: string[] = ['nano', 'xrb']
+
+ /** The regex pattern for validating the address */
+ const pattern = new RegExp(
+ `^(${allowedPrefixes.join('|')})_[13]{1}[13456789abcdefghijkmnopqrstuwxyz]{59}$`,
+ )
+
+ /** Validate the syntax of the address */
+ if (!pattern.test(address)) return false
+
+ /** The expected checksum as a base32-encoded string */
+ const expectedChecksum = address.slice(-8)
+
+ /** The public key as a base32-encoded string */
+ const publicKey = address.slice(address.indexOf('_') + 1, -8)
+
+ /** The public key as an array buffer */
+ const publicKeyBuffer = this.decodeNanoBase32(publicKey)
+
+ /** The actual checksum as an array buffer */
+ const actualChecksumBuffer = blake2b(publicKeyBuffer, null, 5).reverse()
+
+ /** The actual checksum as a base32-encoded string */
+ const actualChecksum = this.encodeNanoBase32(actualChecksumBuffer)
+
+ /** Validate the provided checksum against the derived checksum */
+ return expectedChecksum === actualChecksum
+ }
+
readChar(char: string): number {
const idx = this.alphabet.indexOf(char)
if (idx === -1) {
- throw `Invalid character found: ${char}`
+ throw new Error(`Invalid character found: ${char}`)
}
return idx
value = value.shiftedBy(24)
break
default:
- throw `Unkown input unit ${inputUnit}, expected one of the following: RAW, NANO, MRAI, KRAI, RAI`
+ throw new Error(`Unkown input unit ${inputUnit}, expected one of the following: RAW, NANO, MRAI, KRAI, RAI`)
}
switch (outputUnit) {
case 'RAI':
return value.shiftedBy(-24).toFixed(9, 1)
default:
- throw `Unknown output unit ${outputUnit}, expected one of the following: RAW, NANO, MRAI, KRAI, RAI`
+ throw new Error(`Unknown output unit ${outputUnit}, expected one of the following: RAW, NANO, MRAI, KRAI, RAI`)
}
}
{
"name": "nanocurrency-web",
- "version": "1.0.1",
+ "version": "1.0.5",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
{
"name": "nanocurrency-web",
- "version": "1.0.4",
+ "version": "1.0.5",
"description": "Toolkit for Nano cryptocurrency client side offline integrations",
"author": "Miro Metsänheimo <miro@metsanheimo.fi>",
"license": "MIT",
describe('block signing tests using official test vectors', () => {
it('should create a valid signature for a receive block', () => {
- const result = block.sign({
+ const result = block.receive({
walletBalanceRaw: '18618869000000000000000000000000',
- fromAddress: 'nano_1e5aqegc1jb7qe964u4adzmcezyo6o146zb8hm6dft8tkp79za3sxwjym5rx',
- toAddress: 'nano_3kyb49tqpt39ekc49kbej51ecsjqnimnzw1swxz4boix4ctm93w517umuiw8',
+ transactionHash: 'CBC911F57B6827649423C92C88C0C56637A4274FF019E77E24D61D12B5338783',
+ toAddress: 'nano_1e5aqegc1jb7qe964u4adzmcezyo6o146zb8hm6dft8tkp79za3sxwjym5rx',
representativeAddress: 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou',
frontier: '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D',
amountRaw: '7000000000000000000000000000000',
work: 'c5cf86de24b24419',
}, '781186FB9EF17DB6E3D1056550D9FAE5D5BBADA6A6BC370E4CBB938B1DC71DA3')
- expect(result.signature.toUpperCase()).to.equal('EEFFE1EFCCC8F2F6F2F1B79B80ABE855939DD9D6341323186494ADEE775DAADB3B6A6A07A85511F2185F6E739C4A54F1454436E22255A542ED879FD04FEED001')
+ expect(result.signature.toUpperCase()).to.equal('F25D751AD0379A5718E08F3773DA6061A9E18842EF5615163C7F207B804CC2C5DD2720CFCE5FE6A78E4CC108DD9CAB65051526403FA2C24A1ED943BB4EA7880B')
})
it('should create a valid signature for a send block', () => {
- const result = block.sign({
+ const result = block.send({
walletBalanceRaw: '5618869000000000000000000000000',
fromAddress: 'nano_1e5aqegc1jb7qe964u4adzmcezyo6o146zb8hm6dft8tkp79za3sxwjym5rx',
toAddress: 'nano_1q3hqecaw15cjt7thbtxu3pbzr1eihtzzpzxguoc37bj1wc5ffoh7w74gi6p',