diff --git a/src/client/index.ts b/src/client/index.ts index 1fc60958..ecd98434 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -92,7 +92,6 @@ import getOrderbook from '../sugar/orderbook' import { submitTransaction, submitSignedTransaction } from '../sugar/submit' import { ensureClassicAddress } from '../sugar/utils' import combine from '../transaction/combine' -import { sign } from '../transaction/sign' import generateFaucetWallet from '../wallet/generateFaucetWallet' import { @@ -534,7 +533,6 @@ class Client extends EventEmitter { public getBalances = prepend(getBalances, this) public getOrderbook = prepend(getOrderbook, this) - public sign = sign public combine = combine public generateFaucetWallet = prepend(generateFaucetWallet, this) diff --git a/src/models/transactions/payment.ts b/src/models/transactions/payment.ts index caff8da0..be7e8b91 100644 --- a/src/models/transactions/payment.ts +++ b/src/models/transactions/payment.ts @@ -60,7 +60,6 @@ export function validatePayment(tx: Record): void { } if (tx.DestinationTag != null && typeof tx.DestinationTag !== 'number') { - console.log(tx.DestinationTag) throw new ValidationError( 'PaymentTransaction: DestinationTag must be a number', ) diff --git a/src/transaction/sign.ts b/src/transaction/sign.ts deleted file mode 100644 index beeca5e5..00000000 --- a/src/transaction/sign.ts +++ /dev/null @@ -1,253 +0,0 @@ -import BigNumber from 'bignumber.js' -import _ from 'lodash' -import binaryCodec from 'ripple-binary-codec' -import keypairs from 'ripple-keypairs' - -import type { Client, Wallet } from '..' -import { ValidationError } from '../common/errors' -import { Transaction } from '../models/transactions' -import { xrpToDrops } from '../utils' -import { computeSignedTransactionHash } from '../utils/hashes' - -import { SignOptions, KeyPair } from './types' - -function computeSignature(tx: object, privateKey: string, signAs?: string) { - const signingData = signAs - ? binaryCodec.encodeForMultisigning(tx, signAs) - : binaryCodec.encodeForSigning(tx) - return keypairs.sign(signingData, privateKey) -} - -function signWithKeypair( - client: Client | null, - txJSON: string, - keypair: KeyPair, - options: SignOptions = { - signAs: '', - }, -): { signedTransaction: string; id: string } { - const tx = JSON.parse(txJSON) - if (tx.TxnSignature || tx.Signers) { - throw new ValidationError( - 'txJSON must not contain "TxnSignature" or "Signers" properties', - ) - } - - if (client != null) { - checkFee(client, tx.Fee) - } - - const txToSignAndEncode = { ...tx } - - txToSignAndEncode.SigningPubKey = options.signAs ? '' : keypair.publicKey - - if (options.signAs) { - const signer = { - Account: options.signAs, - SigningPubKey: keypair.publicKey, - TxnSignature: computeSignature( - txToSignAndEncode, - keypair.privateKey, - options.signAs, - ), - } - txToSignAndEncode.Signers = [{ Signer: signer }] - } else { - txToSignAndEncode.TxnSignature = computeSignature( - txToSignAndEncode, - keypair.privateKey, - ) - } - const serialized = binaryCodec.encode(txToSignAndEncode) - checkTxSerialization(serialized, tx) - return { - signedTransaction: serialized, - id: computeSignedTransactionHash(serialized), - } -} - -/** - * Compares two objects and creates a diff. - * - * @param a - An object to compare. - * @param b - The other object to compare with. - * - * @returns An object containing the differences between the two objects. - */ -function objectDiff(a: object, b: object): object { - const diffs = {} - - // Compare two items and push non-matches to object - const compare = function (i1: any, i2: any, k: string): void { - const type1 = Object.prototype.toString.call(i1) - const type2 = Object.prototype.toString.call(i2) - if (type2 === '[object Undefined]') { - diffs[k] = null // Indicate that the item has been removed - return - } - if (type1 !== type2) { - diffs[k] = i2 // Indicate that the item has changed types - return - } - if (type1 === '[object Object]') { - const objDiff = objectDiff(i1, i2) - if (Object.keys(objDiff).length > 0) { - diffs[k] = objDiff - } - return - } - if (type1 === '[object Array]') { - if (!_.isEqual(i1, i2)) { - diffs[k] = i2 // If arrays do not match, add second item to diffs - } - return - } - if (type1 === '[object Function]') { - if (i1.toString() !== i2.toString()) { - diffs[k] = i2 // If functions differ, add second one to diffs - } - return - } - if (i1 !== i2) { - diffs[k] = i2 - } - } - - // Check items in first object - for (const key in a) { - if (a.hasOwnProperty(key)) { - compare(a[key], b[key], key) - } - } - - // Get items that are in the second object but not the first - for (const key in b) { - if (b.hasOwnProperty(key)) { - if (!a[key] && a[key] !== b[key]) { - diffs[key] = b[key] - } - } - } - - return diffs -} - -/** - * Decode a serialized transaction, remove the fields that are added during the signing process, - * and verify that it matches the transaction prior to signing. - * - * @param serialized - A signed and serialized transaction. - * @param tx - The transaction prior to signing. - * - * @returns This method does not return a value, but throws an error if the check fails. - */ -function checkTxSerialization(serialized: string, tx: Transaction): void { - // Decode the serialized transaction: - const decoded = binaryCodec.decode(serialized) - - // ...And ensure it is equal to the original tx, except: - // - It must have a TxnSignature or Signers (multisign). - if (!decoded.TxnSignature && !decoded.Signers) { - throw new ValidationError( - 'Serialized transaction must have a TxnSignature or Signers property', - ) - } - // - We know that the original tx did not have TxnSignature, so we should delete it: - delete decoded.TxnSignature - // - We know that the original tx did not have Signers, so if it exists, we should delete it: - delete decoded.Signers - - // - If SigningPubKey was not in the original tx, then we should delete it. - // But if it was in the original tx, then we should ensure that it has not been changed. - if (!tx.SigningPubKey) { - delete decoded.SigningPubKey - } - - // - Memos have exclusively hex data which should ignore case. - // Since decode goes to upper case, we set all tx memos to be uppercase for the comparison. - tx.Memos?.map((memo) => { - if (memo.Memo.MemoData) { - memo.Memo.MemoData = memo.Memo.MemoData.toUpperCase() - } - - if (memo.Memo.MemoType) { - memo.Memo.MemoType = memo.Memo.MemoType.toUpperCase() - } - - if (memo.Memo.MemoFormat) { - memo.Memo.MemoFormat = memo.Memo.MemoFormat.toUpperCase() - } - - return memo - }) - - if (!_.isEqual(decoded, tx)) { - const data = { - decoded, - tx, - diff: objectDiff(tx, decoded), - } - const error = new ValidationError( - 'Serialized transaction does not match original txJSON. See `error.data`', - data, - ) - throw error - } -} - -/** - * Check that a given transaction fee does not exceed maxFeeXRP (in drops). - * - * See https://xrpl.org/rippleapi-reference.html#parameters. - * - * @param client - A Client instance. - * @param txFee - The transaction fee in drops, encoded as a string. - * - * @returns This method does not return a value, but throws an error if the check fails. - */ -function checkFee(client: Client, txFee: string): void { - const fee = new BigNumber(txFee) - const maxFeeDrops = xrpToDrops(client.maxFeeXRP) - if (fee.isGreaterThan(maxFeeDrops)) { - throw new ValidationError( - `"Fee" should not exceed "${maxFeeDrops}". ` + - 'To use a higher fee, set `maxFeeXRP` in the Client constructor.', - ) - } -} - -function sign( - this: Client, - txJSON: string, - secret?: any, - options?: SignOptions, - keypair?: KeyPair, -): { signedTransaction: string; id: string } { - if (typeof secret === 'string') { - // we can't validate that the secret matches the account because - // the secret could correspond to the regular key - return signWithKeypair( - this, - txJSON, - keypairs.deriveKeypair(secret), - options, - ) - } - if (!keypair && !secret) { - // Clearer message than 'ValidationError: instance is not exactly one from [subschema 0],[subschema 1]' - throw new ValidationError('sign: Missing secret or keypair.') - } - return signWithKeypair(this, txJSON, keypair || secret, options) -} - -// TODO: move this to Wallet class -function signOffline( - wallet: Wallet, - txJSON: string, - options?: SignOptions, -): { signedTransaction: string; id: string } { - const { publicKey, privateKey } = wallet - return signWithKeypair(null, txJSON, { publicKey, privateKey }, options) -} - -export { sign, signOffline } diff --git a/src/wallet/index.ts b/src/wallet/index.ts index 3d8fe460..ec45163a 100644 --- a/src/wallet/index.ts +++ b/src/wallet/index.ts @@ -1,19 +1,28 @@ import { fromSeed } from 'bip32' import { mnemonicToSeedSync } from 'bip39' -import { classicAddressToXAddress } from 'ripple-address-codec' -import { decode, encodeForSigning } from 'ripple-binary-codec' +import _ from 'lodash' +import { + classicAddressToXAddress, + isValidXAddress, + xAddressToClassicAddress, +} from 'ripple-address-codec' +import { + decode, + encodeForSigning, + encodeForMultisigning, + encode, +} from 'ripple-binary-codec' import { deriveAddress, deriveKeypair, generateSeed, verify, + sign, } from 'ripple-keypairs' import ECDSA from '../common/ecdsa' import { ValidationError } from '../common/errors' import { Transaction } from '../models/transactions' -import { signOffline } from '../transaction/sign' -import { SignOptions } from '../transaction/types' const DEFAULT_ALGORITHM: ECDSA = ECDSA.ed25519 const DEFAULT_DERIVATION_PATH = "m/44'/144'/0'/0/0" @@ -30,6 +39,12 @@ function hexFromBuffer(buffer: Buffer): string { class Wallet { public readonly publicKey: string public readonly privateKey: string + /** + * This only is correct if this wallet corresponds to your + * [master keypair](https://xrpl.org/cryptographic-keys.html#master-key-pair). If this wallet represents a + * [regular keypair](https://xrpl.org/cryptographic-keys.html#regular-key-pair) this will provide an incorrect address. + * TODO: Add support for Regular Keys to Wallet (And their corresponding impact on figuring out classicAddress). + */ public readonly classicAddress: string public readonly seed?: string @@ -79,6 +94,7 @@ class Wallet { * @param algorithm - The digital signature algorithm to generate an address fro. * @returns A Wallet derived from a secret (AKA a seed). */ + // eslint-disable-next-line @typescript-eslint/member-ordering -- Member is used as a function here public static fromSecret = Wallet.fromSeed /** @@ -144,16 +160,49 @@ class Wallet { /** * Signs a transaction offline. * + * @param this - Wallet instance. * @param transaction - A transaction to be signed offline. - * @param options - Options to include for signing. + * @param multisignAddress - Multisign only. An account address corresponding to the multi-signature being added. If this + * wallet represents your [master keypair](https://xrpl.org/cryptographic-keys.html#master-key-pair) you can get your account address + * with the Wallet.getClassicAddress() function. * @returns A signed transaction. + * @throws ValidationError if the transaction is already signed or does not encode/decode to same result. */ public signTransaction( + this: Wallet, transaction: Transaction, - options: SignOptions = { signAs: '' }, + multisignAddress?: string, ): string { - return signOffline(this, JSON.stringify(transaction), options) - .signedTransaction + if (transaction.TxnSignature || transaction.Signers) { + throw new ValidationError( + 'txJSON must not contain "TxnSignature" or "Signers" properties', + ) + } + + const txToSignAndEncode = { ...transaction } + + txToSignAndEncode.SigningPubKey = multisignAddress ? '' : this.publicKey + + if (multisignAddress) { + const signer = { + Account: multisignAddress, + SigningPubKey: this.publicKey, + TxnSignature: computeSignature( + txToSignAndEncode, + this.privateKey, + multisignAddress, + ), + } + txToSignAndEncode.Signers = [{ Signer: signer }] + } else { + txToSignAndEncode.TxnSignature = computeSignature( + txToSignAndEncode, + this.privateKey, + ) + } + const serialized = encode(txToSignAndEncode) + this.checkTxSerialization(serialized, transaction) + return serialized } /** @@ -181,13 +230,104 @@ class Wallet { } /** - * Gets the classic address of the account this wallet represents. + * Gets the classic address of the account this wallet represents. This only is correct if this wallet corresponds + * to your [master keypair](https://xrpl.org/cryptographic-keys.html#master-key-pair). If this wallet represents a + * [regular keypair](https://xrpl.org/cryptographic-keys.html#regular-key-pair) this will provide an incorrect address. * * @returns A classic address. */ public getClassicAddress(): string { - return deriveAddress(this.publicKey) + return this.classicAddress + } + + /** + * Decode a serialized transaction, remove the fields that are added during the signing process, + * and verify that it matches the transaction prior to signing. This gives the user a sanity check + * to ensure that what they try to encode matches the message that will be recieved by rippled. + * + * @param serialized - A signed and serialized transaction. + * @param tx - The transaction prior to signing. + * @throws A ValidationError if the transaction does not have a TxnSignature/Signers property, or if + * the serialized Transaction desn't match the original transaction. + */ + // eslint-disable-next-line class-methods-use-this -- Helper for organization purposes + private checkTxSerialization(serialized: string, tx: Transaction): void { + // Decode the serialized transaction: + const decoded = decode(serialized) + const txCopy = { ...tx } + + // ...And ensure it is equal to the original tx, except: + // - It must have a TxnSignature or Signers (multisign). + if (!decoded.TxnSignature && !decoded.Signers) { + throw new ValidationError( + 'Serialized transaction must have a TxnSignature or Signers property', + ) + } + // - We know that the original tx did not have TxnSignature, so we should delete it: + delete decoded.TxnSignature + // - We know that the original tx did not have Signers, so if it exists, we should delete it: + delete decoded.Signers + + // - If SigningPubKey was not in the original tx, then we should delete it. + // But if it was in the original tx, then we should ensure that it has not been changed. + if (!tx.SigningPubKey) { + delete decoded.SigningPubKey + } + + // - Memos have exclusively hex data which should ignore case. + // Since decode goes to upper case, we set all tx memos to be uppercase for the comparison. + txCopy.Memos?.map((memo) => { + const memoCopy = { ...memo } + if (memo.Memo.MemoData) { + memoCopy.Memo.MemoData = memo.Memo.MemoData.toUpperCase() + } + + if (memo.Memo.MemoType) { + memoCopy.Memo.MemoType = memo.Memo.MemoType.toUpperCase() + } + + if (memo.Memo.MemoFormat) { + memoCopy.Memo.MemoFormat = memo.Memo.MemoFormat.toUpperCase() + } + + return memo + }) + if (!_.isEqual(decoded, tx)) { + const data = { + decoded, + tx, + } + const error = new ValidationError( + 'Serialized transaction does not match original txJSON. See error.data', + data, + ) + throw error + } } } +/** + * Signs a transaction with the proper signing encoding. + * + * @param tx - A transaction to sign. + * @param privateKey - A key to sign the transaction with. + * @param signAs - Multisign only. An account address to include in the Signer field. + * Can be either a classic address or an XAddress. + * @returns A signed transaction in the proper format. + */ +function computeSignature( + tx: Transaction, + privateKey: string, + signAs?: string, +): string { + if (signAs) { + const classicAddress = isValidXAddress(signAs) + ? xAddressToClassicAddress(signAs).classicAddress + : signAs + + return sign(encodeForMultisigning(tx, classicAddress), privateKey) + } + return sign(encodeForSigning(tx), privateKey) +} + export default Wallet diff --git a/src/wallet/signer.ts b/src/wallet/signer.ts index d0a09eef..e01575f1 100644 --- a/src/wallet/signer.ts +++ b/src/wallet/signer.ts @@ -28,7 +28,7 @@ import Wallet from '.' function sign(wallet: Wallet, tx: Transaction, forMultisign = false): string { return wallet.signTransaction( tx, - forMultisign ? { signAs: wallet.getClassicAddress() } : { signAs: '' }, + forMultisign ? wallet.getClassicAddress() : '', ) } diff --git a/test/client/sign.ts b/test/client/sign.ts deleted file mode 100644 index a30c0a90..00000000 --- a/test/client/sign.ts +++ /dev/null @@ -1,376 +0,0 @@ -import { assert, expect } from 'chai' -import binary from 'ripple-binary-codec' - -import { ValidationError } from '../../src/common/errors' -import requests from '../fixtures/requests' -import responses from '../fixtures/responses' -import { setupClient, teardownClient } from '../setupClient' - -const { sign: REQUEST_FIXTURES } = requests -const { sign: RESPONSE_FIXTURES } = responses - -describe('client.sign', function () { - beforeEach(setupClient) - afterEach(teardownClient) - it('sign', async function () { - const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV' - const result = this.client.sign(REQUEST_FIXTURES.normal.txJSON, secret) - assert.deepEqual(result, RESPONSE_FIXTURES.normal) - }) - - it('sign with lowercase hex data in memo (hex should be case insensitive)', async function () { - const secret = 'shd2nxpFD6iBRKWsRss2P4tKMWyy9' - const lowercaseMemoTxJson = { - TransactionType: 'Payment', - Flags: 2147483648, - Account: 'rwiZ3q3D3QuG4Ga2HyGdq3kPKJRGctVG8a', - Amount: '10000000', - LastLedgerSequence: 14000999, - Destination: 'rUeEBYXHo8vF86Rqir3zWGRQ84W9efdAQd', - Fee: '12', - Sequence: 12, - SourceTag: 8888, - DestinationTag: 9999, - Memos: [ - { - Memo: { - MemoType: - '687474703a2f2f6578616d706c652e636f6d2f6d656d6f2f67656e65726963', - MemoData: '72656e74', - }, - }, - ], - } - - const txParams = JSON.stringify(lowercaseMemoTxJson) - const result = this.client.sign(txParams, secret) - assert.deepEqual(result, { - signedTransaction: - '120000228000000023000022B8240000000C2E0000270F201B00D5A36761400000000098968068400000000000000C73210305E09ED602D40AB1AF65646A4007C2DAC17CB6CDACDE301E74FB2D728EA057CF744730450221009C00E8439E017CA622A5A1EE7643E26B4DE9C808DE2ABE45D33479D49A4CEC66022062175BE8733442FA2A4D9A35F85A57D58252AE7B19A66401FE238B36FA28E5A081146C1856D0E36019EA75C56D7E8CBA6E35F9B3F71583147FB49CD110A1C46838788CD12764E3B0F837E0DDF9EA7C1F687474703A2F2F6578616D706C652E636F6D2F6D656D6F2F67656E657269637D0472656E74E1F1', - id: '41B9CB78D8E18A796CDD4B0BC6FB0EA19F64C4F25FDE23049197852CAB71D10D', - }) - }) - - it('EscrowExecution', async function () { - const secret = 'snoPBrXtMeMyMHUVTgbuqAfg1SUTb' - const result = this.client.sign(REQUEST_FIXTURES.escrow.txJSON, secret) - assert.deepEqual(result, RESPONSE_FIXTURES.escrow) - }) - - it('signAs', async function () { - const txJSON = REQUEST_FIXTURES.signAs - const secret = 'snoPBrXtMeMyMHUVTgbuqAfg1SUTb' - const signature = this.client.sign(JSON.stringify(txJSON), secret, { - signAs: 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', - }) - assert.deepEqual(signature, RESPONSE_FIXTURES.signAs) - }) - - it('withKeypair', async function () { - const keypair = { - privateKey: - '00ACCD3309DB14D1A4FC9B1DAE608031F4408C85C73EE05E035B7DC8B25840107A', - publicKey: - '02F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8', - } - const result = this.client.sign(REQUEST_FIXTURES.normal.txJSON, keypair) - assert.deepEqual(result, RESPONSE_FIXTURES.normal) - }) - - it('withKeypair already signed', async function () { - const keypair = { - privateKey: - '00ACCD3309DB14D1A4FC9B1DAE608031F4408C85C73EE05E035B7DC8B25840107A', - publicKey: - '02F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8', - } - const result = this.client.sign(REQUEST_FIXTURES.normal.txJSON, keypair) - assert.throws(() => { - const tx = JSON.stringify(binary.decode(result.signedTransaction)) - this.client.sign(tx, keypair) - }, /txJSON must not contain "TxnSignature" or "Signers" properties/u) - }) - - it('withKeypair EscrowExecution', async function () { - const keypair = { - privateKey: - '001ACAAEDECE405B2A958212629E16F2EB46B153EEE94CDD350FDEFF52795525B7', - publicKey: - '0330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD020', - } - const result = this.client.sign(REQUEST_FIXTURES.escrow.txJSON, keypair) - assert.deepEqual(result, RESPONSE_FIXTURES.escrow) - }) - - it('withKeypair signAs', async function () { - const txJSON = REQUEST_FIXTURES.signAs - const keypair = { - privateKey: - '001ACAAEDECE405B2A958212629E16F2EB46B153EEE94CDD350FDEFF52795525B7', - publicKey: - '0330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD020', - } - const signature = this.client.sign(JSON.stringify(txJSON), keypair, { - signAs: 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', - }) - assert.deepEqual(signature, RESPONSE_FIXTURES.signAs) - }) - - it('already signed', async function () { - const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV' - const result = this.client.sign(REQUEST_FIXTURES.normal.txJSON, secret) - assert.throws(() => { - const tx = JSON.stringify(binary.decode(result.signedTransaction)) - this.client.sign(tx, secret) - }, /txJSON must not contain "TxnSignature" or "Signers" properties/u) - }) - - it('succeeds - no flags', async function () { - const txJSON = - '{"TransactionType":"Payment","Account":"r45Rev1EXGxy2hAUmJPCne97KUE7qyrD3j","Destination":"rQ3PTWGLCbPz8ZCicV5tCX3xuymojTng5r","Amount":"20000000","Sequence":1,"Fee":"12"}' - const secret = 'shotKgaEotpcYsshSE39vmSnBDRim' - const result = this.client.sign(txJSON, secret) - const expectedResult = { - signedTransaction: - '1200002400000001614000000001312D0068400000000000000C7321022B05847086686F9D0499B13136B94AD4323EE1B67D4C429ECC987AB35ACFA34574473045022100C104B7B97C31FACA4597E7D6FCF13BD85BD11375963A62A0AC45B0061236E39802207784F157F6A98DFC85B051CDDF61CC3084C4F5750B82674801C8E9950280D1998114EE3046A5DDF8422C40DDB93F1D522BB4FE6419158314FDB08D07AAA0EB711793A3027304D688E10C3648', - id: '0596925967F541BF332FF6756645B2576A9858414B5B363DC3D34915BE8A70D6', - } - const decoded = binary.decode(result.signedTransaction) - assert( - decoded.Flags == null, - `Flags = ${decoded.Flags as number}, should be undefined`, - ) - assert.deepEqual(result, expectedResult) - }) - - it('sign succeeds with source.amount/destination.minAmount', async function () { - // See also: 'preparePayment with source.amount/destination.minAmount' - - const txJSON = - '{"TransactionType":"Payment","Account":"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59","Destination":"rEX4LtGJubaUcMWCJULcy4NVxGT9ZEMVRq","Amount":{"currency":"USD","issuer":"rMaa8VLBTjwTJWA2kSme4Sqgphhr6Lr6FH","value":"999999999999999900000000000000000000000000000000000000000000000000000000000000000000000000000000"},"Flags":2147614720,"SendMax":{"currency":"GBP","issuer":"rpat5TmYjDsnFSStmgTumFgXCM9eqsWPro","value":"0.1"},"DeliverMin":{"currency":"USD","issuer":"rMaa8VLBTjwTJWA2kSme4Sqgphhr6Lr6FH","value":"0.1248548562296331"},"Sequence":23,"LastLedgerSequence":8820051,"Fee":"12"}' - const secret = 'shotKgaEotpcYsshSE39vmSnBDRim' - const result = this.client.sign(txJSON, secret) - const expectedResult = { - signedTransaction: - '12000022800200002400000017201B0086955361EC6386F26FC0FFFF0000000000000000000000005553440000000000DC596C88BCDE4E818D416FCDEEBF2C8656BADC9A68400000000000000C69D4438D7EA4C6800000000000000000000000000047425000000000000C155FFE99C8C91F67083CEFFDB69EBFE76348CA6AD4446F8C5D8A5E0B0000000000000000000000005553440000000000DC596C88BCDE4E818D416FCDEEBF2C8656BADC9A7321022B05847086686F9D0499B13136B94AD4323EE1B67D4C429ECC987AB35ACFA34574473045022100D9634523D8E232D4A7807A71856023D82AC928FA29848571B820867898413B5F022041AC00EC1F81A26A6504EBF844A38CC3204694EF2CC1A97A87632721631F93DA81145E7B112523F68D2F5E879DB4EAC51C6698A6930483149F500E50C2F016CA01945E5A1E5846B61EF2D376', - id: '1C558AA9B926C24FB6BBD6950B2DB1350A83F9F12E4385208867907019761A2D', - } - const decoded = binary.decode(result.signedTransaction) - assert( - decoded.Flags === 2147614720, - `Flags = ${decoded.Flags as number}, should be 2147614720`, - ) - assert.deepEqual(result, expectedResult) - }) - - it('throws when encoded tx does not match decoded tx - AccountSet', async function () { - const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV' - const request = { - // TODO: This fails when address is X-address - txJSON: `{"Flags":2147483648,"TransactionType":"AccountSet","Account":"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59","Domain":"6578616D706C652E636F6D","LastLedgerSequence":8820051,"Fee":"1.2","Sequence":23,"SigningPubKey":"02F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8"}`, - instructions: { - fee: '0.0000012', - sequence: 23, - maxLedgerVersion: 8820051, - }, - } - - assert.throws(() => { - this.client.sign(request.txJSON, secret) - }, /1\.2 is an illegal amount/u) - }) - - it('throws when encoded tx does not match decoded tx - higher fee', async function () { - const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV' - const request = { - // TODO: This fails when address is X-address - txJSON: `{"Flags":2147483648,"TransactionType":"AccountSet","Account":"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59","Domain":"6578616D706C652E636F6D","LastLedgerSequence":8820051,"Fee":"1123456.7","Sequence":23,"SigningPubKey":"02F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8"}`, - instructions: { - fee: '1.1234567', - sequence: 23, - maxLedgerVersion: 8820051, - }, - } - - assert.throws(() => { - this.client.sign(request.txJSON, secret) - }, /1123456\.7 is an illegal amount/u) - }) - - it('permits fee exceeding 2000000 drops when maxFeeXRP is higher than 2 XRP', async function () { - this.client.maxFeeXRP = '2.1' - const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV' - const request = { - // TODO: This fails when address is X-address - txJSON: `{"Flags":2147483648,"TransactionType":"AccountSet","Account":"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59","LastLedgerSequence":8820051,"Fee":"2010000","Sequence":23,"SigningPubKey":"02F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8"}`, - instructions: { - fee: '2.01', - sequence: 23, - maxLedgerVersion: 8820051, - }, - } - - const result = this.client.sign(request.txJSON, secret) - - const expectedResponse = { - signedTransaction: - '12000322800000002400000017201B008695536840000000001EAB90732102F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D87446304402200203F219F5371D2C6506888B1B02B27E74998F7A42D412C32FE319AC1A5B8DEF02205959A1B02253ACCCE542759E9886466C56D16B04676FA492AD34AA0E877E91F381145E7B112523F68D2F5E879DB4EAC51C6698A69304', - id: '061D5593E0A117F389826419CAC049A73C7CFCA65A20B788781D41240143D864', - } - - assert.deepEqual(result, expectedResponse) - }) - - it('sign with ticket', async function () { - const secret = 'sn7n5R1cR5Y3fRFkuWXA94Ts1frVJ' - const result = this.client.sign(REQUEST_FIXTURES.ticket.txJSON, secret) - assert.deepEqual(result, RESPONSE_FIXTURES.ticket) - }) - - it('throws when Fee exceeds maxFeeXRP (in drops)', async function () { - const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV' - const request = { - txJSON: `{"Flags":2147483648,"TransactionType":"AccountSet","Account":"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59","Domain":"6578616D706C652E636F6D","LastLedgerSequence":8820051,"Fee":"2010000","Sequence":23,"SigningPubKey":"02F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8"}`, - instructions: { - fee: '2.01', - sequence: 23, - maxLedgerVersion: 8820051, - }, - } - - assert.throws(() => { - this.client.sign(request.txJSON, secret) - }, /Fee" should not exceed "2000000"\. To use a higher fee, set `maxFeeXRP` in the Client constructor\./u) - }) - - it('throws when Fee exceeds maxFeeXRP (in drops) - custom maxFeeXRP', async function () { - this.client.maxFeeXRP = '1.9' - const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV' - const request = { - txJSON: `{"Flags":2147483648,"TransactionType":"AccountSet","Account":"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59","Domain":"6578616D706C652E636F6D","LastLedgerSequence":8820051,"Fee":"2010000","Sequence":23,"SigningPubKey":"02F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8"}`, - instructions: { - fee: '2.01', - sequence: 23, - maxLedgerVersion: 8820051, - }, - } - - assert.throws(() => { - this.client.sign(request.txJSON, secret) - }, /Fee" should not exceed "1900000"\. To use a higher fee, set `maxFeeXRP` in the Client constructor\./u) - }) - - it('sign with paths', async function () { - const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV' - const payment = { - TransactionType: 'Payment', - Account: 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', - Destination: 'rKT4JX4cCof6LcDYRz8o3rGRu7qxzZ2Zwj', - Amount: { - currency: 'USD', - issuer: 'rVnYNK9yuxBz4uP8zC8LEFokM2nqH3poc', - value: - '999999999999999900000000000000000000000000000000000000000000000000000000000000000000000000000000', - }, - Flags: 2147614720, - SendMax: '100', - DeliverMin: { - currency: 'USD', - issuer: 'rVnYNK9yuxBz4uP8zC8LEFokM2nqH3poc', - value: '0.00004579644712312366', - }, - Paths: [ - [{ currency: 'USD', issuer: 'rVnYNK9yuxBz4uP8zC8LEFokM2nqH3poc' }], - ], - LastLedgerSequence: 15696358, - Sequence: 1, - Fee: '12', - } - const result = this.client.sign(JSON.stringify(payment), secret) - assert.deepEqual(result, { - signedTransaction: - '12000022800200002400000001201B00EF81E661EC6386F26FC0FFFF0000000000000000000000005553440000000000054F6F784A58F9EFB0A9EB90B83464F9D166461968400000000000000C6940000000000000646AD3504529A0465E2E0000000000000000000000005553440000000000054F6F784A58F9EFB0A9EB90B83464F9D1664619732102F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D87446304402200A693FB5CA6B21250EBDFD8CFF526EE0DF7C9E4E31EB0660692E75E6A93BF5F802203CC39463DDA21386898CA31E18AD1A6828647D65741DD637BAD71BC83E29DB9481145E7B112523F68D2F5E879DB4EAC51C6698A693048314CA6EDC7A28252DAEA6F2045B24F4D7C333E146170112300000000000000000000000005553440000000000054F6F784A58F9EFB0A9EB90B83464F9D166461900', - id: '78874FE5F5299FEE3EA85D3CF6C1FB1F1D46BB08F716662A3E3D1F0ADE4EF796', - }) - }) - - it('succeeds - prepared payment', async function () { - const payment = { - TransactionType: 'Payment', - Account: 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', - Destination: 'rQ3PTWGLCbPz8ZCicV5tCX3xuymojTng5r', - Amount: '1', - Flags: 2147483648, - Sequence: 23, - LastLedgerSequence: 8819954, - Fee: '12', - } - const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV' - const result = this.client.sign(JSON.stringify(payment), secret) - const expectedResult = { - signedTransaction: - '12000022800000002400000017201B008694F261400000000000000168400000000000000C732102F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D874473045022100A9C91D4CFAE45686146EE0B56D4C53A2E7C2D672FB834D43E0BE2D2E9106519A022075DDA2F92DE552B0C45D83D4E6D35889B3FBF51BFBBD9B25EBF70DE3C96D0D6681145E7B112523F68D2F5E879DB4EAC51C6698A693048314FDB08D07AAA0EB711793A3027304D688E10C3648', - id: '88D6B913C66279EA31ADC25C5806C48B2D4E5680261666790A736E1961217700', - } - assert.deepEqual(result, expectedResult) - }) - - it('throws when encoded tx does not match decoded tx - prepared payment', async function () { - const payment = { - TransactionType: 'Payment', - Account: 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', - Destination: 'rQ3PTWGLCbPz8ZCicV5tCX3xuymojTng5r', - Amount: '1.1234567', - Flags: 2147483648, - Sequence: 23, - LastLedgerSequence: 8819954, - Fee: '12', - } - const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV' - assert.throws(() => { - this.client.sign(JSON.stringify(payment), secret) - }, /^1.1234567 is an illegal amount/u) - }) - - it('throws when encoded tx does not match decoded tx - prepared order', async function () { - const offerCreate = { - TransactionType: 'OfferCreate', - Account: 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', - TakerGets: { - currency: 'USD', - issuer: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', - value: '3.140000', - }, - TakerPays: '31415000000', - Flags: 2148007936, - Sequence: 123, - LastLedgerSequence: 8819954, - Fee: '12', - } - const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV' - - try { - this.client.sign(JSON.stringify(offerCreate), secret) - return await Promise.reject( - new Error('this.client.sign should have thrown'), - ) - } catch (error) { - if (error instanceof ValidationError) { - assert.equal(error.name, 'ValidationError') - assert.equal( - error.message, - 'Serialized transaction does not match original txJSON. See `error.data`', - ) - expect(error.data).to.deep.include({ - diff: { - TakerGets: { - value: '3.14', - }, - }, - }) - } else { - assert(false) - } - } - }) -}) diff --git a/test/fixtures/requests/sign.json b/test/fixtures/requests/sign.json index d5f39af8..b8003858 100644 --- a/test/fixtures/requests/sign.json +++ b/test/fixtures/requests/sign.json @@ -1,8 +1,10 @@ { - "txJSON": "{\"Flags\":2147483648,\"TransactionType\":\"AccountSet\",\"Account\":\"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59\",\"Domain\":\"6578616D706C652E636F6D\",\"LastLedgerSequence\":8820051,\"Fee\":\"12\",\"Sequence\":23,\"SigningPubKey\":\"02F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8\"}", - "instructions": { - "fee": "0.000012", - "sequence": 23, - "maxLedgerVersion": 8820051 - } + "TransactionType":"AccountSet", + "Flags":2147483648, + "Sequence":23, + "LastLedgerSequence":8820051, + "Fee":"12", + "SigningPubKey":"02A8A44DB3D4C73EEEE11DFE54D2029103B776AA8A8D293A91D645977C9DF5F544", + "Domain":"6578616D706C652E636F6D", + "Account":"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59" } diff --git a/test/fixtures/requests/signEscrow.json b/test/fixtures/requests/signEscrow.json index 36a8e3bd..f252d7e2 100644 --- a/test/fixtures/requests/signEscrow.json +++ b/test/fixtures/requests/signEscrow.json @@ -1,8 +1,12 @@ { - "txJSON": "{\"TransactionType\":\"EscrowFinish\",\"Account\":\"rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh\",\"Owner\":\"rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh\",\"OfferSequence\":2,\"Condition\":\"712C36933822AD3A3D136C5DF97AA863B69F9CE88B2D6CE6BDD11BFDE290C19D\",\"Fulfillment\":\"74686973206D757374206861766520333220636861726163746572732E2E2E2E\",\"Flags\":2147483648,\"LastLedgerSequence\":102,\"Fee\":\"12\",\"Sequence\":1}", - "instructions": { - "fee": "0.000012", - "sequence": 1, - "maxLedgerVersion": 102 - } + "TransactionType":"EscrowFinish", + "Account":"rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "Owner":"rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "OfferSequence":2, + "Condition":"712C36933822AD3A3D136C5DF97AA863B69F9CE88B2D6CE6BDD11BFDE290C19D", + "Fulfillment":"74686973206D757374206861766520333220636861726163746572732E2E2E2E", + "Flags":2147483648, + "LastLedgerSequence":102, + "Fee":"12", + "Sequence":1 } diff --git a/test/fixtures/requests/signTicket.json b/test/fixtures/requests/signTicket.json index 021265f7..12d0df6c 100644 --- a/test/fixtures/requests/signTicket.json +++ b/test/fixtures/requests/signTicket.json @@ -1,9 +1,8 @@ { - "txJSON": "{\"TransactionType\": \"TicketCreate\", \"TicketCount\": 1, \"Account\": \"r4SDqUD1ZcfoZrhnsZ94XNFKxYL4oHYJyA\", \"Sequence\": 0, \"TicketSequence\": 23, \"Fee\": \"10000\"}", - "instructions": { - "ticketSequence": 23, - "maxLedgerVersion": 8820051 - } + "TransactionType":"TicketCreate", + "TicketCount":1, + "Account":"r4SDqUD1ZcfoZrhnsZ94XNFKxYL4oHYJyA", + "Sequence":0, + "TicketSequence":23, + "Fee":"10000" } - - diff --git a/test/fixtures/responses/sign.json b/test/fixtures/responses/sign.json index 150d42cd..d0063340 100644 --- a/test/fixtures/responses/sign.json +++ b/test/fixtures/responses/sign.json @@ -1,4 +1,4 @@ { - "signedTransaction": "12000322800000002400000017201B0086955368400000000000000C732102F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8744630440220680070A157682D9EB510E8AD58C35DC9C8346B155077D73792E88120B7A3B6B1022079537D3300C9B4D2D3D62ACCE1E66CDA893F9612CB2577ADEC8154B933765336770B6578616D706C652E636F6D81145E7B112523F68D2F5E879DB4EAC51C6698A69304", - "id": "10B54D31384A49336C36A5907E3C28227139E282D3C7F734FEA351DE446F3674" + "signedTransaction": "12000322800000002400000017201B0086955368400000000000000C732102A8A44DB3D4C73EEEE11DFE54D2029103B776AA8A8D293A91D645977C9DF5F54474463044022025464FA5466B6E28EEAD2E2D289A7A36A11EB9B269D211F9C76AB8E8320694E002205D5F99CB56E5A996E5636A0E86D029977BEFA232B7FB64ABA8F6E29DC87A9E89770B6578616D706C652E636F6D81145E7B112523F68D2F5E879DB4EAC51C6698A69304", + "id": "93F6C6CE73C092AA005103223F3A1F557F4C097A2943D96760F6490F04379917" } diff --git a/test/fixtures/responses/signAs.json b/test/fixtures/responses/signAs.json index afe039b8..d71cec23 100644 --- a/test/fixtures/responses/signAs.json +++ b/test/fixtures/responses/signAs.json @@ -1,4 +1,4 @@ { - "signedTransaction": "120000240000000261400000003B9ACA00684000000000000032730081142E244E6F20104E57C0C60BD823CB312BF10928C78314B5F762798A53D543A014CAF8B297CFF8F2F937E8F3E01073210330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD02074473045022100BB6FC77F26BC88587204CAA79B2230C420D7EC937B8AC3A0CF9B0BE988BAB0D002203BF86893BA3B764375FFFAD9D54A4AAEDABD07C4D72ADB9C1B20C10B4DD712898114B5F762798A53D543A014CAF8B297CFF8F2F937E8E1F1", - "id": "AB7632D7C07E591658635CED6A5DDE832B22CA066907CB131DEFAAA925B98185" + "signedTransaction": "120000240000000261400000003B9ACA00684000000000000032730081142E244E6F20104E57C0C60BD823CB312BF10928C78314B5F762798A53D543A014CAF8B297CFF8F2F937E8F3E010732102A8A44DB3D4C73EEEE11DFE54D2029103B776AA8A8D293A91D645977C9DF5F54474473045022100B3F8205578C6A68D3BBD27650F5D2E983718D502C250C5147F07B7EDD8E8583E02207B892818BD58E328C2797F15694A505937861586D527849065B582523E390B128114B3263BD0A9BF9DFDBBBBD07F536355FF477BF0E9E1F1", + "id": "D8CF5FC93CFE5E131A34599AFB7CE186A5B8D1B9F069E35F4634AD3B27837E35" } diff --git a/test/fixtures/responses/signEscrow.json b/test/fixtures/responses/signEscrow.json index e676db50..48cd6efe 100644 --- a/test/fixtures/responses/signEscrow.json +++ b/test/fixtures/responses/signEscrow.json @@ -1,4 +1,4 @@ { - "signedTransaction": "12000222800000002400000001201900000002201B0000006668400000000000000C73210330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD0207446304402205CDD40611008680E151EF6C5C70A6DFBF7977DE73800E04F1B6F1FE8688BBE3E022075DB3990E6CBF0BCD990D92FA13E3D3F95510CA2CCBAB92BD2CE288FA6F2F34870102074686973206D757374206861766520333220636861726163746572732E2E2E2E701120712C36933822AD3A3D136C5DF97AA863B69F9CE88B2D6CE6BDD11BFDE290C19D8114B5F762798A53D543A014CAF8B297CFF8F2F937E88214B5F762798A53D543A014CAF8B297CFF8F2F937E8", - "id": "E76178CD799A82BAB6EE83A70DE0060F0BDC8BDE29980F0832D791D8D9D5CACC" + "signedTransaction": "12000222800000002400000001201900000002201B0000006668400000000000000C732102A8A44DB3D4C73EEEE11DFE54D2029103B776AA8A8D293A91D645977C9DF5F5447446304402204652E8572AEED964451C603EB110AC9945A65E3C5C288D144BB02F259755F6E202205B64E27293248F0650A3F7A4FD66BC16A61F4883AC3ED8EE8A48EF569C06812070102074686973206D757374206861766520333220636861726163746572732E2E2E2E701120712C36933822AD3A3D136C5DF97AA863B69F9CE88B2D6CE6BDD11BFDE290C19D8114B5F762798A53D543A014CAF8B297CFF8F2F937E88214B5F762798A53D543A014CAF8B297CFF8F2F937E8", + "id": "645B7676DF057E4F5E83F970A18B3751B6813807F1030A8D2F482D02DC885106" } diff --git a/test/fixtures/responses/signTicket.json b/test/fixtures/responses/signTicket.json index 9d25ad04..f8f9a5f6 100644 --- a/test/fixtures/responses/signTicket.json +++ b/test/fixtures/responses/signTicket.json @@ -1,4 +1,4 @@ { - "signedTransaction": "12000A2400000000202800000001202900000017684000000000002710732103E985C55BDCE4171394A0521AA84C71F81425680A7CE510AEF49662CF5A78D38674473045022100A77F102B632779C0E3F25B8715CB8FF2A15A702F3A39D1E6416C981B604D2E0302207E1DB8418D547E8AE322F49585E1C554E8759C0FBF7B88158BE3D0EE6B2E4E0A8114EB1FC04FDA0248FB6DE5BA4235425773D61DF0F3", - "id": "9F1002A8DB9D06D5F3AB2070387F17E12421DCE8444EED13E5F6928291EB4F43" + "signedTransaction": "12000A2400000000202800000001202900000017684000000000002710732102A8A44DB3D4C73EEEE11DFE54D2029103B776AA8A8D293A91D645977C9DF5F54474473045022100896CFE083767C88B9539DE2F28894429BC2760865161792D576C090BB93E1EAA02203015D30BC59245C8CFB8A9D78386B91B251DCB946A4C0FAB12A5FA41C202FF3A8114EB1FC04FDA0248FB6DE5BA4235425773D61DF0F3", + "id": "0AC60B1E1F063904D9D9D0E9D03F2E9C8D41BC6FC872D5B8BF87E15BBF9669BB" } diff --git a/test/wallet/index.ts b/test/wallet/index.ts index 3b21d52d..b2f013c6 100644 --- a/test/wallet/index.ts +++ b/test/wallet/index.ts @@ -1,9 +1,14 @@ import { assert } from 'chai' - -import { Payment } from 'xrpl-local' +import { decode } from 'ripple-binary-codec/dist' import ECDSA from '../../src/common/ecdsa' +import { Transaction } from '../../src/models/transactions' import Wallet from '../../src/wallet' +import requests from '../fixtures/requests' +import responses from '../fixtures/responses' + +const { sign: REQUEST_FIXTURES } = requests +const { sign: RESPONSE_FIXTURES } = responses /** * Wallet testing. @@ -181,27 +186,267 @@ describe('Wallet', function () { }) describe('signTransaction', function () { - const publicKey = - '030E58CDD076E798C84755590AAF6237CA8FAE821070A59F648B517A30DC6F589D' - const privateKey = - '00141BA006D3363D2FB2785E8DF4E44D3A49908780CB4FB51F6D217C08C021429F' - const address = 'rhvh5SrgBL5V8oeV9EpDuVszeJSSCEkbPc' + let wallet: Wallet - it('signs a transaction offline', function () { - const txJSON: Payment = { + beforeEach(function () { + wallet = Wallet.fromSeed('ss1x3KLrSvfg7irFc1D929WXZ7z9H') + }) + + it('signTransaction successfully', async function () { + const result = wallet.signTransaction( + REQUEST_FIXTURES.normal as Transaction, + ) + assert.deepEqual(result, RESPONSE_FIXTURES.normal.signedTransaction) + }) + + it('signTransaction with lowercase hex data in memo (hex should be case insensitive)', async function () { + const secret = 'shd2nxpFD6iBRKWsRss2P4tKMWyy9' + const lowercaseMemoTx: Transaction = { TransactionType: 'Payment', - Account: address, + Flags: 2147483648, + Account: 'rwiZ3q3D3QuG4Ga2HyGdq3kPKJRGctVG8a', + Amount: '10000000', + LastLedgerSequence: 14000999, + Destination: 'rUeEBYXHo8vF86Rqir3zWGRQ84W9efdAQd', + Fee: '12', + Sequence: 12, + SourceTag: 8888, + DestinationTag: 9999, + Memos: [ + { + Memo: { + MemoType: + '687474703a2f2f6578616d706c652e636f6d2f6d656d6f2f67656e65726963', + MemoData: '72656e74', + }, + }, + ], + } + + const result = Wallet.fromSeed(secret).signTransaction(lowercaseMemoTx) + assert.equal( + result, + '120000228000000023000022B8240000000C2E0000270F201B00D5A36761400000000098968068400000000000000C73210305E09ED602D40AB1AF65646A4007C2DAC17CB6CDACDE301E74FB2D728EA057CF744730450221009C00E8439E017CA622A5A1EE7643E26B4DE9C808DE2ABE45D33479D49A4CEC66022062175BE8733442FA2A4D9A35F85A57D58252AE7B19A66401FE238B36FA28E5A081146C1856D0E36019EA75C56D7E8CBA6E35F9B3F71583147FB49CD110A1C46838788CD12764E3B0F837E0DDF9EA7C1F687474703A2F2F6578616D706C652E636F6D2F6D656D6F2F67656E657269637D0472656E74E1F1', + ) + }) + + it('signTransaction with EscrowFinish', async function () { + const result = wallet.signTransaction( + REQUEST_FIXTURES.escrow as Transaction, + ) + assert.deepEqual(result, RESPONSE_FIXTURES.escrow.signedTransaction) + }) + + it('signTransaction with multisignAddress', async function () { + const signature = wallet.signTransaction( + REQUEST_FIXTURES.signAs as Transaction, + wallet.getClassicAddress(), + ) + assert.deepEqual(signature, RESPONSE_FIXTURES.signAs.signedTransaction) + }) + + it('signTransaction with X Address and no given tag for multisignAddress', async function () { + const signature = wallet.signTransaction( + REQUEST_FIXTURES.signAs as Transaction, + wallet.getXAddress(), + ) + assert.deepEqual(signature, RESPONSE_FIXTURES.signAs.signedTransaction) + }) + + it('signTransaction with X Address and tag for multisignAddress', async function () { + const signature = wallet.signTransaction( + REQUEST_FIXTURES.signAs as Transaction, + wallet.getXAddress(0), + ) + // Adding a tag changes the classicAddress, which changes the signature from RESPONSE_FIXTURES.signAs + const expectedSignature = + '120000240000000261400000003B9ACA00684000000000000032730081142E244E6F20104E57C0C60BD823CB312BF10928C78314B5F762798A53D543A014CAF8B297CFF8F2F937E8F3E0102300000000732102A8A44DB3D4C73EEEE11DFE54D2029103B776AA8A8D293A91D645977C9DF5F54474473045022100B3F8205578C6A68D3BBD27650F5D2E983718D502C250C5147F07B7EDD8E8583E02207B892818BD58E328C2797F15694A505937861586D527849065B582523E390B128114B3263BD0A9BF9DFDBBBBD07F536355FF477BF0E9E1F1' + assert.deepEqual(signature, expectedSignature) + }) + + it('signTransaction throws when given a transaction that is already signed', async function () { + const result = wallet.signTransaction( + REQUEST_FIXTURES.normal as Transaction, + ) + assert.throws(() => { + const tx = decode(result) as unknown as Transaction + wallet.signTransaction(tx) + }, /txJSON must not contain "TxnSignature" or "Signers" properties/u) + }) + + it('signTransaction with an EscrowExecution transaction', async function () { + const result = wallet.signTransaction( + REQUEST_FIXTURES.escrow as Transaction, + ) + assert.deepEqual(result, RESPONSE_FIXTURES.escrow.signedTransaction) + }) + + it('signTransaction succeeds when given a transaction with no flags', async function () { + const tx: Transaction = { + TransactionType: 'Payment', + Account: 'r45Rev1EXGxy2hAUmJPCne97KUE7qyrD3j', Destination: 'rQ3PTWGLCbPz8ZCicV5tCX3xuymojTng5r', Amount: '20000000', Sequence: 1, Fee: '12', - SigningPubKey: publicKey, } - const wallet = new Wallet(publicKey, privateKey) - const signedTx: string = wallet.signTransaction(txJSON) + const result = wallet.signTransaction(tx) + const expectedResult = + '1200002400000001614000000001312D0068400000000000000C732102A8A44DB3D4C73EEEE11DFE54D2029103B776AA8A8D293A91D645977C9DF5F5447446304402201C0A74EE8ECF5ED83734D7171FB65C01D90D67040DEDCC66414BD546CE302B5802205356843841BFFF60D15F5F5F9FB0AB9D66591778140AB2D137FF576D9DEC44BC8114EE3046A5DDF8422C40DDB93F1D522BB4FE6419158314FDB08D07AAA0EB711793A3027304D688E10C3648' + const decoded = decode(result) + assert( + decoded.Flags == null, + `Flags = ${JSON.stringify(decoded.Flags)}, should be undefined`, + ) + assert.equal(result, expectedResult) + }) - // TODO: Check the output of the signature against a known result - assert.isString(signedTx) + it('signTransaction succeeds with source.amount/destination.minAmount', async function () { + // See also: 'preparePayment with source.amount/destination.minAmount' + + const tx: Transaction = { + TransactionType: 'Payment', + Account: 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', + Destination: 'rEX4LtGJubaUcMWCJULcy4NVxGT9ZEMVRq', + Amount: { + currency: 'USD', + issuer: 'rMaa8VLBTjwTJWA2kSme4Sqgphhr6Lr6FH', + value: + '999999999999999900000000000000000000000000000000000000000000000000000000000000000000000000000000', + }, + Flags: 2147614720, + SendMax: { + currency: 'GBP', + issuer: 'rpat5TmYjDsnFSStmgTumFgXCM9eqsWPro', + value: '0.1', + }, + DeliverMin: { + currency: 'USD', + issuer: 'rMaa8VLBTjwTJWA2kSme4Sqgphhr6Lr6FH', + value: '0.1248548562296331', + }, + Sequence: 23, + LastLedgerSequence: 8820051, + Fee: '12', + } + + const result = wallet.signTransaction(tx) + const expectedResult = + '12000022800200002400000017201B0086955361EC6386F26FC0FFFF0000000000000000000000005553440000000000DC596C88BCDE4E818D416FCDEEBF2C8656BADC9A68400000000000000C69D4438D7EA4C6800000000000000000000000000047425000000000000C155FFE99C8C91F67083CEFFDB69EBFE76348CA6AD4446F8C5D8A5E0B0000000000000000000000005553440000000000DC596C88BCDE4E818D416FCDEEBF2C8656BADC9A732102A8A44DB3D4C73EEEE11DFE54D2029103B776AA8A8D293A91D645977C9DF5F544744630440220297E0C7670C7DA491E0D649E62C123D988BA93FD7EA1B9141F1D376CDDF902F502205AF1936B22B18BBA7793A88ABEEABADB4CE0E4C3BE583066480F2F476B5ED08E81145E7B112523F68D2F5E879DB4EAC51C6698A6930483149F500E50C2F016CA01945E5A1E5846B61EF2D376' + const decoded = decode(result) + assert( + decoded.Flags === 2147614720, + `Flags = ${JSON.stringify(decoded.Flags)}, should be 2147614720`, + ) + assert.deepEqual(result, expectedResult) + }) + + it('signTransaction throws when encoded tx does not match decoded tx because of illegal small fee', async function () { + const tx: Transaction = { + Flags: 2147483648, + TransactionType: 'AccountSet', + Account: 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', + Domain: '6578616D706C652E636F6D', + LastLedgerSequence: 8820051, + Fee: '1.2', + Sequence: 23, + SigningPubKey: + '02F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8', + } + + assert.throws(() => { + wallet.signTransaction(tx) + }, /1\.2 is an illegal amount/u) + }) + + it('signTransaction throws when encoded tx does not match decoded tx because of illegal higher fee', async function () { + const tx: Transaction = { + Flags: 2147483648, + TransactionType: 'AccountSet', + Account: 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', + Domain: '6578616D706C652E636F6D', + LastLedgerSequence: 8820051, + Fee: '1123456.7', + Sequence: 23, + SigningPubKey: + '02F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8', + } + + assert.throws(() => { + wallet.signTransaction(tx) + }, /1123456\.7 is an illegal amount/u) + }) + + it('signTransaction with a ticket transaction', async function () { + const result = wallet.signTransaction( + REQUEST_FIXTURES.ticket as Transaction, + ) + assert.deepEqual(result, RESPONSE_FIXTURES.ticket.signedTransaction) + }) + + it('signTransaction with a Payment transaction with paths', async function () { + const payment: Transaction = { + TransactionType: 'Payment', + Account: 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', + Destination: 'rKT4JX4cCof6LcDYRz8o3rGRu7qxzZ2Zwj', + Amount: { + currency: 'USD', + issuer: 'rVnYNK9yuxBz4uP8zC8LEFokM2nqH3poc', + value: + '999999999999999900000000000000000000000000000000000000000000000000000000000000000000000000000000', + }, + Flags: 2147614720, + SendMax: '100', + DeliverMin: { + currency: 'USD', + issuer: 'rVnYNK9yuxBz4uP8zC8LEFokM2nqH3poc', + value: '0.00004579644712312366', + }, + Paths: [ + [{ currency: 'USD', issuer: 'rVnYNK9yuxBz4uP8zC8LEFokM2nqH3poc' }], + ], + LastLedgerSequence: 15696358, + Sequence: 1, + Fee: '12', + } + const result = wallet.signTransaction(payment) + assert.deepEqual( + result, + '12000022800200002400000001201B00EF81E661EC6386F26FC0FFFF0000000000000000000000005553440000000000054F6F784A58F9EFB0A9EB90B83464F9D166461968400000000000000C6940000000000000646AD3504529A0465E2E0000000000000000000000005553440000000000054F6F784A58F9EFB0A9EB90B83464F9D1664619732102A8A44DB3D4C73EEEE11DFE54D2029103B776AA8A8D293A91D645977C9DF5F54474463044022049AD75980A5088EBCD768547E06427736BD8C4396B9BD3762CA8C1341BD7A4F9022060C94071C3BDF99FAB4BEB7C0578D6EBEE083157B470699645CCE4738A41D61081145E7B112523F68D2F5E879DB4EAC51C6698A693048314CA6EDC7A28252DAEA6F2045B24F4D7C333E146170112300000000000000000000000005553440000000000054F6F784A58F9EFB0A9EB90B83464F9D166461900', + ) + }) + + it('signTransaction with a prepared payment', async function () { + const payment: Transaction = { + TransactionType: 'Payment', + Account: 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', + Destination: 'rQ3PTWGLCbPz8ZCicV5tCX3xuymojTng5r', + Amount: '1', + Flags: 2147483648, + Sequence: 23, + LastLedgerSequence: 8819954, + Fee: '12', + } + const result = wallet.signTransaction(payment) + const expectedResult = + '12000022800000002400000017201B008694F261400000000000000168400000000000000C732102A8A44DB3D4C73EEEE11DFE54D2029103B776AA8A8D293A91D645977C9DF5F54474473045022100E8929B68B137AB2AAB1AD3A4BB253883B0C8C318DC8BB39579375751B8E54AC502206893B2D61244AFE777DAC9FA3D9DDAC7780A9810AF4B322D629784FD626B8CE481145E7B112523F68D2F5E879DB4EAC51C6698A693048314FDB08D07AAA0EB711793A3027304D688E10C3648' + assert.deepEqual(result, expectedResult) + }) + + it('signTransaction throws when an illegal amount is provided', async function () { + const payment: Transaction = { + TransactionType: 'Payment', + Account: 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', + Destination: 'rQ3PTWGLCbPz8ZCicV5tCX3xuymojTng5r', + Amount: '1.1234567', + Flags: 2147483648, + Sequence: 23, + LastLedgerSequence: 8819954, + Fee: '12', + } + assert.throws(() => { + wallet.signTransaction(payment) + }, /^1.1234567 is an illegal amount/u) }) })