mirror of
https://github.com/Xahau/xahau.js.git
synced 2025-11-19 19:55:51 +00:00
refactor: Define PaymentTransaction model (#1542)
- Defines a TypeScript type for PaymentTransaction - Provides an optional function to users for verifying a PaymentTransaction instance at runtime: verifyPaymentTransaction() - Adds tests for verifyPaymentTransaction() - Adds isFlagEnabled() util to be used for models
This commit is contained in:
committed by
Mayukha Vadari
parent
c1edab547a
commit
bec487cf71
@@ -110,9 +110,6 @@ export function verifyBaseTransaction(common: BaseTransaction): void {
|
||||
if (common.Sequence !== undefined && typeof common.Sequence !== 'number')
|
||||
throw new ValidationError("BaseTransaction: invalid Sequence")
|
||||
|
||||
if (common.Flags !== undefined && typeof common.Flags !== 'number')
|
||||
throw new ValidationError("BaseTransaction: invalid Flags")
|
||||
|
||||
if (common.AccountTxnID !== undefined
|
||||
&& typeof common.AccountTxnID !== 'string')
|
||||
throw new ValidationError("BaseTransaction: invalid AccountTxnID")
|
||||
|
||||
@@ -7,3 +7,4 @@ export * from './checkCash'
|
||||
export * from './checkCancel'
|
||||
export * from './accountDelete'
|
||||
export * from './signerListSet'
|
||||
export * from './paymentTransaction'
|
||||
|
||||
109
src/models/transactions/paymentTransaction.ts
Normal file
109
src/models/transactions/paymentTransaction.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { ValidationError } from '../../common/errors'
|
||||
import { Amount, Path } from '../common'
|
||||
import { isFlagEnabled } from '../utils'
|
||||
import { BaseTransaction, isAmount, GlobalFlags, verifyBaseTransaction } from './common'
|
||||
|
||||
export enum PaymentTransactionFlagsEnum {
|
||||
tfNoDirectRipple = 0x00010000,
|
||||
tfPartialPayment = 0x00020000,
|
||||
tfLimitQuality = 0x00040000,
|
||||
}
|
||||
|
||||
export interface PaymentTransactionFlags extends GlobalFlags {
|
||||
tfNoDirectRipple?: boolean
|
||||
tfPartialPayment?: boolean
|
||||
tfLimitQuality?: boolean
|
||||
}
|
||||
export interface PaymentTransaction extends BaseTransaction {
|
||||
TransactionType: 'Payment'
|
||||
Amount: Amount
|
||||
Destination: string
|
||||
DestinationTag?: number
|
||||
InvoiceID?: string
|
||||
Paths?: Path[]
|
||||
SendMax?: Amount
|
||||
DeliverMin?: Amount
|
||||
Flags?: number | PaymentTransactionFlags
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PaymentTransaction} tx A Payment Transaction.
|
||||
* @returns {void}
|
||||
* @throws {ValidationError} When the PaymentTransaction is malformed.
|
||||
*/
|
||||
export function verifyPaymentTransaction(tx: PaymentTransaction): void {
|
||||
verifyBaseTransaction(tx)
|
||||
|
||||
if (tx.Amount === undefined) {
|
||||
throw new ValidationError('PaymentTransaction: missing field Amount')
|
||||
}
|
||||
|
||||
if (!isAmount(tx.Amount)) {
|
||||
throw new ValidationError('PaymentTransaction: invalid Amount')
|
||||
}
|
||||
|
||||
if (tx.Destination === undefined) {
|
||||
throw new ValidationError('PaymentTransaction: missing field Destination')
|
||||
}
|
||||
|
||||
if (!isAmount(tx.Destination)) {
|
||||
throw new ValidationError('PaymentTransaction: invalid Destination')
|
||||
}
|
||||
|
||||
if (tx.DestinationTag !== undefined && typeof tx.DestinationTag !== 'number') {
|
||||
throw new ValidationError('PaymentTransaction: DestinationTag must be a number')
|
||||
}
|
||||
|
||||
if (tx.InvoiceID !== undefined && typeof tx.InvoiceID !== 'string') {
|
||||
throw new ValidationError('PaymentTransaction: InvoiceID must be a string')
|
||||
}
|
||||
|
||||
if (tx.Paths !== undefined && !isPaths(tx.Paths)) {
|
||||
throw new ValidationError('PaymentTransaction: invalid Paths')
|
||||
}
|
||||
|
||||
if (tx.SendMax !== undefined && !isAmount(tx.SendMax)) {
|
||||
throw new ValidationError('PaymentTransaction: invalid SendMax')
|
||||
}
|
||||
|
||||
if (tx.DeliverMin !== undefined) {
|
||||
const isTfPartialPayment = typeof tx.Flags === 'number' ?
|
||||
isFlagEnabled(tx.Flags, PaymentTransactionFlagsEnum.tfPartialPayment) :
|
||||
tx.Flags?.tfPartialPayment ?? false
|
||||
|
||||
if (!isTfPartialPayment) {
|
||||
throw new ValidationError('PaymentTransaction: tfPartialPayment flag required with DeliverMin')
|
||||
}
|
||||
|
||||
if (!isAmount(tx.DeliverMin)) {
|
||||
throw new ValidationError('PaymentTransaction: invalid DeliverMin')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isPaths(paths: Path[]): boolean {
|
||||
if (!Array.isArray(paths) || paths.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (const i in paths) {
|
||||
const path = paths[i]
|
||||
if (!Array.isArray(path) || path.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (const j in path) {
|
||||
const pathStep = path[j]
|
||||
const { account, currency, issuer } = pathStep
|
||||
if (
|
||||
(account !== undefined && typeof account !== 'string') ||
|
||||
(currency !== undefined && typeof currency !== 'string') ||
|
||||
(issuer !== undefined && typeof issuer !== 'string')
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
import Metadata from "../common/metadata";
|
||||
import { AccountDelete } from "./accountDelete";
|
||||
import { AccountSet } from "./accountSet";
|
||||
import { CheckCancel } from "./checkCancel";
|
||||
import { CheckCash } from "./checkCash";
|
||||
import { CheckCreate } from "./checkCreate";
|
||||
import Metadata from "../common/metadata"
|
||||
import { AccountDelete } from "./accountDelete"
|
||||
import { AccountSet } from "./accountSet"
|
||||
import { CheckCancel } from "./checkCancel"
|
||||
import { CheckCash } from "./checkCash"
|
||||
import { CheckCreate } from "./checkCreate"
|
||||
import { OfferCancel } from "./offerCancel"
|
||||
import { OfferCreate } from "./offerCreate";
|
||||
import { SignerListSet } from "./signerListSet";
|
||||
import { OfferCreate } from "./offerCreate"
|
||||
import { PaymentTransaction } from "./paymentTransaction"
|
||||
import { SignerListSet } from "./signerListSet"
|
||||
|
||||
export type Transaction =
|
||||
AccountSet
|
||||
| AccountDelete
|
||||
AccountDelete
|
||||
| AccountSet
|
||||
| CheckCancel
|
||||
| CheckCash
|
||||
| CheckCreate
|
||||
@@ -21,7 +22,7 @@ export type Transaction =
|
||||
| OfferCancel
|
||||
// | OfferCancel
|
||||
| OfferCreate
|
||||
// | PaymentTransaction
|
||||
| PaymentTransaction
|
||||
// | PaymentChannelClaim
|
||||
// | PaymentChannelCreate
|
||||
// | PaymentChannelFund
|
||||
@@ -31,6 +32,6 @@ export type Transaction =
|
||||
// | TrustSet
|
||||
|
||||
export interface TransactionAndMetadata {
|
||||
transaction: Transaction;
|
||||
transaction: Transaction
|
||||
metadata: Metadata
|
||||
}
|
||||
|
||||
@@ -8,3 +8,14 @@
|
||||
export function onlyHasFields(obj: object, fields: Array<string>): boolean {
|
||||
return Object.keys(obj).every((key:string) => fields.includes(key))
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform bitwise AND (&) to check if a flag is enabled within Flags (as a number).
|
||||
*
|
||||
* @param {number} Flags A number that represents flags enabled.
|
||||
* @param {number} checkFlag A specific flag to check if it's enabled within Flags.
|
||||
* @returns {boolean} True if checkFlag is enabled within Flags.
|
||||
*/
|
||||
export function isFlagEnabled(Flags: number, checkFlag: number): boolean {
|
||||
return (checkFlag & Flags) === checkFlag
|
||||
}
|
||||
|
||||
@@ -103,21 +103,6 @@ describe('Transaction Verification', function () {
|
||||
)
|
||||
})
|
||||
|
||||
it (`Handles invalid Flags`, () => {
|
||||
const invalidFlags = {
|
||||
Account: "r97KeayHuEsDwyU1yPBVtMLLoQr79QcRFe",
|
||||
TransactionType: "Payment",
|
||||
Flags: "1000"
|
||||
} as any
|
||||
|
||||
|
||||
assert.throws(
|
||||
() => verifyBaseTransaction(invalidFlags),
|
||||
ValidationError,
|
||||
"BaseTransaction: invalid Flags"
|
||||
)
|
||||
})
|
||||
|
||||
it (`Handles invalid LastLedgerSequence`, () => {
|
||||
const invalidLastLedgerSequence = {
|
||||
Account: "r97KeayHuEsDwyU1yPBVtMLLoQr79QcRFe",
|
||||
|
||||
132
test/models/paymentTransaction.ts
Normal file
132
test/models/paymentTransaction.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { ValidationError } from 'xrpl-local/common/errors'
|
||||
import { PaymentTransactionFlagsEnum, verifyPaymentTransaction } from './../../src/models/transactions/paymentTransaction'
|
||||
import { assert } from 'chai'
|
||||
|
||||
/**
|
||||
* PaymentTransaction Verification Testing
|
||||
*
|
||||
* Providing runtime verification testing for each specific transaction type
|
||||
*/
|
||||
describe('Payment Transaction Verification', () => {
|
||||
let paymentTransaction
|
||||
|
||||
beforeEach(() => {
|
||||
paymentTransaction = {
|
||||
TransactionType: 'Payment',
|
||||
Account: 'rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo',
|
||||
Amount: '1234',
|
||||
Destination: 'rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy',
|
||||
DestinationTag: 1,
|
||||
InvoiceID: '6F1DFD1D0FE8A32E40E1F2C05CF1C15545BAB56B617F9C6C2D63A6B704BEF59B',
|
||||
Paths: [[{ account: 'aw0efji', currency: 'XRP', issuer: 'apsoeijf90wp34fh'}]],
|
||||
SendMax: '100000000',
|
||||
} as any
|
||||
})
|
||||
|
||||
it (`verifies valid PaymentTransaction`, () => {
|
||||
assert.doesNotThrow(() => verifyPaymentTransaction(paymentTransaction))
|
||||
})
|
||||
|
||||
it (`throws when Amount is missing`, () => {
|
||||
delete paymentTransaction.Amount
|
||||
assert.throws(
|
||||
() => verifyPaymentTransaction(paymentTransaction),
|
||||
ValidationError,
|
||||
'PaymentTransaction: missing field Amount'
|
||||
)
|
||||
})
|
||||
|
||||
it (`throws when Amount is invalid`, () => {
|
||||
paymentTransaction.Amount = 1234
|
||||
assert.throws(
|
||||
() => verifyPaymentTransaction(paymentTransaction),
|
||||
ValidationError,
|
||||
'PaymentTransaction: invalid Amount'
|
||||
)
|
||||
})
|
||||
|
||||
it (`throws when Destination is missing`, () => {
|
||||
delete paymentTransaction.Destination
|
||||
assert.throws(
|
||||
() => verifyPaymentTransaction(paymentTransaction),
|
||||
ValidationError,
|
||||
'PaymentTransaction: missing field Destination'
|
||||
)
|
||||
})
|
||||
|
||||
it (`throws when Destination is invalid`, () => {
|
||||
paymentTransaction.Destination = 7896214
|
||||
assert.throws(
|
||||
() => verifyPaymentTransaction(paymentTransaction),
|
||||
ValidationError,
|
||||
'PaymentTransaction: invalid Destination'
|
||||
)
|
||||
})
|
||||
|
||||
it (`throws when DestinationTag is not a number`, () => {
|
||||
paymentTransaction.DestinationTag = '1'
|
||||
assert.throws(
|
||||
() => verifyPaymentTransaction(paymentTransaction),
|
||||
ValidationError,
|
||||
'PaymentTransaction: DestinationTag must be a number'
|
||||
)
|
||||
})
|
||||
|
||||
it (`throws when InvoiceID is not a string`, () => {
|
||||
paymentTransaction.InvoiceID = 19832
|
||||
assert.throws(
|
||||
() => verifyPaymentTransaction(paymentTransaction),
|
||||
ValidationError,
|
||||
'PaymentTransaction: InvoiceID must be a string'
|
||||
)
|
||||
})
|
||||
|
||||
it (`throws when Paths is invalid`, () => {
|
||||
paymentTransaction.Paths = [[{ account: 123 }]]
|
||||
assert.throws(
|
||||
() => verifyPaymentTransaction(paymentTransaction),
|
||||
ValidationError,
|
||||
'PaymentTransaction: invalid Paths'
|
||||
)
|
||||
})
|
||||
|
||||
it (`throws when SendMax is invalid`, () => {
|
||||
paymentTransaction.SendMax = 100000000
|
||||
assert.throws(
|
||||
() => verifyPaymentTransaction(paymentTransaction),
|
||||
ValidationError,
|
||||
'PaymentTransaction: invalid SendMax'
|
||||
)
|
||||
})
|
||||
|
||||
it (`verifies valid DeliverMin with tfPartialPayment flag set as a number`, () => {
|
||||
paymentTransaction.DeliverMin = '10000'
|
||||
paymentTransaction.Flags = PaymentTransactionFlagsEnum.tfPartialPayment,
|
||||
assert.doesNotThrow(() => verifyPaymentTransaction(paymentTransaction))
|
||||
})
|
||||
|
||||
it (`verifies valid DeliverMin with tfPartialPayment flag set as a boolean`, () => {
|
||||
paymentTransaction.DeliverMin = '10000'
|
||||
paymentTransaction.Flags = { tfPartialPayment: true }
|
||||
assert.doesNotThrow(() => verifyPaymentTransaction(paymentTransaction))
|
||||
})
|
||||
|
||||
it (`throws when DeliverMin is invalid`, () => {
|
||||
paymentTransaction.DeliverMin = 10000
|
||||
paymentTransaction.Flags = { tfPartialPayment: true }
|
||||
assert.throws(
|
||||
() => verifyPaymentTransaction(paymentTransaction),
|
||||
ValidationError,
|
||||
'PaymentTransaction: invalid DeliverMin'
|
||||
)
|
||||
})
|
||||
|
||||
it (`throws when tfPartialPayment flag is missing with valid DeliverMin`, () => {
|
||||
paymentTransaction.DeliverMin = '10000'
|
||||
assert.throws(
|
||||
() => verifyPaymentTransaction(paymentTransaction),
|
||||
ValidationError,
|
||||
'PaymentTransaction: tfPartialPayment flag required with DeliverMin'
|
||||
)
|
||||
})
|
||||
})
|
||||
29
test/models/utils.ts
Normal file
29
test/models/utils.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { isFlagEnabled } from '../../src/models/utils'
|
||||
import { assert } from 'chai'
|
||||
|
||||
/**
|
||||
* Utils Testing
|
||||
*
|
||||
* Provides tests for utils used in models
|
||||
*/
|
||||
describe('Models Utils', () => {
|
||||
describe('isFlagEnabled', () => {
|
||||
let flags
|
||||
const flag1 = 0x00010000
|
||||
const flag2 = 0x00020000
|
||||
|
||||
beforeEach(() => {
|
||||
flags = 0x00000000
|
||||
})
|
||||
|
||||
it('verifies a flag is enabled', () => {
|
||||
flags += flag1 + flag2
|
||||
assert.isTrue(isFlagEnabled(flags, flag1))
|
||||
})
|
||||
|
||||
it('verifies a flag is not enabled', () => {
|
||||
flags += flag2
|
||||
assert.isFalse(isFlagEnabled(flags, flag1))
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user