fix: tx fields for accounts allowing empty string (#2525)

Fields that were encoded as STAccounts were turning '' into rrrrrrrrrrrrrrrrrrrrrhoLvTp the ACCOUNT_ZERO account. They will now throw a ValidationError.

Invalid addresses on a transaction now throws a ValidationError when submitting a transaction instead of Error('checksum_invalid').

This change updates some error messages for fields a few fields that aren't accounts, DestinationTag or NFTokenID. They will still be of type ValidationError.

Fixes: #2517 and #2475

---------

Co-authored-by: Elliot Lee <github.public@intelliot.com>
This commit is contained in:
Caleb Kniffen
2023-10-18 10:25:57 -05:00
committed by GitHub
parent 8e929c5a57
commit 65bf5d40ea
28 changed files with 244 additions and 190 deletions

View File

@@ -9,10 +9,14 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr
* Add pseudo transaction types to `tx` and `ledger` method responses. * Add pseudo transaction types to `tx` and `ledger` method responses.
* Add missing `type` param to `ledger_data` and `ledger` requests * Add missing `type` param to `ledger_data` and `ledger` requests
* Type assertions around `PreviousTxnID` and `PreviousTxnLgrSeq` missing on some ledger objects * Type assertions around `PreviousTxnID` and `PreviousTxnLgrSeq` missing on some ledger objects
* Transaction fields that represent an address no longer allow an empty string (`''`). If you want to specify [ACCOUNT_ZERO](https://xrpl.org/addresses.html#special-addresses), you can specify `rrrrrrrrrrrrrrrrrrrrrhoLvTp`. ⚠️ **WARNING:** `rrrrrrrrrrrrrrrrrrrrrhoLvTp` is a black hole address, with no corresponding private key. Accounts/funds controlled by this address are not accessible.
* Invalid addresses on a transaction now throws a `ValidationError` when submitting a transaction instead of `Error('checksum_invalid')`
### Updated ### Updated
* Make `LedgerEntryResponse` a generic so it can be used like `LedgerEntryResponse<Escrow>` * Make `LedgerEntryResponse` a generic so it can be used like `LedgerEntryResponse<Escrow>`
* Clean up typing of `type` param and the response property `account_objects` of the `account_objects` request. * Clean up typing of `type` param and the response property `account_objects` of the `account_objects` request.
* Error messages for fields that equate to an address, `DestinationTag`, or `NFTokenID`. They will still be of type `ValidationError`.
* Add alias type of `Account` to improve intellisense for Transaction fields that equate to an address.
### Changed ### Changed
* Removed sidechain-devnet faucet support as it is being moved to Devnet * Removed sidechain-devnet faucet support as it is being moved to Devnet

View File

@@ -1,6 +1,12 @@
import { ValidationError } from '../../errors' import {
Account,
import { BaseTransaction, validateBaseTransaction } from './common' BaseTransaction,
isAccount,
isString,
validateBaseTransaction,
validateOptionalField,
validateRequiredField,
} from './common'
/** /**
* The NFTokenBurn transaction is used to remove an NFToken object from the * The NFTokenBurn transaction is used to remove an NFToken object from the
@@ -20,7 +26,7 @@ export interface NFTokenBurn extends BaseTransaction {
* in the NFToken, either the issuer account or an account authorized by the * in the NFToken, either the issuer account or an account authorized by the
* issuer, i.e. MintAccount. * issuer, i.e. MintAccount.
*/ */
Account: string Account: Account
/** /**
* Identifies the NFToken object to be removed by the transaction. * Identifies the NFToken object to be removed by the transaction.
*/ */
@@ -30,7 +36,7 @@ export interface NFTokenBurn extends BaseTransaction {
* Account. Only used to burn tokens which have the lsfBurnable flag enabled * Account. Only used to burn tokens which have the lsfBurnable flag enabled
* and are not owned by the signing account. * and are not owned by the signing account.
*/ */
Owner?: string Owner?: Account
} }
/** /**
@@ -41,8 +47,6 @@ export interface NFTokenBurn extends BaseTransaction {
*/ */
export function validateNFTokenBurn(tx: Record<string, unknown>): void { export function validateNFTokenBurn(tx: Record<string, unknown>): void {
validateBaseTransaction(tx) validateBaseTransaction(tx)
validateRequiredField(tx, 'NFTokenID', isString)
if (tx.NFTokenID == null) { validateOptionalField(tx, 'Owner', isAccount)
throw new ValidationError('NFTokenBurn: missing field NFTokenID')
}
} }

View File

@@ -8,6 +8,9 @@ import {
validateBaseTransaction, validateBaseTransaction,
isAmount, isAmount,
parseAmountValue, parseAmountValue,
isAccount,
validateOptionalField,
Account,
} from './common' } from './common'
/** /**
@@ -67,7 +70,7 @@ export interface NFTokenCreateOffer extends BaseTransaction {
* (since an offer to sell a token one doesn't already hold * (since an offer to sell a token one doesn't already hold
* is meaningless). * is meaningless).
*/ */
Owner?: string Owner?: Account
/** /**
* Indicates the time after which the offer will no longer * Indicates the time after which the offer will no longer
* be valid. The value is the number of seconds since the * be valid. The value is the number of seconds since the
@@ -79,7 +82,7 @@ export interface NFTokenCreateOffer extends BaseTransaction {
* accepted by the specified account. Attempts by other * accepted by the specified account. Attempts by other
* accounts to accept this offer MUST fail. * accounts to accept this offer MUST fail.
*/ */
Destination?: string Destination?: Account
Flags?: number | NFTokenCreateOfferFlagsInterface Flags?: number | NFTokenCreateOfferFlagsInterface
} }
@@ -126,6 +129,9 @@ export function validateNFTokenCreateOffer(tx: Record<string, unknown>): void {
) )
} }
validateOptionalField(tx, 'Destination', isAccount)
validateOptionalField(tx, 'Owner', isAccount)
if (tx.NFTokenID == null) { if (tx.NFTokenID == null) {
throw new ValidationError('NFTokenCreateOffer: missing field NFTokenID') throw new ValidationError('NFTokenCreateOffer: missing field NFTokenID')
} }

View File

@@ -1,7 +1,14 @@
import { ValidationError } from '../../errors' import { ValidationError } from '../../errors'
import { isHex } from '../utils' import { isHex } from '../utils'
import { BaseTransaction, GlobalFlags, validateBaseTransaction } from './common' import {
Account,
BaseTransaction,
GlobalFlags,
isAccount,
validateBaseTransaction,
validateOptionalField,
} from './common'
/** /**
* Transaction Flags for an NFTokenMint Transaction. * Transaction Flags for an NFTokenMint Transaction.
@@ -68,7 +75,7 @@ export interface NFTokenMint extends BaseTransaction {
* present, the `MintAccount` field in the `AccountRoot` of the `Issuer` * present, the `MintAccount` field in the `AccountRoot` of the `Issuer`
* field must match the `Account`, otherwise the transaction will fail. * field must match the `Account`, otherwise the transaction will fail.
*/ */
Issuer?: string Issuer?: Account
/** /**
* Specifies the fee charged by the issuer for secondary sales of the Token, * Specifies the fee charged by the issuer for secondary sales of the Token,
* if such sales are allowed. Valid values for this field are between 0 and * if such sales are allowed. Valid values for this field are between 0 and
@@ -109,6 +116,8 @@ export function validateNFTokenMint(tx: Record<string, unknown>): void {
) )
} }
validateOptionalField(tx, 'Issuer', isAccount)
if (typeof tx.URI === 'string' && tx.URI === '') { if (typeof tx.URI === 'string' && tx.URI === '') {
throw new ValidationError('NFTokenMint: URI must not be empty string') throw new ValidationError('NFTokenMint: URI must not be empty string')
} }

View File

@@ -4,9 +4,10 @@ import {
BaseTransaction, BaseTransaction,
isAmount, isAmount,
isXChainBridge, isXChainBridge,
isString,
validateBaseTransaction, validateBaseTransaction,
validateRequiredField, validateRequiredField,
isAccount,
Account,
} from './common' } from './common'
/** /**
@@ -38,7 +39,7 @@ export interface XChainAccountCreateCommit extends BaseTransaction {
/** /**
* The destination account on the destination chain. * The destination account on the destination chain.
*/ */
Destination: string Destination: Account
/** /**
* The amount, in XRP, to use for account creation. This must be greater than or * The amount, in XRP, to use for account creation. This must be greater than or
@@ -62,7 +63,7 @@ export function validateXChainAccountCreateCommit(
validateRequiredField(tx, 'SignatureReward', isAmount) validateRequiredField(tx, 'SignatureReward', isAmount)
validateRequiredField(tx, 'Destination', isString) validateRequiredField(tx, 'Destination', isAccount)
validateRequiredField(tx, 'Amount', isAmount) validateRequiredField(tx, 'Amount', isAmount)
} }

View File

@@ -1,7 +1,9 @@
import { Amount, XChainBridge } from '../common' import { Amount, XChainBridge } from '../common'
import { import {
Account,
BaseTransaction, BaseTransaction,
isAccount,
isAmount, isAmount,
isNumber, isNumber,
isString, isString,
@@ -29,23 +31,23 @@ export interface XChainAddAccountCreateAttestation extends BaseTransaction {
/** /**
* The account that should receive this signer's share of the SignatureReward. * The account that should receive this signer's share of the SignatureReward.
*/ */
AttestationRewardAccount: string AttestationRewardAccount: Account
/** /**
* The account on the door account's signer list that is signing the transaction. * The account on the door account's signer list that is signing the transaction.
*/ */
AttestationSignerAccount: string AttestationSignerAccount: Account
/** /**
* The destination account for the funds on the destination chain. * The destination account for the funds on the destination chain.
*/ */
Destination: string Destination: Account
/** /**
* The account on the source chain that submitted the {@link XChainAccountCreateCommit} * The account on the source chain that submitted the {@link XChainAccountCreateCommit}
* transaction that triggered the event associated with the attestation. * transaction that triggered the event associated with the attestation.
*/ */
OtherChainSource: string OtherChainSource: Account
/** /**
* The public key used to verify the signature. * The public key used to verify the signature.
@@ -91,13 +93,13 @@ export function validateXChainAddAccountCreateAttestation(
validateRequiredField(tx, 'Amount', isAmount) validateRequiredField(tx, 'Amount', isAmount)
validateRequiredField(tx, 'AttestationRewardAccount', isString) validateRequiredField(tx, 'AttestationRewardAccount', isAccount)
validateRequiredField(tx, 'AttestationSignerAccount', isString) validateRequiredField(tx, 'AttestationSignerAccount', isAccount)
validateRequiredField(tx, 'Destination', isString) validateRequiredField(tx, 'Destination', isAccount)
validateRequiredField(tx, 'OtherChainSource', isString) validateRequiredField(tx, 'OtherChainSource', isAccount)
validateRequiredField(tx, 'PublicKey', isString) validateRequiredField(tx, 'PublicKey', isString)

View File

@@ -1,7 +1,9 @@
import { Amount, XChainBridge } from '../common' import { Amount, XChainBridge } from '../common'
import { import {
Account,
BaseTransaction, BaseTransaction,
isAccount,
isAmount, isAmount,
isNumber, isNumber,
isString, isString,
@@ -28,24 +30,24 @@ export interface XChainAddClaimAttestation extends BaseTransaction {
/** /**
* The account that should receive this signer's share of the SignatureReward. * The account that should receive this signer's share of the SignatureReward.
*/ */
AttestationRewardAccount: string AttestationRewardAccount: Account
/** /**
* The account on the door account's signer list that is signing the transaction. * The account on the door account's signer list that is signing the transaction.
*/ */
AttestationSignerAccount: string AttestationSignerAccount: Account
/** /**
* The destination account for the funds on the destination chain (taken from * The destination account for the funds on the destination chain (taken from
* the {@link XChainCommit} transaction). * the {@link XChainCommit} transaction).
*/ */
Destination?: string Destination?: Account
/** /**
* The account on the source chain that submitted the {@link XChainCommit} * The account on the source chain that submitted the {@link XChainCommit}
* transaction that triggered the event associated with the attestation. * transaction that triggered the event associated with the attestation.
*/ */
OtherChainSource: string OtherChainSource: Account
/** /**
* The public key used to verify the attestation signature. * The public key used to verify the attestation signature.
@@ -87,13 +89,13 @@ export function validateXChainAddClaimAttestation(
validateRequiredField(tx, 'Amount', isAmount) validateRequiredField(tx, 'Amount', isAmount)
validateRequiredField(tx, 'AttestationRewardAccount', isString) validateRequiredField(tx, 'AttestationRewardAccount', isAccount)
validateRequiredField(tx, 'AttestationSignerAccount', isString) validateRequiredField(tx, 'AttestationSignerAccount', isAccount)
validateOptionalField(tx, 'Destination', isString) validateOptionalField(tx, 'Destination', isAccount)
validateRequiredField(tx, 'OtherChainSource', isString) validateRequiredField(tx, 'OtherChainSource', isAccount)
validateRequiredField(tx, 'PublicKey', isString) validateRequiredField(tx, 'PublicKey', isString)

View File

@@ -1,7 +1,9 @@
import { Amount, XChainBridge } from '../common' import { Amount, XChainBridge } from '../common'
import { import {
Account,
BaseTransaction, BaseTransaction,
isAccount,
isAmount, isAmount,
isNumber, isNumber,
isString, isString,
@@ -38,7 +40,7 @@ export interface XChainClaim extends BaseTransaction {
* sequence number and collected signatures won't be destroyed, and the * sequence number and collected signatures won't be destroyed, and the
* transaction can be rerun with a different destination. * transaction can be rerun with a different destination.
*/ */
Destination: string Destination: Account
/** /**
* An integer destination tag. * An integer destination tag.
@@ -69,7 +71,7 @@ export function validateXChainClaim(tx: Record<string, unknown>): void {
(inp) => isNumber(inp) || isString(inp), (inp) => isNumber(inp) || isString(inp),
) )
validateRequiredField(tx, 'Destination', isString) validateRequiredField(tx, 'Destination', isAccount)
validateOptionalField(tx, 'DestinationTag', isNumber) validateOptionalField(tx, 'DestinationTag', isNumber)

View File

@@ -1,7 +1,9 @@
import { Amount, XChainBridge } from '../common' import { Amount, XChainBridge } from '../common'
import { import {
Account,
BaseTransaction, BaseTransaction,
isAccount,
isAmount, isAmount,
isNumber, isNumber,
isString, isString,
@@ -41,7 +43,7 @@ export interface XChainCommit extends BaseTransaction {
* destination chain will need to submit a {@link XChainClaim} transaction to * destination chain will need to submit a {@link XChainClaim} transaction to
* claim the funds. * claim the funds.
*/ */
OtherChainDestination?: string OtherChainDestination?: Account
/** /**
* The asset to commit, and the quantity. This must match the door account's * The asset to commit, and the quantity. This must match the door account's
@@ -68,7 +70,7 @@ export function validateXChainCommit(tx: Record<string, unknown>): void {
(inp) => isNumber(inp) || isString(inp), (inp) => isNumber(inp) || isString(inp),
) )
validateOptionalField(tx, 'OtherChainDestination', isString) validateOptionalField(tx, 'OtherChainDestination', isAccount)
validateRequiredField(tx, 'Amount', isAmount) validateRequiredField(tx, 'Amount', isAmount)
} }

View File

@@ -1,9 +1,10 @@
import { Amount, XChainBridge } from '../common' import { Amount, XChainBridge } from '../common'
import { import {
Account,
BaseTransaction, BaseTransaction,
isAccount,
isAmount, isAmount,
isString,
isXChainBridge, isXChainBridge,
validateBaseTransaction, validateBaseTransaction,
validateRequiredField, validateRequiredField,
@@ -33,7 +34,7 @@ export interface XChainCreateClaimID extends BaseTransaction {
/** /**
* The account that must send the {@link XChainCommit} transaction on the source chain. * The account that must send the {@link XChainCommit} transaction on the source chain.
*/ */
OtherChainSource: string OtherChainSource: Account
} }
/** /**
@@ -49,5 +50,5 @@ export function validateXChainCreateClaimID(tx: Record<string, unknown>): void {
validateRequiredField(tx, 'SignatureReward', isAmount) validateRequiredField(tx, 'SignatureReward', isAmount)
validateRequiredField(tx, 'OtherChainSource', isString) validateRequiredField(tx, 'OtherChainSource', isAccount)
} }

View File

@@ -1,6 +1,12 @@
import { ValidationError } from '../../errors' import {
Account,
import { BaseTransaction, validateBaseTransaction } from './common' BaseTransaction,
isAccount,
isNumber,
validateBaseTransaction,
validateOptionalField,
validateRequiredField,
} from './common'
/** /**
* An AccountDelete transaction deletes an account and any objects it owns in * An AccountDelete transaction deletes an account and any objects it owns in
@@ -16,7 +22,7 @@ export interface AccountDelete extends BaseTransaction {
* sending account. Must be a funded account in the ledger, and must not be. * sending account. Must be a funded account in the ledger, and must not be.
* the sending account. * the sending account.
*/ */
Destination: string Destination: Account
/** /**
* Arbitrary destination tag that identifies a hosted recipient or other. * Arbitrary destination tag that identifies a hosted recipient or other.
* information for the recipient of the deleted account's leftover XRP. * information for the recipient of the deleted account's leftover XRP.
@@ -33,18 +39,6 @@ export interface AccountDelete extends BaseTransaction {
export function validateAccountDelete(tx: Record<string, unknown>): void { export function validateAccountDelete(tx: Record<string, unknown>): void {
validateBaseTransaction(tx) validateBaseTransaction(tx)
if (tx.Destination === undefined) { validateRequiredField(tx, 'Destination', isAccount)
throw new ValidationError('AccountDelete: missing field Destination') validateOptionalField(tx, 'DestinationTag', isNumber)
}
if (typeof tx.Destination !== 'string') {
throw new ValidationError('AccountDelete: invalid Destination')
}
if (
tx.DestinationTag !== undefined &&
typeof tx.DestinationTag !== 'number'
) {
throw new ValidationError('AccountDelete: invalid DestinationTag')
}
} }

View File

@@ -1,8 +1,12 @@
import { isValidClassicAddress } from 'ripple-address-codec'
import { ValidationError } from '../../errors' import { ValidationError } from '../../errors'
import { BaseTransaction, validateBaseTransaction } from './common' import {
Account,
BaseTransaction,
isAccount,
validateBaseTransaction,
validateOptionalField,
} from './common'
/** /**
* Enum for AccountSet Flags. * Enum for AccountSet Flags.
@@ -155,7 +159,7 @@ export interface AccountSet extends BaseTransaction {
* Sets an alternate account that is allowed to mint NFTokens on this * Sets an alternate account that is allowed to mint NFTokens on this
* account's behalf using NFTokenMint's `Issuer` field. * account's behalf using NFTokenMint's `Issuer` field.
*/ */
NFTokenMinter?: string NFTokenMinter?: Account
} }
const MIN_TICK_SIZE = 3 const MIN_TICK_SIZE = 3
@@ -167,16 +171,11 @@ const MAX_TICK_SIZE = 15
* @param tx - An AccountSet Transaction. * @param tx - An AccountSet Transaction.
* @throws When the AccountSet is Malformed. * @throws When the AccountSet is Malformed.
*/ */
// eslint-disable-next-line max-lines-per-function, max-statements -- okay for this method, only a little over // eslint-disable-next-line max-lines-per-function -- okay for this method, only a little over
export function validateAccountSet(tx: Record<string, unknown>): void { export function validateAccountSet(tx: Record<string, unknown>): void {
validateBaseTransaction(tx) validateBaseTransaction(tx)
if ( validateOptionalField(tx, 'NFTokenMinter', isAccount)
tx.NFTokenMinter !== undefined &&
!isValidClassicAddress(String(tx.NFTokenMinter))
) {
throw new ValidationError('AccountSet: invalid NFTokenMinter')
}
if (tx.ClearFlag !== undefined) { if (tx.ClearFlag !== undefined) {
if (typeof tx.ClearFlag !== 'number') { if (typeof tx.ClearFlag !== 'number') {

View File

@@ -5,6 +5,11 @@ import {
BaseTransaction, BaseTransaction,
validateBaseTransaction, validateBaseTransaction,
isIssuedCurrency, isIssuedCurrency,
isAccount,
validateRequiredField,
validateOptionalField,
isNumber,
Account,
} from './common' } from './common'
/** /**
@@ -17,7 +22,7 @@ import {
export interface CheckCreate extends BaseTransaction { export interface CheckCreate extends BaseTransaction {
TransactionType: 'CheckCreate' TransactionType: 'CheckCreate'
/** The unique address of the account that can cash the Check. */ /** The unique address of the account that can cash the Check. */
Destination: string Destination: Account
/** /**
* Maximum amount of source currency the Check is allowed to debit the * Maximum amount of source currency the Check is allowed to debit the
* sender, including transfer fees on non-XRP currencies. The Check can only * sender, including transfer fees on non-XRP currencies. The Check can only
@@ -56,9 +61,8 @@ export function validateCheckCreate(tx: Record<string, unknown>): void {
throw new ValidationError('CheckCreate: missing field SendMax') throw new ValidationError('CheckCreate: missing field SendMax')
} }
if (tx.Destination === undefined) { validateRequiredField(tx, 'Destination', isAccount)
throw new ValidationError('CheckCreate: missing field Destination') validateOptionalField(tx, 'DestinationTag', isNumber)
}
if ( if (
typeof tx.SendMax !== 'string' && typeof tx.SendMax !== 'string' &&
@@ -68,17 +72,6 @@ export function validateCheckCreate(tx: Record<string, unknown>): void {
throw new ValidationError('CheckCreate: invalid SendMax') throw new ValidationError('CheckCreate: invalid SendMax')
} }
if (typeof tx.Destination !== 'string') {
throw new ValidationError('CheckCreate: invalid Destination')
}
if (
tx.DestinationTag !== undefined &&
typeof tx.DestinationTag !== 'number'
) {
throw new ValidationError('CheckCreate: invalid DestinationTag')
}
if (tx.Expiration !== undefined && typeof tx.Expiration !== 'number') { if (tx.Expiration !== undefined && typeof tx.Expiration !== 'number') {
throw new ValidationError('CheckCreate: invalid Expiration') throw new ValidationError('CheckCreate: invalid Expiration')
} }

View File

@@ -1,3 +1,4 @@
import { isValidClassicAddress, isValidXAddress } from 'ripple-address-codec'
import { TRANSACTION_TYPES } from 'ripple-binary-codec' import { TRANSACTION_TYPES } from 'ripple-binary-codec'
import { ValidationError } from '../../errors' import { ValidationError } from '../../errors'
@@ -118,6 +119,24 @@ export function isIssuedCurrency(
) )
} }
/**
* Must be a valid account address
*/
export type Account = string
/**
* Verify a string is in fact a valid account address.
*
* @param account - The object to check the form and type of.
* @returns Whether the account is properly formed account for a transaction.
*/
export function isAccount(account: unknown): account is Account {
return (
typeof account === 'string' &&
(isValidClassicAddress(account) || isValidXAddress(account))
)
}
/** /**
* Verify the form and type of an Amount at runtime. * Verify the form and type of an Amount at runtime.
* *
@@ -203,7 +222,7 @@ export interface GlobalFlags {}
*/ */
export interface BaseTransaction { export interface BaseTransaction {
/** The unique address of the transaction sender. */ /** The unique address of the transaction sender. */
Account: string Account: Account
/** /**
* The type of transaction. Valid types include: `Payment`, `OfferCreate`, * The type of transaction. Valid types include: `Payment`, `OfferCreate`,
* `TrustSet`, and many others. * `TrustSet`, and many others.

View File

@@ -1,6 +1,12 @@
import { ValidationError } from '../../errors' import { ValidationError } from '../../errors'
import { BaseTransaction, validateBaseTransaction } from './common' import {
Account,
BaseTransaction,
isAccount,
validateBaseTransaction,
validateRequiredField,
} from './common'
/** /**
* Return escrowed XRP to the sender. * Return escrowed XRP to the sender.
@@ -10,7 +16,7 @@ import { BaseTransaction, validateBaseTransaction } from './common'
export interface EscrowCancel extends BaseTransaction { export interface EscrowCancel extends BaseTransaction {
TransactionType: 'EscrowCancel' TransactionType: 'EscrowCancel'
/** Address of the source account that funded the escrow payment. */ /** Address of the source account that funded the escrow payment. */
Owner: string Owner: Account
/** /**
* Transaction sequence (or Ticket number) of EscrowCreate transaction that. * Transaction sequence (or Ticket number) of EscrowCreate transaction that.
* created the escrow to cancel. * created the escrow to cancel.
@@ -27,13 +33,7 @@ export interface EscrowCancel extends BaseTransaction {
export function validateEscrowCancel(tx: Record<string, unknown>): void { export function validateEscrowCancel(tx: Record<string, unknown>): void {
validateBaseTransaction(tx) validateBaseTransaction(tx)
if (tx.Owner == null) { validateRequiredField(tx, 'Owner', isAccount)
throw new ValidationError('EscrowCancel: missing Owner')
}
if (typeof tx.Owner !== 'string') {
throw new ValidationError('EscrowCancel: Owner must be a string')
}
if (tx.OfferSequence == null) { if (tx.OfferSequence == null) {
throw new ValidationError('EscrowCancel: missing OfferSequence') throw new ValidationError('EscrowCancel: missing OfferSequence')

View File

@@ -1,6 +1,14 @@
import { ValidationError } from '../../errors' import { ValidationError } from '../../errors'
import { BaseTransaction, validateBaseTransaction } from './common' import {
Account,
BaseTransaction,
isAccount,
isNumber,
validateBaseTransaction,
validateOptionalField,
validateRequiredField,
} from './common'
/** /**
* Sequester XRP until the escrow process either finishes or is canceled. * Sequester XRP until the escrow process either finishes or is canceled.
@@ -16,7 +24,7 @@ export interface EscrowCreate extends BaseTransaction {
*/ */
Amount: string Amount: string
/** Address to receive escrowed XRP. */ /** Address to receive escrowed XRP. */
Destination: string Destination: Account
/** /**
* The time, in seconds since the Ripple Epoch, when this escrow expires. * The time, in seconds since the Ripple Epoch, when this escrow expires.
* This value is immutable; the funds can only be returned the sender after. * This value is immutable; the funds can only be returned the sender after.
@@ -58,13 +66,8 @@ export function validateEscrowCreate(tx: Record<string, unknown>): void {
throw new ValidationError('EscrowCreate: Amount must be a string') throw new ValidationError('EscrowCreate: Amount must be a string')
} }
if (tx.Destination === undefined) { validateRequiredField(tx, 'Destination', isAccount)
throw new ValidationError('EscrowCreate: missing field Destination') validateOptionalField(tx, 'DestinationTag', isNumber)
}
if (typeof tx.Destination !== 'string') {
throw new ValidationError('EscrowCreate: Destination must be a string')
}
if (tx.CancelAfter === undefined && tx.FinishAfter === undefined) { if (tx.CancelAfter === undefined && tx.FinishAfter === undefined) {
throw new ValidationError( throw new ValidationError(
@@ -89,11 +92,4 @@ export function validateEscrowCreate(tx: Record<string, unknown>): void {
if (tx.Condition !== undefined && typeof tx.Condition !== 'string') { if (tx.Condition !== undefined && typeof tx.Condition !== 'string') {
throw new ValidationError('EscrowCreate: Condition must be a string') throw new ValidationError('EscrowCreate: Condition must be a string')
} }
if (
tx.DestinationTag !== undefined &&
typeof tx.DestinationTag !== 'number'
) {
throw new ValidationError('EscrowCreate: DestinationTag must be a number')
}
} }

View File

@@ -1,6 +1,12 @@
import { ValidationError } from '../../errors' import { ValidationError } from '../../errors'
import { BaseTransaction, validateBaseTransaction } from './common' import {
Account,
BaseTransaction,
isAccount,
validateBaseTransaction,
validateRequiredField,
} from './common'
/** /**
* Deliver XRP from a held payment to the recipient. * Deliver XRP from a held payment to the recipient.
@@ -10,7 +16,7 @@ import { BaseTransaction, validateBaseTransaction } from './common'
export interface EscrowFinish extends BaseTransaction { export interface EscrowFinish extends BaseTransaction {
TransactionType: 'EscrowFinish' TransactionType: 'EscrowFinish'
/** Address of the source account that funded the held payment. */ /** Address of the source account that funded the held payment. */
Owner: string Owner: Account
/** /**
* Transaction sequence of EscrowCreate transaction that created the held. * Transaction sequence of EscrowCreate transaction that created the held.
* payment to finish. * payment to finish.
@@ -37,13 +43,7 @@ export interface EscrowFinish extends BaseTransaction {
export function validateEscrowFinish(tx: Record<string, unknown>): void { export function validateEscrowFinish(tx: Record<string, unknown>): void {
validateBaseTransaction(tx) validateBaseTransaction(tx)
if (tx.Owner == null) { validateRequiredField(tx, 'Owner', isAccount)
throw new ValidationError('EscrowFinish: missing field Owner')
}
if (typeof tx.Owner !== 'string') {
throw new ValidationError('EscrowFinish: Owner must be a string')
}
if (tx.OfferSequence == null) { if (tx.OfferSequence == null) {
throw new ValidationError('EscrowFinish: missing field OfferSequence') throw new ValidationError('EscrowFinish: missing field OfferSequence')

View File

@@ -7,6 +7,11 @@ import {
isAmount, isAmount,
GlobalFlags, GlobalFlags,
validateBaseTransaction, validateBaseTransaction,
isAccount,
validateRequiredField,
validateOptionalField,
isNumber,
Account,
} from './common' } from './common'
/** /**
@@ -112,7 +117,7 @@ export interface Payment extends BaseTransaction {
*/ */
Amount: Amount Amount: Amount
/** The unique address of the account receiving the payment. */ /** The unique address of the account receiving the payment. */
Destination: string Destination: Account
/** /**
* Arbitrary tag that identifies the reason for the payment to the * Arbitrary tag that identifies the reason for the payment to the
* destination, or a hosted recipient to pay. * destination, or a hosted recipient to pay.
@@ -163,19 +168,8 @@ export function validatePayment(tx: Record<string, unknown>): void {
throw new ValidationError('PaymentTransaction: invalid Amount') throw new ValidationError('PaymentTransaction: invalid Amount')
} }
if (tx.Destination === undefined) { validateRequiredField(tx, 'Destination', isAccount)
throw new ValidationError('PaymentTransaction: missing field Destination') validateOptionalField(tx, 'DestinationTag', isNumber)
}
if (!isAmount(tx.Destination)) {
throw new ValidationError('PaymentTransaction: invalid Destination')
}
if (tx.DestinationTag != null && typeof tx.DestinationTag !== 'number') {
throw new ValidationError(
'PaymentTransaction: DestinationTag must be a number',
)
}
if (tx.InvoiceID !== undefined && typeof tx.InvoiceID !== 'string') { if (tx.InvoiceID !== undefined && typeof tx.InvoiceID !== 'string') {
throw new ValidationError('PaymentTransaction: InvoiceID must be a string') throw new ValidationError('PaymentTransaction: InvoiceID must be a string')

View File

@@ -1,6 +1,14 @@
import { ValidationError } from '../../errors' import { ValidationError } from '../../errors'
import { BaseTransaction, validateBaseTransaction } from './common' import {
Account,
BaseTransaction,
isAccount,
isNumber,
validateBaseTransaction,
validateOptionalField,
validateRequiredField,
} from './common'
/** /**
* Create a unidirectional channel and fund it with XRP. The address sending * Create a unidirectional channel and fund it with XRP. The address sending
@@ -21,7 +29,7 @@ export interface PaymentChannelCreate extends BaseTransaction {
* Address to receive XRP claims against this channel. This is also known as * Address to receive XRP claims against this channel. This is also known as
* the "destination address" for the channel. * the "destination address" for the channel.
*/ */
Destination: string Destination: Account
/** /**
* Amount of time the source address must wait before closing the channel if * Amount of time the source address must wait before closing the channel if
* it has unclaimed XRP. * it has unclaimed XRP.
@@ -54,7 +62,6 @@ export interface PaymentChannelCreate extends BaseTransaction {
* @param tx - An PaymentChannelCreate Transaction. * @param tx - An PaymentChannelCreate Transaction.
* @throws When the PaymentChannelCreate is Malformed. * @throws When the PaymentChannelCreate is Malformed.
*/ */
// eslint-disable-next-line max-lines-per-function -- okay for this function, there's a lot of things to check
export function validatePaymentChannelCreate( export function validatePaymentChannelCreate(
tx: Record<string, unknown>, tx: Record<string, unknown>,
): void { ): void {
@@ -68,15 +75,8 @@ export function validatePaymentChannelCreate(
throw new ValidationError('PaymentChannelCreate: Amount must be a string') throw new ValidationError('PaymentChannelCreate: Amount must be a string')
} }
if (tx.Destination === undefined) { validateRequiredField(tx, 'Destination', isAccount)
throw new ValidationError('PaymentChannelCreate: missing Destination') validateOptionalField(tx, 'DestinationTag', isNumber)
}
if (typeof tx.Destination !== 'string') {
throw new ValidationError(
'PaymentChannelCreate: Destination must be a string',
)
}
if (tx.SettleDelay === undefined) { if (tx.SettleDelay === undefined) {
throw new ValidationError('PaymentChannelCreate: missing SettleDelay') throw new ValidationError('PaymentChannelCreate: missing SettleDelay')
@@ -103,13 +103,4 @@ export function validatePaymentChannelCreate(
'PaymentChannelCreate: CancelAfter must be a number', 'PaymentChannelCreate: CancelAfter must be a number',
) )
} }
if (
tx.DestinationTag !== undefined &&
typeof tx.DestinationTag !== 'number'
) {
throw new ValidationError(
'PaymentChannelCreate: DestinationTag must be a number',
)
}
} }

View File

@@ -16,7 +16,7 @@ describe('NFTokenCreateOffer', function () {
TransactionType: 'NFTokenCreateOffer', TransactionType: 'NFTokenCreateOffer',
NFTokenID: NFTOKEN_ID, NFTokenID: NFTOKEN_ID,
Amount: '1', Amount: '1',
Owner: 'r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ', Owner: 'rcXY84C4g14iFp6taFXjjQGVeHqSCh9RX',
Expiration: 1000, Expiration: 1000,
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Destination: 'r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ', Destination: 'r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ',
@@ -104,7 +104,7 @@ describe('NFTokenCreateOffer', function () {
const invalid = { const invalid = {
TransactionType: 'NFTokenCreateOffer', TransactionType: 'NFTokenCreateOffer',
Amount: '1', Amount: '1',
Owner: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXe', Owner: 'rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn',
Expiration: 1000, Expiration: 1000,
Destination: 'r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ', Destination: 'r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
@@ -124,7 +124,7 @@ describe('NFTokenCreateOffer', function () {
TransactionType: 'NFTokenCreateOffer', TransactionType: 'NFTokenCreateOffer',
NFTokenID: NFTOKEN_ID, NFTokenID: NFTOKEN_ID,
Amount: 1, Amount: 1,
Owner: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXe', Owner: 'rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn',
Expiration: 1000, Expiration: 1000,
Destination: 'r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ', Destination: 'r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
@@ -142,7 +142,7 @@ describe('NFTokenCreateOffer', function () {
it(`throws w/ missing Amount`, function () { it(`throws w/ missing Amount`, function () {
const invalid = { const invalid = {
TransactionType: 'NFTokenCreateOffer', TransactionType: 'NFTokenCreateOffer',
Owner: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXe', Owner: 'rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn',
Expiration: 1000, Expiration: 1000,
NFTokenID: NFTOKEN_ID, NFTokenID: NFTOKEN_ID,
Destination: 'r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ', Destination: 'r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ',
@@ -162,7 +162,7 @@ describe('NFTokenCreateOffer', function () {
const invalid = { const invalid = {
TransactionType: 'NFTokenCreateOffer', TransactionType: 'NFTokenCreateOffer',
Expiration: 1000, Expiration: 1000,
Owner: 'r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ', Owner: 'rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
NFTokenID: NFTOKEN_ID, NFTokenID: NFTOKEN_ID,
Flags: NFTokenCreateOfferFlags.tfSellNFToken, Flags: NFTokenCreateOfferFlags.tfSellNFToken,

View File

@@ -58,12 +58,12 @@ describe('AccountDelete', function () {
assert.throws( assert.throws(
() => validateAccountDelete(invalidDestination), () => validateAccountDelete(invalidDestination),
ValidationError, ValidationError,
'AccountDelete: invalid Destination', 'AccountDelete: invalid field Destination',
) )
assert.throws( assert.throws(
() => validate(invalidDestination), () => validate(invalidDestination),
ValidationError, ValidationError,
'AccountDelete: invalid Destination', 'AccountDelete: invalid field Destination',
) )
}) })
@@ -81,13 +81,13 @@ describe('AccountDelete', function () {
assert.throws( assert.throws(
() => validateAccountDelete(invalidDestinationTag), () => validateAccountDelete(invalidDestinationTag),
ValidationError, ValidationError,
'AccountDelete: invalid DestinationTag', 'AccountDelete: invalid field DestinationTag',
) )
assert.throws( assert.throws(
() => validate(invalidDestinationTag), () => validate(invalidDestinationTag),
ValidationError, ValidationError,
'AccountDelete: invalid DestinationTag', 'AccountDelete: invalid field DestinationTag',
) )
}) })
}) })

View File

@@ -155,12 +155,12 @@ describe('AccountSet', function () {
assert.throws( assert.throws(
() => validateAccountSet(account), () => validateAccountSet(account),
ValidationError, ValidationError,
'AccountSet: invalid NFTokenMinter', 'AccountSet: invalid field NFTokenMinter',
) )
assert.throws( assert.throws(
() => validate(account), () => validate(account),
ValidationError, ValidationError,
'AccountSet: invalid NFTokenMinter', 'AccountSet: invalid field NFTokenMinter',
) )
}) })
}) })

View File

@@ -42,12 +42,12 @@ describe('CheckCreate', function () {
assert.throws( assert.throws(
() => validateCheckCreate(invalidDestination), () => validateCheckCreate(invalidDestination),
ValidationError, ValidationError,
'CheckCreate: invalid Destination', 'CheckCreate: invalid field Destination',
) )
assert.throws( assert.throws(
() => validate(invalidDestination), () => validate(invalidDestination),
ValidationError, ValidationError,
'CheckCreate: invalid Destination', 'CheckCreate: invalid field Destination',
) )
}) })
@@ -92,12 +92,12 @@ describe('CheckCreate', function () {
assert.throws( assert.throws(
() => validateCheckCreate(invalidDestinationTag), () => validateCheckCreate(invalidDestinationTag),
ValidationError, ValidationError,
'CheckCreate: invalid DestinationTag', 'CheckCreate: invalid field DestinationTag',
) )
assert.throws( assert.throws(
() => validate(invalidDestinationTag), () => validate(invalidDestinationTag),
ValidationError, ValidationError,
'CheckCreate: invalid DestinationTag', 'CheckCreate: invalid field DestinationTag',
) )
}) })

View File

@@ -38,12 +38,12 @@ describe('EscrowCancel', function () {
assert.throws( assert.throws(
() => validateEscrowCancel(cancel), () => validateEscrowCancel(cancel),
ValidationError, ValidationError,
'EscrowCancel: missing Owner', 'EscrowCancel: missing field Owner',
) )
assert.throws( assert.throws(
() => validate(cancel), () => validate(cancel),
ValidationError, ValidationError,
'EscrowCancel: missing Owner', 'EscrowCancel: missing field Owner',
) )
}) })
@@ -68,12 +68,12 @@ describe('EscrowCancel', function () {
assert.throws( assert.throws(
() => validateEscrowCancel(cancel), () => validateEscrowCancel(cancel),
ValidationError, ValidationError,
'EscrowCancel: Owner must be a string', 'EscrowCancel: invalid field Owner',
) )
assert.throws( assert.throws(
() => validate(cancel), () => validate(cancel),
ValidationError, ValidationError,
'EscrowCancel: Owner must be a string', 'EscrowCancel: invalid field Owner',
) )
}) })

View File

@@ -67,12 +67,12 @@ describe('EscrowCreate', function () {
assert.throws( assert.throws(
() => validateEscrowCreate(escrow), () => validateEscrowCreate(escrow),
ValidationError, ValidationError,
'EscrowCreate: Destination must be a string', 'EscrowCreate: invalid field Destination',
) )
assert.throws( assert.throws(
() => validate(escrow), () => validate(escrow),
ValidationError, ValidationError,
'EscrowCreate: Destination must be a string', 'EscrowCreate: invalid field Destination',
) )
}) })
@@ -137,12 +137,12 @@ describe('EscrowCreate', function () {
assert.throws( assert.throws(
() => validateEscrowCreate(escrow), () => validateEscrowCreate(escrow),
ValidationError, ValidationError,
'EscrowCreate: DestinationTag must be a number', 'EscrowCreate: invalid field DestinationTag',
) )
assert.throws( assert.throws(
() => validate(escrow), () => validate(escrow),
ValidationError, ValidationError,
'EscrowCreate: DestinationTag must be a number', 'EscrowCreate: invalid field DestinationTag',
) )
}) })

