diff --git a/package.json b/package.json index 9d870ab3..6a513354 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,8 @@ "@types/lodash": "^4.14.136", "@types/ws": "^7.2.0", "bignumber.js": "^9.0.0", + "bip32": "^2.0.6", + "bip39": "^3.0.4", "https-proxy-agent": "^5.0.0", "jsonschema": "1.2.2", "lodash": "^4.17.4", diff --git a/src/Wallet.ts b/src/Wallet.ts new file mode 100644 index 00000000..09982b42 --- /dev/null +++ b/src/Wallet.ts @@ -0,0 +1,112 @@ +import {fromSeed} from 'bip32' +import {mnemonicToSeedSync} from 'bip39' +import {decode, encodeForSigning} from 'ripple-binary-codec' +import {deriveKeypair, generateSeed, verify} from 'ripple-keypairs' +import ECDSA from './common/ecdsa' +import {SignedTransaction} from './common/types/objects' +import {signOffline} from './transaction/sign' +import {SignOptions} from './transaction/types' +import {ValidationError} from './common/errors' + +/** + * A utility for deriving a wallet composed of a keypair (publicKey/privateKey). + * A wallet can be derived from either a seed, mnemnoic, or entropy (array of random numbers). + * It provides functionality to sign/verify transactions offline. + */ +class Wallet { + readonly publicKey: string + readonly privateKey: string + private static readonly defaultAlgorithm: ECDSA = ECDSA.ed25519 + private static readonly defaultDerivationPath: string = "m/44'/144'/0'/0/0" + + constructor(publicKey: string, privateKey: string) { + this.publicKey = publicKey + this.privateKey = privateKey + } + + /** + * Derives a wallet from a seed. + * @param {string} seed A string used to generate a keypair (publicKey/privateKey) to derive a wallet. + * @param {ECDSA} algorithm The digital signature algorithm to generate an address for. + * @returns {Wallet} A Wallet derived from a seed. + */ + static fromSeed(seed: string, algorithm: ECDSA = Wallet.defaultAlgorithm): Wallet { + return Wallet.deriveWallet(seed, algorithm) + } + + /** + * Derives a wallet from a mnemonic. + * @param {string} mnemonic A string consisting of words (whitespace delimited) used to derive a wallet. + * @param {string} derivationPath The path to derive a keypair (publicKey/privateKey) from a seed (that was converted from a mnemonic). + * @returns {Wallet} A Wallet derived from a mnemonic. + */ + static fromMnemonic( + mnemonic: string, + derivationPath: string = Wallet.defaultDerivationPath + ): Wallet { + const seed = mnemonicToSeedSync(mnemonic) + const masterNode = fromSeed(seed) + const node = masterNode.derivePath(derivationPath) + if (node.privateKey === undefined) { + throw new ValidationError('Unable to derive privateKey from mnemonic input') + } + + const publicKey = Wallet.hexFromBuffer(node.publicKey) + const privateKey = Wallet.hexFromBuffer(node.privateKey) + return new Wallet(publicKey, `00${privateKey}`) + } + + /** + * Derives a wallet from an entropy (array of random numbers). + * @param {Uint8Array | number[]} entropy An array of random numbers to generate a seed used to derive a wallet. + * @param {ECDSA} algorithm The digital signature algorithm to generate an address for. + * @returns {Wallet} A Wallet derived from an entropy. + */ + static fromEntropy( + entropy: Uint8Array | number[], + algorithm: ECDSA = Wallet.defaultAlgorithm + ): Wallet { + const options = { + entropy: Uint8Array.from(entropy), + algorithm + } + const seed = generateSeed(options) + return Wallet.deriveWallet(seed, algorithm) + } + + private static hexFromBuffer(buffer: Buffer): string { + return buffer.toString('hex').toUpperCase() + } + + private static deriveWallet(seed: string, algorithm: ECDSA = Wallet.defaultAlgorithm): Wallet { + const {publicKey, privateKey} = deriveKeypair(seed, {algorithm}) + return new Wallet(publicKey, privateKey) + } + + /** + * Signs a transaction offline. + * @param {object} transaction A transaction to be signed offline. + * @param {SignOptions} options Options to include for signing. + * @returns {SignedTransaction} A signed transaction. + */ + signTransaction( + transaction: any, // TODO: transaction should be typed with Transaction type. + options: SignOptions = {signAs: ''} + ): SignedTransaction { + return signOffline(this, JSON.stringify(transaction), options) + } + + /** + * Verifies a signed transaction offline. + * @param {string} signedTransaction A signed transaction (hex string of signTransaction result) to be verified offline. + * @returns {boolean} Returns true if a signedTransaction is valid. + */ + verifyTransaction(signedTransaction: string): boolean { + const tx = decode(signedTransaction) + const messageHex: string = encodeForSigning(tx) + const signature = tx.TxnSignature + return verify(messageHex, signature, this.publicKey) + } +} + +export default Wallet diff --git a/src/api.ts b/src/api.ts index f5eb1634..6f1ef1ce 100644 --- a/src/api.ts +++ b/src/api.ts @@ -45,7 +45,7 @@ import prepareCheckCancel from './transaction/check-cancel' import prepareCheckCash from './transaction/check-cash' import prepareSettings from './transaction/settings' import prepareTicketCreate from './transaction/ticket' -import sign from './transaction/sign' +import {sign} from './transaction/sign' import combine from './transaction/combine' import submit from './transaction/submit' import {generateAddress, generateXAddress} from './offline/utils' diff --git a/src/common/ecdsa.ts b/src/common/ecdsa.ts new file mode 100644 index 00000000..affb7587 --- /dev/null +++ b/src/common/ecdsa.ts @@ -0,0 +1,6 @@ +enum ECDSA { + ed25519 = 'ed25519', + secp256k1 = 'ecdsa-secp256k1', +} + +export default ECDSA diff --git a/src/common/types/objects/transactions.ts b/src/common/types/objects/transactions.ts index 1e8d1cdc..70b860fa 100644 --- a/src/common/types/objects/transactions.ts +++ b/src/common/types/objects/transactions.ts @@ -20,3 +20,8 @@ export interface OfferCreateTransaction { Memos?: Memo[] OfferSequence?: number } + +export interface SignedTransaction { + signedTransaction: string + id: string +} diff --git a/src/index.ts b/src/index.ts index 48cd68ee..aedff091 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,9 @@ export * from './transaction/types' export * from './common/types/objects/ledger' -export * from './offline/utils'; +export * from './offline/utils' // Broadcast api is experimental export {RippleAPIBroadcast} from './broadcast' + +export * from './Wallet' diff --git a/src/offline/generate-address.ts b/src/offline/generate-address.ts index f1f3c6ab..e4ef3f0c 100644 --- a/src/offline/generate-address.ts +++ b/src/offline/generate-address.ts @@ -1,6 +1,7 @@ import {classicAddressToXAddress} from 'ripple-address-codec' import keypairs from 'ripple-keypairs' import {errors, validate} from '../common' +import ECDSA from '../common/ecdsa' export type GeneratedAddress = { xAddress: string @@ -14,7 +15,7 @@ export interface GenerateAddressOptions { entropy?: Uint8Array | number[] // The digital signature algorithm to generate an address for. Can be `ecdsa-secp256k1` (default) or `ed25519`. - algorithm?: 'ecdsa-secp256k1' | 'ed25519' + algorithm?: ECDSA // Specifies whether the address is intended for use on a test network such as Testnet or Devnet. // If `true`, the address should only be used for testing, and will start with `T`. @@ -30,7 +31,7 @@ function generateAddressAPI(options: GenerateAddressOptions = {}): GeneratedAddr try { const generateSeedOptions: { entropy?: Uint8Array - algorithm?: 'ecdsa-secp256k1' | 'ed25519' + algorithm?: ECDSA } = { algorithm: options.algorithm } diff --git a/src/transaction/sign.ts b/src/transaction/sign.ts index 8d4bf8e7..048c9b7c 100644 --- a/src/transaction/sign.ts +++ b/src/transaction/sign.ts @@ -7,6 +7,8 @@ import {SignOptions, KeyPair, TransactionJSON} from './types' import BigNumber from 'bignumber.js' import {xrpToDrops} from '../common' import {RippleAPI} from '..' +import Wallet from '../Wallet' +import {SignedTransaction} from '../common/types/objects' const validate = utils.common.validate function computeSignature(tx: object, privateKey: string, signAs?: string) { @@ -23,8 +25,9 @@ function signWithKeypair( options: SignOptions = { signAs: '' } -): {signedTransaction: string; id: string} { +): SignedTransaction { validate.sign({txJSON, keypair}) + const isOnline = !!api; const tx = JSON.parse(txJSON) if (tx.TxnSignature || tx.Signers) { @@ -33,7 +36,9 @@ function signWithKeypair( ) } - checkFee(api, tx.Fee) + if (isOnline) { + checkFee(api, tx.Fee) + } const txToSignAndEncode = Object.assign({}, tx) @@ -219,7 +224,7 @@ function sign( secret?: any, options?: SignOptions, keypair?: KeyPair -): {signedTransaction: string; id: string} { +): SignedTransaction { if (typeof secret === 'string') { // we can't validate that the secret matches the account because // the secret could correspond to the regular key @@ -241,4 +246,19 @@ function sign( } } -export default sign +// TODO: move this to Wallet class +function signOffline( + wallet: Wallet, + txJSON: string, + options?: SignOptions, +): SignedTransaction { + const {publicKey, privateKey} = wallet + return signWithKeypair( + null, + txJSON, + {publicKey, privateKey}, + options, + ) +} + +export {sign, signOffline} diff --git a/test/api/generateAddress/index.ts b/test/api/generateAddress/index.ts index e76b2dd2..78aef15c 100644 --- a/test/api/generateAddress/index.ts +++ b/test/api/generateAddress/index.ts @@ -2,6 +2,7 @@ import assert from 'assert-diff' import responses from '../../fixtures/responses' import {TestSuite} from '../../utils' import {GenerateAddressOptions} from '../../../src/offline/generate-address' +import ECDSA from '../../../src/common/ecdsa' const {generateAddress: RESPONSE_FIXTURES} = responses /** @@ -65,7 +66,7 @@ export default { 'generateAddress with algorithm `ecdsa-secp256k1`': async (api) => { // GIVEN we want to use 'ecdsa-secp256k1' - const options: GenerateAddressOptions = {algorithm: 'ecdsa-secp256k1'} + const options: GenerateAddressOptions = {algorithm: ECDSA.secp256k1} // WHEN generating an address const account = api.generateAddress(options) @@ -86,7 +87,7 @@ export default { 'generateAddress with algorithm `ed25519`': async (api) => { // GIVEN we want to use 'ed25519' - const options: GenerateAddressOptions = {algorithm: 'ed25519'} + const options: GenerateAddressOptions = {algorithm: ECDSA.ed25519} // WHEN generating an address const account = api.generateAddress(options) @@ -105,7 +106,7 @@ export default { ) => { // GIVEN we want to use 'ecdsa-secp256k1' with entropy of zero const options: GenerateAddressOptions = { - algorithm: 'ecdsa-secp256k1', + algorithm: ECDSA.secp256k1, entropy: new Array(16).fill(0) } @@ -119,7 +120,7 @@ export default { 'generateAddress with algorithm `ed25519` and given entropy': async (api) => { // GIVEN we want to use 'ed25519' with entropy of zero const options: GenerateAddressOptions = { - algorithm: 'ed25519', + algorithm: ECDSA.ed25519, entropy: new Array(16).fill(0) } @@ -142,7 +143,7 @@ export default { ) => { // GIVEN we want to use 'ecdsa-secp256k1' with entropy of zero const options: GenerateAddressOptions = { - algorithm: 'ecdsa-secp256k1', + algorithm: ECDSA.secp256k1, entropy: new Array(16).fill(0), includeClassicAddress: true } @@ -159,7 +160,7 @@ export default { ) => { // GIVEN we want to use 'ed25519' with entropy of zero const options: GenerateAddressOptions = { - algorithm: 'ed25519', + algorithm: ECDSA.ed25519, entropy: new Array(16).fill(0), includeClassicAddress: true } @@ -183,7 +184,7 @@ export default { ) => { // GIVEN we want to use 'ecdsa-secp256k1' with entropy of zero const options: GenerateAddressOptions = { - algorithm: 'ecdsa-secp256k1', + algorithm: ECDSA.secp256k1, entropy: new Array(16).fill(0), includeClassicAddress: true, test: true @@ -205,7 +206,7 @@ export default { ) => { // GIVEN we want to use 'ed25519' with entropy of zero const options: GenerateAddressOptions = { - algorithm: 'ed25519', + algorithm: ECDSA.ed25519, entropy: new Array(16).fill(0), includeClassicAddress: true, test: true diff --git a/test/api/generateXAddress/index.ts b/test/api/generateXAddress/index.ts index 189c2cfe..098aeb41 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 ECDSA from '../../../src/common/ecdsa' import {GenerateAddressOptions} from '../../../src/offline/generate-address' /** @@ -70,7 +71,7 @@ export default { 'generateXAddress with algorithm `ecdsa-secp256k1`': async (api) => { // GIVEN we want to use 'ecdsa-secp256k1' - const options: GenerateAddressOptions = {algorithm: 'ecdsa-secp256k1'} + const options: GenerateAddressOptions = {algorithm: ECDSA.secp256k1} // WHEN generating an X-address const account = api.generateXAddress(options) @@ -94,7 +95,7 @@ export default { 'generateXAddress with algorithm `ed25519`': async (api) => { // GIVEN we want to use 'ed25519' - const options: GenerateAddressOptions = {algorithm: 'ed25519'} + const options: GenerateAddressOptions = {algorithm: ECDSA.ed25519} // WHEN generating an X-address const account = api.generateXAddress(options) @@ -116,7 +117,7 @@ export default { ) => { // GIVEN we want to use 'ecdsa-secp256k1' with entropy of zero const options: GenerateAddressOptions = { - algorithm: 'ecdsa-secp256k1', + algorithm: ECDSA.secp256k1, entropy: new Array(16).fill(0) } @@ -132,7 +133,7 @@ export default { ) => { // GIVEN we want to use 'ed25519' with entropy of zero const options: GenerateAddressOptions = { - algorithm: 'ed25519', + algorithm: ECDSA.ed25519, entropy: new Array(16).fill(0) } @@ -151,7 +152,7 @@ export default { ) => { // GIVEN we want to use 'ecdsa-secp256k1' with entropy of zero const options: GenerateAddressOptions = { - algorithm: 'ecdsa-secp256k1', + algorithm: ECDSA.secp256k1, entropy: new Array(16).fill(0), includeClassicAddress: true } @@ -168,7 +169,7 @@ export default { ) => { // GIVEN we want to use 'ed25519' with entropy of zero const options: GenerateAddressOptions = { - algorithm: 'ed25519', + algorithm: ECDSA.ed25519, entropy: new Array(16).fill(0), includeClassicAddress: true } @@ -190,7 +191,7 @@ export default { ) => { // GIVEN we want to use 'ecdsa-secp256k1' with entropy of zero const options: GenerateAddressOptions = { - algorithm: 'ecdsa-secp256k1', + algorithm: ECDSA.secp256k1, entropy: new Array(16).fill(0), includeClassicAddress: true, test: true @@ -211,7 +212,7 @@ export default { ) => { // GIVEN we want to use 'ed25519' with entropy of zero const options: GenerateAddressOptions = { - algorithm: 'ed25519', + algorithm: ECDSA.ed25519, entropy: new Array(16).fill(0), includeClassicAddress: true, test: true diff --git a/test/wallet/fromEntropy/index.ts b/test/wallet/fromEntropy/index.ts new file mode 100644 index 00000000..ad09f7e4 --- /dev/null +++ b/test/wallet/fromEntropy/index.ts @@ -0,0 +1,54 @@ +import assert from 'assert-diff' +import {TestSuite} from '../../utils' +import ECDSA from '../../../src/common/ecdsa' +import Wallet from '../../../src/Wallet' + +const entropy: number[] = new Array(16).fill(0) +const publicKey: string = + '0390A196799EE412284A5D80BF78C3E84CBB80E1437A0AECD9ADF94D7FEAAFA284' +const privateKey: string = + '002512BBDFDBB77510883B7DCCBEF270B86DEAC8B64AC762873D75A1BEE6298665' +const publicKeyED25519: string = + 'ED1A7C082846CFF58FF9A892BA4BA2593151CCF1DBA59F37714CC9ED39824AF85F' +const privateKeyED25519: string = + 'ED0B6CBAC838DFE7F47EA1BD0DF00EC282FDF45510C92161072CCFB84035390C4D' + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'Wallet.fromEntropy with entropy only': async (api) => { + // WHEN deriving a wallet from an entropy + const wallet = Wallet.fromEntropy(entropy) + + // THEN we get a wallet with a keypair (publicKey/privateKey) + assert.equal(wallet.publicKey, publicKeyED25519) + assert.equal(wallet.privateKey, privateKeyED25519) + }, + + 'Wallet.fromEntropy with algorithm ecdsa-secp256k1': async (api) => { + // GIVEN an entropy using ecdsa-secp256k1 + const algorithm = ECDSA.secp256k1 + + // WHEN deriving a wallet from an entropy + const wallet = Wallet.fromEntropy(entropy, algorithm) + + // THEN we get a wallet with a keypair (publicKey/privateKey) + assert.equal(wallet.publicKey, publicKey) + assert.equal(wallet.privateKey, privateKey) + }, + + 'Wallet.fromEntropy with algorithm ed25519': async (api) => { + // GIVEN an entropy using ed25519 + const algorithm = ECDSA.ed25519 + + // WHEN deriving a wallet from an entropy + const wallet = Wallet.fromEntropy(entropy, algorithm) + + // THEN we get a wallet with a keypair (publicKey/privateKey) + assert.equal(wallet.publicKey, publicKeyED25519) + assert.equal(wallet.privateKey, privateKeyED25519) + }, +} diff --git a/test/wallet/fromMnemonic/index.ts b/test/wallet/fromMnemonic/index.ts new file mode 100644 index 00000000..1f4ff1e9 --- /dev/null +++ b/test/wallet/fromMnemonic/index.ts @@ -0,0 +1,39 @@ +import assert from 'assert-diff' +import {TestSuite} from '../../utils' +import Wallet from '../../../src/Wallet' + +const mnemonic = + 'try milk link drift aware pass obtain again music stick pluck fold' +const publicKey = + '0257B550BA2FDCCF0ADDA3DEB2A5411700F3ADFDCC7C68E1DCD1E2B63E6B0C63E6' +const privateKey = + '008F942B6E229C0E9CEE47E7A94253DABB6A9855F4BA2D8A741FA31851A1D423C3' + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'Wallet.fromMnemonic using default derivation path': async (api) => { + // GIVEN no derivation path + // WHEN deriving a wallet from a mnemonic without a derivation path + const wallet = Wallet.fromMnemonic(mnemonic) + + // THEN we get a wallet with a keypair (publicKey/privateKey) + assert.equal(wallet.publicKey, publicKey) + assert.equal(wallet.privateKey, privateKey) + }, + + 'Wallet.fromMnemonic using an input derivation path': async (api) => { + // GIVEN a derivation path + const derivationPath = "m/44'/144'/0'/0/0" + + // WHEN deriving a wallet from a mnemonic without a derivation path + const wallet = Wallet.fromMnemonic(mnemonic, derivationPath) + + // THEN we get a wallet with a keypair (publicKey/privateKey) + assert.equal(wallet.publicKey, publicKey) + assert.equal(wallet.privateKey, privateKey) + }, +} diff --git a/test/wallet/fromSeed/index.ts b/test/wallet/fromSeed/index.ts new file mode 100644 index 00000000..3dc56139 --- /dev/null +++ b/test/wallet/fromSeed/index.ts @@ -0,0 +1,50 @@ +import assert from 'assert-diff' +import {TestSuite} from '../../utils' +import ECDSA from '../../../src/common/ecdsa' +import Wallet from '../../../src/Wallet' + +const seed = 'ssL9dv2W5RK8L3tuzQxYY6EaZhSxW' +const publicKey = + '030E58CDD076E798C84755590AAF6237CA8FAE821070A59F648B517A30DC6F589D' +const privateKey = + '00141BA006D3363D2FB2785E8DF4E44D3A49908780CB4FB51F6D217C08C021429F' + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'Wallet.fromSeed with empty options object': async (api) => { + // WHEN deriving a wallet from a seed + const wallet = Wallet.fromSeed(seed) + + // THEN we get a wallet with a keypair (publicKey/privateKey) + assert.equal(wallet.publicKey, publicKey) + assert.equal(wallet.privateKey, privateKey) + }, + + 'Wallet.fromSeed with algorithm ecdsa-secp256k1': async (api) => { + // GIVEN we want to use ecdsa-secp256k1 + const algorithm = ECDSA.secp256k1 + + // WHEN deriving a wallet from a seed + const wallet = Wallet.fromSeed(seed, algorithm) + + // THEN we get a wallet with a keypair (publicKey/privateKey) + assert.equal(wallet.publicKey, publicKey) + assert.equal(wallet.privateKey, privateKey) + }, + + 'Wallet.fromSeed with algorithm ed25519': async (api) => { + // GIVEN we want to use ed25519 + const algorithm = ECDSA.ed25519 + + // WHEN deriving a wallet from a seed + const wallet = Wallet.fromSeed(seed, algorithm) + + // THEN we get a wallet with a keypair (publicKey/privateKey) + assert.equal(wallet.publicKey, publicKey) + assert.equal(wallet.privateKey, privateKey) + }, +} diff --git a/test/wallet/signTransaction/index.ts b/test/wallet/signTransaction/index.ts new file mode 100644 index 00000000..89959bbe --- /dev/null +++ b/test/wallet/signTransaction/index.ts @@ -0,0 +1,38 @@ +import {RippleAPI} from 'ripple-api' +import {TestSuite} from '../../utils' +import Wallet from '../../../src/Wallet' + +const {schemaValidator} = RippleAPI._PRIVATE +const publicKey = + '030E58CDD076E798C84755590AAF6237CA8FAE821070A59F648B517A30DC6F589D' +const privateKey = + '00141BA006D3363D2FB2785E8DF4E44D3A49908780CB4FB51F6D217C08C021429F' +const address = 'rhvh5SrgBL5V8oeV9EpDuVszeJSSCEkbPc' + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'sign transaction offline with txJSON': async (api) => { + // GIVEN a transaction + const txJSON = { + TransactionType: 'Payment', + Account: address, + Destination: 'rQ3PTWGLCbPz8ZCicV5tCX3xuymojTng5r', + Amount: '20000000', + Sequence: 1, + Fee: '12', + SigningPubKey: publicKey + } + const wallet = new Wallet(publicKey, privateKey) + + // WHEN signing a transaction offline + const signedTx: {signedTransaction: string; id: string} = + wallet.signTransaction(txJSON) + + // THEN we get a signedTransaction + schemaValidator.schemaValidate('sign', signedTx) + }, +} diff --git a/test/wallet/verifyTransaction/index.ts b/test/wallet/verifyTransaction/index.ts new file mode 100644 index 00000000..b71f911e --- /dev/null +++ b/test/wallet/verifyTransaction/index.ts @@ -0,0 +1,46 @@ +import assert from 'assert-diff' +import {TestSuite} from '../../utils' +import Wallet from 'ripple-api/Wallet' + +const publicKey = + '030E58CDD076E798C84755590AAF6237CA8FAE821070A59F648B517A30DC6F589D' +const privateKey = + '00141BA006D3363D2FB2785E8DF4E44D3A49908780CB4FB51F6D217C08C021429F' +const prepared = { + signedTransaction: + '1200002400000001614000000001312D0068400000000000000C7321030E58CDD076E798C84755590AAF6237CA8FAE821070A59F648B517A30DC6F589D74473045022100CAF99A63B241F5F62B456C68A593D2835397101533BB5D0C4DC17362AC22046F022016A2CA2CF56E777B10E43B56541A4C2FB553E7E298CDD39F7A8A844DA491E51D81142AF1861DEC1316AEEC995C94FF9E2165B1B784608314FDB08D07AAA0EB711793A3027304D688E10C3648', + id: '30D9ECA2A7FB568C5A8607E5850D9567572A9E7C6094C26BEFD4DC4C2CF2657A' +} + +/** + * Every test suite exports their tests in the default object. + * - Check out the "TestSuite" type for documentation on the interface. + * - Check out "test/api/index.ts" for more information about the test runner. + */ +export default { + 'verify transaction offline when a signed transaction is valid': async (api) => { + // GIVEN a transaction that has been signed by the same wallet + const wallet = new Wallet(publicKey, privateKey) + + // WHEN verifying a signed transaction + const isVerified: boolean = wallet.verifyTransaction(prepared.signedTransaction) + + // THEN we get a valid response + assert.equal(isVerified, true) + }, + + "verify transaction offline when signed transaction isn't valid": async (api) => { + // GIVEN a transaction that has been signed by a different wallet + const diffPublicKey = + '02F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8' + const diffPrivateKey = + '00ACCD3309DB14D1A4FC9B1DAE608031F4408C85C73EE05E035B7DC8B25840107A' + const wallet = new Wallet(diffPublicKey, diffPrivateKey) + + // WHEN verifying a signed transaction + const isVerified: boolean = wallet.verifyTransaction(prepared.signedTransaction) + + // THEN we get an invalid response + assert.equal(isVerified, false) + }, +} diff --git a/yarn.lock b/yarn.lock index a1dd744a..81966e32 100644 --- a/yarn.lock +++ b/yarn.lock @@ -323,6 +323,16 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.7.10.tgz#7aa732cc47341c12a16b7d562f519c2383b6d4fc" integrity sha512-S63Dlv4zIPb8x6MMTgDq5WWRJQe56iBEY0O3SOFA9JrRienkOVDXSXBjjJw6HTNQYSE2JI6GMCR6LVbIMHJVvA== +"@types/node@10.12.18": + version "10.12.18" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.18.tgz#1d3ca764718915584fcd9f6344621b7672665c67" + integrity sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ== + +"@types/node@11.11.6": + version "11.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-11.11.6.tgz#df929d1bb2eee5afdda598a41930fe50b43eaa6a" + integrity sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ== + "@types/ws@^7.2.0": version "7.4.7" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702" @@ -731,7 +741,7 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base-x@3.0.8: +base-x@3.0.8, base-x@^3.0.2: version "3.0.8" resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.8.tgz#1e1106c2537f0162e8b52474a557ebb09000018d" integrity sha512-Rl/1AWP4J/zRrk54hhlxH4drNxPJXYUaKffODVI53/dAsV4t9fBxyxYKAVPU1XBHxYwOWP9h9H0hM2MVw4YfJA== @@ -758,6 +768,36 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== +bindings@^1.3.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" + integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== + dependencies: + file-uri-to-path "1.0.0" + +bip32@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/bip32/-/bip32-2.0.6.tgz#6a81d9f98c4cd57d05150c60d8f9e75121635134" + integrity sha512-HpV5OMLLGTjSVblmrtYRfFFKuQB+GArM0+XP8HGWfJ5vxYBqo+DesvJwOdC2WJ3bCkZShGf0QIfoIpeomVzVdA== + dependencies: + "@types/node" "10.12.18" + bs58check "^2.1.1" + create-hash "^1.2.0" + create-hmac "^1.1.7" + tiny-secp256k1 "^1.1.3" + typeforce "^1.11.5" + wif "^2.0.6" + +bip39@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/bip39/-/bip39-3.0.4.tgz#5b11fed966840b5e1b8539f0f54ab6392969b2a0" + integrity sha512-YZKQlb752TrUWqHWj7XAwCSjYEgGAk+/Aas3V7NyjQeZYsztO8JnQUaCWhcnL4T+jL8nvB8typ2jRPzTlgugNw== + dependencies: + "@types/node" "11.11.6" + create-hash "^1.1.0" + pbkdf2 "^3.0.9" + randombytes "^2.0.1" + bl@^4.0.3: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" @@ -767,7 +807,7 @@ bl@^4.0.3: inherits "^2.0.4" readable-stream "^3.4.0" -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9: +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.8, bn.js@^4.11.9: version "4.12.0" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== @@ -872,6 +912,22 @@ browserslist@^4.14.5, browserslist@^4.16.6: escalade "^3.1.1" node-releases "^1.1.71" +bs58@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a" + integrity sha1-vhYedsNU9veIrkBx9j806MTwpCo= + dependencies: + base-x "^3.0.2" + +bs58check@<3.0.0, bs58check@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/bs58check/-/bs58check-2.1.2.tgz#53b018291228d82a5aa08e7d796fdafda54aebfc" + integrity sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA== + dependencies: + bs58 "^4.0.0" + create-hash "^1.1.0" + safe-buffer "^5.1.2" + buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" @@ -1381,7 +1437,7 @@ electron-to-chromium@^1.3.723: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.792.tgz#791b0d8fcf7411885d086193fb49aaef0c1594ca" integrity sha512-RM2O2xrNarM7Cs+XF/OE2qX/aBROyOZqqgP+8FXMXSuWuUqCfUUzg7NytQrzZU3aSqk1Qq6zqnVkJsbfMkIatg== -elliptic@^6.5.2, elliptic@^6.5.3, elliptic@^6.5.4: +elliptic@^6.4.0, elliptic@^6.5.2, elliptic@^6.5.3, elliptic@^6.5.4: version "6.5.4" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== @@ -1720,6 +1776,11 @@ file-entry-cache@^5.0.1: dependencies: flat-cache "^2.0.1" +file-uri-to-path@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" + integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== + filelist@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.2.tgz#80202f21462d4d1c2e214119b1807c1bc0380e5b" @@ -2664,6 +2725,11 @@ mute-stream@0.0.8: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== +nan@^2.13.2: + version "2.15.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" + integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== + nanoid@3.1.23: version "3.1.23" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81" @@ -2920,7 +2986,7 @@ path-parse@^1.0.6: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -pbkdf2@^3.0.3: +pbkdf2@^3.0.3, pbkdf2@^3.0.9: version "3.1.2" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075" integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA== @@ -3624,6 +3690,17 @@ through@^2.3.6, through@^2.3.8: resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= +tiny-secp256k1@^1.1.3: + version "1.1.6" + resolved "https://registry.yarnpkg.com/tiny-secp256k1/-/tiny-secp256k1-1.1.6.tgz#7e224d2bee8ab8283f284e40e6b4acb74ffe047c" + integrity sha512-FmqJZGduTyvsr2cF3375fqGHUovSwDi/QytexX1Se4BPuPZpTE5Ftp5fg+EFSuEf3lhZqgCRjEG3ydUQ/aNiwA== + dependencies: + bindings "^1.3.0" + bn.js "^4.11.8" + create-hmac "^1.1.7" + elliptic "^6.4.0" + nan "^2.13.2" + tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -3732,6 +3809,11 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" +typeforce@^1.11.5: + version "1.18.0" + resolved "https://registry.yarnpkg.com/typeforce/-/typeforce-1.18.0.tgz#d7416a2c5845e085034d70fcc5b6cc4a90edbfdc" + integrity sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g== + typescript@^3.9.9: version "3.9.10" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8" @@ -4015,6 +4097,13 @@ wide-align@1.1.3: dependencies: string-width "^1.0.2 || 2" +wif@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/wif/-/wif-2.0.6.tgz#08d3f52056c66679299726fade0d432ae74b4704" + integrity sha1-CNP1IFbGZnkplyb63g1DKudLRwQ= + dependencies: + bs58check "<3.0.0" + wildcard@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec"