├── .gitignore ├── .travis.yml ├── src ├── index.ts ├── types │ └── index.ts ├── computed │ └── index.ts ├── methods │ ├── encode.ts │ └── decode.ts └── utils │ └── index.ts ├── jest.config.js ├── tsconfig.json ├── samples └── es5.js ├── @types └── ripple-address-codec │ └── index.d.ts ├── LICENSE ├── package.json ├── tslint.json ├── README.md └── test └── api.spec.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules 3 | /dist 4 | yarn-error.log -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6 4 | - 8 5 | - 10 6 | - 11 7 | script: 8 | - yarn clean 9 | - yarn test 10 | - yarn build -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Encode from './methods/encode' 2 | import Decode from './methods/decode' 3 | 4 | /* Export ==================================================================== */ 5 | export {Encode, Decode} 6 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ["/test"], 3 | transform: { 4 | "^.+\\.ts?$": "ts-jest" 5 | }, 6 | testEnvironment: "node", 7 | testRegex: "(.*|(\\.|/)(test|spec))\\.ts?$", 8 | moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"] 9 | }; 10 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | type Destination = { 4 | account: string 5 | tag?: null | number | string 6 | networkID?: null | number | string 7 | test?: boolean 8 | } 9 | 10 | type CodecOptions = { 11 | version: number | Uint32Array 12 | expectedLength: number 13 | } 14 | 15 | export { 16 | Destination, 17 | CodecOptions 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": ["es6"], 6 | "declaration": true, 7 | "outDir": "./dist", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "resolveJsonModule": true, 11 | "typeRoots": ["./node_modules/@types", "./@types"] 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules", "test"] 15 | } 16 | -------------------------------------------------------------------------------- /samples/es5.js: -------------------------------------------------------------------------------- 1 | const {Encode, Decode} = require('../dist/') 2 | 3 | const tagged = Encode({ 4 | account: 'rPEPPER7kfTD9w2To4CQk6UCfuHM9c6GDY', 5 | tag: 1337 6 | }) 7 | console.log('Encoded', tagged) 8 | 9 | const untagged = Decode(tagged) 10 | console.log('Decoded', untagged) 11 | 12 | console.log() 13 | 14 | const taggedTest = Encode({ 15 | account: 'rPEPPER7kfTD9w2To4CQk6UCfuHM9c6GDY', 16 | tag: 1337, 17 | test: true 18 | }) 19 | console.log('Encoded for test address', taggedTest) 20 | 21 | const untaggedTest = Decode(taggedTest) 22 | console.log('Decoded for test address', untaggedTest) 23 | 24 | console.log() 25 | -------------------------------------------------------------------------------- /src/computed/index.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import * as utils from '../utils' 4 | import * as types from '../types' 5 | 6 | const CodecOutputLength = 37 7 | 8 | const IsTest = (destination: types.Destination | string): boolean => { 9 | if (typeof destination === 'string' && destination.match(/^T/)) { 10 | return true 11 | } 12 | if (typeof destination === 'object' && destination.test === true) { 13 | return true 14 | } 15 | return false 16 | } 17 | 18 | const CodecOptions = (destination: types.Destination | string): types.CodecOptions => { 19 | const char = IsTest(destination) 20 | ? 'T' 21 | : 'X' 22 | return { 23 | version: utils.findPrefix(char, CodecOutputLength), 24 | expectedLength: CodecOutputLength 25 | } 26 | } 27 | 28 | export { 29 | CodecOptions, 30 | IsTest 31 | } 32 | -------------------------------------------------------------------------------- /@types/ripple-address-codec/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'ripple-address-codec' { 2 | export type EncodingOptions = { 3 | version: number | Uint32Array 4 | expectedLength: number 5 | } 6 | 7 | export type Codec = { 8 | alphabet: string 9 | codec: object 10 | base: number 11 | encode: (bytes: Uint32Array, options?: EncodingOptions) => string 12 | decode: (address: string, options?: EncodingOptions) => Uint32Array 13 | decodeRaw: (template: string) => Uint32Array 14 | } 15 | 16 | export type Codecs = { 17 | ripple: Codec 18 | } 19 | 20 | export const codecs: Codecs 21 | export const decodeAddress: (accountAddress: string) => Uint32Array 22 | export const encodeAddress: (account: Uint32Array) => string 23 | export const isValidAddress: (accountAddress: string) => Uint32Array 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Wietse Wind 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 | -------------------------------------------------------------------------------- /src/methods/encode.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import codec from 'ripple-address-codec' 3 | import * as utils from '../utils' 4 | import * as types from '../types' 5 | import * as computed from '../computed' 6 | 7 | export default function Encode(destination: types.Destination): string { 8 | assert.equal(typeof destination, 'object', 'Input should contain object') 9 | assert.notEqual(destination, null, 'Input should contain object') 10 | assert.strictEqual(Object.keys(destination).indexOf('account') > -1, true, 'Input should contain `account`') 11 | 12 | const account: string = destination.account 13 | const tag: null | number = destination.tag === null || destination.tag === undefined 14 | ? null 15 | : Number(destination.tag) 16 | const nid: null | number = destination.networkID === null || destination.networkID === undefined 17 | ? null 18 | : Number(destination.networkID) 19 | 20 | const accountHex: string = utils.toHex(codec.decodeAddress(account)) 21 | 22 | const tagTypeHex: string = tag === null 23 | ? (nid === null ? '00' : '80') 24 | : (nid === null ? '01' : '81') 25 | const tagHex: string = utils.uInt32_ToUInt32LE(tag || 0) 26 | const nidHex: string = utils.uInt32_ToUInt32LE(nid || 0) 27 | 28 | const bytes: Uint32Array = utils.addChecksum(utils.toBytes( 29 | (destination.test ? '0493' : '0544') + accountHex + tagTypeHex + tagHex + nidHex)) 30 | 31 | return codec.codecs.ripple.encode(bytes) 32 | } 33 | -------------------------------------------------------------------------------- /src/methods/decode.ts: -------------------------------------------------------------------------------- 1 | import codec from 'ripple-address-codec' 2 | import * as utils from '../utils' 3 | import * as types from '../types' 4 | import * as computed from '../computed' 5 | 6 | export default function Decode(encodedDestination: string): types.Destination { 7 | const decoded: Uint32Array = codec.codecs.ripple.decode(encodedDestination) 8 | const hex: string = utils.toHex(decoded) 9 | const cksum: string = utils.makeChecksum(hex.slice(0,62)) 10 | const isTest: boolean = hex.slice(0,4) === '0493' 11 | if ((hex.slice(0,4) !== '0544' && !isTest) || hex.length !== 70 || hex.slice(62, 70) !== cksum) { 12 | throw new Error('checksum_invalid') 13 | } 14 | /* 15 | throw new Error( 16 | 'Invalid X-Address when decoding ' + 17 | encodedDestination + ' front: ' + hex.slice(0,4) + ' len: ' + hex.length 18 | + ' checksum: ' + hex.slice(62, 70) + ' computed: ' + cksum 19 | ); 20 | */ 21 | const accountHex: string = hex.slice(4, 44) 22 | const tagTypeHex: string = hex.slice(44, 46) 23 | const tagHex: string = hex.slice(46, 54) 24 | const nidHex: string = hex.slice(54, 62) 25 | 26 | return { 27 | account: codec.encodeAddress(utils.toBytes(accountHex)), 28 | tag: tagTypeHex === '01' || tagTypeHex === '81' 29 | ? String(utils.uInt32LE_ToUInt32(tagHex) || 0) 30 | : null, 31 | networkID: tagTypeHex === '80' || tagTypeHex === '81' 32 | ? String(utils.uInt32LE_ToUInt32(nidHex) || 0) 33 | : null, 34 | test: isTest 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xrpl-tagged-address-codec", 3 | "version": "1.0.0", 4 | "description": "Encode and Decode an XRPL account address and destination tag to/from X-formatted (tagged) address", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "prepublish": "npm run clean && npm run lint && npm run test && npm run build", 9 | "clean": "rm -rf dist", 10 | "build": "tsc", 11 | "test": "jest --verbose", 12 | "lint": "tslint -p ./", 13 | "browserify": "npm run prepublish && browserify -r ./dist/:xrpl-tagged-address-codec -o dist/xrpl-tagged-address-codec-browser.js" 14 | }, 15 | "files": [ 16 | "dist/**/*.js", 17 | "dist/**/*.js.map", 18 | "dist/**/*.d.ts" 19 | ], 20 | "directories": { 21 | "test": "test" 22 | }, 23 | "dependencies": { 24 | "ripple-address-codec": "^2.0.1" 25 | }, 26 | "devDependencies": { 27 | "@types/jest": "^24.0.23", 28 | "@types/node": "^12.12.20", 29 | "browserify": "^16.5.0", 30 | "jest": "^24.9.0", 31 | "ts-jest": "^24.2.0", 32 | "tslint": "^5.20.1", 33 | "tslint-eslint-rules": "^5.4.0", 34 | "typescript": "^3.7.3" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "git://github.com/xrp-community/xrpl-tagged-address-codec.git" 39 | }, 40 | "bugs": { 41 | "url": "https://github.com/xrp-community/xrpl-tagged-address-codec/issues" 42 | }, 43 | "homepage": "https://github.com/xrp-community/xrpl-tagged-address-codec#readme", 44 | "license": "MIT", 45 | "readmeFilename": "README.md", 46 | "keywords": [ 47 | "xrp", 48 | "xrpl-ledger", 49 | "encoding", 50 | "codec", 51 | "address" 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import codec from 'ripple-address-codec' 4 | import * as crypto from 'crypto' 5 | 6 | function toHex (bytes: Uint32Array): string { 7 | return Buffer.from(bytes).toString('hex').toUpperCase() 8 | } 9 | 10 | function toBytes (hex: string): Uint32Array { 11 | return new Uint32Array(Buffer.from(hex, 'hex').toJSON().data) 12 | } 13 | 14 | function uInt32_ToUInt32LE (integer: number): string { 15 | const buf = Buffer.alloc(4) 16 | buf.writeUInt32LE(integer, 0) 17 | return buf.toString('hex').toUpperCase() 18 | } 19 | 20 | function uInt32LE_ToUInt32 (hex: string): number { 21 | return Buffer.from(hex, 'hex').readUInt32LE(0) 22 | } 23 | 24 | function findPrefix (desiredPrefix: string, payloadLength: number): number | Uint32Array { 25 | // Thanks @sublimator 26 | 27 | const rippleCodec = codec.codecs.ripple 28 | 29 | if (rippleCodec.base !== 58) { 30 | throw new Error('Only works for base58') 31 | } 32 | const factor = Math.log(256) / Math.log(rippleCodec.base) 33 | const totalLength = payloadLength + 4 // for checksum 34 | const chars = totalLength * factor 35 | const requiredChars = Math.ceil(chars + 0.2) 36 | const alphabetPosition = Math.floor((rippleCodec.alphabet.length) / 2) - 1 37 | const padding = rippleCodec.alphabet[alphabetPosition] 38 | const rcPad = new Array(requiredChars + 1).join(padding) 39 | const template = desiredPrefix + rcPad 40 | const bytes = rippleCodec.decodeRaw(template) 41 | const version = bytes.slice(0, -totalLength) 42 | return version 43 | } 44 | 45 | function makeChecksum(hex: string): string { 46 | if (hex.length % 2 === 1) { 47 | hex = '0' + hex 48 | } 49 | 50 | let ret = Buffer.from( 51 | crypto.createHash('sha256') 52 | .update( 53 | crypto.createHash('sha256') 54 | .update(Buffer.from(hex, 'hex')) 55 | .digest()).digest()).slice(0,4).toString('hex').toUpperCase() 56 | 57 | if (ret.length < 8) { 58 | ret = '0'.repeat(8 - ret.length) 59 | } 60 | 61 | return ret 62 | } 63 | 64 | function addChecksum(bytes: Uint32Array): Uint32Array { 65 | return new Uint32Array( 66 | Buffer.concat( 67 | [ 68 | Buffer.from(bytes), 69 | Buffer.from(makeChecksum(toHex(bytes)), 'hex').slice(0,4) 70 | ] 71 | ).toJSON().data 72 | ) 73 | } 74 | 75 | 76 | export { 77 | toHex, 78 | toBytes, 79 | uInt32_ToUInt32LE, 80 | uInt32LE_ToUInt32, 81 | findPrefix, 82 | makeChecksum, 83 | addChecksum 84 | } 85 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint-eslint-rules" 4 | ], 5 | "linterOptions": { 6 | "exclude": [ 7 | "node_modules/**" 8 | ] 9 | }, 10 | "rules": { 11 | "ban": [true, ["alert"]], 12 | "no-arg": true, 13 | "no-conditional-assignment": true, 14 | "no-console": false, 15 | "no-constant-condition": true, 16 | "no-control-regex": true, 17 | "no-debugger": true, 18 | "no-duplicate-case": true, 19 | "no-empty": true, 20 | "no-empty-character-class": true, 21 | "no-eval": true, 22 | "no-ex-assign": true, 23 | "no-extra-boolean-cast": true, 24 | "no-extra-semi": true, 25 | "no-switch-case-fall-through": true, 26 | "no-inner-declarations": [true, "functions"], 27 | "no-invalid-regexp": true, 28 | // this rule would cause problems with mocha test cases, 29 | "no-invalid-this": false, 30 | "no-irregular-whitespace": true, 31 | "ter-no-irregular-whitespace": true, 32 | "label-position": true, 33 | "indent": [true, "spaces", 2], 34 | "linebreak-style": [true, "unix"], 35 | "no-multi-spaces": true, 36 | "no-consecutive-blank-lines": [true, 2], 37 | "no-unused-expression": true, 38 | "no-construct": true, 39 | "no-duplicate-variable": true, 40 | "no-regex-spaces": true, 41 | "no-shadowed-variable": true, 42 | "ter-no-sparse-arrays": true, 43 | "no-trailing-whitespace": true, 44 | "no-string-throw": true, 45 | "no-unexpected-multiline": true, 46 | "no-var-keyword": true, 47 | "no-magic-numbers": false, 48 | "array-bracket-spacing": [true, "never"], 49 | "ter-arrow-body-style": false, 50 | "ter-arrow-parens": [true, "as-needed"], 51 | "ter-arrow-spacing": true, 52 | "block-spacing": true, 53 | "brace-style": [true, "1tbs", {"allowSingleLine": true}], 54 | "variable-name": false, 55 | "trailing-comma": [true, {"multiline": "never", "singleline": "never"}], 56 | "cyclomatic-complexity": [false, 11], 57 | "curly": [true, "all"], 58 | "switch-default": false, 59 | "eofline": true, 60 | "triple-equals": true, 61 | "forin": false, 62 | "handle-callback-err": true, 63 | "ter-max-len": [true, 120], 64 | "new-parens": true, 65 | "object-curly-spacing": [true, "never"], 66 | "object-literal-shorthand": false, 67 | "one-variable-per-declaration": [true, "ignore-for-loop"], 68 | "ter-prefer-arrow-callback": false, 69 | "prefer-const": true, 70 | "object-literal-key-quotes": false, 71 | "quotemark": [true, "single"], 72 | "radix": true, 73 | "semicolon": [true, "never"], 74 | "space-in-parens": [true, "never"], 75 | "comment-format": [true, "check-space"], 76 | "use-isnan": true, 77 | "valid-typeof": true 78 | } 79 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XRPL Tagged Address Codec [![npm version](https://badge.fury.io/js/xrpl-tagged-address-codec.svg?1)](https://www.npmjs.com/xrpl-tagged-address-codec) 2 | 3 | #### Encode and Decode an XRPL account address and destination tag to/from X-formatted (tagged) address. 4 | 5 | Destination tags provide a way for exchanges, payment processors, corporates or entities which accept incoming payments, escrows, checks and similar transcations to use a single receiving wallet while being able to disambiguate incoming transactions by instructing the senders to include a destination tag. 6 | 7 | This package allows encoding and decoding from an XRPL address and destination tag to / from 'Tagged Addresses', containing both the destination account address and tag in one string. This way users can simply copy-paste the string, eliminating possible user error when copying / entering a numeric destination tag. 8 | 9 | #### Hopefully all exchanges, wallets & other software using destination tags will implement this address codec. A migration period will be required to allow users to enter both address formats. 10 | 11 | #### The website [https://xrpaddress.info](https://xrpaddress.info/) is available for users, exchanges and developers to provide some context and best practices. 12 | 13 | ## Use 14 | 15 | - [Browserified sample](https://jsfiddle.net/WietseWind/05rpvbag/) 16 | - [RunKit sample in node](https://runkit.com/wietsewind/5cbf111b51e3ee00127b2b59) 17 | 18 | ### 1. Import 19 | 20 | ##### Node 21 | 22 | ``` 23 | const {Encode, Decode} = require('xrpl-tagged-address-codec') 24 | ``` 25 | ... and use: `Encode()` / `Decode()` or: 26 | 27 | ``` 28 | const codec = require('xrpl-tagged-address-codec') 29 | ``` 30 | 31 | ... and use: `codec.Encode()` / `codec.Decode()` 32 | 33 | 34 | ##### TypeScript 35 | ``` 36 | import {Encode, Decode} from 'xrpl-tagged-address-codec' 37 | ``` 38 | 39 | ... and use: `Encode()` / `Decode()` or: 40 | 41 | ``` 42 | import * as codec from 'xrpl-tagged-address-codec' 43 | ``` 44 | 45 | ... and use: `codec.Encode()` / `codec.Decode()` 46 | 47 | 48 | ### 2. Encode / Decode 49 | 50 | #### Encode a separate account address and destination tag: 51 | 52 | ``` 53 | const tagged = Encode({ 54 | account: 'rPEPPER7kfTD9w2To4CQk6UCfuHM9c6GDY', 55 | tag: 1337, 56 | test: false 57 | }) 58 | ``` 59 | 60 | The output will be a tagged address (string). The `tag` and `test` can be omitted, rendering `tag` to be **null** and `test` to be **false**. 61 | 62 | 63 | #### Decode a tagged address: 64 | 65 | ``` 66 | const tagged = 'XVLhHMPHU98es4dbozjVtdWzVrDjtV8xvjGQTYPiAx6gwDC' 67 | const untagged = Decode(tagged) 68 | 69 | ``` 70 | 71 | The output will be a destination object containing the untagged address (r....), the destination tag (null or a string containing the destination tag) and a test (bool) indicator. 72 | 73 | ## Development 74 | 75 | Run npm run prepublish to clean, lint, test and build. Or just run npm run build, npm run test or npm run lint. 76 | 77 | Tests are in `./test`. Run `tsc -w` if you want are developing and want to auto-build to `./dist` when you make changes on the fly. 78 | 79 | Scripts: 80 | 81 | - Build: `npm run build`, output: `./dist` 82 | - Test: `npm run test` 83 | - Lint: `npm run lint` 84 | - Clean, test, lint and build: `npm run prepublish` 85 | - Browserify: `npm run browserify`, output: `dist/xrpl-tagged-address-codec-browser.js` 86 | 87 | ## Credits 88 | 89 | This concept is based on the [concept](https://github.com/xrp-community/standards-drafts/issues/6) from [@nbougalis](https://github.com/nbougalis) 90 | 91 | Big thanks to [@sublimator](https://github.com/sublimator) for his fiddles, ideas and fixes and [@intelliot](https://github.com/intelliot) for the idea of adding an `X` / `T` prefix for (new) address recognizability. 92 | -------------------------------------------------------------------------------- /test/api.spec.ts: -------------------------------------------------------------------------------- 1 | import {Encode, Decode} from '../src' 2 | import * as utils from '../src/utils' 3 | import codec from 'ripple-address-codec' 4 | 5 | const account = 'rPEPPER7kfTD9w2To4CQk6UCfuHM9c6GDY' 6 | const encodeDecodeTests = [ 7 | // No Network ID 8 | { 9 | title: 'without tag', 10 | encoded: { 11 | livenet: 'XV5sbjUmgPpvXv4ixFWZ5ptAYZ6PD2gYsjNFQLKYW33DzBm', 12 | test: 'TVd2rqMkYL2AyS97NdELcpeiprNBjwLZzuUG5rZnaewsahi' 13 | }, 14 | networkID: null 15 | }, 16 | { 17 | title: 'with null tag', 18 | encoded: { 19 | livenet: 'XV5sbjUmgPpvXv4ixFWZ5ptAYZ6PD2gYsjNFQLKYW33DzBm', 20 | test: 'TVd2rqMkYL2AyS97NdELcpeiprNBjwLZzuUG5rZnaewsahi' 21 | }, 22 | tag: null, 23 | networkID: null 24 | }, 25 | { 26 | title: 'with tag zero (0, number)', 27 | encoded: { 28 | livenet: 'XV5sbjUmgPpvXv4ixFWZ5ptAYZ6PD2m4Er6SnvjVLpMWPjR', 29 | test: 'TVd2rqMkYL2AyS97NdELcpeiprNBjwRQUBetPbyrvXSTuxU' 30 | }, 31 | tag: 0, 32 | networkID: null 33 | }, 34 | { 35 | title: 'with tag 13371337 (number)', 36 | encoded: { 37 | livenet: 'XV5sbjUmgPpvXv4ixFWZ5ptAYZ6PD2qwGkhgc48zzcx6Gkr', 38 | test: 'TVd2rqMkYL2AyS97NdELcpeiprNBjwVUDvp3vhpXbNhLwJi' 39 | }, 40 | tag: 13371337, 41 | networkID: null 42 | }, 43 | { 44 | title: 'with tag "13371337" (string)', 45 | encoded: { 46 | livenet: 'XV5sbjUmgPpvXv4ixFWZ5ptAYZ6PD2qwGkhgc48zzcx6Gkr', 47 | test: 'TVd2rqMkYL2AyS97NdELcpeiprNBjwVUDvp3vhpXbNhLwJi' 48 | }, 49 | tag: '13371337', 50 | networkID: null 51 | }, 52 | // Network ID 53 | { 54 | title: 'without tag »» [ with network id 123 ]', 55 | encoded: { 56 | livenet: 'XV5sbjUmgPpvXv4ixFWZ5ptAYZ6PDmLtxsy23iRGyYEyDRg', 57 | test: 'TVd2rqMkYL2AyS97NdELcpeiprNBjLzvuDApSXeo2fZEVhh' 58 | }, 59 | networkID: '123' 60 | }, 61 | { 62 | title: 'with null tag »» [ with network id null ]', 63 | encoded: { 64 | livenet: 'XV5sbjUmgPpvXv4ixFWZ5ptAYZ6PD2gYsjNFQLKYW33DzBm', 65 | test: 'TVd2rqMkYL2AyS97NdELcpeiprNBjwLZzuUG5rZnaewsahi' 66 | }, 67 | tag: null, 68 | networkID: null 69 | }, 70 | { 71 | title: 'with tag zero (0, number) »» [ with network id 9999999 ]', 72 | encoded: { 73 | livenet: 'XV5sbjUmgPpvXv4ixFWZ5ptAYZ6PDmRj9LSBqDLudKPRHn8', 74 | test: 'TVd2rqMkYL2AyS97NdELcpeiprNBjMnmaWPe3t2SDySjrXA' 75 | }, 76 | tag: 0, 77 | networkID: '9999999' 78 | }, 79 | { 80 | title: 'with tag "13371337" (string) »» [ with network id 0 ]', 81 | encoded: { 82 | livenet: 'XV5sbjUmgPpvXv4ixFWZ5ptAYZ6PDmVXBhoRzKdzkFX5SZ7', 83 | test: 'TVd2rqMkYL2AyS97NdELcpeiprNBjM9Z3NktHz1XMMALj7p' 84 | }, 85 | tag: '13371337', 86 | networkID: '0' 87 | } 88 | ] 89 | 90 | describe('XRPL Tagged Adress Codec', () => { 91 | 92 | describe('Ripple Address Codec', () => { 93 | it('should have the correct alphabet', () => { 94 | const alphabet = 'rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz' 95 | expect(codec.codecs.ripple.alphabet).toEqual(alphabet) 96 | }) 97 | }) 98 | 99 | describe('Error handling', () => { 100 | it('should only accept an object', () => { 101 | expect(() => { 102 | // @ts-ignore 103 | const e = Encode(null) 104 | }).toThrow('Input should contain object') 105 | }) 106 | it('should rquire the `account` key', () => { 107 | expect(() => { 108 | // @ts-ignore 109 | const e = Encode({address: 'x'}) 110 | }).toThrow('Input should contain `account`') 111 | }) 112 | }) 113 | 114 | describe('Utils', () => { 115 | it('should encode toHex', () => { 116 | const computed = utils.toHex(new Uint32Array([100, 3, 200])) 117 | expect(computed).toEqual('6403C8') 118 | }) 119 | it('should encode toBytes', () => { 120 | const computed = utils.toBytes('abcd1234') 121 | expect(computed).toEqual(new Uint32Array([171, 205, 18, 52])) 122 | }) 123 | it('should encode uInt32 to UInt32LE', () => { 124 | const computed = utils.uInt32_ToUInt32LE(1337) 125 | expect(computed).toEqual('39050000') 126 | }) 127 | it('should decode UInt32 to uInt32LE', () => { 128 | const computed = utils.uInt32LE_ToUInt32('39050000') 129 | expect(computed).toEqual(1337) 130 | }) 131 | it('should be able to compute a prefix', () => { 132 | const computed = utils.findPrefix('X', 29) 133 | expect(computed).toEqual([5, 68]) 134 | }) 135 | }) 136 | 137 | describe('Errors', () => { 138 | it('should detect an invalid r-address', () => { 139 | expect(() => { 140 | const e = Encode({account: 'rXXXXXXXXXX'}) 141 | }).toThrow('checksum_invalid') 142 | }) 143 | it('should detect an invalid tagged livenet address', () => { 144 | expect(() => { 145 | const e = Decode('Xxxxxxxxxxxx') 146 | }).toThrow('checksum_invalid') 147 | }) 148 | it('should detect an invalid tagged test address', () => { 149 | expect(() => { 150 | const e = Decode('Tttttttttttt') 151 | }).toThrow('checksum_invalid') 152 | }) 153 | }) 154 | 155 | const nets = ['livenet', 'test'] 156 | nets.forEach(n => { 157 | const isTest = n === 'test' 158 | describe(n, () => { 159 | describe('Encoding (' + n + ')', () => { 160 | encodeDecodeTests.forEach(t => { 161 | it('should encode ' + t.title, () => { 162 | const encoded = Encode({ 163 | account, 164 | tag: t.tag, 165 | networkID: t?.networkID, 166 | test: isTest 167 | }) 168 | const taggedAddress = isTest 169 | ? t.encoded.test 170 | : t.encoded.livenet 171 | expect(encoded).toEqual(taggedAddress) 172 | }) 173 | }) 174 | }) 175 | 176 | describe('Decoding (' + n + ')', () => { 177 | encodeDecodeTests.forEach(t => { 178 | it('should decode ' + t.title, () => { 179 | const taggedAddress = isTest 180 | ? t.encoded.test 181 | : t.encoded.livenet 182 | const decoded = Decode(taggedAddress) 183 | expect(decoded).toEqual({ 184 | account, 185 | tag: typeof t.tag === 'string' || typeof t.tag === 'number' 186 | ? String(t.tag) 187 | : null, 188 | test: isTest, 189 | networkID: t?.networkID 190 | }) 191 | }) 192 | }) 193 | }) 194 | }) 195 | }) 196 | 197 | }) 198 | --------------------------------------------------------------------------------