View File

@@ -48,12 +48,12 @@ describe('EscrowFinish', function () {
assert.throws( assert.throws(
() => validateEscrowFinish(escrow), () => validateEscrowFinish(escrow),
ValidationError, ValidationError,
'EscrowFinish: Owner must be a string', 'EscrowFinish: invalid field Owner',
) )
assert.throws( assert.throws(
() => validate(escrow), () => validate(escrow),
ValidationError, ValidationError,
'EscrowFinish: Owner must be a string', 'EscrowFinish: invalid field Owner',
) )
}) })

View File

@@ -102,12 +102,12 @@ describe('Payment', function () {
assert.throws( assert.throws(
() => validatePayment(paymentTransaction), () => validatePayment(paymentTransaction),
ValidationError, ValidationError,
'PaymentTransaction: missing field Destination', 'Payment: missing field Destination',
) )
assert.throws( assert.throws(
() => validate(paymentTransaction), () => validate(paymentTransaction),
ValidationError, ValidationError,
'PaymentTransaction: missing field Destination', 'Payment: missing field Destination',
) )
}) })
@@ -116,12 +116,47 @@ describe('Payment', function () {
assert.throws( assert.throws(
() => validatePayment(paymentTransaction), () => validatePayment(paymentTransaction),
ValidationError, ValidationError,
'PaymentTransaction: invalid Destination', 'Payment: invalid field Destination',
) )
assert.throws( assert.throws(
() => validate(paymentTransaction), () => validate(paymentTransaction),
ValidationError, ValidationError,
'PaymentTransaction: invalid Destination', 'Payment: invalid field Destination',
)
})
it(`throws when Destination is invalid classic address`, function () {
paymentTransaction.Destination = 'rABCD'
assert.throws(
() => validatePayment(paymentTransaction),
ValidationError,
'Payment: invalid field Destination',
)
assert.throws(
() => validate(paymentTransaction),
ValidationError,
'Payment: invalid field Destination',
)
})
it(`does not throw when Destination is a valid x-address`, function () {
paymentTransaction.Destination =
'X7WZKEeNVS2p9Tire9DtNFkzWBZbFtSiS2eDBib7svZXuc2'
assert.doesNotThrow(() => validatePayment(paymentTransaction))
assert.doesNotThrow(() => validate(paymentTransaction))
})
it(`throws when Destination is an empty string`, function () {
paymentTransaction.Destination = ''
assert.throws(
() => validatePayment(paymentTransaction),
ValidationError,
'Payment: invalid field Destination',
)
assert.throws(
() => validate(paymentTransaction),
ValidationError,
'Payment: invalid field Destination',
) )
}) })
@@ -130,12 +165,12 @@ describe('Payment', function () {
assert.throws( assert.throws(
() => validatePayment(paymentTransaction), () => validatePayment(paymentTransaction),
ValidationError, ValidationError,
'PaymentTransaction: DestinationTag must be a number', 'Payment: invalid field DestinationTag',
) )
assert.throws( assert.throws(
() => validate(paymentTransaction), () => validate(paymentTransaction),
ValidationError, ValidationError,
'PaymentTransaction: DestinationTag must be a number', 'Payment: invalid field DestinationTag',
) )
}) })

View File

@@ -61,12 +61,12 @@ describe('PaymentChannelCreate', function () {
assert.throws( assert.throws(
() => validatePaymentChannelCreate(channel), () => validatePaymentChannelCreate(channel),
ValidationError, ValidationError,
'PaymentChannelCreate: missing Destination', 'PaymentChannelCreate: missing field Destination',
) )
assert.throws( assert.throws(
() => validate(channel), () => validate(channel),
ValidationError, ValidationError,
'PaymentChannelCreate: missing Destination', 'PaymentChannelCreate: missing field Destination',
) )
}) })
@@ -121,12 +121,12 @@ describe('PaymentChannelCreate', function () {
assert.throws( assert.throws(
() => validatePaymentChannelCreate(channel), () => validatePaymentChannelCreate(channel),
ValidationError, ValidationError,
'PaymentChannelCreate: Destination must be a string', 'PaymentChannelCreate: invalid field Destination',
) )
assert.throws( assert.throws(
() => validate(channel), () => validate(channel),
ValidationError, ValidationError,
'PaymentChannelCreate: Destination must be a string', 'PaymentChannelCreate: invalid field Destination',
) )
}) })
@@ -166,12 +166,12 @@ describe('PaymentChannelCreate', function () {
assert.throws( assert.throws(
() => validatePaymentChannelCreate(channel), () => validatePaymentChannelCreate(channel),
ValidationError, ValidationError,
'PaymentChannelCreate: DestinationTag must be a number', 'PaymentChannelCreate: invalid field DestinationTag',
) )
assert.throws( assert.throws(
() => validate(channel), () => validate(channel),
ValidationError, ValidationError,
'PaymentChannelCreate: DestinationTag must be a number', 'PaymentChannelCreate: invalid field DestinationTag',
) )
}) })