diff --git a/package-lock.json b/package-lock.json index 5b7011d0c1..112ff8aab8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "@types/node": "^14.18.35", "@types/ws": "^8.2.0", "@typescript-eslint/eslint-plugin": "^5.28.0", - "@typescript-eslint/parser": "^5.28 .0", + "@typescript-eslint/parser": "^5.28.0", "@xrplf/eslint-config": "^1.9.1", "@xrplf/prettier-config": "^1.9.1", "assert": "^2.0.0", @@ -2797,7 +2797,6 @@ "version": "1.0.30", "resolved": "https://registry.npmjs.org/@types/brorand/-/brorand-1.0.30.tgz", "integrity": "sha512-moU/Mp0MA5vFNGj1/A7Z5TpNC1uyS82I6KZp0Oxk9OKC2XD0S6aQGLVv9ryBYAs259Cq7h9iM1jN9zbuCrUI9w==", - "dev": true, "dependencies": { "@types/node": "*" } @@ -2934,8 +2933,7 @@ "node_modules/@types/node": { "version": "14.18.36", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.36.tgz", - "integrity": "sha512-FXKWbsJ6a1hIrRxv+FoukuHnGTgEzKYGi7kilfMae96AL9UNkPFNWJEEYWzdRI9ooIkbr4AKldyuSTLql06vLQ==", - "dev": true + "integrity": "sha512-FXKWbsJ6a1hIrRxv+FoukuHnGTgEzKYGi7kilfMae96AL9UNkPFNWJEEYWzdRI9ooIkbr4AKldyuSTLql06vLQ==" }, "node_modules/@types/normalize-package-data": { "version": "2.4.1", @@ -16184,6 +16182,16 @@ "resolved": "packages/xrpl", "link": true }, + "node_modules/xrpl-secret-numbers": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/xrpl-secret-numbers/-/xrpl-secret-numbers-0.3.4.tgz", + "integrity": "sha512-B3m0OLRsmNLQpN/BUR15+LC4yejM/pdneoWgijfBYbgjVVnpyCF5+Ur7zbAs4nCAlBUZYXnxp+o/rSNZkke9jQ==", + "dependencies": { + "@types/brorand": "^1.0.30", + "brorand": "^1.1.0", + "ripple-keypairs": "^1.1.5" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -16317,7 +16325,8 @@ "ripple-address-codec": "^4.3.0", "ripple-binary-codec": "^1.8.0", "ripple-keypairs": "^1.3.0", - "ws": "^8.2.2" + "ws": "^8.2.2", + "xrpl-secret-numbers": "^0.3.3" }, "devDependencies": { "@geut/browser-node-core": "^2.0.13", @@ -18553,7 +18562,6 @@ "version": "1.0.30", "resolved": "https://registry.npmjs.org/@types/brorand/-/brorand-1.0.30.tgz", "integrity": "sha512-moU/Mp0MA5vFNGj1/A7Z5TpNC1uyS82I6KZp0Oxk9OKC2XD0S6aQGLVv9ryBYAs259Cq7h9iM1jN9zbuCrUI9w==", - "dev": true, "requires": { "@types/node": "*" } @@ -18690,8 +18698,7 @@ "@types/node": { "version": "14.18.36", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.36.tgz", - "integrity": "sha512-FXKWbsJ6a1hIrRxv+FoukuHnGTgEzKYGi7kilfMae96AL9UNkPFNWJEEYWzdRI9ooIkbr4AKldyuSTLql06vLQ==", - "dev": true + "integrity": "sha512-FXKWbsJ6a1hIrRxv+FoukuHnGTgEzKYGi7kilfMae96AL9UNkPFNWJEEYWzdRI9ooIkbr4AKldyuSTLql06vLQ==" }, "@types/normalize-package-data": { "version": "2.4.1", @@ -29040,7 +29047,18 @@ "ripple-binary-codec": "^1.8.0", "ripple-keypairs": "^1.3.0", "typedoc": "^0.24.6", - "ws": "^8.2.2" + "ws": "^8.2.2", + "xrpl-secret-numbers": "^0.3.3" + } + }, + "xrpl-secret-numbers": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/xrpl-secret-numbers/-/xrpl-secret-numbers-0.3.4.tgz", + "integrity": "sha512-B3m0OLRsmNLQpN/BUR15+LC4yejM/pdneoWgijfBYbgjVVnpyCF5+Ur7zbAs4nCAlBUZYXnxp+o/rSNZkke9jQ==", + "requires": { + "@types/brorand": "^1.0.30", + "brorand": "^1.1.0", + "ripple-keypairs": "^1.1.5" } }, "xtend": { diff --git a/package.json b/package.json index 5d30d38eea..58b1f8fc8c 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "@types/node": "^14.18.35", "@types/ws": "^8.2.0", "@typescript-eslint/eslint-plugin": "^5.28.0", - "@typescript-eslint/parser": "^5.28 .0", + "@typescript-eslint/parser": "^5.28.0", "@xrplf/eslint-config": "^1.9.1", "@xrplf/prettier-config": "^1.9.1", "assert": "^2.0.0", diff --git a/packages/xrpl/HISTORY.md b/packages/xrpl/HISTORY.md index 7ec340a10f..3ad397fa39 100644 --- a/packages/xrpl/HISTORY.md +++ b/packages/xrpl/HISTORY.md @@ -3,6 +3,9 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xrpl-announce) for release announcements. We recommend that xrpl.js (ripple-lib) users stay up-to-date with the latest stable release. ## Unreleased +### Added +* Add `walletFromSecretNumbers` to derive a wallet from [XLS-12](https://github.com/XRPLF/XRPL-Standards/issues/15). Currently only works with `secp256k1` keys, but will work with `ED25519` keys as part of 3.0 via [#2376](https://github.com/XRPLF/xrpl.js/pull/2376). + ## 2.10.0 (2023-08-07) ### Added diff --git a/packages/xrpl/package.json b/packages/xrpl/package.json index 663502abff..539f2e2a78 100644 --- a/packages/xrpl/package.json +++ b/packages/xrpl/package.json @@ -30,7 +30,8 @@ "ripple-address-codec": "^4.3.0", "ripple-binary-codec": "^1.8.0", "ripple-keypairs": "^1.3.0", - "ws": "^8.2.2" + "ws": "^8.2.2", + "xrpl-secret-numbers": "^0.3.3" }, "devDependencies": { "@geut/browser-node-core": "^2.0.13", diff --git a/packages/xrpl/src/Wallet/walletFromSecretNumbers.ts b/packages/xrpl/src/Wallet/walletFromSecretNumbers.ts new file mode 100644 index 0000000000..46e110b7c2 --- /dev/null +++ b/packages/xrpl/src/Wallet/walletFromSecretNumbers.ts @@ -0,0 +1,37 @@ +import { Account } from 'xrpl-secret-numbers' + +import ECDSA from '../ECDSA' + +import { Wallet } from '.' + +/** + * Derives a wallet from secret numbers. + * NOTE: This uses a default encoding algorithm of secp256k1 to match the popular wallet + * [Xumm (aka Xaman)](https://xumm.app/)'s behavior. + * This may be different from the DEFAULT_ALGORITHM for other ways to generate a Wallet. + * + * @param secretNumbers - A string consisting of 8 times 6 numbers (whitespace delimited) used to derive a wallet. + * @param opts - (Optional) Options to derive a Wallet. + * @param opts.masterAddress - Include if a Wallet uses a Regular Key Pair. It must be the master address of the account. + * @param opts.algorithm - The digital signature algorithm to generate an address for. + * @returns A Wallet derived from secret numbers. + * @throws ValidationError if unable to derive private key from secret number input. + */ +export function walletFromSecretNumbers( + secretNumbers: string[] | string, + opts?: { masterAddress?: string; algorithm?: ECDSA }, +): Wallet { + const secret = new Account(secretNumbers).getFamilySeed() + const updatedOpts: { masterAddress?: string; algorithm?: ECDSA } = { + masterAddress: undefined, + algorithm: undefined, + } + // Use secp256k1 since that's the algorithm used by popular wallets like Xumm when generating secret number accounts + if (opts === undefined) { + updatedOpts.algorithm = ECDSA.secp256k1 + } else { + updatedOpts.masterAddress = opts.masterAddress + updatedOpts.algorithm = opts.algorithm ?? ECDSA.secp256k1 + } + return Wallet.fromSecret(secret, updatedOpts) +} diff --git a/packages/xrpl/src/index.ts b/packages/xrpl/src/index.ts index accedcc222..e89397a988 100644 --- a/packages/xrpl/src/index.ts +++ b/packages/xrpl/src/index.ts @@ -13,6 +13,8 @@ export * from './errors' export { Wallet } from './Wallet' +export { walletFromSecretNumbers } from './Wallet/walletFromSecretNumbers' + export { keyToRFC1751Mnemonic, rfc1751MnemonicToKey } from './Wallet/rfc1751' export * from './Wallet/signer' diff --git a/packages/xrpl/test/wallet/index.test.ts b/packages/xrpl/test/wallet/index.test.ts index f7497d55ff..b34917b1d8 100644 --- a/packages/xrpl/test/wallet/index.test.ts +++ b/packages/xrpl/test/wallet/index.test.ts @@ -1,7 +1,12 @@ import { assert } from 'chai' import { decode } from 'ripple-binary-codec' -import { NFTokenMint, Payment, Transaction } from '../../src' +import { + NFTokenMint, + Payment, + Transaction, + walletFromSecretNumbers, +} from '../../src' import ECDSA from '../../src/ECDSA' import { Wallet } from '../../src/Wallet' import requests from '../fixtures/requests' @@ -297,6 +302,86 @@ describe('Wallet', function () { }) }) + describe('fromSecretNumbers', function () { + const secretNumbersString = + '399150 474506 009147 088773 432160 282843 253738 605430' + const secretNumbersArray = [ + '399150', + '474506', + '009147', + '088773', + '432160', + '282843', + '253738', + '605430', + ] + + const publicKey = + '03BFC2F7AE242C3493187FA0B72BE97B2DF71194FB772E507FF9DEA0AD13CA1625' + const privateKey = + '00B6FE8507D977E46E988A8A94DB3B8B35E404B60F8B11AC5213FA8B5ABC8A8D19' + // TODO: Uncomment when the `deriveKeypair` fix is merged so `deriveSecretNumbers` with ed25519 works. + // const publicKeyED25519 = + // 'ED8079E575450E256C496578480020A33E19B579D58A2DB8FF13FC6B05B9229DE3' + // const privateKeyED25519 = + // 'EDD2AF6288A903DED9860FC62E778600A985BDF804E40BD8266505553E3222C3DA' + + it('derives a wallet using default algorithm', function () { + const wallet = walletFromSecretNumbers(secretNumbersString) + + assert.equal(wallet.publicKey, publicKey) + assert.equal(wallet.privateKey, privateKey) + }) + + it('derives a wallet from secret numbers as an array using default algorithm', function () { + const wallet = walletFromSecretNumbers(secretNumbersArray) + + assert.equal(wallet.publicKey, publicKey) + assert.equal(wallet.privateKey, privateKey) + }) + + it('derives a wallet using algorithm ecdsa-secp256k1', function () { + const algorithm = ECDSA.secp256k1 + const wallet = walletFromSecretNumbers(secretNumbersString, { + algorithm, + }) + + assert.equal(wallet.publicKey, publicKey) + assert.equal(wallet.privateKey, privateKey) + }) + + // TODO: Uncomment this test when the `deriveKeypair` fix is merged + // it('derives a wallet using algorithm ed25519', function () { + // const algorithm = ECDSA.ed25519 + // const wallet = Wallet.fromSecretNumbers(secretNumbersString, { + // algorithm, + // }) + + // assert.equal(wallet.publicKey, publicKeyED25519) + // assert.equal(wallet.privateKey, privateKeyED25519) + // }) + + it('derives a wallet using a Regular Key Pair', function () { + const masterAddress = 'rUAi7pipxGpYfPNg3LtPcf2ApiS8aw9A93' + const regularKeyPair = { + secretNumbers: + '399150 474506 009147 088773 432160 282843 253738 605430', + publicKey: + '03BFC2F7AE242C3493187FA0B72BE97B2DF71194FB772E507FF9DEA0AD13CA1625', + privateKey: + '00B6FE8507D977E46E988A8A94DB3B8B35E404B60F8B11AC5213FA8B5ABC8A8D19', + } + + const wallet = walletFromSecretNumbers(regularKeyPair.secretNumbers, { + masterAddress, + }) + + assert.equal(wallet.publicKey, regularKeyPair.publicKey) + assert.equal(wallet.privateKey, regularKeyPair.privateKey) + assert.equal(wallet.classicAddress, masterAddress) + }) + }) + describe('fromEntropy', function () { let entropy: number[] const publicKey =