UNPKG

@nextrope/xrpl

Version:

A TypeScript/JavaScript API for interacting with the XRP Ledger in Node.js and the browser

313 lines (285 loc) 8.99 kB
import { ValidationError } from '../../errors' import { Amount, Path, MPTAmount } from '../common' import { isFlagEnabled } from '../utils' import { BaseTransaction, isAmount, GlobalFlagsInterface, validateBaseTransaction, isAccount, isDomainID, validateRequiredField, validateOptionalField, isNumber, Account, validateCredentialsList, MAX_AUTHORIZED_CREDENTIALS, isArray, } from './common' import type { TransactionMetadataBase } from './metadata' /** * Enum representing values for Payment Transaction Flags. * * @category Transaction Flags */ export enum PaymentFlags { /** * Do not use the default path; only use paths included in the Paths field. * This is intended to force the transaction to take arbitrage opportunities. * Most clients do not need this. */ tfNoRippleDirect = 0x00010000, /** * If the specified Amount cannot be sent without spending more than SendMax, * reduce the received amount instead of failing outright. See Partial. * Payments for more details. */ tfPartialPayment = 0x00020000, /** * Only take paths where all the conversions have an input:output ratio that * is equal or better than the ratio of Amount:SendMax. See Limit Quality for * details. */ tfLimitQuality = 0x00040000, } /** * Map of flags to boolean values representing {@link Payment} transaction * flags. * * @category Transaction Flags * * @example * ```typescript * const partialPayment: Payment = { * TransactionType: 'Payment', * Account: 'rM9WCfJU6udpFkvKThRaFHDMsp7L8rpgN', * Amount: { * currency: 'FOO', * value: '4000', * issuer: 'rPzwM2JfCSDjhbesdTCqFjWWdK7eFtTwZz', * }, * Destination: 'rPzwM2JfCSDjhbesdTCqFjWWdK7eFtTwZz', * Flags: { * tfPartialPayment: true * } * } * * // Autofill the tx to see how flags actually look compared to the interface usage. * const autofilledTx = await client.autofill(partialPayment) * console.log(autofilledTx) * // { * // TransactionType: 'Payment', * // Account: 'rM9WCfJU6udpFkvKThRaFHDMsp7L8rpgN', * // Amount: { * // currency: 'FOO', * // value: '4000', * // issuer: 'rPzwM2JfCSDjhbesdTCqFjWWdK7eFtTwZz' * // }, * // Destination: 'rPzwM2JfCSDjhbesdTCqFjWWdK7eFtTwZz', * // Flags: 131072, * // Sequence: 21970996, * // Fee: '12', * // LastLedgerSequence: 21971016 * // } * ``` */ export interface PaymentFlagsInterface extends GlobalFlagsInterface { /** * Do not use the default path; only use paths included in the Paths field. * This is intended to force the transaction to take arbitrage opportunities. * Most clients do not need this. */ tfNoRippleDirect?: boolean /** * If the specified Amount cannot be sent without spending more than SendMax, * reduce the received amount instead of failing outright. See Partial. * Payments for more details. */ tfPartialPayment?: boolean /** * Only take paths where all the conversions have an input:output ratio that * is equal or better than the ratio of Amount:SendMax. See Limit Quality for * details. */ tfLimitQuality?: boolean } /** * A Payment transaction represents a transfer of value from one account to * another. * * @category Transaction Models */ export interface Payment extends BaseTransaction { TransactionType: 'Payment' /** * The amount of currency to deliver. For non-XRP amounts, the nested field * names MUST be lower-case. If the tfPartialPayment flag is set, deliver up * to this amount instead. */ Amount: Amount | MPTAmount DeliverMax?: Amount | MPTAmount /** The unique address of the account receiving the payment. */ Destination: Account /** * Arbitrary tag that identifies the reason for the payment to the * destination, or a hosted recipient to pay. */ DestinationTag?: number /** * Arbitrary 256-bit hash representing a specific reason or identifier for * this payment. */ InvoiceID?: string /** * Array of payment paths to be used for this transaction. Must be omitted * for XRP-to-XRP transactions. */ Paths?: Path[] /** * Highest amount of source currency this transaction is allowed to cost, * including transfer fees, exchange rates, and slippage . Does not include * the XRP destroyed as a cost for submitting the transaction. For non-XRP * amounts, the nested field names MUST be lower-case. Must be supplied for * cross-currency/cross-issue payments. Must be omitted for XRP-to-XRP * Payments. */ SendMax?: Amount | MPTAmount /** * Minimum amount of destination currency this transaction should deliver. * Only valid if this is a partial payment. For non-XRP amounts, the nested * field names are lower-case. */ DeliverMin?: Amount | MPTAmount /** * Credentials associated with the sender of this transaction. * The credentials included must not be expired. */ CredentialIDs?: string[] /** * The domain the sender intends to use. Both the sender and destination must * be part of this domain. The DomainID can be included if the sender intends * it to be a cross-currency payment (i.e. if the payment is going to interact * with the DEX). The domain will only play it's role if there is a path that * crossing an orderbook. * * Note: it's still possible that DomainID is included but the payment does * not interact with DEX, it simply means that the DomainID will be ignored * during payment paths. */ DomainID?: string Flags?: number | PaymentFlagsInterface } export interface PaymentMetadata extends TransactionMetadataBase { DeliveredAmount?: Amount | MPTAmount delivered_amount?: Amount | MPTAmount | 'unavailable' } /** * Verify the form and type of a Payment at runtime. * * @param tx - A Payment Transaction. * @throws When the Payment is malformed. */ export function validatePayment(tx: Record<string, unknown>): void { validateBaseTransaction(tx) if (tx.Amount === undefined) { throw new ValidationError('PaymentTransaction: missing field Amount') } if (!isAmount(tx.Amount)) { throw new ValidationError('PaymentTransaction: invalid Amount') } validateRequiredField(tx, 'Destination', isAccount) validateOptionalField(tx, 'DestinationTag', isNumber) validateCredentialsList( tx.CredentialIDs, tx.TransactionType, true, MAX_AUTHORIZED_CREDENTIALS, ) if (tx.InvoiceID !== undefined && typeof tx.InvoiceID !== 'string') { throw new ValidationError('PaymentTransaction: InvoiceID must be a string') } validateOptionalField(tx, 'DomainID', isDomainID, { txType: 'PaymentTransaction', paramName: 'DomainID', }) 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') } checkPartialPayment(tx) } function checkPartialPayment(tx: Record<string, unknown>): void { if (tx.DeliverMin != null) { if (tx.Flags == null) { throw new ValidationError( 'PaymentTransaction: tfPartialPayment flag required with DeliverMin', ) } // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Only used by JS const flags = tx.Flags as number | PaymentFlagsInterface const isTfPartialPayment = typeof flags === 'number' ? isFlagEnabled(flags, PaymentFlags.tfPartialPayment) : 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 isPathStep(pathStep: Record<string, unknown>): boolean { if (pathStep.account !== undefined && typeof pathStep.account !== 'string') { return false } if ( pathStep.currency !== undefined && typeof pathStep.currency !== 'string' ) { return false } if (pathStep.issuer !== undefined && typeof pathStep.issuer !== 'string') { return false } if ( pathStep.account !== undefined && pathStep.currency === undefined && pathStep.issuer === undefined ) { return true } if (pathStep.currency !== undefined || pathStep.issuer !== undefined) { return true } return false } function isPath(path: unknown): path is Path { if (!Array.isArray(path) || path.length === 0) { return false } for (const pathStep of path) { if (!isPathStep(pathStep)) { return false } } return true } function isPaths(paths: unknown): paths is Path[] { if (!isArray(paths) || paths.length === 0) { return false } for (const path of paths) { if (!isArray(path) || path.length === 0) { return false } if (!isPath(path)) { return false } } return true }