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')
|
if (common.Sequence !== undefined && typeof common.Sequence !== 'number')
|
||||||
throw new ValidationError("BaseTransaction: invalid Sequence")
|
throw new ValidationError("BaseTransaction: invalid Sequence")
|
||||||
|
|
||||||
if (common.Flags !== undefined && typeof common.Flags !== 'number')
|
|
||||||
throw new ValidationError("BaseTransaction: invalid Flags")
|
|
||||||
|
|
||||||
if (common.AccountTxnID !== undefined
|
if (common.AccountTxnID !== undefined
|
||||||
&& typeof common.AccountTxnID !== 'string')
|
&& typeof common.AccountTxnID !== 'string')
|
||||||
throw new ValidationError("BaseTransaction: invalid AccountTxnID")
|
throw new ValidationError("BaseTransaction: invalid AccountTxnID")
|
||||||
|
|||||||
@@ -7,3 +7,4 @@ export * from './checkCash'
|
|||||||
export * from './checkCancel'
|
export * from './checkCancel'
|
||||||
export * from './accountDelete'
|
export * from './accountDelete'
|
||||||
export * from './signerListSet'
|
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 Metadata from "../common/metadata"
|
||||||
import { AccountDelete } from "./accountDelete";
|
import { AccountDelete } from "./accountDelete"
|
||||||
import { AccountSet } from "./accountSet";
|
import { AccountSet } from "./accountSet"
|
||||||
import { CheckCancel } from "./checkCancel";
|
import { CheckCancel } from "./checkCancel"
|
||||||
import { CheckCash } from "./checkCash";
|
import { CheckCash } from "./checkCash"
|
||||||
import { CheckCreate } from "./checkCreate";
|
import { CheckCreate } from "./checkCreate"
|
||||||
import { OfferCancel } from "./offerCancel"
|
import { OfferCancel } from "./offerCancel"
|
||||||
import { OfferCreate } from "./offerCreate";
|
import { OfferCreate } from "./offerCreate"
|
||||||
import { SignerListSet } from "./signerListSet";
|
import { PaymentTransaction } from "./paymentTransaction"
|
||||||
|
import { SignerListSet } from "./signerListSet"
|
||||||
|
|
||||||
export type Transaction =
|
export type Transaction =
|
||||||
AccountSet
|
AccountDelete
|
||||||
| AccountDelete
|
| AccountSet
|
||||||
| CheckCancel
|
| CheckCancel
|
||||||
| CheckCash
|
| CheckCash
|
||||||
| CheckCreate
|
| CheckCreate
|
||||||
@@ -21,7 +22,7 @@ export type Transaction =
|
|||||||
| OfferCancel
|
| OfferCancel
|
||||||
// | OfferCancel
|
// | OfferCancel
|
||||||
| OfferCreate
|
| OfferCreate
|
||||||
// | PaymentTransaction
|
| PaymentTransaction
|
||||||
// | PaymentChannelClaim
|
// | PaymentChannelClaim
|
||||||
// | PaymentChannelCreate
|
// | PaymentChannelCreate
|
||||||
// | PaymentChannelFund
|
// | PaymentChannelFund
|
||||||
@@ -31,6 +32,6 @@ export type Transaction =
|
|||||||
// | TrustSet
|
// | TrustSet
|
||||||
|
|
||||||
export interface TransactionAndMetadata {
|
export interface TransactionAndMetadata {
|
||||||
transaction: Transaction;
|
transaction: Transaction
|
||||||
metadata: Metadata
|
metadata: Metadata
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,4 +7,15 @@
|
|||||||
*/
|
*/
|
||||||
export function onlyHasFields(obj: object, fields: Array<string>): boolean {
|
export function onlyHasFields(obj: object, fields: Array<string>): boolean {
|
||||||
return Object.keys(obj).every((key:string) => fields.includes(key))
|
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`, () => {
|
it (`Handles invalid LastLedgerSequence`, () => {
|
||||||
const invalidLastLedgerSequence = {
|
const invalidLastLedgerSequence = {
|
||||||
Account: "r97KeayHuEsDwyU1yPBVtMLLoQr79QcRFe",
|
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