From db17bb1c57aeaffdcfa59da50e49155d5be8fe63 Mon Sep 17 00:00:00 2001 From: Omar Khan Date: Wed, 8 Sep 2021 12:49:44 -0400 Subject: [PATCH] implement Autofill Transaction (#1574) Implements autofill() and setTransactionFlagsToNumber() to allow a Client to autofill fields in a Transaction. --- src/client/index.ts | 18 ++ src/common/fee.ts | 2 +- src/ledger/autofill.ts | 194 ++++++++++++++ src/models/transactions/common.ts | 4 +- src/models/transactions/offerCreate.ts | 8 + .../transactions/paymentChannelClaim.ts | 6 + src/models/utils/index.ts | 84 +++++- test/client/autofill.ts | 241 ++++++++++++++++++ test/models/utils.ts | 128 +++++++++- 9 files changed, 677 insertions(+), 8 deletions(-) create mode 100644 src/ledger/autofill.ts create mode 100644 test/client/autofill.ts diff --git a/src/client/index.ts b/src/client/index.ts index 4866f3fc..2b0e884c 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -23,6 +23,7 @@ import { import { constants, errors, txFlags, ensureClassicAddress } from '../common' import { ValidationError, XrplError } from '../common/errors' import getFee from '../common/fee' +import autofill from '../ledger/autofill' import getBalances from '../ledger/balances' import { getOrderbook, formatBidsAndAsks } from '../ledger/orderbook' import getPaths from '../ledger/pathfind' @@ -169,6 +170,20 @@ function getCollectKeyFromCommand(command: string): string | null { } } +/** + * It returns a function that prepends params to the given func. + * A sugar function for JavaScript .bind() without the "this" (keyword) binding. + * + * @param func - A function to prepend params. + * @param params - Parameters to prepend to a function. + * @returns A function bound with params. + */ +// eslint-disable-next-line @typescript-eslint/ban-types -- expected param types +function prepend(func: Function, ...params: unknown[]): Function { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return -- safe to return + return func.bind(null, ...params) +} + interface MarkerRequest extends BaseRequest { limit?: number marker?: unknown @@ -198,6 +213,9 @@ class Client extends EventEmitter { // number. Defaults to '2'. public readonly maxFeeXRP: string + // TODO: Use partial for other instance methods as well. + public autofill = prepend(autofill, this) + /** * Creates a new Client with a websocket connection to a rippled server. * diff --git a/src/common/fee.ts b/src/common/fee.ts index 9873d476..790eb8a0 100644 --- a/src/common/fee.ts +++ b/src/common/fee.ts @@ -16,7 +16,7 @@ const BASE_10 = 10 */ export default async function getFee( this: Client, - cushion: number | null, + cushion?: number, ): Promise { const feeCushion = cushion ?? this.feeCushion diff --git a/src/ledger/autofill.ts b/src/ledger/autofill.ts new file mode 100644 index 00000000..16bd8aa8 --- /dev/null +++ b/src/ledger/autofill.ts @@ -0,0 +1,194 @@ +import BigNumber from 'bignumber.js' +import { xAddressToClassicAddress, isValidXAddress } from 'ripple-address-codec' + +import type { Client } from '..' +import { ValidationError } from '../common/errors' +import { AccountInfoRequest, LedgerRequest } from '../models/methods' +import { Transaction } from '../models/transactions' +import { setTransactionFlagsToNumber } from '../models/utils' +import { xrpToDrops } from '../utils' + +// 20 drops +const LEDGER_OFFSET = 20 +// 5 XRP +const ACCOUNT_DELETE_FEE = 5000000 +interface ClassicAccountAndTag { + classicAccount: string + tag: number | false | undefined +} + +/** + * Autofills fields in a transaction. + * + * @param client - A client. + * @param tx - A transaction to autofill fields. + * @param signersCount - The expected number of signers for this transaction. Used for multisign. + * @returns An autofilled transaction. + */ +async function autofill( + client: Client, + tx: Transaction, + signersCount?: number, +): Promise { + setValidAddresses(tx) + + setTransactionFlagsToNumber(tx) + + const promises: Array> = [] + if (tx.Sequence == null) { + promises.push(setNextValidSequenceNumber(client, tx)) + } + if (tx.Fee == null) { + promises.push(calculateFeePerTransactionType(client, tx, signersCount)) + } + if (tx.LastLedgerSequence == null) { + promises.push(setLatestValidatedLedgerSequence(client, tx)) + } + + return Promise.all(promises).then(() => tx) +} + +function setValidAddresses(tx: Transaction): void { + validateAccountAddress(tx, 'Account', 'SourceTag') + // eslint-disable-next-line @typescript-eslint/dot-notation -- Destination can exist on Transaction + if (tx['Destination'] != null) { + validateAccountAddress(tx, 'Destination', 'DestinationTag') + } + + // DepositPreauth: + convertToClassicAddress(tx, 'Authorize') + convertToClassicAddress(tx, 'Unauthorize') + // EscrowCancel, EscrowFinish: + convertToClassicAddress(tx, 'Owner') + // SetRegularKey: + convertToClassicAddress(tx, 'RegularKey') +} + +function validateAccountAddress( + tx: Transaction, + accountField: string, + tagField: string, +): void { + // if X-address is given, convert it to classic address + const { classicAccount, tag } = getClassicAccountAndTag(tx[accountField]) + // eslint-disable-next-line no-param-reassign -- param reassign is safe + tx[accountField] = classicAccount + + if (tag != null) { + if (tx[tagField] && tx[tagField] !== tag) { + throw new ValidationError( + `The ${tagField}, if present, must match the tag of the ${accountField} X-address`, + ) + } + // eslint-disable-next-line no-param-reassign -- param reassign is safe + tx[tagField] = tag + } +} + +function getClassicAccountAndTag( + Account: string, + expectedTag?: number, +): ClassicAccountAndTag { + if (isValidXAddress(Account)) { + const classic = xAddressToClassicAddress(Account) + if (expectedTag != null && classic.tag !== expectedTag) { + throw new ValidationError( + 'address includes a tag that does not match the tag specified in the transaction', + ) + } + return { + classicAccount: classic.classicAddress, + tag: classic.tag, + } + } + return { + classicAccount: Account, + tag: expectedTag, + } +} + +function convertToClassicAddress(tx: Transaction, fieldName: string): void { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- assignment is safe + const account = tx[fieldName] + if (typeof account === 'string') { + const { classicAccount } = getClassicAccountAndTag(account) + // eslint-disable-next-line no-param-reassign -- param reassign is safe + tx[fieldName] = classicAccount + } +} + +async function setNextValidSequenceNumber( + client: Client, + tx: Transaction, +): Promise { + const request: AccountInfoRequest = { + command: 'account_info', + account: tx.Account, + } + const data = await client.request(request) + // eslint-disable-next-line no-param-reassign, require-atomic-updates -- param reassign is safe with no race condition + tx.Sequence = data.result.account_data.Sequence +} + +async function calculateFeePerTransactionType( + client: Client, + tx: Transaction, + signersCount = 0, +): Promise { + // netFee is usually 0.00001 XRP (10 drops) + const netFeeXRP: string = await client.getFee() + const netFeeDrops: string = xrpToDrops(netFeeXRP) + let baseFee: BigNumber = new BigNumber(netFeeDrops) + + // EscrowFinish Transaction with Fulfillment + if (tx.TransactionType === 'EscrowFinish' && tx.Fulfillment != null) { + const fulfillmentBytesSize: number = Math.ceil(tx.Fulfillment.length / 2) + // 10 drops × (33 + (Fulfillment size in bytes / 16)) + const product = new BigNumber( + // eslint-disable-next-line @typescript-eslint/no-magic-numbers -- expected use of magic numbers + scaleValue(netFeeDrops, 33 + fulfillmentBytesSize / 16), + ) + baseFee = product.dp(0, BigNumber.ROUND_CEIL) + } + + // AccountDelete Transaction + if (tx.TransactionType === 'AccountDelete') { + baseFee = new BigNumber(ACCOUNT_DELETE_FEE) + } + + // Multi-signed Transaction + // 10 drops × (1 + Number of Signatures Provided) + if (signersCount > 0) { + baseFee = BigNumber.sum(baseFee, scaleValue(netFeeDrops, 1 + signersCount)) + } + + const maxFeeDrops = xrpToDrops(client.maxFeeXRP) + const totalFee = + tx.TransactionType === 'AccountDelete' + ? baseFee + : BigNumber.min(baseFee, maxFeeDrops) + + // Round up baseFee and return it as a string + // eslint-disable-next-line no-param-reassign, @typescript-eslint/no-magic-numbers -- param reassign is safe, base 10 magic num + tx.Fee = totalFee.dp(0, BigNumber.ROUND_CEIL).toString(10) +} + +function scaleValue(value, multiplier): string { + return new BigNumber(value).times(multiplier).toString() +} + +async function setLatestValidatedLedgerSequence( + client: Client, + tx: Transaction, +): Promise { + const request: LedgerRequest = { + command: 'ledger', + ledger_index: 'validated', + } + const data = await client.request(request) + const ledgerSequence = data.result.ledger_index + // eslint-disable-next-line no-param-reassign -- param reassign is safe + tx.LastLedgerSequence = ledgerSequence + LEDGER_OFFSET +} + +export default autofill diff --git a/src/models/transactions/common.ts b/src/models/transactions/common.ts index 03fbc1d5..784fe82c 100644 --- a/src/models/transactions/common.ts +++ b/src/models/transactions/common.ts @@ -95,9 +95,7 @@ export function isAmount(amount: unknown): boolean { ) } -export interface GlobalFlags { - tfFullyCanonicalSig: boolean -} +export interface GlobalFlags {} export interface BaseTransaction { Account: string diff --git a/src/models/transactions/offerCreate.ts b/src/models/transactions/offerCreate.ts index 5cd427b2..4e248fed 100644 --- a/src/models/transactions/offerCreate.ts +++ b/src/models/transactions/offerCreate.ts @@ -9,6 +9,14 @@ import { isAmount, } from './common' +// eslint-disable-next-line no-shadow -- variable declaration is unique +export enum OfferCreateFlagsEnum { + tfPassive = 0x00010000, + tfImmediateOrCancel = 0x00020000, + tfFillOrKill = 0x00040000, + tfSell = 0x00080000, +} + export interface OfferCreateFlags extends GlobalFlags { tfPassive?: boolean tfImmediateOrCancel?: boolean diff --git a/src/models/transactions/paymentChannelClaim.ts b/src/models/transactions/paymentChannelClaim.ts index 424dee3f..55957675 100644 --- a/src/models/transactions/paymentChannelClaim.ts +++ b/src/models/transactions/paymentChannelClaim.ts @@ -3,6 +3,12 @@ import { ValidationError } from '../../common/errors' import { BaseTransaction, GlobalFlags, verifyBaseTransaction } from './common' +// eslint-disable-next-line no-shadow -- variable declaration is unique +export enum PaymentChannelClaimFlagsEnum { + tfRenew = 0x00010000, + tfClose = 0x00020000, +} + export interface PaymentChannelClaimFlags extends GlobalFlags { tfRenew?: boolean tfClose?: boolean diff --git a/src/models/utils/index.ts b/src/models/utils/index.ts index 04101078..81fc1202 100644 --- a/src/models/utils/index.ts +++ b/src/models/utils/index.ts @@ -1,3 +1,20 @@ +/* eslint-disable no-param-reassign -- param reassign is safe */ +/* eslint-disable no-bitwise -- flags require bitwise operations */ +import { ValidationError } from '../../common/errors' +// eslint-disable-next-line import/no-cycle -- cycle is safe +import { + OfferCreateFlags, + OfferCreateFlagsEnum, + PaymentChannelClaimFlags, + PaymentChannelClaimFlagsEnum, + PaymentTransactionFlags, + PaymentTransactionFlagsEnum, + Transaction, + TrustSetFlags, + TrustSetFlagsEnum, +} from '../transactions' +import type { GlobalFlags } from '../transactions/common' + /** * Verify that all fields of an object are in fields. * @@ -20,6 +37,71 @@ export function onlyHasFields( * @returns True if checkFlag is enabled within Flags. */ export function isFlagEnabled(Flags: number, checkFlag: number): boolean { - // eslint-disable-next-line no-bitwise -- Flags require bitwise operations return (checkFlag & Flags) === checkFlag } + +/** + * Sets a transaction's flags to its numeric representation. + * + * @param tx - A transaction to set its flags to its numeric representation. + */ +export function setTransactionFlagsToNumber(tx: Transaction): void { + if (tx.Flags == null) { + tx.Flags = 0 + return + } + if (typeof tx.Flags === 'number') { + return + } + + switch (tx.TransactionType) { + case 'OfferCreate': + tx.Flags = convertOfferCreateFlagsToNumber(tx.Flags) + return + case 'PaymentChannelClaim': + tx.Flags = convertPaymentChannelClaimFlagsToNumber(tx.Flags) + return + case 'Payment': + tx.Flags = convertPaymentTransactionFlagsToNumber(tx.Flags) + return + case 'TrustSet': + tx.Flags = convertTrustSetFlagsToNumber(tx.Flags) + return + default: + tx.Flags = 0 + } +} + +function convertOfferCreateFlagsToNumber(flags: OfferCreateFlags): number { + return reduceFlags(flags, OfferCreateFlagsEnum) +} + +function convertPaymentChannelClaimFlagsToNumber( + flags: PaymentChannelClaimFlags, +): number { + return reduceFlags(flags, PaymentChannelClaimFlagsEnum) +} + +function convertPaymentTransactionFlagsToNumber( + flags: PaymentTransactionFlags, +): number { + return reduceFlags(flags, PaymentTransactionFlagsEnum) +} + +function convertTrustSetFlagsToNumber(flags: TrustSetFlags): number { + return reduceFlags(flags, TrustSetFlagsEnum) +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- added ValidationError check for flagEnum +function reduceFlags(flags: GlobalFlags, flagEnum: any): number { + return Object.keys(flags).reduce((resultFlags, flag) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- safe member access + if (flagEnum[flag] == null) { + throw new ValidationError( + `flag ${flag} doesn't exist in flagEnum: ${JSON.stringify(flagEnum)}`, + ) + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- safe member access + return flags[flag] ? resultFlags | flagEnum[flag] : resultFlags + }, 0) +} diff --git a/test/client/autofill.ts b/test/client/autofill.ts new file mode 100644 index 00000000..7e0404fc --- /dev/null +++ b/test/client/autofill.ts @@ -0,0 +1,241 @@ +import { assert } from 'chai' + +import { + AccountDelete, + EscrowFinish, + Payment, + Transaction, +} from '../../src/models/transactions' +import rippled from '../fixtures/rippled' +import { setupClient, teardownClient } from '../setupClient' + +const Fee = '10' +const Sequence = 1432 +const LastLedgerSequence = 2908734 + +describe('client.autofill', function () { + beforeEach(setupClient) + afterEach(teardownClient) + + it('should not autofill if fields are present', async function () { + const tx: Transaction = { + TransactionType: 'DepositPreauth', + Account: 'rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf', + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', + Fee, + Sequence, + LastLedgerSequence, + } + const txResult = await this.client.autofill(tx) + + assert.strictEqual(txResult.Fee, Fee) + assert.strictEqual(txResult.Sequence, Sequence) + assert.strictEqual(txResult.LastLedgerSequence, LastLedgerSequence) + }) + + it('converts Account & Destination X-address to their classic address', async function () { + const tx: Payment = { + TransactionType: 'Payment', + Account: 'XVLhHMPHU98es4dbozjVtdWzVrDjtV18pX8yuPT7y4xaEHi', + Amount: '1234', + Destination: 'X7AcgcsBL6XDcUb289X4mJ8djcdyKaB5hJDWMArnXr61cqZ', + } + this.mockRippled.addResponse('account_info', rippled.account_info.normal) + this.mockRippled.addResponse('server_info', rippled.server_info.normal) + this.mockRippled.addResponse('ledger', rippled.ledger.normal) + + const txResult = await this.client.autofill(tx) + + assert.strictEqual(txResult.Account, 'rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf') + assert.strictEqual( + txResult.Destination, + 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59', + ) + }) + + it("should autofill Sequence when it's missing", async function () { + const tx: Transaction = { + TransactionType: 'DepositPreauth', + Account: 'rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf', + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', + Fee, + LastLedgerSequence, + } + this.mockRippled.addResponse('account_info', { + status: 'success', + type: 'response', + result: { + account_data: { + Sequence: 23, + }, + }, + }) + const txResult = await this.client.autofill(tx) + + assert.strictEqual(txResult.Sequence, 23) + }) + + describe('when autofill Fee is missing', function () { + it('should autofill Fee of a Transaction', async function () { + const tx: Transaction = { + TransactionType: 'DepositPreauth', + Account: 'rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf', + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', + Sequence, + LastLedgerSequence, + } + this.mockRippled.addResponse('server_info', { + status: 'success', + type: 'response', + result: { + info: { + validated_ledger: { + base_fee_xrp: 0.00001, + }, + }, + }, + }) + const txResult = await this.client.autofill(tx) + + assert.strictEqual(txResult.Fee, '12') + }) + + it('should autofill Fee of an EscrowFinish transaction', async function () { + const tx: EscrowFinish = { + Account: 'rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn', + TransactionType: 'EscrowFinish', + Owner: 'rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn', + OfferSequence: 7, + Condition: + 'A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100', + Fulfillment: 'A0028000', + } + this.mockRippled.addResponse('account_info', rippled.account_info.normal) + this.mockRippled.addResponse('ledger', rippled.ledger.normal) + this.mockRippled.addResponse('server_info', { + status: 'success', + type: 'response', + result: { + info: { + validated_ledger: { + base_fee_xrp: 0.00001, + }, + }, + }, + }) + const txResult = await this.client.autofill(tx) + + assert.strictEqual(txResult.Fee, '399') + }) + + it('should autofill Fee of an AccountDelete transaction', async function () { + const tx: AccountDelete = { + Account: 'rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn', + TransactionType: 'AccountDelete', + Destination: 'X7AcgcsBL6XDcUb289X4mJ8djcdyKaB5hJDWMArnXr61cqZ', + } + this.mockRippled.addResponse('account_info', rippled.account_info.normal) + this.mockRippled.addResponse('ledger', rippled.ledger.normal) + this.mockRippled.addResponse('server_info', { + status: 'success', + type: 'response', + result: { + info: { + validated_ledger: { + base_fee_xrp: 0.00001, + }, + }, + }, + }) + const txResult = await this.client.autofill(tx) + + assert.strictEqual(txResult.Fee, '5000000') + }) + + it('should autofill Fee of an EscrowFinish transaction with signersCount', async function () { + const tx: EscrowFinish = { + Account: 'rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn', + TransactionType: 'EscrowFinish', + Owner: 'rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn', + OfferSequence: 7, + Condition: + 'A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100', + Fulfillment: 'A0028000', + } + this.mockRippled.addResponse('account_info', rippled.account_info.normal) + this.mockRippled.addResponse('ledger', rippled.ledger.normal) + this.mockRippled.addResponse('server_info', { + status: 'success', + type: 'response', + result: { + info: { + validated_ledger: { + base_fee_xrp: 0.00001, + }, + }, + }, + }) + const txResult = await this.client.autofill(tx, 4) + + assert.strictEqual(txResult.Fee, '459') + }) + }) + + it("should autofill LastLedgerSequence when it's missing", async function () { + const tx: Transaction = { + TransactionType: 'DepositPreauth', + Account: 'rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf', + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', + Fee, + Sequence, + } + this.mockRippled.addResponse('ledger', { + status: 'success', + type: 'response', + result: { + ledger_index: 9038214, + }, + }) + const txResult = await this.client.autofill(tx) + assert.strictEqual(txResult.LastLedgerSequence, 9038234) + }) + + it('should autofill fields when all are missing', async function () { + const tx: Transaction = { + TransactionType: 'DepositPreauth', + Account: 'rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf', + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', + } + this.mockRippled.addResponse('account_info', { + status: 'success', + type: 'response', + result: { + account_data: { + Sequence: 23, + }, + }, + }) + this.mockRippled.addResponse('ledger', { + status: 'success', + type: 'response', + result: { + ledger_index: 9038214, + }, + }) + this.mockRippled.addResponse('server_info', { + status: 'success', + type: 'response', + result: { + info: { + validated_ledger: { + base_fee_xrp: 0.00001, + }, + }, + }, + }) + const txResult = await this.client.autofill(tx) + assert.strictEqual(txResult.Fee, '12') + assert.strictEqual(txResult.Sequence, 23) + assert.strictEqual(txResult.LastLedgerSequence, 9038234) + }) +}) diff --git a/test/models/utils.ts b/test/models/utils.ts index 6ff4b5cf..ae616e26 100644 --- a/test/models/utils.ts +++ b/test/models/utils.ts @@ -1,6 +1,21 @@ +/* eslint-disable no-bitwise -- flags require bitwise operations */ import { assert } from 'chai' -import { isFlagEnabled } from '../../src/models/utils' +import { + DepositPreauth, + OfferCreate, + OfferCreateFlagsEnum, + PaymentChannelClaim, + PaymentChannelClaimFlagsEnum, + Payment, + PaymentTransactionFlagsEnum, + TrustSet, + TrustSetFlagsEnum, +} from '../../src/models/transactions' +import { + isFlagEnabled, + setTransactionFlagsToNumber, +} from '../../src/models/utils' /** * Utils Testing. @@ -18,13 +33,120 @@ describe('Models Utils', function () { }) it('verifies a flag is enabled', function () { - flags += flag1 + flag2 + flags |= flag1 | flag2 assert.isTrue(isFlagEnabled(flags, flag1)) }) it('verifies a flag is not enabled', function () { - flags += flag2 + flags |= flag2 assert.isFalse(isFlagEnabled(flags, flag1)) }) }) + + describe('setTransactionFlagsToNumber', function () { + it('sets OfferCreateFlags to its numeric value', function () { + const tx: OfferCreate = { + Account: 'r3rhWeE31Jt5sWmi4QiGLMZnY3ENgqw96W', + Fee: '10', + TakerGets: { + currency: 'DSH', + issuer: 'rcXY84C4g14iFp6taFXjjQGVeHqSCh9RX', + value: '43.11584856965009', + }, + TakerPays: '12928290425', + TransactionType: 'OfferCreate', + TxnSignature: + '3045022100D874CDDD6BB24ED66E83B1D3574D3ECAC753A78F26DB7EBA89EAB8E7D72B95F802207C8CCD6CEA64E4AE2014E59EE9654E02CA8F03FE7FCE0539E958EAE182234D91', + Flags: { + tfPassive: true, + tfImmediateOrCancel: false, + tfFillOrKill: true, + tfSell: false, + }, + } + + const { tfPassive, tfFillOrKill } = OfferCreateFlagsEnum + const expected: number = tfPassive | tfFillOrKill + + setTransactionFlagsToNumber(tx) + assert.strictEqual(tx.Flags, expected) + }) + + it('sets PaymentChannelClaimFlags to its numeric value', function () { + const tx: PaymentChannelClaim = { + Account: 'r...', + TransactionType: 'PaymentChannelClaim', + Channel: + 'C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198', + Flags: { + tfRenew: true, + tfClose: false, + }, + } + + const { tfRenew } = PaymentChannelClaimFlagsEnum + const expected: number = tfRenew + + setTransactionFlagsToNumber(tx) + assert.strictEqual(tx.Flags, expected) + }) + + it('sets PaymentTransactionFlags to its numeric value', function () { + const tx: Payment = { + TransactionType: 'Payment', + Account: 'rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo', + Amount: '1234', + Destination: 'rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy', + Flags: { + tfNoDirectRipple: false, + tfPartialPayment: true, + tfLimitQuality: true, + }, + } + + const { tfPartialPayment, tfLimitQuality } = PaymentTransactionFlagsEnum + const expected: number = tfPartialPayment | tfLimitQuality + + setTransactionFlagsToNumber(tx) + assert.strictEqual(tx.Flags, expected) + }) + + it('sets TrustSetFlags to its numeric value', function () { + const tx: TrustSet = { + TransactionType: 'TrustSet', + Account: 'rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo', + LimitAmount: { + currency: 'XRP', + issuer: 'rcXY84C4g14iFp6taFXjjQGVeHqSCh9RX', + value: '4329.23', + }, + QualityIn: 1234, + QualityOut: 4321, + Flags: { + tfSetfAuth: true, + tfSetNoRipple: false, + tfClearNoRipple: true, + tfSetFreeze: false, + tfClearFreeze: true, + }, + } + + const { tfSetfAuth, tfClearNoRipple, tfClearFreeze } = TrustSetFlagsEnum + const expected: number = tfSetfAuth | tfClearNoRipple | tfClearFreeze + + setTransactionFlagsToNumber(tx) + assert.strictEqual(tx.Flags, expected) + }) + + it('sets other transaction types flags to its numeric value', function () { + const tx: DepositPreauth = { + TransactionType: 'DepositPreauth', + Account: 'rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo', + Flags: {}, + } + + setTransactionFlagsToNumber(tx) + assert.strictEqual(tx.Flags, 0) + }) + }) })