├── .eslintrc.js ├── .gitignore ├── LICENSE.md ├── README.md ├── package-lock.json ├── package.json ├── src └── index.ts └── test └── index.spec.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'env': { 3 | 'browser': true, 4 | 'es6': true, 5 | }, 6 | 'extends': [ 7 | 'eslint:recommended', 8 | 'airbnb-base', 9 | 'plugin:@typescript-eslint/eslint-recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | ], 12 | 'parser': '@typescript-eslint/parser', 13 | 'parserOptions': { 14 | 'ecmaVersion': 2018, 15 | 'sourceType': 'module', 16 | }, 17 | 'plugins': ['@typescript-eslint'], 18 | 'rules': { 19 | 'arrow-body-style': 0, 20 | 'arrow-parens': ['warn', 'always'], 21 | 'eol-last': ['error', 'always'], 22 | 'indent': ['error', 'tab', {'SwitchCase': 1}], 23 | 'no-confusing-arrow': ['error', {'allowParens': true}], 24 | 'no-new': 0, 25 | 'no-param-reassign': ['error', {'props': false}], 26 | 'no-tabs': 0, 27 | 'no-unused-expressions': [2, {'allowTernary': true}], 28 | 'operator-assignment': 0, 29 | 'prefer-destructuring': 0, 30 | 'quote-props': ['warn', 'consistent'], 31 | }, 32 | 'settings': { 33 | 'import/resolver': { 34 | 'node': { 35 | 'extensions': ['.js', '.ts'], 36 | }, 37 | }, 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .rts2_cache_* 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Alec Rios 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nano Address Validator 2 | 3 | Nano Address Validator is a thoroughly-tested library for validating addresses of the [Nano](https://nano.org/en) cryptocurrency. Its process consists of not only syntax analysis but also checksum verification. It can even validate addresses of Nano forks, such as [Banano](https://banano.cc/), by accepting any number of allowed prefixes. 4 | 5 | ## Address Specifications 6 | 7 | ``` 8 | nano_3t6k35gi95xu6tergt6p69ck76ogmitsa8mnijtpxm9fkcm736xtoncuohr3 9 | └─┬─┘└────────────────────────┬─────────────────────────┘└──┬───┘ 10 | A B C 11 | ``` 12 | 13 | **A. Prefix** - An address must begin with either `nano_` (modern prefix) or `xrb_` (legacy prefix). 14 | 15 | Because Nano was originally named RaiBlocks, the prefix `xrb_` was used (the `x` denoting a non-national currency, per the ISO 4217 currency code standard). After rebranding, the `nano_` prefix was introduced. As of Nano Node v19, the legacy prefix is deprecated, though it will continue to be supported. 16 | 17 | **B. Public Key** - An address must contain a 52-character encoded public key, which begins with either `1` or `3`. 18 | 19 | A raw address is a 256-bit unsigned integer in hexadecimal format. This is translated into a 260-bit number (padded at the start with four zeros) and encoded into a human-friendly string using a special base32 algorithm. This algorithm divides the 260-bit number into 52 5-bit segments and maps each segment to a character in an alphabet (`13456789abcdefghijkmnopqrstuwxyz`) that omits characters easily confused for others (`02lv`). Because the first segment is padded with zeros, its pattern is either `00000` (`1`) or `00001` (`3`). Thus, the encoded public key always begins with one of those characters. 20 | 21 | **C. Checksum** - An address must contain an 8-character encoded checksum of the public key. 22 | 23 | The address contains a checksum of the public key in order to prevent typographical errors. A hash is generated from the unencoded public key using Blake2b with an 8-bit digest, which is then encoded using the same base32 algorithm as the public key and appended to the address. Thus, the final 8 characters of an address must match the derived checksum of the public key. 24 | 25 | ## Validation Process 26 | 27 | The validation process consists of two major operations: 28 | 29 | 1. Verifying that the address is syntactically correct as far as prefix, structure, length, and alphabet. 30 | 2. Deriving the checksum of the public key and verifying that it matches the checksum provided within the address. 31 | 32 | ## Installation 33 | 34 | ``` 35 | npm install nano-address-validator 36 | ``` 37 | 38 | ## API 39 | 40 | ``` ts 41 | /** 42 | * Checks whether a Nano address is valid. 43 | * 44 | * @param {string} address The address to check. 45 | * @param {string | string[]} [prefix = ['nano', 'xrb']] The allowed prefix(es). 46 | * 47 | * @throws {Error} Address must be defined. 48 | * @throws {TypeError} Address must be a string. 49 | * @throws {TypeError} Prefix must be a string or an array of strings. 50 | * 51 | * @returns {boolean} Whether the address is valid. 52 | */ 53 | export default function (address: string, prefix?: string | string[]): boolean; 54 | ``` 55 | 56 | ## Examples 57 | 58 | ``` js 59 | import isValid from 'nano-address-validator'; 60 | 61 | const nanoAddress = 'nano_3t6k35gi95xu6tergt6p69ck76ogmitsa8mnijtpxm9fkcm736xtoncuohr3'; 62 | const bananoAddress = 'ban_1bananobh5rat99qfgt1ptpieie5swmoth87thi74qgbfrij7dcgjiij94xr'; 63 | 64 | // Validate a Nano address. 65 | isValid(nanoAddress); // true 66 | 67 | // Validate a Banano address. 68 | isValid(bananoAddress, 'ban'); // true 69 | 70 | // Validate Nano and Banano addresses. 71 | isValid(nanoAddress, ['ban', 'nano', 'xrb']); // true 72 | isValid(bananoAddress, ['ban', 'nano', 'xrb']); // true 73 | ``` 74 | 75 | ## See Also 76 | 77 | - [Nano Unit Converter](https://github.com/alecrios/nano-unit-converter) - Converts Nano amounts from one unit to another. 78 | - [Nano URI Generator](https://github.com/alecrios/nano-uri-generator) - Generates Nano URIs for sending amounts, changing representatives, and more. 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nano-address-validator", 3 | "version": "3.0.0", 4 | "description": "Validates Nano addresses for syntax and checksum integrity", 5 | "source": "src/index.ts", 6 | "types": "dist/index.d.ts", 7 | "main": "dist/nano-address-validator.js", 8 | "umd:main": "dist/nano-address-validator.umd.js", 9 | "module": "dist/nano-address-validator.m.js", 10 | "files": [ 11 | "src", 12 | "dist" 13 | ], 14 | "scripts": { 15 | "test": "mocha", 16 | "lint": "eslint src --ext .ts", 17 | "build": "microbundle", 18 | "dev": "microbundle watch" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/alecrios/nano-address-validator.git" 23 | }, 24 | "keywords": [ 25 | "nano", 26 | "address", 27 | "validator", 28 | "validation", 29 | "cryptocurrency" 30 | ], 31 | "author": "Alec Rios", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/alecrios/nano-address-validator/issues" 35 | }, 36 | "homepage": "https://github.com/alecrios/nano-address-validator", 37 | "devDependencies": { 38 | "@typescript-eslint/eslint-plugin": "^2.3.3", 39 | "@typescript-eslint/parser": "^2.3.3", 40 | "chai": "^4.1.2", 41 | "eslint": "^5.0.1", 42 | "eslint-config-airbnb-base": "^13.2.0", 43 | "eslint-plugin-import": "^2.18.2", 44 | "microbundle": "^0.11.0", 45 | "mocha": "^4.0.1", 46 | "typescript": "^3.6.3" 47 | }, 48 | "dependencies": { 49 | "blakejs": "^1.1.0", 50 | "nano-base32": "^1.0.1" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import blake from 'blakejs'; 2 | import nanoBase32 from 'nano-base32'; 3 | 4 | /** 5 | * Checks whether a Nano address is valid. 6 | * 7 | * @param {string} address The address to check. 8 | * @param {string | string[]} [prefix = ['nano', 'xrb']] The allowed prefix(es). 9 | * 10 | * @throws {Error} Address must be defined. 11 | * @throws {TypeError} Address must be a string. 12 | * @throws {TypeError} Prefix must be a string or an array of strings. 13 | * 14 | * @returns {boolean} Whether the address is valid. 15 | */ 16 | export default function (address: string, prefix: string | string[] = ['nano', 'xrb']): boolean { 17 | // Ensure the address is provided. 18 | if (address === undefined) { 19 | throw Error('Address must be defined.'); 20 | } 21 | 22 | // Ensure the address is a string. 23 | if (typeof address !== 'string') { 24 | throw TypeError('Address must be a string.'); 25 | } 26 | 27 | /** The array of allowed prefixes. */ 28 | let allowedPrefixes: string[]; 29 | 30 | // Ensure the prefix(es) is/are valid. 31 | if (Array.isArray(prefix)) { 32 | if (prefix.some((currentPrefix) => typeof currentPrefix !== 'string')) { 33 | throw TypeError('Prefix must be a string or an array of strings.'); 34 | } 35 | 36 | allowedPrefixes = prefix; 37 | } else if (typeof prefix === 'string') { 38 | allowedPrefixes = [prefix]; 39 | } else { 40 | throw TypeError('Prefix must be a string or an array of strings.'); 41 | } 42 | 43 | /** The regex pattern for validating the address. */ 44 | const pattern = new RegExp( 45 | `^(${allowedPrefixes.join('|')})_[13]{1}[13456789abcdefghijkmnopqrstuwxyz]{59}$`, 46 | ); 47 | 48 | // Validate the syntax of the address. 49 | if (!pattern.test(address)) return false; 50 | 51 | /** The expected checksum as a base32-encoded string. */ 52 | const expectedChecksum = address.slice(-8); 53 | 54 | /** The public key as a base32-encoded string. */ 55 | const publicKey = address.slice(address.indexOf('_') + 1, -8); 56 | 57 | /** The public key as an array buffer. */ 58 | const publicKeyBuffer = nanoBase32.decode(publicKey); 59 | 60 | /** The actual checksum as an array buffer. */ 61 | const actualChecksumBuffer = blake.blake2b(publicKeyBuffer, null, 5).reverse(); 62 | 63 | /** The actual checksum as a base32-encoded string. */ 64 | const actualChecksum = nanoBase32.encode(actualChecksumBuffer); 65 | 66 | // Validate the provided checksum against the derived checksum. 67 | return expectedChecksum === actualChecksum; 68 | } 69 | -------------------------------------------------------------------------------- /test/index.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const isValid = require('../dist/index'); 3 | 4 | const validAddresses = [ 5 | 'nano_14cuejfpr58epnpxenirusimsrbwxbecin7a3izq1injptecc31qsjwquoe6', 6 | 'nano_3jwrszth46rk1mu7rmb4rhm54us8yg1gw3ipodftqtikf5yqdyr7471nsg1k', 7 | 'nano_3uip1jmeo4irjuua9xiyosq6fkgogwd6bf5uqopb1m6mfq6g3n8cna6h3tuk', 8 | 'nano_1ipx847tk8o46pwxt5qjdbncjqcbwcc1rrmqnkztrfjy5k7z4imsrata9est', 9 | 'nano_19773kh38t6dtqsseq8oa5jk8o7ycas3t77d1xznoktdipcbofkzuhcjzeba', 10 | 'nano_3ing74j39b544e9w4yrur9fzuwges71ddo83ahgskzhzaa3ytzr7ra3jfsgi', 11 | 'nano_3wtbdcjrgro1cc87bnnia6ht1jqu96q9km6qttrza7ioxbxr7yqxzf1psugd', 12 | 'nano_1m1e8m7ryf4zhx4xk9tegfxt8g5mpertjwfowtjanpjji1ttnkseunagbspj', 13 | 'nano_3hua6a116y4jmbeaf63zi6mn8gf5s4n3eyxa3q4m5ibabi6pmegfubky3wpi', 14 | 'nano_1azif31ho333hnfb39acaua7jcgha4cjio4n5rc543jain37j8n7dqi6g8jo', 15 | 'nano_3dx4o17y4xcg5aeo1h7an5z8onk139hhs9oa37xkb1uzqnxfzphzpfjwyw4x', 16 | 'nano_1b7t1fxn8uuj51dsxfi6mzp7mb3m9pocyznqzh9zfb1etzkdufgpdinq553q', 17 | 'nano_38zpmsje8de6tgkan8yf3t86e31444qkznxyah6zqtqckex1nec97wo94xc9', 18 | 'nano_36gyiognuibzsnsuqrntnpqa51xic5iickdeggcaugb79uh3nmrdtf7gg4t6', 19 | 'nano_1h31pb3b4puzuy9ijc4yucce73gewd3bm3zpugiq46sbwsnzw8cge6hxnfzb', 20 | 'nano_19so3616wepcwjj868z7pfnsciegjyp68omt3c13qoepnccskgyjayjksyrc', 21 | 'nano_1niabkx3gbxit5j5yyqcpas71dkffggbr6zpd3heui8rpoocm5xqbdwq44oh', 22 | 'nano_3re5wi4pjkpdjs9z1dn7oxitcapimkoyp1b1bqh1tj4e1hbpcj6h41zz8biz', 23 | 'nano_3iirom9oxgqeu6xmsun9rq8b4ybb783mpk4aqdsojo9zi66yi1y6kqw1qmt3', 24 | 'nano_36xysg5opeeaowmeazjd9oy6xtrexy1cf65jw77i5kt83do94twe918ag6dp', 25 | 'nano_3k5grtxarxfooa9xf5qhaspaeixw939aaab6z391yzpyo8tgoh8x9kko3bfm', 26 | 'nano_35syfzh8yx5zaypmoumtfe6pe9n5d9fo1f5b8in3ks3kcby8bwkm754gi31j', 27 | 'nano_34m3ts1rpirfubibcbmj1ds8kjqdi7ypihirqw4fnqxthh6er33m7p4y93zx', 28 | 'nano_3winfya6ngotsxndyyf4o1ue57ff1rmtwiz81w6qez6hxfy4fgm5ybmhjzrt', 29 | 'nano_3eeio87pksrgndzcrdbasnhmmoyyqikemndrc8au8ygyt9xjp3rdjcyh6ia1', 30 | ]; 31 | 32 | describe('isValid', () => { 33 | it('should throw error if address is not provided', () => { 34 | expect(() => isValid()).to.throw(Error); 35 | }); 36 | 37 | it('should throw error if address is not a string', () => { 38 | expect(() => isValid(123)).to.throw(Error); 39 | }); 40 | 41 | it('should throw error for invalid allowed prefixes', () => { 42 | expect(() => isValid(validAddresses[0], null)).to.throw(TypeError); 43 | expect(() => isValid(validAddresses[0], 123)).to.throw(TypeError); 44 | expect(() => isValid(validAddresses[0], ['a', 1, {}])).to.throw(TypeError); 45 | }); 46 | 47 | it('should return false for invalid prefixes', () => { 48 | const addresses = [ 49 | '_3t6k35gi95xu6tergt6p69ck76ogmitsa8mnijtpxm9fkcm736xtoncuohr3', 50 | 'foo_3t6k35gi95xu6tergt6p69ck76ogmitsa8mnijtpxm9fkcm736xtoncuohr3', 51 | 'NANO_3t6k35gi95xu6tergt6p69ck76ogmitsa8mnijtpxm9fkcm736xtoncuohr3', 52 | ]; 53 | 54 | addresses.forEach((address) => { 55 | expect(isValid(address)).to.equal(false); 56 | }); 57 | }); 58 | 59 | it('should return false for invalid characters', () => { 60 | const addresses = [ 61 | 'xrb_3uip1jmeo4irjuua9xiyosq6fkgogwd6bf5uqopb1m6mfq6g3n8cna6h3tu0', 62 | 'xrb_3uip1jmeo4irjuua9xiyosq6fkgogwd6bf5uqopb1m6mfq6g3n8cna6h3tu2', 63 | 'xrb_3uip1jmeo4irjuua9xiyosq6fkgogwd6bf5uqopb1m6mfq6g3n8cna6h3tul', 64 | 'xrb_3uip1jmeo4irjuua9xiyosq6fkgogwd6bf5uqopb1m6mfq6g3n8cna6h3tuv', 65 | ]; 66 | 67 | addresses.forEach((address) => { 68 | expect(isValid(address)).to.equal(false); 69 | }); 70 | }); 71 | 72 | it('should return false for invalid casing', () => { 73 | const addresses = [ 74 | 'XRB_14CUEJFPR58EPNPXENIRUSIMSRBWXBECIN7A3IZQ1INJPTECC31QSJWQUOE6', 75 | 'XRB_1NIABKX3GBXIT5J5YYQCPAS71DKFFGGBR6ZPD3HEUI8RPOOCM5XQBDWQ44OH', 76 | 'XRB_1H31PB3B4PUZUY9IJC4YUCCE73GEWD3BM3ZPUGIQ46SBWSNZW8CGE6HXNFZB', 77 | ]; 78 | 79 | addresses.forEach((address) => { 80 | expect(isValid(address)).to.equal(false); 81 | }); 82 | }); 83 | 84 | it('should return false for public keys not starting with 1 or 3', () => { 85 | const addresses = [ 86 | 'xrb_03ezf4od79h1tgj9aiu4djzcmmguendtjfuhwfukhuucboua8cpoihmh8byo', 87 | 'xrb_23ezf4od79h1tgj9aiu4djzcmmguendtjfuhwfukhuucboua8cpoihmh8byo', 88 | 'xrb_43ezf4od79h1tgj9aiu4djzcmmguendtjfuhwfukhuucboua8cpoihmh8byo', 89 | ]; 90 | 91 | addresses.forEach((address) => { 92 | expect(isValid(address)).to.equal(false); 93 | }); 94 | }); 95 | 96 | it('should return false for lack of an underscore', () => { 97 | const addresses = [ 98 | 'xrb35jjmmmh81kydepzeuf9oec8hzkay7msr6yxagzxpcht7thwa5bus5tomgz9', 99 | 'xrb1ipx847tk8o46pwxt5qjdbncjqcbwcc1rrmqnkztrfjy5k7z4imsrata9est', 100 | 'xrb3wm37qz19zhei7nzscjcopbrbnnachs4p1gnwo5oroi3qonw6inwgoeuufdp', 101 | ]; 102 | 103 | addresses.forEach((address) => { 104 | expect(isValid(address)).to.equal(false); 105 | }); 106 | }); 107 | 108 | it('should return false for improper length', () => { 109 | const addresses = [ 110 | 'xrb_11111111111111111111111111111111111111111111', 111 | 'xrb_1111111111111111111111111111111111111111111111111111', 112 | 'xrb_111111111111111111111111111111111111111111111111111111111111hifc8npp', 113 | ]; 114 | 115 | addresses.forEach((address) => { 116 | expect(isValid(address)).to.equal(false); 117 | }); 118 | }); 119 | 120 | it('should return false for whitespace', () => { 121 | const addresses = [ 122 | ' nano_3jwrszth46rk1mu7rmb4rhm54us8yg1gw3ipodftqtikf5yqdyr7471nsg1k', 123 | 'nano_3jwrszth46rk1mu7rmb4rhm54us8yg1gw3ipodftqtikf5yqdyr7471nsg1k ', 124 | ' nano_3jwrszth46rk1mu7rmb4rhm54us8yg1gw3ipodftqtikf5yqdyr7471nsg1k', 125 | ' nano_3jwrszth46rk1mu7rmb4rhm54us8yg1gw3ipodftqtikf5yqdyr7471nsg1k ', 126 | 'nano_ 3jwrszth46rk1mu7rmb4rhm54us8yg1gw3ipodftqtikf5yqdyr7471nsg1k', 127 | 'nano _3jwrszth46rk1mu7rmb4rhm54us8yg1gw3ipodftqtikf5yqdyr7471nsg1k', 128 | ]; 129 | 130 | addresses.forEach((address) => { 131 | expect(isValid(address)).to.equal(false); 132 | }); 133 | }); 134 | 135 | it('should return true for syntactically valid addresses with good checksums', () => { 136 | validAddresses.forEach((address) => { 137 | expect(isValid(address)).to.equal(true); 138 | }); 139 | }); 140 | 141 | it('should return false for syntactically valid addresses with bad checksums', () => { 142 | validAddresses.forEach((address) => { 143 | // Change the last character to invalidate the checksum. 144 | const addressWithBadChecksum = address.slice(-1) !== 'a' 145 | ? address.replace(/.$/, 'a') 146 | : address.replace(/.$/, 'b'); 147 | 148 | expect(isValid(addressWithBadChecksum)).to.equal(false); 149 | }); 150 | }); 151 | 152 | it('should return true for valid addresses with alternate valid prefixes', () => { 153 | validAddresses.forEach((address) => { 154 | // Change the last prefix. 155 | const addressWithAlternatePrefix = address.replace(/nano_/, 'ban_'); 156 | 157 | expect(isValid(addressWithAlternatePrefix, 'ban')).to.equal(true); 158 | }); 159 | }); 160 | }); 161 | --------------------------------------------------------------------------------