diff --git a/package.json b/package.json index fb16a339..96f044de 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "lodash.isequal": "^4.5.0", "ripple-address-codec": "^4.0.0", "ripple-binary-codec": "^0.2.5", - "ripple-keypairs": "^1.0.0-beta.6", + "ripple-keypairs": "^1.0.0", "ripple-lib-transactionparser": "0.8.2", "ws": "^7.2.0" }, @@ -49,7 +49,7 @@ "nyc": "^15.0.0", "prettier": "^1.19.1", "ts-node": "^8.4.1", - "typescript": "^3.6.4", + "typescript": "^3.7.5", "webpack": "^4.41.2", "webpack-bundle-analyzer": "^3.6.0", "webpack-cli": "^3.3.9" diff --git a/src/offline/generate-address.ts b/src/offline/generate-address.ts index 44672c94..5b51f90a 100644 --- a/src/offline/generate-address.ts +++ b/src/offline/generate-address.ts @@ -28,10 +28,13 @@ export interface GenerateAddressOptions { function generateAddressAPI(options: GenerateAddressOptions): GeneratedAddress { validate.generateAddress({options}) try { - const secret = keypairs.generateSeed({ - entropy: Uint8Array.from(options.entropy), + const generateSeedOptions: { entropy?: Uint8Array; algorithm?: "ecdsa-secp256k1" | "ed25519"; } = { algorithm: options.algorithm - }) + } + if (options.entropy) { + generateSeedOptions.entropy = Uint8Array.from(options.entropy) + } + const secret = keypairs.generateSeed(generateSeedOptions) const keypair = keypairs.deriveKeypair(secret) const classicAddress = keypairs.deriveAddress(keypair.publicKey) const returnValue: any = { diff --git a/test/api/generateAddress/index.ts b/test/api/generateAddress/index.ts index ad6b24fe..92161cf4 100644 --- a/test/api/generateAddress/index.ts +++ b/test/api/generateAddress/index.ts @@ -1,6 +1,7 @@ import assert from 'assert-diff' import responses from '../../fixtures/responses' import {TestSuite} from '../../utils' +import { GenerateAddressOptions } from '../../../src/offline/generate-address' const {generateAddress: RESPONSE_FIXTURES} = responses /** @@ -9,22 +10,192 @@ const {generateAddress: RESPONSE_FIXTURES} = responses * - Check out "test/api/index.ts" for more information about the test runner. */ export default { - 'generateAddress': async (api, address) => { - function random(): number[] { + 'generateAddress': async (api) => { + // GIVEN entropy of all zeros + function random() { return new Array(16).fill(0) } + assert.deepEqual( + // WHEN generating an address api.generateAddress({entropy: random()}), + + // THEN we get the expected return value RESPONSE_FIXTURES ) }, - 'generateAddress invalid': async (api, address) => { + 'generateAddress invalid entropy': async (api) => { assert.throws(() => { + // GIVEN entropy of 1 byte function random() { return new Array(1).fill(0) } + + // WHEN generating an address api.generateAddress({entropy: random()}) + + // THEN an UnexpectedError is thrown + // because 16 bytes of entropy are required }, api.errors.UnexpectedError) + }, + + 'generateAddress with no options object': async (api) => { + // GIVEN no options + + // WHEN generating an address + const account = api.generateAddress() + + // THEN we get an object with an address starting with 'r' and a secret starting with 's' + assert(account.address.startsWith('r'), 'Address must start with `r`') + assert(account.secret.startsWith('s'), 'Secret must start with `s`') + }, + + 'generateAddress with empty options object': async (api) => { + // GIVEN an empty options object + const options = {} + + // WHEN generating an address + const account = api.generateAddress(options) + + // THEN we get an object with an address starting with 'r' and a secret starting with 's' + assert(account.address.startsWith('r'), 'Address must start with `r`') + assert(account.secret.startsWith('s'), 'Secret must start with `s`') + }, + + 'generateAddress with algorithm `ecdsa-secp256k1`': async (api) => { + // GIVEN we want to use 'ecdsa-secp256k1' + const options: GenerateAddressOptions = {algorithm: 'ecdsa-secp256k1'} + + // WHEN generating an address + const account = api.generateAddress(options) + + // THEN we get an object with an address starting with 'r' and a secret starting with 's' (not 'sEd') + assert(account.address.startsWith('r'), 'Address must start with `r`') + assert.deepEqual(account.secret.slice(0, 1), 's', `Secret ${account.secret} must start with 's'`) + assert.notStrictEqual(account.secret.slice(0, 3), 'sEd', `secp256k1 secret ${account.secret} must not start with 'sEd'`) + }, + + 'generateAddress with algorithm `ed25519`': async (api) => { + // GIVEN we want to use 'ed25519' + const options: GenerateAddressOptions = {algorithm: 'ed25519'} + + // WHEN generating an address + const account = api.generateAddress(options) + + // THEN we get an object with an address starting with 'r' and a secret starting with 'sEd' + assert(account.address.startsWith('r'), 'Address must start with `r`') + assert.deepEqual(account.secret.slice(0, 3), 'sEd', `Ed25519 secret ${account.secret} must start with 'sEd'`) + }, + + 'generateAddress with algorithm `ecdsa-secp256k1` and given entropy': async (api) => { + // GIVEN we want to use 'ecdsa-secp256k1' with entropy of zero + const options: GenerateAddressOptions = {algorithm: 'ecdsa-secp256k1', entropy: new Array(16).fill(0)} + + // WHEN generating an address + const account = api.generateAddress(options) + + // THEN we get the expected return value + assert.deepEqual(account, responses.generateAddress) + }, + + 'generateAddress with algorithm `ed25519` and given entropy': async (api) => { + // GIVEN we want to use 'ed25519' with entropy of zero + const options: GenerateAddressOptions = {algorithm: 'ed25519', entropy: new Array(16).fill(0)} + + // WHEN generating an address + const account = api.generateAddress(options) + + // THEN we get the expected return value + assert.deepEqual(account, { + + // generateAddress return value always includes xAddress to encourage X-address adoption + xAddress: 'X7xq1YJ4xmLSGGLhuakFQB9CebWYthQkgsvFC4LGFH871HB', + + classicAddress: "r9zRhGr7b6xPekLvT6wP4qNdWMryaumZS7", + address: "r9zRhGr7b6xPekLvT6wP4qNdWMryaumZS7", + secret: 'sEdSJHS4oiAdz7w2X2ni1gFiqtbJHqE' + }) + }, + + 'generateAddress with algorithm `ecdsa-secp256k1` and given entropy; include classic address': async (api) => { + // GIVEN we want to use 'ecdsa-secp256k1' with entropy of zero + const options: GenerateAddressOptions = {algorithm: 'ecdsa-secp256k1', entropy: new Array(16).fill(0), includeClassicAddress: true} + + // WHEN generating an address + const account = api.generateAddress(options) + + // THEN we get the expected return value + assert.deepEqual(account, responses.generateAddress) + }, + + 'generateAddress with algorithm `ed25519` and given entropy; include classic address': async (api) => { + // GIVEN we want to use 'ed25519' with entropy of zero + const options: GenerateAddressOptions = {algorithm: 'ed25519', entropy: new Array(16).fill(0), includeClassicAddress: true} + + // WHEN generating an address + const account = api.generateAddress(options) + + // THEN we get the expected return value + assert.deepEqual(account, { + + // generateAddress return value always includes xAddress to encourage X-address adoption + xAddress: 'X7xq1YJ4xmLSGGLhuakFQB9CebWYthQkgsvFC4LGFH871HB', + + secret: 'sEdSJHS4oiAdz7w2X2ni1gFiqtbJHqE', + classicAddress: "r9zRhGr7b6xPekLvT6wP4qNdWMryaumZS7", + address: "r9zRhGr7b6xPekLvT6wP4qNdWMryaumZS7" + }) + }, + + 'generateAddress with algorithm `ecdsa-secp256k1` and given entropy; include classic address; for test network use': async (api) => { + // GIVEN we want to use 'ecdsa-secp256k1' with entropy of zero + const options: GenerateAddressOptions = {algorithm: 'ecdsa-secp256k1', entropy: new Array(16).fill(0), includeClassicAddress: true, test: true} + + // WHEN generating an address + const account = api.generateAddress(options) + + // THEN we get the expected return value + const response = Object.assign({}, responses.generateAddress, { + + // generateAddress return value always includes xAddress to encourage X-address adoption + xAddress: 'TVG3TcCD58BD6MZqsNuTihdrhZwR8SzvYS8U87zvHsAcNw4' + + }) + assert.deepEqual(account, response) + }, + + 'generateAddress with algorithm `ed25519` and given entropy; include classic address; for test network use': async (api) => { + // GIVEN we want to use 'ed25519' with entropy of zero + const options: GenerateAddressOptions = {algorithm: 'ed25519', entropy: new Array(16).fill(0), includeClassicAddress: true, test: true} + + // WHEN generating an address + const account = api.generateAddress(options) + + // THEN we get the expected return value + assert.deepEqual(account, { + + // generateAddress return value always includes xAddress to encourage X-address adoption + xAddress: 'T7t4HeTMF5tT68agwuVbJwu23ssMPeh8dDtGysZoQiij1oo', + + secret: 'sEdSJHS4oiAdz7w2X2ni1gFiqtbJHqE', + classicAddress: "r9zRhGr7b6xPekLvT6wP4qNdWMryaumZS7", + address: "r9zRhGr7b6xPekLvT6wP4qNdWMryaumZS7" + }) + }, + + 'generateAddress for test network use': async (api) => { + // GIVEN we want an address for test network use + const options: GenerateAddressOptions = {test: true} + + // WHEN generating an address + const account = api.generateAddress(options) + + // THEN we get an object with xAddress starting with 'T' and a secret starting with 's' + + // generateAddress return value always includes xAddress to encourage X-address adoption + assert.deepEqual(account.xAddress.slice(0, 1), 'T', 'Test addresses start with T') + + assert.deepEqual(account.secret.slice(0, 1), 's', `Secret ${account.secret} must start with 's'`) } } diff --git a/test/api/generateXAddress/index.ts b/test/api/generateXAddress/index.ts index 33ffe163..0af11d41 100644 --- a/test/api/generateXAddress/index.ts +++ b/test/api/generateXAddress/index.ts @@ -1,6 +1,7 @@ import assert from 'assert-diff' import responses from '../../fixtures/responses' import {TestSuite} from '../../utils' +import { GenerateAddressOptions } from '../../../src/offline/generate-address' /** * Every test suite exports their tests in the default object. @@ -8,22 +9,175 @@ import {TestSuite} from '../../utils' * - Check out "test/api/index.ts" for more information about the test runner. */ export default { - 'generateXAddress': async (api, address) => { + 'generateXAddress': async (api) => { + // GIVEN entropy of all zeros function random() { return new Array(16).fill(0) } + assert.deepEqual( + // WHEN generating an X-address api.generateXAddress({entropy: random()}), + + // THEN we get the expected return value responses.generateXAddress ) }, - 'generateXAddress invalid': async (api, address) => { + 'generateXAddress invalid entropy': async (api) => { assert.throws(() => { + // GIVEN entropy of 1 byte function random() { return new Array(1).fill(0) } + + // WHEN generating an X-address api.generateXAddress({entropy: random()}) + + // THEN an UnexpectedError is thrown + // because 16 bytes of entropy are required }, api.errors.UnexpectedError) + }, + + 'generateXAddress with no options object': async (api) => { + // GIVEN no options + + // WHEN generating an X-address + const account = api.generateXAddress() + + // THEN we get an object with an xAddress starting with 'X' and a secret starting with 's' + assert(account.xAddress.startsWith('X'), 'By default X-addresses start with X') + assert(account.secret.startsWith('s'), 'Secrets start with s') + }, + + 'generateXAddress with empty options object': async (api) => { + // GIVEN an empty options object + const options = {} + + // WHEN generating an X-address + const account = api.generateXAddress(options) + + // THEN we get an object with an xAddress starting with 'X' and a secret starting with 's' + assert(account.xAddress.startsWith('X'), 'By default X-addresses start with X') + assert(account.secret.startsWith('s'), 'Secrets start with s') + }, + + 'generateXAddress with algorithm `ecdsa-secp256k1`': async (api) => { + // GIVEN we want to use 'ecdsa-secp256k1' + const options: GenerateAddressOptions = {algorithm: 'ecdsa-secp256k1'} + + // WHEN generating an X-address + const account = api.generateXAddress(options) + + // THEN we get an object with an xAddress starting with 'X' and a secret starting with 's' + assert(account.xAddress.startsWith('X'), 'By default X-addresses start with X') + assert.deepEqual(account.secret.slice(0, 1), 's', `Secret ${account.secret} must start with 's'`) + assert.notStrictEqual(account.secret.slice(0, 3), 'sEd', `secp256k1 secret ${account.secret} must not start with 'sEd'`) + }, + + 'generateXAddress with algorithm `ed25519`': async (api) => { + // GIVEN we want to use 'ed25519' + const options: GenerateAddressOptions = {algorithm: 'ed25519'} + + // WHEN generating an X-address + const account = api.generateXAddress(options) + + // THEN we get an object with an xAddress starting with 'X' and a secret starting with 'sEd' + assert(account.xAddress.startsWith('X'), 'By default X-addresses start with X') + assert.deepEqual(account.secret.slice(0, 3), 'sEd', `Ed25519 secret ${account.secret} must start with 'sEd'`) + }, + + 'generateXAddress with algorithm `ecdsa-secp256k1` and given entropy': async (api) => { + // GIVEN we want to use 'ecdsa-secp256k1' with entropy of zero + const options: GenerateAddressOptions = {algorithm: 'ecdsa-secp256k1', entropy: new Array(16).fill(0)} + + // WHEN generating an X-address + const account = api.generateXAddress(options) + + // THEN we get the expected return value + assert.deepEqual(account, responses.generateXAddress) + }, + + 'generateXAddress with algorithm `ed25519` and given entropy': async (api) => { + // GIVEN we want to use 'ed25519' with entropy of zero + const options: GenerateAddressOptions = {algorithm: 'ed25519', entropy: new Array(16).fill(0)} + + // WHEN generating an X-address + const account = api.generateXAddress(options) + + // THEN we get the expected return value + assert.deepEqual(account, { + xAddress: 'X7xq1YJ4xmLSGGLhuakFQB9CebWYthQkgsvFC4LGFH871HB', + secret: 'sEdSJHS4oiAdz7w2X2ni1gFiqtbJHqE' + }) + }, + + 'generateXAddress with algorithm `ecdsa-secp256k1` and given entropy; include classic address': async (api) => { + // GIVEN we want to use 'ecdsa-secp256k1' with entropy of zero + const options: GenerateAddressOptions = {algorithm: 'ecdsa-secp256k1', entropy: new Array(16).fill(0), includeClassicAddress: true} + + // WHEN generating an X-address + const account = api.generateXAddress(options) + + // THEN we get the expected return value + assert.deepEqual(account, responses.generateAddress) + }, + + 'generateXAddress with algorithm `ed25519` and given entropy; include classic address': async (api) => { + // GIVEN we want to use 'ed25519' with entropy of zero + const options: GenerateAddressOptions = {algorithm: 'ed25519', entropy: new Array(16).fill(0), includeClassicAddress: true} + + // WHEN generating an X-address + const account = api.generateXAddress(options) + + // THEN we get the expected return value + assert.deepEqual(account, { + xAddress: 'X7xq1YJ4xmLSGGLhuakFQB9CebWYthQkgsvFC4LGFH871HB', + secret: 'sEdSJHS4oiAdz7w2X2ni1gFiqtbJHqE', + classicAddress: "r9zRhGr7b6xPekLvT6wP4qNdWMryaumZS7", + address: "r9zRhGr7b6xPekLvT6wP4qNdWMryaumZS7" + }) + }, + + 'generateXAddress with algorithm `ecdsa-secp256k1` and given entropy; include classic address; for test network use': async (api) => { + // GIVEN we want to use 'ecdsa-secp256k1' with entropy of zero + const options: GenerateAddressOptions = {algorithm: 'ecdsa-secp256k1', entropy: new Array(16).fill(0), includeClassicAddress: true, test: true} + + // WHEN generating an X-address + const account = api.generateXAddress(options) + + // THEN we get the expected return value + const response = Object.assign({}, responses.generateAddress, { + xAddress: 'TVG3TcCD58BD6MZqsNuTihdrhZwR8SzvYS8U87zvHsAcNw4' + }) + assert.deepEqual(account, response) + }, + + 'generateXAddress with algorithm `ed25519` and given entropy; include classic address; for test network use': async (api) => { + // GIVEN we want to use 'ed25519' with entropy of zero + const options: GenerateAddressOptions = {algorithm: 'ed25519', entropy: new Array(16).fill(0), includeClassicAddress: true, test: true} + + // WHEN generating an X-address + const account = api.generateXAddress(options) + + // THEN we get the expected return value + assert.deepEqual(account, { + xAddress: 'T7t4HeTMF5tT68agwuVbJwu23ssMPeh8dDtGysZoQiij1oo', + secret: 'sEdSJHS4oiAdz7w2X2ni1gFiqtbJHqE', + classicAddress: "r9zRhGr7b6xPekLvT6wP4qNdWMryaumZS7", + address: "r9zRhGr7b6xPekLvT6wP4qNdWMryaumZS7" + }) + }, + + 'generateXAddress for test network use': async (api) => { + // GIVEN we want an X-address for test network use + const options: GenerateAddressOptions = {test: true} + + // WHEN generating an X-address + const account = api.generateXAddress(options) + + // THEN we get an object with xAddress starting with 'T' and a secret starting with 's' + assert.deepEqual(account.xAddress.slice(0, 1), 'T', 'Test X-addresses start with T') + assert.deepEqual(account.secret.slice(0, 1), 's', `Secret ${account.secret} must start with 's'`) } } diff --git a/test/ripple-api-test.ts b/test/ripple-api-test.ts index ce77c1a3..ce8f38b2 100644 --- a/test/ripple-api-test.ts +++ b/test/ripple-api-test.ts @@ -32,21 +32,33 @@ describe('RippleAPI [Test Runner]', function() { // Run all the tests: for (const {name: methodName, tests, config} of allTestSuites) { describe(`api.${methodName}`, () => { - // Run each test with the original-style address. - describe(`[Original Address]`, () => { - for (const [testName, fn] of tests) { + // Run each test that does not use an address. + for (const [testName, fn] of tests) { + if (fn.length === 1) { it(testName, function() { return fn(this.api, addresses.ACCOUNT) }) } + } + // Run each test with a classic address. + describe(`[Classic Address]`, () => { + for (const [testName, fn] of tests) { + if (fn.length === 2) { + it(testName, function() { + return fn(this.api, addresses.ACCOUNT) + }) + } + } }) - // Run each test with the newer, x-address style. + // Run each test with an X-address. if (!config.skipXAddress) { describe(`[X-address]`, () => { for (const [testName, fn] of tests) { - it(testName, function() { - return fn(this.api, addresses.ACCOUNT_X) - }) + if (fn.length === 2) { + it(testName, function() { + return fn(this.api, addresses.ACCOUNT_X) + }) + } } }) } diff --git a/yarn.lock b/yarn.lock index f6f27a35..a5f0c039 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4121,7 +4121,7 @@ ripple-binary-codec@^0.2.5: lodash "^4.17.15" ripple-address-codec "^4.0.0" -ripple-keypairs@^1.0.0-beta.6: +ripple-keypairs@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/ripple-keypairs/-/ripple-keypairs-1.0.0.tgz#8f1c604f89daeac5e61b7eebbbca2da99da2bacf" integrity sha512-MQ3d6fU3D+Cqu5ma4dfkfa+KakN2sKpVVVN0FeJyAYPVIGXu8Rcvd1g028TdwYAZcSYk0tGn5UhHxd0gUG3T8g== @@ -4801,7 +4801,7 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@^3.6.4: +typescript@^3.7.5: version "3.7.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.5.tgz#0692e21f65fd4108b9330238aac11dd2e177a1ae" integrity sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==