diff --git a/README.md b/README.md index 4b3b78fd..c0b9c0b8 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,7 @@ npm install wallet-address-validator * Ripple/XRP, `'ripple'` or `'XRP'` * Snowgem/SNG, `'snowgem'` or `'SNG'` +* Stellar/XLM, `'stellar'` or `'XLM'` * Vertcoin/VTC, `'vertcoin'` or `'VTC'` diff --git a/package.json b/package.json index f5d35e7f..2682c063 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ }, "dependencies": { "base-x": "^3.0.4", + "crc": "^3.8.0", "jssha": "2.3.1" }, "devDependencies": { diff --git a/src/currencies.js b/src/currencies.js index 5a12f0e3..4fe17037 100644 --- a/src/currencies.js +++ b/src/currencies.js @@ -3,6 +3,7 @@ var ETHValidator = require('./ethereum_validator'); var BTCValidator = require('./bitcoin_validator'); var XMRValidator = require('./monero_validator'); var NANOValidator = require('./nano_validator'); +var XLMValidator = require('./stellar_validator'); // defines P2PKH and P2SH address types for standard (prod) and testnet networks var CURRENCIES = [{ @@ -209,6 +210,10 @@ var CURRENCIES = [{ name: 'raiblocks', symbol: 'xrb', validator: NANOValidator, +}, { + name: 'stellar', + symbol: 'xlm', + validator: XLMValidator, }]; diff --git a/src/stellar_validator.js b/src/stellar_validator.js new file mode 100644 index 00000000..f6af4dec --- /dev/null +++ b/src/stellar_validator.js @@ -0,0 +1,46 @@ +var baseX = require('base-x'); +var crc = require('crc'); +var cryptoUtils = require('./crypto/utils'); + +var ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; + +var base32 = baseX(ALPHABET); +var regexp = new RegExp('^[' + ALPHABET + ']{56}$'); +var ed25519PublicKeyVersionByte = (6 << 3); + +function swap16(number) { + var lower = number & 0xFF; + var upper = (number >> 8) & 0xFF; + return (lower << 8) | upper; +} + +function numberToHex(number) { + var hex = number.toString(16); + if(hex.length % 2 === 1) { + hex = '0' + hex; + } + return hex; +} + +module.exports = { + isValidAddress: function (address) { + if (regexp.test(address)) { + return this.verifyChecksum(address); + } + + return false; + }, + + verifyChecksum: function (address) { + // based on https://github.com/stellar/js-stellar-base/blob/master/src/strkey.js#L126 + var bytes = base32.decode(address); + if (bytes[0] !== ed25519PublicKeyVersionByte) { + return false; + } + + var computedChecksum = numberToHex(swap16(crc.crc16xmodem(bytes.slice(0, -2)))); + var checksum = cryptoUtils.toHex(bytes.slice(-2)); + + return computedChecksum === checksum + } +}; diff --git a/test/wallet_address_validator.js b/test/wallet_address_validator.js index 808c8de5..ab37af99 100644 --- a/test/wallet_address_validator.js +++ b/test/wallet_address_validator.js @@ -382,6 +382,19 @@ describe('WAValidator.validate()', function () { valid('xrb_1q79ahdr36uqn38p5tp5sqwkn73rnpj1k8obtuetdbjcx37d5gahhd1u9cuh', 'nano'); valid('nano_1q79ahdr36uqn38p5tp5sqwkn73rnpj1k8obtuetdbjcx37d5gahhd1u9cuh', 'nano'); }); + + it('should return true for correct stellar addresses', function () { + valid('GBBM6BKZPEHWYO3E3YKREDPQXMS4VK35YLNU7NFBRI26RAN7GI5POFBB', 'stellar'); + valid('GB7KKHHVYLDIZEKYJPAJUOTBE5E3NJAXPSDZK7O6O44WR3EBRO5HRPVT', 'stellar'); + valid('GD6WVYRVID442Y4JVWFWKWCZKB45UGHJAABBJRS22TUSTWGJYXIUR7N2', 'stellar'); + valid('GBCG42WTVWPO4Q6OZCYI3D6ZSTFSJIXIS6INCIUF23L6VN3ADE4337AP', 'stellar'); + valid('GDFX463YPLCO2EY7NGFMI7SXWWDQAMASGYZXCG2LATOF3PP5NQIUKBPT', 'stellar'); + valid('GBXEODUMM3SJ3QSX2VYUWFU3NRP7BQRC2ERWS7E2LZXDJXL2N66ZQ5PT', 'stellar'); + valid('GAJHORKJKDDEPYCD6URDFODV7CVLJ5AAOJKR6PG2VQOLWFQOF3X7XLOG', 'stellar'); + valid('GACXQEAXYBEZLBMQ2XETOBRO4P66FZAJENDHOQRYPUIXZIIXLKMZEXBJ', 'stellar'); + valid('GDD3XRXU3G4DXHVRUDH7LJM4CD4PDZTVP4QHOO4Q6DELKXUATR657OZV', 'stellar'); + valid('GDTYVCTAUQVPKEDZIBWEJGKBQHB4UGGXI2SXXUEW7LXMD4B7MK37CWLJ', 'stellar'); + }); }); describe('invalid results', function () { @@ -595,5 +608,19 @@ describe('WAValidator.validate()', function () { invalid('xrb_1111111112111111111111111111111111111111111111111111hifc8npp', 'nano'); invalid('nano_111111111111111111111111111111111111111111111111111hifc8npp', 'nano'); }); + + it('should return false for incorrect stellar addresses', function () { + commonTests('stellar'); + invalid('SBGWKM3CD4IL47QN6X54N6Y33T3JDNVI6AIJ6CD5IM47HG3IG4O36XCU', 'stellar'); + invalid('GBPXX0A5N4JYPESHAADMQKBPWZWQDQ64ZV6ZL2S3LAGW4SY7NTCMWIVL', 'stellar'); + invalid('GCFZB6L25D26RQFDWSSBDEYQ32JHLRMTT44ZYE3DZQUTYOL7WY43PLBG++', 'stellar'); + invalid('GADE5QJ2TY7S5ZB65Q43DFGWYWCPHIYDJ2326KZGAGBN7AE5UY6JVDRRA', 'stellar'); + invalid('GB6OWYST45X57HCJY5XWOHDEBULB6XUROWPIKW77L5DSNANBEQGUPADT2', 'stellar'); + invalid('GB6OWYST45X57HCJY5XWOHDEBULB6XUROWPIKW77L5DSNANBEQGUPADT2T', 'stellar'); + invalid('GDXIIZTKTLVYCBHURXL2UPMTYXOVNI7BRAEFQCP6EZCY4JLKY4VKFNLT', 'stellar'); + invalid('SAB5556L5AN5KSR5WF7UOEFDCIODEWEO7H2UR4S5R62DFTQOGLKOVZDY', 'stellar'); + invalid('gWRYUerEKuz53tstxEuR3NCkiQDcV4wzFHmvLnZmj7PUqxW2wt', 'stellar'); + invalid('g4VPBPrHZkfE8CsjuG2S4yBQNd455UWmk', 'stellar'); + }); }); });