Remove sign.ts and move functionality into Wallet (#1620)

* Move sign.ts functionality into Wallet
* Move the corresponding tests to wallet's test cases and simplify them for readability
* Delete sign.ts
This commit is contained in:
Jackson Mills
2021-09-17 12:16:57 -07:00
committed by Mayukha Vadari
parent 05e1d4d3c5
commit 6fac420d9f
14 changed files with 443 additions and 685 deletions

View File

@@ -92,7 +92,6 @@ import getOrderbook from '../sugar/orderbook'
import { submitTransaction, submitSignedTransaction } from '../sugar/submit' import { submitTransaction, submitSignedTransaction } from '../sugar/submit'
import { ensureClassicAddress } from '../sugar/utils' import { ensureClassicAddress } from '../sugar/utils'
import combine from '../transaction/combine' import combine from '../transaction/combine'
import { sign } from '../transaction/sign'
import generateFaucetWallet from '../wallet/generateFaucetWallet' import generateFaucetWallet from '../wallet/generateFaucetWallet'
import { import {
@@ -534,7 +533,6 @@ class Client extends EventEmitter {
public getBalances = prepend(getBalances, this) public getBalances = prepend(getBalances, this)
public getOrderbook = prepend(getOrderbook, this) public getOrderbook = prepend(getOrderbook, this)
public sign = sign
public combine = combine public combine = combine
public generateFaucetWallet = prepend(generateFaucetWallet, this) public generateFaucetWallet = prepend(generateFaucetWallet, this)

View File

@@ -60,7 +60,6 @@ export function validatePayment(tx: Record<string, unknown>): void {
} }
if (tx.DestinationTag != null && typeof tx.DestinationTag !== 'number') { if (tx.DestinationTag != null && typeof tx.DestinationTag !== 'number') {
console.log(tx.DestinationTag)
throw new ValidationError( throw new ValidationError(
'PaymentTransaction: DestinationTag must be a number', 'PaymentTransaction: DestinationTag must be a number',
) )

View File

@@ -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 }

View File

@@ -1,19 +1,28 @@
import { fromSeed } from 'bip32' import { fromSeed } from 'bip32'
import { mnemonicToSeedSync } from 'bip39' import { mnemonicToSeedSync } from 'bip39'
import { classicAddressToXAddress } from 'ripple-address-codec' import _ from 'lodash'
import { decode, encodeForSigning } from 'ripple-binary-codec' import {
classicAddressToXAddress,
isValidXAddress,
xAddressToClassicAddress,
} from 'ripple-address-codec'
import {
decode,
encodeForSigning,
encodeForMultisigning,
encode,
} from 'ripple-binary-codec'
import { import {
deriveAddress, deriveAddress,
deriveKeypair, deriveKeypair,
generateSeed, generateSeed,
verify, verify,
sign,
} from 'ripple-keypairs' } from 'ripple-keypairs'
import ECDSA from '../common/ecdsa' import ECDSA from '../common/ecdsa'
import { ValidationError } from '../common/errors' import { ValidationError } from '../common/errors'
import { Transaction } from '../models/transactions' import { Transaction } from '../models/transactions'
import { signOffline } from '../transaction/sign'
import { SignOptions } from '../transaction/types'
const DEFAULT_ALGORITHM: ECDSA = ECDSA.ed25519 const DEFAULT_ALGORITHM: ECDSA = ECDSA.ed25519
const DEFAULT_DERIVATION_PATH = "m/44'/144'/0'/0/0" const DEFAULT_DERIVATION_PATH = "m/44'/144'/0'/0/0"
@@ -30,6 +39,12 @@ function hexFromBuffer(buffer: Buffer): string {
class Wallet { class Wallet {
public readonly publicKey: string public readonly publicKey: string
public readonly privateKey: 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 classicAddress: string
public readonly seed?: string public readonly seed?: string
@@ -79,6 +94,7 @@ class Wallet {
* @param algorithm - The digital signature algorithm to generate an address fro. * @param algorithm - The digital signature algorithm to generate an address fro.
* @returns A Wallet derived from a secret (AKA a seed). * @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 public static fromSecret = Wallet.fromSeed
/** /**
@@ -144,16 +160,49 @@ class Wallet {
/** /**
* Signs a transaction offline. * Signs a transaction offline.
* *
* @param this - Wallet instance.
* @param transaction - A transaction to be signed offline. * @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. * @returns A signed transaction.
* @throws ValidationError if the transaction is already signed or does not encode/decode to same result.
*/ */
public signTransaction( public signTransaction(
this: Wallet,
transaction: Transaction, transaction: Transaction,
options: SignOptions = { signAs: '' }, multisignAddress?: string,
): string { ): string {
return signOffline(this, JSON.stringify(transaction), options) if (transaction.TxnSignature || transaction.Signers) {
.signedTransaction 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. * @returns A classic address.
*/ */
public getClassicAddress(): string { 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 export default Wallet

View File

@@ -28,7 +28,7 @@ import Wallet from '.'
function sign(wallet: Wallet, tx: Transaction, forMultisign = false): string { function sign(wallet: Wallet, tx: Transaction, forMultisign = false): string {
return wallet.signTransaction( return wallet.signTransaction(
tx, tx,
forMultisign ? { signAs: wallet.getClassicAddress() } : { signAs: '' }, forMultisign ? wallet.getClassicAddress() : '',
) )
} }

View File

@@ -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)
}
}
})
})

View File

@@ -1,8 +1,10 @@
{ {
"txJSON": "{\"Flags\":2147483648,\"TransactionType\":\"AccountSet\",\"Account\":\"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59\",\"Domain\":\"6578616D706C652E636F6D\",\"LastLedgerSequence\":8820051,\"Fee\":\"12\",\"Sequence\":23,\"SigningPubKey\":\"02F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8\"}", "TransactionType":"AccountSet",
"instructions": { "Flags":2147483648,
"fee": "0.000012", "Sequence":23,
"sequence": 23, "LastLedgerSequence":8820051,
"maxLedgerVersion": 8820051 "Fee":"12",
} "SigningPubKey":"02A8A44DB3D4C73EEEE11DFE54D2029103B776AA8A8D293A91D645977C9DF5F544",
"Domain":"6578616D706C652E636F6D",
"Account":"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59"
} }

View File

@@ -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}", "TransactionType":"EscrowFinish",
"instructions": { "Account":"rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
"fee": "0.000012", "Owner":"rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
"sequence": 1, "OfferSequence":2,
"maxLedgerVersion": 102 "Condition":"712C36933822AD3A3D136C5DF97AA863B69F9CE88B2D6CE6BDD11BFDE290C19D",
} "Fulfillment":"74686973206D757374206861766520333220636861726163746572732E2E2E2E",
"Flags":2147483648,
"LastLedgerSequence":102,
"Fee":"12",
"Sequence":1
} }

View File

@@ -1,9 +1,8 @@
{ {
"txJSON": "{\"TransactionType\": \"TicketCreate\", \"TicketCount\": 1, \"Account\": \"r4SDqUD1ZcfoZrhnsZ94XNFKxYL4oHYJyA\", \"Sequence\": 0, \"TicketSequence\": 23, \"Fee\": \"10000\"}", "TransactionType":"TicketCreate",
"instructions": { "TicketCount":1,
"ticketSequence": 23, "Account":"r4SDqUD1ZcfoZrhnsZ94XNFKxYL4oHYJyA",
"maxLedgerVersion": 8820051 "Sequence":0,
} "TicketSequence":23,
"Fee":"10000"
} }

View File

@@ -1,4 +1,4 @@
{ {
"signedTransaction": "12000322800000002400000017201B0086955368400000000000000C732102F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8744630440220680070A157682D9EB510E8AD58C35DC9C8346B155077D73792E88120B7A3B6B1022079537D3300C9B4D2D3D62ACCE1E66CDA893F9612CB2577ADEC8154B933765336770B6578616D706C652E636F6D81145E7B112523F68D2F5E879DB4EAC51C6698A69304", "signedTransaction": "12000322800000002400000017201B0086955368400000000000000C732102A8A44DB3D4C73EEEE11DFE54D2029103B776AA8A8D293A91D645977C9DF5F54474463044022025464FA5466B6E28EEAD2E2D289A7A36A11EB9B269D211F9C76AB8E8320694E002205D5F99CB56E5A996E5636A0E86D029977BEFA232B7FB64ABA8F6E29DC87A9E89770B6578616D706C652E636F6D81145E7B112523F68D2F5E879DB4EAC51C6698A69304",
"id": "10B54D31384A49336C36A5907E3C28227139E282D3C7F734FEA351DE446F3674" "id": "93F6C6CE73C092AA005103223F3A1F557F4C097A2943D96760F6490F04379917"
} }

View File

@@ -1,4 +1,4 @@
{ {
"signedTransaction": "120000240000000261400000003B9ACA00684000000000000032730081142E244E6F20104E57C0C60BD823CB312BF10928C78314B5F762798A53D543A014CAF8B297CFF8F2F937E8F3E01073210330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD02074473045022100BB6FC77F26BC88587204CAA79B2230C420D7EC937B8AC3A0CF9B0BE988BAB0D002203BF86893BA3B764375FFFAD9D54A4AAEDABD07C4D72ADB9C1B20C10B4DD712898114B5F762798A53D543A014CAF8B297CFF8F2F937E8E1F1", "signedTransaction": "120000240000000261400000003B9ACA00684000000000000032730081142E244E6F20104E57C0C60BD823CB312BF10928C78314B5F762798A53D543A014CAF8B297CFF8F2F937E8F3E010732102A8A44DB3D4C73EEEE11DFE54D2029103B776AA8A8D293A91D645977C9DF5F54474473045022100B3F8205578C6A68D3BBD27650F5D2E983718D502C250C5147F07B7EDD8E8583E02207B892818BD58E328C2797F15694A505937861586D527849065B582523E390B128114B3263BD0A9BF9DFDBBBBD07F536355FF477BF0E9E1F1",
"id": "AB7632D7C07E591658635CED6A5DDE832B22CA066907CB131DEFAAA925B98185" "id": "D8CF5FC93CFE5E131A34599AFB7CE186A5B8D1B9F069E35F4634AD3B27837E35"
} }

View File

@@ -1,4 +1,4 @@
{ {
"signedTransaction": "12000222800000002400000001201900000002201B0000006668400000000000000C73210330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD0207446304402205CDD40611008680E151EF6C5C70A6DFBF7977DE73800E04F1B6F1FE8688BBE3E022075DB3990E6CBF0BCD990D92FA13E3D3F95510CA2CCBAB92BD2CE288FA6F2F34870102074686973206D757374206861766520333220636861726163746572732E2E2E2E701120712C36933822AD3A3D136C5DF97AA863B69F9CE88B2D6CE6BDD11BFDE290C19D8114B5F762798A53D543A014CAF8B297CFF8F2F937E88214B5F762798A53D543A014CAF8B297CFF8F2F937E8", "signedTransaction": "12000222800000002400000001201900000002201B0000006668400000000000000C732102A8A44DB3D4C73EEEE11DFE54D2029103B776AA8A8D293A91D645977C9DF5F5447446304402204652E8572AEED964451C603EB110AC9945A65E3C5C288D144BB02F259755F6E202205B64E27293248F0650A3F7A4FD66BC16A61F4883AC3ED8EE8A48EF569C06812070102074686973206D757374206861766520333220636861726163746572732E2E2E2E701120712C36933822AD3A3D136C5DF97AA863B69F9CE88B2D6CE6BDD11BFDE290C19D8114B5F762798A53D543A014CAF8B297CFF8F2F937E88214B5F762798A53D543A014CAF8B297CFF8F2F937E8",
"id": "E76178CD799A82BAB6EE83A70DE0060F0BDC8BDE29980F0832D791D8D9D5CACC" "id": "645B7676DF057E4F5E83F970A18B3751B6813807F1030A8D2F482D02DC885106"
} }

View File

@@ -1,4 +1,4 @@
{ {
"signedTransaction": "12000A2400000000202800000001202900000017684000000000002710732103E985C55BDCE4171394A0521AA84C71F81425680A7CE510AEF49662CF5A78D38674473045022100A77F102B632779C0E3F25B8715CB8FF2A15A702F3A39D1E6416C981B604D2E0302207E1DB8418D547E8AE322F49585E1C554E8759C0FBF7B88158BE3D0EE6B2E4E0A8114EB1FC04FDA0248FB6DE5BA4235425773D61DF0F3", "signedTransaction": "12000A2400000000202800000001202900000017684000000000002710732102A8A44DB3D4C73EEEE11DFE54D2029103B776AA8A8D293A91D645977C9DF5F54474473045022100896CFE083767C88B9539DE2F28894429BC2760865161792D576C090BB93E1EAA02203015D30BC59245C8CFB8A9D78386B91B251DCB946A4C0FAB12A5FA41C202FF3A8114EB1FC04FDA0248FB6DE5BA4235425773D61DF0F3",
"id": "9F1002A8DB9D06D5F3AB2070387F17E12421DCE8444EED13E5F6928291EB4F43" "id": "0AC60B1E1F063904D9D9D0E9D03F2E9C8D41BC6FC872D5B8BF87E15BBF9669BB"
} }

View File

@@ -1,9 +1,14 @@
import { assert } from 'chai' import { assert } from 'chai'
import { decode } from 'ripple-binary-codec/dist'
import { Payment } from 'xrpl-local'
import ECDSA from '../../src/common/ecdsa' import ECDSA from '../../src/common/ecdsa'
import { Transaction } from '../../src/models/transactions'
import Wallet from '../../src/wallet' 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. * Wallet testing.
@@ -181,27 +186,267 @@ describe('Wallet', function () {
}) })
describe('signTransaction', function () { describe('signTransaction', function () {
const publicKey = let wallet: Wallet
'030E58CDD076E798C84755590AAF6237CA8FAE821070A59F648B517A30DC6F589D'
const privateKey =
'00141BA006D3363D2FB2785E8DF4E44D3A49908780CB4FB51F6D217C08C021429F'
const address = 'rhvh5SrgBL5V8oeV9EpDuVszeJSSCEkbPc'
it('signs a transaction offline', function () { beforeEach(function () {
const txJSON: Payment = { 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', 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', Destination: 'rQ3PTWGLCbPz8ZCicV5tCX3xuymojTng5r',
Amount: '20000000', Amount: '20000000',
Sequence: 1, Sequence: 1,
Fee: '12', Fee: '12',
SigningPubKey: publicKey,
} }
const wallet = new Wallet(publicKey, privateKey) const result = wallet.signTransaction(tx)
const signedTx: string = wallet.signTransaction(txJSON) 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 it('signTransaction succeeds with source.amount/destination.minAmount', async function () {
assert.isString(signedTx) // 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)
}) })
}) })