feat: add support for current sidechain design (#2039)

* Update definitions.json

* add new st types

* add tests

* add XChainClaim tx

* add XChainCommit tx

* add XChainCreateBridge tx

* add XChainCreateClaimID tx

* update definitions.json

* rename Bridge -> XChainBridge in binary codec, fix tests

* rename Bridge -> XChainBridge in models

* add codec support for XChainAddAttestation

* add XChainAddAttestation model

* undo debugging change

* fix linting issues

* update definitions.json for new rippled code, add new tests/update old tests

* add/update models

* update history

* update binary-codec

* add XChainModifyBridge model

* update RPCs

* update to latest rippled

* more fixes

* fix definitions.json

* fix spacing

* update definitions.json to avoid conflict with amm

* update definitions.json to resolve amm conflicts

* audit code

* more updates

* update rpcs

* switch to beta version

* add destination tag to XChainClaim

* rename IssuedCurrency -> Issue to match rippled

* update Issue form

* fix account object filters

* fix issue from typing

* fix LedgerEntry types

* fix attestation destination type

* Update definitions.json

* rename XChainAddAttestation -> XChainAddAttestationBatch

* add XChainAddClaimAttestation

* add XChainAddAccountCreateAttestation

* remove XChainAddAttestationBatch

* update definitions

* fix attestation txns

* fix attestation object

* add validate for new txs

* add Bridge ledger object

* add XChainOwnedClaimID ledger object

* add XChainOwnedCreateAccountClaimID ledger object

* update account_objects

* update models to latest rippled

* fix minor issues

* fix bridge ledger_entry

* add XChainModifyBridge flag

* Update definitions.json

* add rbc tests for the new txs

* update validateXChainModifyBridge

* add validate methods to other xchain txs

* fix isXChainBridge

* fix isIssue typing

* fix model types

* update changelog

* switch prepare to prepublishOnly

* add docs

* fix AccountObjectsType filter

* export common types

* fix account_objects filter

* update LedgerEntry

* add sidechain faucet info

* add snippet

* improve snippet

* fix spacing issues

* update ledger_entry

* remove AMMDelete tests for now

* Update definitions.json

* fix unit tests

* convert createValidate script to JS
* remove unneeded linter ignores

* respond to comments

* more snippet fixes

* make validate functions more readable

* add getXChainClaimID method to parse metadata

* re-add linter rules

* clean up common

* fix getXChainClaimID test

* return undefined for failed tx

* test: add model tests for new sidechain transactions (#2059)

* add XChainAddAttestation tests, fix model

* add XChainClaim model tests

* add XChainCommit model tests, fix typo

* add XChainCreateBridge model tests

* add XChainCreateClaimID model tests

* add XChainModifyBridge tests

* update to most recent version of code

* remove XChainAddAttestationBatch tests

* add more validation tests

* switch createValidateTests to JS
This commit is contained in:
Mayukha Vadari
2023-09-26 10:01:21 -04:00
committed by GitHub
parent d7323a5fcf
commit 91e7369f1b
50 changed files with 4341 additions and 295 deletions

View File

@@ -84,7 +84,6 @@ module.exports = {
'max-statements': 'off',
// Snippets have logs on console to better understand the working.
'no-console': 'off',
'import/no-extraneous-dependencies': 'off',
},
},
{
@@ -147,5 +146,17 @@ module.exports = {
'import/no-unused-modules': 'off',
},
},
{
files: ['tools/*.ts', 'tools/*.js'],
rules: {
'no-console': ['off'],
'node/no-process-exit': ['off'],
'@typescript-eslint/no-magic-numbers': ['off'],
'max-lines-per-function': ['off'],
'max-statements': ['off'],
complexity: ['off'],
'max-depth': ['warn', 3],
},
},
],
}

View File

@@ -3,9 +3,9 @@
Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xrpl-announce) for release announcements. We recommend that xrpl.js (ripple-lib) users stay up-to-date with the latest stable release.
## Unreleased
### Added
* Added `ports` field to `ServerInfoResponse`
* Support for the XChainBridge amendment.
### Fixed
* Fix request model fields related to AMM

View File

@@ -0,0 +1,172 @@
/* eslint-disable max-depth -- needed for attestation checking */
/* eslint-disable @typescript-eslint/consistent-type-assertions -- needed here */
/* eslint-disable no-await-in-loop -- needed here */
import {
AccountObjectsRequest,
LedgerEntry,
Client,
XChainAccountCreateCommit,
XChainBridge,
XChainCommit,
XChainCreateClaimID,
xrpToDrops,
Wallet,
getXChainClaimID,
} from '../../src'
async function sleep(sec: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, sec * 1000)
})
}
const lockingClient = new Client(
'wss://sidechain-net1.devnet.rippletest.net:51233',
)
const issuingClient = new Client(
'wss://sidechain-net2.devnet.rippletest.net:51233',
)
const MAX_LEDGERS_WAITED = 5
const LEDGER_CLOSE_TIME = 4
void bridgeTransfer()
async function bridgeTransfer(): Promise<void> {
await lockingClient.connect()
await issuingClient.connect()
const lockingChainDoor = 'rMAXACCrp3Y8PpswXcg3bKggHX76V3F8M4'
const accountObjectsRequest: AccountObjectsRequest = {
command: 'account_objects',
account: lockingChainDoor,
type: 'bridge',
}
const lockingAccountObjects = (
await lockingClient.request(accountObjectsRequest)
).result.account_objects
// There will only be one here - a door account can only have one bridge per currency
const bridgeData = lockingAccountObjects.filter(
(obj) =>
obj.LedgerEntryType === 'Bridge' &&
obj.XChainBridge.LockingChainIssue.currency === 'XRP',
)[0] as LedgerEntry.Bridge
const bridge: XChainBridge = bridgeData.XChainBridge
console.log(bridge)
console.log('Creating wallet on the locking chain via the faucet...')
const { wallet: wallet1 } = await lockingClient.fundWallet()
console.log(wallet1)
const wallet2 = Wallet.generate()
console.log(
`Creating ${wallet2.classicAddress} on the issuing chain via the bridge...`,
)
const fundTx: XChainAccountCreateCommit = {
TransactionType: 'XChainAccountCreateCommit',
Account: wallet1.classicAddress,
XChainBridge: bridge,
SignatureReward: bridgeData.SignatureReward,
Destination: wallet2.classicAddress,
Amount: (
parseInt(bridgeData.MinAccountCreateAmount as string, 10) * 2
).toString(),
}
const fundResponse = await lockingClient.submitAndWait(fundTx, {
wallet: wallet1,
})
console.log(fundResponse)
console.log(
'Waiting for the attestation to go through... (usually 8-12 seconds)',
)
let ledgersWaited = 0
let initialBalance = '0'
while (ledgersWaited < MAX_LEDGERS_WAITED) {
await sleep(LEDGER_CLOSE_TIME)
try {
initialBalance = await issuingClient.getXrpBalance(wallet2.classicAddress)
console.log(
`Wallet ${wallet2.classicAddress} has been funded with a balance of ${initialBalance} XRP`,
)
break
} catch (_error) {
ledgersWaited += 1
if (ledgersWaited === MAX_LEDGERS_WAITED) {
// This error should never be hit if the bridge is running
throw Error('Destination account creation via the bridge failed.')
}
}
}
console.log(
`Transferring funds from ${wallet1.classicAddress} on the locking chain to ` +
`${wallet2.classicAddress} on the issuing_chain...`,
)
// Fetch the claim ID for the transfer
console.log('Step 1: Fetching the claim ID for the transfer...')
const claimIdTx: XChainCreateClaimID = {
TransactionType: 'XChainCreateClaimID',
Account: wallet2.classicAddress,
XChainBridge: bridge,
SignatureReward: bridgeData.SignatureReward,
OtherChainSource: wallet1.classicAddress,
}
const claimIdResult = await issuingClient.submitAndWait(claimIdTx, {
wallet: wallet2,
})
console.log(claimIdResult)
// Extract new claim ID from metadata
const xchainClaimId = getXChainClaimID(claimIdResult.result.meta)
if (xchainClaimId == null) {
// This shouldn't trigger assuming the transaction succeeded
throw Error('Could not extract XChainClaimID')
}
console.log(`Claim ID for the transfer: ${xchainClaimId}`)
console.log(
'Step 2: Locking the funds on the locking chain with an XChainCommit transaction...',
)
const commitTx: XChainCommit = {
TransactionType: 'XChainCommit',
Account: wallet1.classicAddress,
Amount: xrpToDrops(1),
XChainBridge: bridge,
XChainClaimID: xchainClaimId,
OtherChainDestination: wallet2.classicAddress,
}
const commitResult = await lockingClient.submitAndWait(commitTx, {
wallet: wallet1,
})
console.log(commitResult)
console.log(
'Waiting for the attestation to go through... (usually 8-12 seconds)',
)
ledgersWaited = 0
while (ledgersWaited < MAX_LEDGERS_WAITED) {
await sleep(LEDGER_CLOSE_TIME)
const currentBalance = await issuingClient.getXrpBalance(
wallet2.classicAddress,
)
console.log(initialBalance, currentBalance)
if (parseFloat(currentBalance) > parseFloat(initialBalance)) {
console.log('Transfer is complete')
console.log(
`New balance of ${wallet2.classicAddress} is ${currentBalance} XRP`,
)
break
}
ledgersWaited += 1
if (ledgersWaited === MAX_LEDGERS_WAITED) {
throw Error('Bridge transfer failed.')
}
}
await lockingClient.disconnect()
await issuingClient.disconnect()
}

View File

@@ -2,7 +2,7 @@ export type LedgerIndex = number | ('validated' | 'closed' | 'current')
export interface XRP {
currency: 'XRP'
issuer: never
issuer?: never
}
export interface IssuedCurrency {
@@ -148,3 +148,10 @@ export interface AuthAccount {
Account: string
}
}
export interface XChainBridge {
LockingChainDoor: string
LockingChainIssue: Currency
IssuingChainDoor: string
IssuingChainIssue: Currency
}

View File

@@ -0,0 +1,84 @@
import { Amount, XChainBridge } from '../common'
import BaseLedgerEntry from './BaseLedgerEntry'
/**
* A Bridge objects represents a cross-chain bridge and includes information about
* the door accounts, assets, signature rewards, and the minimum account create
* amount.
*
* @category Ledger Entries
*/
export default interface Bridge extends BaseLedgerEntry {
LedgerEntryType: 'Bridge'
/** The door account that owns the bridge. */
Account: string
/**
* The total amount, in XRP, to be rewarded for providing a signature for
* cross-chain transfer or for signing for the cross-chain reward. This amount
* will be split among the signers.
*/
SignatureReward: Amount
/**
* The minimum amount, in XRP, required for an {@link XChainAccountCreateCommit}
* transaction. If this isn't present, the {@link XChainAccountCreateCommit}
* transaction will fail. This field can only be present on XRP-XRP bridges.
*/
MinAccountCreateAmount?: string
/**
* The door accounts and assets of the bridge this object correlates to.
*/
XChainBridge: XChainBridge
/**
* The value of the next XChainClaimID to be created.
*/
XChainClaimID: string
/**
* A counter used to order the execution of account create transactions. It is
* incremented every time a successful {@link XChainAccountCreateCommit}
* transaction is run for the source chain.
*/
XChainAccountCreateCount: string
/**
* A counter used to order the execution of account create transactions. It is
* incremented every time a {@link XChainAccountCreateCommit} transaction is
* "claimed" on the destination chain. When the "claim" transaction is run on
* the destination chain, the XChainAccountClaimCount must match the value that
* the XChainAccountCreateCount had at the time the XChainAccountClaimCount was
* run on the source chain. This orders the claims so that they run in the same
* order that the XChainAccountCreateCommit transactions ran on the source chain,
* to prevent transaction replay.
*/
XChainAccountClaimCount: string
/**
* A bit-map of boolean flags. No flags are defined for Bridges, so this value
* is always 0.
*/
Flags: 0
/**
* A hint indicating which page of the sender's owner directory links to this
* object, in case the directory consists of multiple pages.
*/
OwnerNode: string
/**
* The identifying hash of the transaction that most recently modified this
* object.
*/
PreviousTxnID: string
/**
* The index of the ledger that contains the transaction that most recently
* modified this object.
*/
PreviousTxnLgrSeq: number
}

View File

@@ -1,6 +1,7 @@
import AccountRoot from './AccountRoot'
import Amendments from './Amendments'
import AMM from './AMM'
import Bridge from './Bridge'
import Check from './Check'
import DepositPreauth from './DepositPreauth'
import DirectoryNode from './DirectoryNode'
@@ -13,11 +14,14 @@ import PayChannel from './PayChannel'
import RippleState from './RippleState'
import SignerList from './SignerList'
import Ticket from './Ticket'
import XChainOwnedClaimID from './XChainOwnedClaimID'
import XChainOwnedCreateAccountClaimID from './XChainOwnedCreateAccountClaimID'
type LedgerEntry =
| AccountRoot
| Amendments
| AMM
| Bridge
| Check
| DepositPreauth
| DirectoryNode
@@ -30,5 +34,7 @@ type LedgerEntry =
| RippleState
| SignerList
| Ticket
| XChainOwnedClaimID
| XChainOwnedCreateAccountClaimID
export default LedgerEntry

View File

@@ -0,0 +1,89 @@
import { Amount } from 'ripple-binary-codec/dist/types'
import { XChainBridge } from '../common'
import BaseLedgerEntry from './BaseLedgerEntry'
/**
* An XChainOwnedClaimID object represents one cross-chain transfer of value
* and includes information of the account on the source chain that locks or
* burns the funds on the source chain.
*
* @category Ledger Entries
*/
export default interface XChainOwnedClaimID extends BaseLedgerEntry {
LedgerEntryType: 'XChainOwnedClaimID'
/** The account that checked out this unique claim ID value. */
Account: string
/**
* The door accounts and assets of the bridge this object correlates to.
*/
XChainBridge: XChainBridge
/**
* The unique sequence number for a cross-chain transfer.
*/
XChainClaimID: string
/**
* The account that must send the corresponding {@link XChainCommit} on the
* source chain. The destination may be specified in the {@link XChainCommit}
* transaction, which means that if the OtherChainSource isn't specified,
* another account can try to specify a different destination and steal the
* funds. This also allows tracking only a single set of signatures, since we
* know which account will send the {@link XChainCommit} transaction.
*/
OtherChainSource: string
/**
* Attestations collected from the witness servers. This includes the parameters
* needed to recreate the message that was signed, including the amount, which
* chain (locking or issuing), optional destination, and reward account for that
* signature.
*/
XChainClaimAttestations: Array<{
// TODO: add docs
XChainClaimProofSig: {
Amount: Amount
AttestationRewardAccount: string
AttestationSignerAccount: string
Destination?: string
PublicKey: string
WasLockingChainSend: 0 | 1
}
}>
/**
* The total amount to pay the witness servers for their signatures. It must be at
* least the value of the SignatureReward in the {@link Bridge} ledger object.
*/
SignatureReward: string
/**
* A bit-map of boolean flags. No flags are defined for XChainOwnedClaimIDs,
* so this value is always 0.
*/
Flags: 0
/**
* A hint indicating which page of the sender's owner directory links to this
* object, in case the directory consists of multiple pages.
*/
OwnerNode: string
/**
* The identifying hash of the transaction that most recently modified this
* object.
*/
PreviousTxnID: string
/**
* The index of the ledger that contains the transaction that most recently
* modified this object.
*/
PreviousTxnLgrSeq: number
}

View File

@@ -0,0 +1,74 @@
import { XChainBridge } from '../common'
import BaseLedgerEntry from './BaseLedgerEntry'
/**
* The XChainOwnedCreateAccountClaimID ledger object is used to collect attestations
* for creating an account via a cross-chain transfer.
*
* @category Ledger Entries
*/
export default interface XChainOwnedCreateAccountClaimID
extends BaseLedgerEntry {
LedgerEntryType: 'XChainOwnedCreateAccountClaimID'
/** The account that owns this object. */
Account: string
/**
* The door accounts and assets of the bridge this object correlates to.
*/
XChainBridge: XChainBridge
/**
* An integer that determines the order that accounts created through
* cross-chain transfers must be performed. Smaller numbers must execute
* before larger numbers.
*/
XChainAccountCreateCount: number
/**
* Attestations collected from the witness servers. This includes the parameters
* needed to recreate the message that was signed, including the amount, destination,
* signature reward amount, and reward account for that signature. With the
* exception of the reward account, all signatures must sign the message created with
* common parameters.
*/
XChainCreateAccountAttestations: Array<{
// TODO: add docs
XChainCreateAccountProofSig: {
Amount: string
AttestationRewardAccount: string
AttestationSignerAccount: string
Destination: string
PublicKey: string
WasLockingChainSend: 0 | 1
}
}>
/**
* A bit-map of boolean flags. No flags are defined for,
* XChainOwnedCreateAccountClaimIDs, so this value is always 0.
*/
Flags: 0
/**
* A hint indicating which page of the sender's owner directory links to this
* object, in case the directory consists of multiple pages.
*/
OwnerNode: string
/**
* The identifying hash of the transaction that most recently modified this
* object.
*/
PreviousTxnID: string
/**
* The index of the ledger that contains the transaction that most recently
* modified this object.
*/
PreviousTxnLgrSeq: number
}

View File

@@ -4,6 +4,7 @@ import AccountRoot, {
} from './AccountRoot'
import Amendments, { Majority, AMENDMENTS_ID } from './Amendments'
import AMM, { VoteSlot } from './AMM'
import Bridge from './Bridge'
import Check from './Check'
import DepositPreauth from './DepositPreauth'
import DirectoryNode from './DirectoryNode'
@@ -24,6 +25,8 @@ import PayChannel from './PayChannel'
import RippleState, { RippleStateFlags } from './RippleState'
import SignerList, { SignerListFlags } from './SignerList'
import Ticket from './Ticket'
import XChainOwnedClaimID from './XChainOwnedClaimID'
import XChainOwnedCreateAccountClaimID from './XChainOwnedCreateAccountClaimID'
export {
AccountRoot,
@@ -32,6 +35,7 @@ export {
AMENDMENTS_ID,
Amendments,
AMM,
Bridge,
Check,
DepositPreauth,
DirectoryNode,
@@ -57,5 +61,7 @@ export {
SignerList,
SignerListFlags,
Ticket,
XChainOwnedClaimID,
XChainOwnedCreateAccountClaimID,
VoteSlot,
}

View File

@@ -1,5 +1,6 @@
import {
AMM,
Bridge,
Check,
DepositPreauth,
Escrow,
@@ -8,12 +9,15 @@ import {
RippleState,
SignerList,
Ticket,
XChainOwnedClaimID,
XChainOwnedCreateAccountClaimID,
} from '../ledger'
import { BaseRequest, BaseResponse, LookupByLedgerRequest } from './baseMethod'
export type AccountObjectType =
| 'amm'
| 'bridge'
| 'check'
| 'deposit_preauth'
| 'escrow'
@@ -23,6 +27,8 @@ export type AccountObjectType =
| 'signer_list'
| 'state'
| 'ticket'
| 'xchain_owned_create_account_claim_id'
| 'xchain_owned_claim_id'
/**
* The account_objects command returns the raw ledger format for all objects
@@ -67,6 +73,7 @@ export interface AccountObjectsRequest
*/
export type AccountObject =
| AMM
| Bridge
| Check
| DepositPreauth
| Escrow
@@ -75,6 +82,8 @@ export type AccountObject =
| SignerList
| RippleState
| Ticket
| XChainOwnedClaimID
| XChainOwnedCreateAccountClaimID
/**
* Response expected from an {@link AccountObjectsRequest}.

View File

@@ -1,3 +1,4 @@
import { Currency, XChainBridge } from '../common'
import { LedgerEntry } from '../ledger'
import { BaseRequest, BaseResponse, LookupByLedgerRequest } from './baseMethod'
@@ -152,6 +153,30 @@ export interface LedgerEntryRequest extends BaseRequest, LookupByLedgerRequest {
* Must be the object ID of the NFToken page, as hexadecimal
*/
nft_page?: string
bridge_account?: string
bridge?: XChainBridge
xchain_owned_claim_id?:
| {
locking_chain_door: string
locking_chain_issue: Currency
issuing_chain_door: string
issuing_chain_issue: Currency
xchain_owned_claim_id: string | number
}
| string
xchain_owned_create_account_claim_id?:
| {
locking_chain_door: string
locking_chain_issue: Currency
issuing_chain_door: string
issuing_chain_issue: Currency
xchain_owned_create_account_claim_id: string | number
}
| string
}
/**

View File

@@ -0,0 +1,69 @@
import { isString } from 'lodash'
import { Amount, XChainBridge } from '../common'
import {
BaseTransaction,
isAmount,
isXChainBridge,
validateBaseTransaction,
validateRequiredField,
} from './common'
/**
* The XChainAccountCreateCommit transaction creates a new account on one of the
* chains a bridge connects, which serves as the bridge entrance for that chain.
*
* Warning: This transaction should only be executed if the witness attestations
* will be reliably delivered to the destination chain. If the signatures aren't
* delivered, then account creation will be blocked until attestations are received.
* This can be used maliciously; to disable this transaction on XRP-XRP bridges,
* the bridge's MinAccountCreateAmount shouldn't be present.
*
* @category Transaction Models
*/
export interface XChainAccountCreateCommit extends BaseTransaction {
TransactionType: 'XChainAccountCreateCommit'
/**
* The bridge to create accounts for.
*/
XChainBridge: XChainBridge
/**
* The amount, in XRP, to be used to reward the witness servers for providing
* signatures. This must match the amount on the {@link Bridge} ledger object.
*/
SignatureReward: Amount
/**
* The destination account on the destination chain.
*/
Destination: string
/**
* The amount, in XRP, to use for account creation. This must be greater than or
* equal to the MinAccountCreateAmount specified in the {@link Bridge} ledger object.
*/
Amount: Amount
}
/**
* Verify the form and type of an XChainAccountCreateCommit at runtime.
*
* @param tx - An XChainAccountCreateCommit Transaction.
* @throws When the XChainAccountCreateCommit is malformed.
*/
export function validateXChainAccountCreateCommit(
tx: Record<string, unknown>,
): void {
validateBaseTransaction(tx)
validateRequiredField(tx, 'XChainBridge', isXChainBridge)
validateRequiredField(tx, 'SignatureReward', isAmount)
validateRequiredField(tx, 'Destination', isString)
validateRequiredField(tx, 'Amount', isAmount)
}

View File

@@ -0,0 +1,121 @@
import { Amount, XChainBridge } from '../common'
import {
BaseTransaction,
isAmount,
isNumber,
isString,
isXChainBridge,
validateBaseTransaction,
validateRequiredField,
} from './common'
/**
* The XChainAddAccountCreateAttestation transaction provides an attestation
* from a witness server that a {@link XChainAccountCreateCommit} transaction
* occurred on the other chain.
*
* @category Transaction Models
*/
export interface XChainAddAccountCreateAttestation extends BaseTransaction {
TransactionType: 'XChainAddAccountCreateAttestation'
/**
* The amount committed by the {@link XChainAccountCreateCommit} transaction
* on the source chain.
*/
Amount: Amount
/**
* The account that should receive this signer's share of the SignatureReward.
*/
AttestationRewardAccount: string
/**
* The account on the door account's signer list that is signing the transaction.
*/
AttestationSignerAccount: string
/**
* The destination account for the funds on the destination chain.
*/
Destination: string
/**
* The account on the source chain that submitted the {@link XChainAccountCreateCommit}
* transaction that triggered the event associated with the attestation.
*/
OtherChainSource: string
/**
* The public key used to verify the signature.
*/
PublicKey: string
/**
* The signature attesting to the event on the other chain.
*/
Signature: string
/**
* The signature reward paid in the {@link XChainAccountCreateCommit} transaction.
*/
SignatureReward: Amount
/**
* A boolean representing the chain where the event occurred.
*/
WasLockingChainSend: 0 | 1
/**
* The counter that represents the order that the claims must be processed in.
*/
XChainAccountCreateCount: number | string
/**
* The bridge associated with the attestation.
*/
XChainBridge: XChainBridge
}
/**
* Verify the form and type of an XChainAddAccountCreateAttestation at runtime.
*
* @param tx - An XChainAddAccountCreateAttestation Transaction.
* @throws When the XChainAddAccountCreateAttestation is malformed.
*/
export function validateXChainAddAccountCreateAttestation(
tx: Record<string, unknown>,
): void {
validateBaseTransaction(tx)
validateRequiredField(tx, 'Amount', isAmount)
validateRequiredField(tx, 'AttestationRewardAccount', isString)
validateRequiredField(tx, 'AttestationSignerAccount', isString)
validateRequiredField(tx, 'Destination', isString)
validateRequiredField(tx, 'OtherChainSource', isString)
validateRequiredField(tx, 'PublicKey', isString)
validateRequiredField(tx, 'Signature', isString)
validateRequiredField(tx, 'SignatureReward', isAmount)
validateRequiredField(
tx,
'WasLockingChainSend',
(inp) => inp === 0 || inp === 1,
)
validateRequiredField(
tx,
'XChainAccountCreateCount',
(inp) => isNumber(inp) || isString(inp),
)
validateRequiredField(tx, 'XChainBridge', isXChainBridge)
}

View File

@@ -0,0 +1,115 @@
import { Amount, XChainBridge } from '../common'
import {
BaseTransaction,
isAmount,
isNumber,
isString,
isXChainBridge,
validateBaseTransaction,
validateOptionalField,
validateRequiredField,
} from './common'
/**
* The XChainAddClaimAttestation transaction provides proof from a witness server,
* attesting to an {@link XChainCommit} transaction.
*
* @category Transaction Models
*/
export interface XChainAddClaimAttestation extends BaseTransaction {
TransactionType: 'XChainAddClaimAttestation'
/**
* The amount committed by the {@link XChainCommit} transaction on the source chain.
*/
Amount: Amount
/**
* The account that should receive this signer's share of the SignatureReward.
*/
AttestationRewardAccount: string
/**
* The account on the door account's signer list that is signing the transaction.
*/
AttestationSignerAccount: string
/**
* The destination account for the funds on the destination chain (taken from
* the {@link XChainCommit} transaction).
*/
Destination?: string
/**
* The account on the source chain that submitted the {@link XChainCommit}
* transaction that triggered the event associated with the attestation.
*/
OtherChainSource: string
/**
* The public key used to verify the attestation signature.
*/
PublicKey: string
/**
* The signature attesting to the event on the other chain.
*/
Signature: string
/**
* A boolean representing the chain where the event occurred.
*/
WasLockingChainSend: 0 | 1
/**
* The bridge to use to transfer funds.
*/
XChainBridge: XChainBridge
/**
* The XChainClaimID associated with the transfer, which was included in the
* {@link XChainCommit} transaction.
*/
XChainClaimID: number | string
}
/**
* Verify the form and type of an XChainAddClaimAttestation at runtime.
*
* @param tx - An XChainAddClaimAttestation Transaction.
* @throws When the XChainAddClaimAttestation is malformed.
*/
export function validateXChainAddClaimAttestation(
tx: Record<string, unknown>,
): void {
validateBaseTransaction(tx)
validateRequiredField(tx, 'Amount', isAmount)
validateRequiredField(tx, 'AttestationRewardAccount', isString)
validateRequiredField(tx, 'AttestationSignerAccount', isString)
validateOptionalField(tx, 'Destination', isString)
validateRequiredField(tx, 'OtherChainSource', isString)
validateRequiredField(tx, 'PublicKey', isString)
validateRequiredField(tx, 'Signature', isString)
validateRequiredField(
tx,
'WasLockingChainSend',
(inp) => inp === 0 || inp === 1,
)
validateRequiredField(tx, 'XChainBridge', isXChainBridge)
validateRequiredField(
tx,
'XChainClaimID',
(inp) => isNumber(inp) || isString(inp),
)
}

View File

@@ -0,0 +1,77 @@
import { Amount, XChainBridge } from '../common'
import {
BaseTransaction,
isAmount,
isNumber,
isString,
isXChainBridge,
validateBaseTransaction,
validateOptionalField,
validateRequiredField,
} from './common'
/**
* The XChainClaim transaction completes a cross-chain transfer of value. It
* allows a user to claim the value on the destination chain - the equivalent
* of the value locked on the source chain.
*
* @category Transaction Models
*/
export interface XChainClaim extends BaseTransaction {
TransactionType: 'XChainClaim'
/**
* The bridge to use for the transfer.
*/
XChainBridge: XChainBridge
/**
* The unique integer ID for the cross-chain transfer that was referenced in the
* corresponding {@link XChainCommit} transaction.
*/
XChainClaimID: number | string
/**
* The destination account on the destination chain. It must exist or the
* transaction will fail. However, if the transaction fails in this case, the
* sequence number and collected signatures won't be destroyed, and the
* transaction can be rerun with a different destination.
*/
Destination: string
/**
* An integer destination tag.
*/
DestinationTag?: number
/**
* The amount to claim on the destination chain. This must match the amount
* attested to on the attestations associated with this XChainClaimID.
*/
Amount: Amount
}
/**
* Verify the form and type of an XChainClaim at runtime.
*
* @param tx - An XChainClaim Transaction.
* @throws When the XChainClaim is malformed.
*/
export function validateXChainClaim(tx: Record<string, unknown>): void {
validateBaseTransaction(tx)
validateRequiredField(tx, 'XChainBridge', isXChainBridge)
validateRequiredField(
tx,
'XChainClaimID',
(inp) => isNumber(inp) || isString(inp),
)
validateRequiredField(tx, 'Destination', isString)
validateOptionalField(tx, 'DestinationTag', isNumber)
validateRequiredField(tx, 'Amount', isAmount)
}

View File

@@ -0,0 +1,74 @@
import { Amount, XChainBridge } from '../common'
import {
BaseTransaction,
isAmount,
isNumber,
isString,
isXChainBridge,
validateBaseTransaction,
validateOptionalField,
validateRequiredField,
} from './common'
/**
* The XChainCommit is the second step in a cross-chain transfer. It puts assets
* into trust on the locking chain so that they can be wrapped on the issuing
* chain, or burns wrapped assets on the issuing chain so that they can be returned
* on the locking chain.
*
* @category Transaction Models
*/
export interface XChainCommit extends BaseTransaction {
TransactionType: 'XChainCommit'
/**
* The bridge to use to transfer funds.
*/
XChainBridge: XChainBridge
/**
* The unique integer ID for a cross-chain transfer. This must be acquired on
* the destination chain (via a {@link XChainCreateClaimID} transaction) and
* checked from a validated ledger before submitting this transaction. If an
* incorrect sequence number is specified, the funds will be lost.
*/
XChainClaimID: number | string
/**
* The destination account on the destination chain. If this is not specified,
* the account that submitted the {@link XChainCreateClaimID} transaction on the
* destination chain will need to submit a {@link XChainClaim} transaction to
* claim the funds.
*/
OtherChainDestination?: string
/**
* The asset to commit, and the quantity. This must match the door account's
* LockingChainIssue (if on the locking chain) or the door account's
* IssuingChainIssue (if on the issuing chain).
*/
Amount: Amount
}
/**
* Verify the form and type of an XChainCommit at runtime.
*
* @param tx - An XChainCommit Transaction.
* @throws When the XChainCommit is malformed.
*/
export function validateXChainCommit(tx: Record<string, unknown>): void {
validateBaseTransaction(tx)
validateRequiredField(tx, 'XChainBridge', isXChainBridge)
validateRequiredField(
tx,
'XChainClaimID',
(inp) => isNumber(inp) || isString(inp),
)
validateOptionalField(tx, 'OtherChainDestination', isString)
validateRequiredField(tx, 'Amount', isAmount)
}

View File

@@ -0,0 +1,56 @@
import { Amount, XChainBridge } from '../common'
import {
BaseTransaction,
isAmount,
isXChainBridge,
validateBaseTransaction,
validateOptionalField,
validateRequiredField,
} from './common'
/**
* The XChainCreateBridge transaction creates a new {@link Bridge} ledger object
* and defines a new cross-chain bridge entrance on the chain that the transaction
* is submitted on. It includes information about door accounts and assets for the
* bridge.
*
* @category Transaction Models
*/
export interface XChainCreateBridge extends BaseTransaction {
TransactionType: 'XChainCreateBridge'
/**
* The bridge (door accounts and assets) to create.
*/
XChainBridge: XChainBridge
/**
* The total amount to pay the witness servers for their signatures. This amount
* will be split among the signers.
*/
SignatureReward: Amount
/**
* The minimum amount, in XRP, required for a {@link XChainAccountCreateCommit}
* transaction. If this isn't present, the {@link XChainAccountCreateCommit}
* transaction will fail. This field can only be present on XRP-XRP bridges.
*/
MinAccountCreateAmount?: Amount
}
/**
* Verify the form and type of an XChainCreateBridge at runtime.
*
* @param tx - An XChainCreateBridge Transaction.
* @throws When the XChainCreateBridge is malformed.
*/
export function validateXChainCreateBridge(tx: Record<string, unknown>): void {
validateBaseTransaction(tx)
validateRequiredField(tx, 'XChainBridge', isXChainBridge)
validateRequiredField(tx, 'SignatureReward', isAmount)
validateOptionalField(tx, 'MinAccountCreateAmount', isAmount)
}

View File

@@ -0,0 +1,53 @@
import { Amount, XChainBridge } from '../common'
import {
BaseTransaction,
isAmount,
isString,
isXChainBridge,
validateBaseTransaction,
validateRequiredField,
} from './common'
/**
* The XChainCreateClaimID transaction creates a new cross-chain claim ID that is
* used for a cross-chain transfer. A cross-chain claim ID represents one
* cross-chain transfer of value.
*
* @category Transaction Models
*/
export interface XChainCreateClaimID extends BaseTransaction {
TransactionType: 'XChainCreateClaimID'
/**
* The bridge to create the claim ID for.
*/
XChainBridge: XChainBridge
/**
* The amount, in XRP, to reward the witness servers for providing signatures.
* This must match the amount on the {@link Bridge} ledger object.
*/
SignatureReward: Amount
/**
* The account that must send the {@link XChainCommit} transaction on the source chain.
*/
OtherChainSource: string
}
/**
* Verify the form and type of an XChainCreateClaimID at runtime.
*
* @param tx - An XChainCreateClaimID Transaction.
* @throws When the XChainCreateClaimID is malformed.
*/
export function validateXChainCreateClaimID(tx: Record<string, unknown>): void {
validateBaseTransaction(tx)
validateRequiredField(tx, 'XChainBridge', isXChainBridge)
validateRequiredField(tx, 'SignatureReward', isAmount)
validateRequiredField(tx, 'OtherChainSource', isString)
}

View File

@@ -0,0 +1,77 @@
import { Amount, XChainBridge } from '../common'
import {
BaseTransaction,
GlobalFlags,
isAmount,
isXChainBridge,
validateBaseTransaction,
validateOptionalField,
validateRequiredField,
} from './common'
/**
* Enum representing values of {@link XChainModifyBridge} transaction flags.
*
* @category Transaction Flags
*/
export enum XChainModifyBridgeFlags {
/** Clears the MinAccountCreateAmount of the bridge. */
tfClearAccountCreateAmount = 0x00010000,
}
/**
* Map of flags to boolean values representing {@link XChainModifyBridge} transaction
* flags.
*
* @category Transaction Flags
*/
export interface XChainModifyBridgeFlagsInterface extends GlobalFlags {
/** Clears the MinAccountCreateAmount of the bridge. */
tfClearAccountCreateAmount?: boolean
}
/**
* The XChainModifyBridge transaction allows bridge managers to modify the parameters
* of the bridge.
*
* @category Transaction Models
*/
export interface XChainModifyBridge extends BaseTransaction {
TransactionType: 'XChainModifyBridge'
/**
* The bridge to modify.
*/
XChainBridge: XChainBridge
/**
* The signature reward split between the witnesses for submitting attestations.
*/
SignatureReward?: Amount
/**
* The minimum amount, in XRP, required for a {@link XChainAccountCreateCommit}
* transaction. If this is not present, the {@link XChainAccountCreateCommit}
* transaction will fail. This field can only be present on XRP-XRP bridges.
*/
MinAccountCreateAmount?: Amount
Flags?: number | XChainModifyBridgeFlagsInterface
}
/**
* Verify the form and type of an XChainModifyBridge at runtime.
*
* @param tx - An XChainModifyBridge Transaction.
* @throws When the XChainModifyBridge is malformed.
*/
export function validateXChainModifyBridge(tx: Record<string, unknown>): void {
validateBaseTransaction(tx)
validateRequiredField(tx, 'XChainBridge', isXChainBridge)
validateOptionalField(tx, 'SignatureReward', isAmount)
validateOptionalField(tx, 'MinAccountCreateAmount', isAmount)
}

View File

@@ -1,9 +1,14 @@
/* eslint-disable max-lines-per-function -- Necessary for validateBaseTransaction */
/* eslint-disable max-statements -- Necessary for validateBaseTransaction */
import { TRANSACTION_TYPES } from 'ripple-binary-codec'
import { ValidationError } from '../../errors'
import { Amount, Currency, IssuedCurrencyAmount, Memo, Signer } from '../common'
import {
Amount,
Currency,
IssuedCurrencyAmount,
Memo,
Signer,
XChainBridge,
} from '../common'
import { onlyHasFields } from '../utils'
const MEMO_SIZE = 3
@@ -52,11 +57,32 @@ function isSigner(obj: unknown): boolean {
const XRP_CURRENCY_SIZE = 1
const ISSUE_SIZE = 2
const ISSUED_CURRENCY_SIZE = 3
const XCHAIN_BRIDGE_SIZE = 4
function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object'
}
/**
* Verify the form and type of a string at runtime.
*
* @param str - The object to check the form and type of.
* @returns Whether the string is properly formed.
*/
export function isString(str: unknown): str is string {
return typeof str === 'string'
}
/**
* Verify the form and type of a number at runtime.
*
* @param num - The object to check the form and type of.
* @returns Whether the number is properly formed.
*/
export function isNumber(num: unknown): num is number {
return typeof num === 'number'
}
/**
* Verify the form and type of an IssuedCurrency at runtime.
*
@@ -102,6 +128,73 @@ export function isAmount(amount: unknown): amount is Amount {
return typeof amount === 'string' || isIssuedCurrency(amount)
}
/**
* Verify the form and type of an XChainBridge at runtime.
*
* @param input - The input to check the form and type of.
* @returns Whether the XChainBridge is properly formed.
*/
export function isXChainBridge(input: unknown): input is XChainBridge {
return (
isRecord(input) &&
Object.keys(input).length === XCHAIN_BRIDGE_SIZE &&
typeof input.LockingChainDoor === 'string' &&
isCurrency(input.LockingChainIssue) &&
typeof input.IssuingChainDoor === 'string' &&
isCurrency(input.IssuingChainIssue)
)
}
/* eslint-disable @typescript-eslint/restrict-template-expressions -- tx.TransactionType is checked before any calls */
/**
* Verify the form and type of a required type for a transaction at runtime.
*
* @param tx - The transaction input to check the form and type of.
* @param paramName - The name of the transaction parameter.
* @param checkValidity - The function to use to check the type.
* @throws
*/
export function validateRequiredField(
tx: Record<string, unknown>,
paramName: string,
checkValidity: (inp: unknown) => boolean,
): void {
if (tx[paramName] == null) {
throw new ValidationError(
`${tx.TransactionType}: missing field ${paramName}`,
)
}
if (!checkValidity(tx[paramName])) {
throw new ValidationError(
`${tx.TransactionType}: invalid field ${paramName}`,
)
}
}
/**
* Verify the form and type of an optional type for a transaction at runtime.
*
* @param tx - The transaction input to check the form and type of.
* @param paramName - The name of the transaction parameter.
* @param checkValidity - The function to use to check the type.
* @throws
*/
export function validateOptionalField(
tx: Record<string, unknown>,
paramName: string,
checkValidity: (inp: unknown) => boolean,
): void {
if (tx[paramName] !== undefined && !checkValidity(tx[paramName])) {
throw new ValidationError(
`${tx.TransactionType}: invalid field ${paramName}`,
)
}
}
/* eslint-enable @typescript-eslint/restrict-template-expressions -- checked before */
// eslint-disable-next-line @typescript-eslint/no-empty-interface -- no global flags right now, so this is fine
export interface GlobalFlags {}
@@ -190,14 +283,6 @@ export interface BaseTransaction {
* @throws When the common param is malformed.
*/
export function validateBaseTransaction(common: Record<string, unknown>): void {
if (common.Account === undefined) {
throw new ValidationError('BaseTransaction: missing field Account')
}
if (typeof common.Account !== 'string') {
throw new ValidationError('BaseTransaction: Account not string')
}
if (common.TransactionType === undefined) {
throw new ValidationError('BaseTransaction: missing field TransactionType')
}
@@ -210,27 +295,15 @@ export function validateBaseTransaction(common: Record<string, unknown>): void {
throw new ValidationError('BaseTransaction: Unknown TransactionType')
}
if (common.Fee !== undefined && typeof common.Fee !== 'string') {
throw new ValidationError('BaseTransaction: invalid Fee')
}
validateRequiredField(common, 'Account', isString)
if (common.Sequence !== undefined && typeof common.Sequence !== 'number') {
throw new ValidationError('BaseTransaction: invalid Sequence')
}
validateOptionalField(common, 'Fee', isString)
if (
common.AccountTxnID !== undefined &&
typeof common.AccountTxnID !== 'string'
) {
throw new ValidationError('BaseTransaction: invalid AccountTxnID')
}
validateOptionalField(common, 'Sequence', isNumber)
if (
common.LastLedgerSequence !== undefined &&
typeof common.LastLedgerSequence !== 'number'
) {
throw new ValidationError('BaseTransaction: invalid LastLedgerSequence')
}
validateOptionalField(common, 'AccountTxnID', isString)
validateOptionalField(common, 'LastLedgerSequence', isNumber)
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Only used by JS
const memos = common.Memos as Array<{ Memo?: unknown }> | undefined
@@ -248,33 +321,15 @@ export function validateBaseTransaction(common: Record<string, unknown>): void {
throw new ValidationError('BaseTransaction: invalid Signers')
}
if (common.SourceTag !== undefined && typeof common.SourceTag !== 'number') {
throw new ValidationError('BaseTransaction: invalid SourceTag')
}
validateOptionalField(common, 'SourceTag', isNumber)
if (
common.SigningPubKey !== undefined &&
typeof common.SigningPubKey !== 'string'
) {
throw new ValidationError('BaseTransaction: invalid SigningPubKey')
}
validateOptionalField(common, 'SigningPubKey', isString)
if (
common.TicketSequence !== undefined &&
typeof common.TicketSequence !== 'number'
) {
throw new ValidationError('BaseTransaction: invalid TicketSequence')
}
validateOptionalField(common, 'TicketSequence', isNumber)
if (
common.TxnSignature !== undefined &&
typeof common.TxnSignature !== 'string'
) {
throw new ValidationError('BaseTransaction: invalid TxnSignature')
}
if (common.NetworkID !== undefined && typeof common.NetworkID !== 'number') {
throw new ValidationError('BaseTransaction: invalid NetworkID')
}
validateOptionalField(common, 'TxnSignature', isString)
validateOptionalField(common, 'NetworkID', isNumber)
}
/**

View File

@@ -25,6 +25,7 @@ export {
export { CheckCancel } from './checkCancel'
export { CheckCash } from './checkCash'
export { CheckCreate } from './checkCreate'
export { Clawback } from './clawback'
export { DepositPreauth } from './depositPreauth'
export { EscrowCancel } from './escrowCancel'
export { EscrowCreate } from './escrowCreate'
@@ -63,4 +64,15 @@ export { SignerListSet } from './signerListSet'
export { TicketCreate } from './ticketCreate'
export { TrustSetFlagsInterface, TrustSetFlags, TrustSet } from './trustSet'
export { UNLModify } from './UNLModify'
export { Clawback } from './clawback'
export { XChainAddAccountCreateAttestation } from './XChainAddAccountCreateAttestation'
export { XChainAddClaimAttestation } from './XChainAddClaimAttestation'
export { XChainClaim } from './XChainClaim'
export { XChainCommit } from './XChainCommit'
export { XChainCreateBridge } from './XChainCreateBridge'
export { XChainCreateClaimID } from './XChainCreateClaimID'
export { XChainAccountCreateCommit } from './XChainAccountCreateCommit'
export {
XChainModifyBridge,
XChainModifyBridgeFlags,
XChainModifyBridgeFlagsInterface,
} from './XChainModifyBridge'

View File

@@ -1,3 +1,4 @@
/* eslint-disable max-lines -- need to work with a lot of transactions in a switch statement */
/* eslint-disable max-lines-per-function -- need to work with a lot of Tx verifications */
import { ValidationError } from '../../errors'
@@ -56,6 +57,32 @@ import { SetRegularKey, validateSetRegularKey } from './setRegularKey'
import { SignerListSet, validateSignerListSet } from './signerListSet'
import { TicketCreate, validateTicketCreate } from './ticketCreate'
import { TrustSet, validateTrustSet } from './trustSet'
import {
XChainAccountCreateCommit,
validateXChainAccountCreateCommit,
} from './XChainAccountCreateCommit'
import {
XChainAddAccountCreateAttestation,
validateXChainAddAccountCreateAttestation,
} from './XChainAddAccountCreateAttestation'
import {
XChainAddClaimAttestation,
validateXChainAddClaimAttestation,
} from './XChainAddClaimAttestation'
import { XChainClaim, validateXChainClaim } from './XChainClaim'
import { XChainCommit, validateXChainCommit } from './XChainCommit'
import {
XChainCreateBridge,
validateXChainCreateBridge,
} from './XChainCreateBridge'
import {
XChainCreateClaimID,
validateXChainCreateClaimID,
} from './XChainCreateClaimID'
import {
XChainModifyBridge,
validateXChainModifyBridge,
} from './XChainModifyBridge'
/**
* @category Transaction Models
@@ -92,6 +119,14 @@ export type Transaction =
| SignerListSet
| TicketCreate
| TrustSet
| XChainAddAccountCreateAttestation
| XChainAddClaimAttestation
| XChainClaim
| XChainCommit
| XChainCreateBridge
| XChainCreateClaimID
| XChainAccountCreateCommit
| XChainModifyBridge
/**
* @category Transaction Models
@@ -294,6 +329,38 @@ export function validate(transaction: Record<string, unknown>): void {
validateTrustSet(tx)
break
case 'XChainAddAccountCreateAttestation':
validateXChainAddAccountCreateAttestation(tx)
break
case 'XChainAddClaimAttestation':
validateXChainAddClaimAttestation(tx)
break
case 'XChainClaim':
validateXChainClaim(tx)
break
case 'XChainCommit':
validateXChainCommit(tx)
break
case 'XChainCreateBridge':
validateXChainCreateBridge(tx)
break
case 'XChainCreateClaimID':
validateXChainCreateClaimID(tx)
break
case 'XChainAccountCreateCommit':
validateXChainAccountCreateCommit(tx)
break
case 'XChainModifyBridge':
validateXChainModifyBridge(tx)
break
default:
throw new ValidationError(
`Invalid field TransactionType: ${tx.TransactionType}`,

View File

@@ -15,6 +15,7 @@ import { PaymentFlags } from '../transactions/payment'
import { PaymentChannelClaimFlags } from '../transactions/paymentChannelClaim'
import type { Transaction } from '../transactions/transaction'
import { TrustSetFlags } from '../transactions/trustSet'
import { XChainModifyBridgeFlags } from '../transactions/XChainModifyBridge'
import { isFlagEnabled } from '.'
@@ -78,6 +79,9 @@ export function setTransactionFlagsToNumber(tx: Transaction): void {
case 'TrustSet':
tx.Flags = convertFlagsToNumber(tx.Flags, TrustSetFlags)
return
case 'XChainModifyBridge':
tx.Flags = convertFlagsToNumber(tx.Flags, XChainModifyBridgeFlags)
return
default:
tx.Flags = 0
}

View File

@@ -0,0 +1,64 @@
import { decode } from 'ripple-binary-codec'
import {
CreatedNode,
isCreatedNode,
TransactionMetadata,
} from '../models/transactions/metadata'
/**
* Ensures that the metadata is in a deserialized format to parse.
*
* @param meta - the metadata from a `tx` method call. Can be in json format or binary format.
* @returns the metadata in a deserialized format.
*/
function ensureDecodedMeta(
meta: TransactionMetadata | string,
): TransactionMetadata {
if (typeof meta === 'string') {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Meta is either metadata or serialized metadata.
return decode(meta) as unknown as TransactionMetadata
}
return meta
}
/**
* Gets the XChainClaimID value from the metadata of an `XChainCreateClaimID` transaction.
*
* @param meta - Metadata from the response to submitting and waiting for an XChainCreateClaimID transaction
* or from a `tx` method call.
* @returns The XChainClaimID for the minted NFT.
* @throws if meta is not TransactionMetadata.
*/
export default function getXChainClaimID(
meta: TransactionMetadata | string | undefined,
): string | undefined {
if (typeof meta !== 'string' && meta?.AffectedNodes === undefined) {
throw new TypeError(`Unable to parse the parameter given to getXChainClaimID.
'meta' must be the metadata from an XChainCreateClaimID transaction. Received ${JSON.stringify(
meta,
)} instead.`)
}
const decodedMeta = ensureDecodedMeta(meta)
if (!decodedMeta.TransactionResult) {
throw new TypeError(
'Cannot get XChainClaimID from un-validated transaction',
)
}
if (decodedMeta.TransactionResult !== 'tesSUCCESS') {
return undefined
}
const createdNode = decodedMeta.AffectedNodes.find(
(node) =>
isCreatedNode(node) &&
node.CreatedNode.LedgerEntryType === 'XChainOwnedClaimID',
)
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- necessary here
return (createdNode as CreatedNode).CreatedNode.NewFields
.XChainClaimID as string
}

View File

@@ -25,6 +25,7 @@ import { Transaction } from '../models/transactions/transaction'
import { deriveKeypair, deriveAddress, deriveXAddress } from './derive'
import getBalanceChanges from './getBalanceChanges'
import getNFTokenID from './getNFTokenID'
import getXChainClaimID from './getXChainClaimID'
import {
hashSignedTx,
hashTx,
@@ -220,4 +221,5 @@ export {
encodeForSigningClaim,
getNFTokenID,
parseNFTokenID,
getXChainClaimID,
}

View File

@@ -25,8 +25,12 @@ import successSubmit from './submit.json'
import successSubscribe from './subscribe.json'
import errorSubscribe from './subscribeError.json'
import transaction_entry from './transactionEntry.json'
import NFTokenMint from './tx/NFTokenMint.json'
import NFTokenMint2 from './tx/NFTokenMint2.json'
import OfferCreateSell from './tx/offerCreateSell.json'
import Payment from './tx/payment.json'
import XChainCreateClaimID from './tx/XChainCreateClaimID.json'
import XChainCreateClaimID2 from './tx/XChainCreateClaimID2.json'
import unsubscribe from './unsubscribe.json'
const submit = {
@@ -89,8 +93,12 @@ const server_info = {
}
const tx = {
NFTokenMint,
NFTokenMint2,
Payment,
OfferCreateSell,
XChainCreateClaimID,
XChainCreateClaimID2,
}
const rippled = {

View File

@@ -0,0 +1,118 @@
{
"tx": {
"Account": "rLVUz66tawieqTPAHuTyFTN6pLbHcXiTzd",
"Fee": "20",
"Flags": 2147483648,
"NetworkID": 2552,
"OtherChainSource": "rL5Zd9m5XEoGPddMwYY5H5C8ARcR47b6oM",
"Sequence": 1007784,
"SignatureReward": "100",
"SigningPubKey": "039E925058C740A5B73E49300FC205D058520DE37F2C63C4EE3A0D1B50C4E44080",
"TransactionType": "XChainCreateClaimID",
"TxnSignature": "304402201C6F95B9997FB63DCD9854664707C58C46AA3207612FE32366B77DA084786CAF02205752C58821D7FAFAE26F77DC10AC0AFDDCBCCF4FCBED90E6B8C4523A0EB3E008",
"XChainBridge": {
"IssuingChainDoor": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
"IssuingChainIssue": {
"currency": "XRP"
},
"LockingChainDoor": "rMAXACCrp3Y8PpswXcg3bKggHX76V3F8M4",
"LockingChainIssue": {
"currency": "XRP"
}
},
"date": 1695324353000
},
"meta": {
"AffectedNodes": [
{
"ModifiedNode": {
"FinalFields": {
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
"Flags": 0,
"MinAccountCreateAmount": "10000000",
"OwnerNode": "0",
"SignatureReward": "100",
"XChainAccountClaimCount": "e3",
"XChainAccountCreateCount": "0",
"XChainBridge": {
"IssuingChainDoor": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
"IssuingChainIssue": {
"currency": "XRP"
},
"LockingChainDoor": "rMAXACCrp3Y8PpswXcg3bKggHX76V3F8M4",
"LockingChainIssue": {
"currency": "XRP"
}
},
"XChainClaimID": "b0"
},
"LedgerEntryType": "Bridge",
"LedgerIndex": "114C0DC89656D1B0FB1F4A3426034C3FCE75BCE65D9574B5D96ABC2B24D6C8F1",
"PreviousFields": {
"XChainClaimID": "af"
},
"PreviousTxnID": "3F6F3BBE584115D1A575AB24BA32B47184F2323B65DE5C8C8EE144A55115E0B9",
"PreviousTxnLgrSeq": 1027822
}
},
{
"ModifiedNode": {
"FinalFields": {
"Flags": 0,
"Owner": "rLVUz66tawieqTPAHuTyFTN6pLbHcXiTzd",
"RootIndex": "6C1EA1A93D590E831CCC0EE2CBE26C146A3A6FD36F5854DC5E5AB5CE78FAE49C"
},
"LedgerEntryType": "DirectoryNode",
"LedgerIndex": "6C1EA1A93D590E831CCC0EE2CBE26C146A3A6FD36F5854DC5E5AB5CE78FAE49C"
}
},
{
"CreatedNode": {
"LedgerEntryType": "XChainOwnedClaimID",
"LedgerIndex": "A00BD77AE864509D796B39041AD48E9DEFEC9AF20E5C09CEF2F5DA41D6CFEB1E",
"NewFields": {
"Account": "rLVUz66tawieqTPAHuTyFTN6pLbHcXiTzd",
"OtherChainSource": "rL5Zd9m5XEoGPddMwYY5H5C8ARcR47b6oM",
"SignatureReward": "100",
"XChainBridge": {
"IssuingChainDoor": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
"IssuingChainIssue": {
"currency": "XRP"
},
"LockingChainDoor": "rMAXACCrp3Y8PpswXcg3bKggHX76V3F8M4",
"LockingChainIssue": {
"currency": "XRP"
}
},
"XChainClaimID": "b0"
}
}
},
{
"ModifiedNode": {
"FinalFields": {
"Account": "rLVUz66tawieqTPAHuTyFTN6pLbHcXiTzd",
"Balance": "39999940",
"Flags": 0,
"OwnerCount": 3,
"Sequence": 1007785
},
"LedgerEntryType": "AccountRoot",
"LedgerIndex": "FD919D0BAA90C759DA4C7130AEEF6AE7FA2AF074F5E867D40BCBE1ECD8D8D0EA",
"PreviousFields": {
"Balance": "39999960",
"OwnerCount": 2,
"Sequence": 1007784
},
"PreviousTxnID": "3F6F3BBE584115D1A575AB24BA32B47184F2323B65DE5C8C8EE144A55115E0B9",
"PreviousTxnLgrSeq": 1027822
}
}
],
"TransactionIndex": 0,
"TransactionResult": "tesSUCCESS"
},
"hash": "998E76B9840DA5A6009592A2674D0166A9C4862193193AA46EA6B77A64781FB4",
"ledger_index": 1027837,
"date": 1695324353000
}

View File

@@ -0,0 +1,118 @@
{
"tx": {
"Account": "rwmUSzi5Xp31AjMTEdbvxgWqLETcVNU6Fv",
"Fee": "12",
"Flags": 0,
"LastLedgerSequence": 1027798,
"NetworkID": 2552,
"OtherChainSource": "rBXdfZ7NVpdjRfYajPMpviGgq7HLDeuBdR",
"Sequence": 1027778,
"SignatureReward": "100",
"SigningPubKey": "EDDDD69DF802B8DB82D644EF92E2C1F06AC128A275CDFF86F013180D104ED39D3B",
"TransactionType": "XChainCreateClaimID",
"TxnSignature": "67BE63527EC8A0C872F23E2C4EB97C1F3E7D3FED6D10C8310B9235D3891B6B9343768A080E258F6C3687BFC4B7C5FD429ABB33654C99DE46471FD6F2A7035303",
"XChainBridge": {
"IssuingChainDoor": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
"IssuingChainIssue": {
"currency": "XRP"
},
"LockingChainDoor": "rMAXACCrp3Y8PpswXcg3bKggHX76V3F8M4",
"LockingChainIssue": {
"currency": "XRP"
}
},
"date": 1695324182000
},
"meta": {
"AffectedNodes": [
{
"ModifiedNode": {
"FinalFields": {
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
"Flags": 0,
"MinAccountCreateAmount": "10000000",
"OwnerNode": "0",
"SignatureReward": "100",
"XChainAccountClaimCount": "e2",
"XChainAccountCreateCount": "0",
"XChainBridge": {
"IssuingChainDoor": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
"IssuingChainIssue": {
"currency": "XRP"
},
"LockingChainDoor": "rMAXACCrp3Y8PpswXcg3bKggHX76V3F8M4",
"LockingChainIssue": {
"currency": "XRP"
}
},
"XChainClaimID": "ac"
},
"LedgerEntryType": "Bridge",
"LedgerIndex": "114C0DC89656D1B0FB1F4A3426034C3FCE75BCE65D9574B5D96ABC2B24D6C8F1",
"PreviousFields": {
"XChainClaimID": "ab"
},
"PreviousTxnID": "80C33D1FB349D698CFDB1A85E8368557C5B7219B74DFCB2B05E0B10E2667F902",
"PreviousTxnLgrSeq": 1027779
}
},
{
"ModifiedNode": {
"FinalFields": {
"Account": "rwmUSzi5Xp31AjMTEdbvxgWqLETcVNU6Fv",
"Balance": "19999988",
"Flags": 0,
"OwnerCount": 1,
"Sequence": 1027779
},
"LedgerEntryType": "AccountRoot",
"LedgerIndex": "33442CE111B258424548888D8999F6D064A0866B1300C44AB72E1C5A09765D9D",
"PreviousFields": {
"Balance": "20000000",
"OwnerCount": 0,
"Sequence": 1027778
},
"PreviousTxnID": "7C9ACA230488547B4F39EBCE332447FB90AE59B64C1B03BBF474B509B43739EC",
"PreviousTxnLgrSeq": 1027778
}
},
{
"CreatedNode": {
"LedgerEntryType": "DirectoryNode",
"LedgerIndex": "439684B06C22596B5B86D2F50903B6AA6F68BD07BED636FC6325704B09DE5D61",
"NewFields": {
"Owner": "rwmUSzi5Xp31AjMTEdbvxgWqLETcVNU6Fv",
"RootIndex": "439684B06C22596B5B86D2F50903B6AA6F68BD07BED636FC6325704B09DE5D61"
}
}
},
{
"CreatedNode": {
"LedgerEntryType": "XChainOwnedClaimID",
"LedgerIndex": "8097863E1200B0174006541763AA8F604782DA10C1BD37190D753C699D69C678",
"NewFields": {
"Account": "rwmUSzi5Xp31AjMTEdbvxgWqLETcVNU6Fv",
"OtherChainSource": "rBXdfZ7NVpdjRfYajPMpviGgq7HLDeuBdR",
"SignatureReward": "100",
"XChainBridge": {
"IssuingChainDoor": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
"IssuingChainIssue": {
"currency": "XRP"
},
"LockingChainDoor": "rMAXACCrp3Y8PpswXcg3bKggHX76V3F8M4",
"LockingChainIssue": {
"currency": "XRP"
}
},
"XChainClaimID": "ac"
}
}
}
],
"TransactionIndex": 0,
"TransactionResult": "tesSUCCESS"
},
"hash": "A42C4E7F5BAF8A9BEB56853114EE686D554F15F400B8DA885A344B13C32D07BC",
"ledger_index": 1027780,
"date": 1695324182000
}

View File

@@ -0,0 +1,161 @@
import { assert } from 'chai'
import { validate, ValidationError } from '../../src'
import { validateXChainAccountCreateCommit } from '../../src/models/transactions/XChainAccountCreateCommit'
/**
* XChainAccountCreateCommit Transaction Verification Testing.
*
* Providing runtime verification testing for each specific transaction type.
*/
describe('XChainAccountCreateCommit', function () {
let tx
beforeEach(function () {
tx = {
Account: 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh',
XChainBridge: {
LockingChainDoor: 'rGzx83BVoqTYbGn7tiVAnFw7cbxjin13jL',
LockingChainIssue: {
currency: 'XRP',
},
IssuingChainDoor: 'r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV',
IssuingChainIssue: {
currency: 'XRP',
},
},
Amount: '1000000',
Fee: '10',
Flags: 2147483648,
Destination: 'rGzx83BVoqTYbGn7tiVAnFw7cbxjin13jL',
Sequence: 1,
SignatureReward: '10000',
TransactionType: 'XChainAccountCreateCommit',
} as any
})
it('verifies valid XChainAccountCreateCommit', function () {
assert.doesNotThrow(() => validateXChainAccountCreateCommit(tx))
assert.doesNotThrow(() => validate(tx))
})
it('throws w/ missing XChainBridge', function () {
delete tx.XChainBridge
assert.throws(
() => validateXChainAccountCreateCommit(tx),
ValidationError,
'XChainAccountCreateCommit: missing field XChainBridge',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAccountCreateCommit: missing field XChainBridge',
)
})
it('throws w/ invalid XChainBridge', function () {
tx.XChainBridge = { XChainDoor: 'test' }
assert.throws(
() => validateXChainAccountCreateCommit(tx),
ValidationError,
'XChainAccountCreateCommit: invalid field XChainBridge',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAccountCreateCommit: invalid field XChainBridge',
)
})
it('throws w/ missing SignatureReward', function () {
delete tx.SignatureReward
assert.throws(
() => validateXChainAccountCreateCommit(tx),
ValidationError,
'XChainAccountCreateCommit: missing field SignatureReward',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAccountCreateCommit: missing field SignatureReward',
)
})
it('throws w/ invalid SignatureReward', function () {
tx.SignatureReward = { currency: 'ETH' }
assert.throws(
() => validateXChainAccountCreateCommit(tx),
ValidationError,
'XChainAccountCreateCommit: invalid field SignatureReward',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAccountCreateCommit: invalid field SignatureReward',
)
})
it('throws w/ missing Destination', function () {
delete tx.Destination
assert.throws(
() => validateXChainAccountCreateCommit(tx),
ValidationError,
'XChainAccountCreateCommit: missing field Destination',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAccountCreateCommit: missing field Destination',
)
})
it('throws w/ invalid Destination', function () {
tx.Destination = 123
assert.throws(
() => validateXChainAccountCreateCommit(tx),
ValidationError,
'XChainAccountCreateCommit: invalid field Destination',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAccountCreateCommit: invalid field Destination',
)
})
it('throws w/ missing Amount', function () {
delete tx.Amount
assert.throws(
() => validateXChainAccountCreateCommit(tx),
ValidationError,
'XChainAccountCreateCommit: missing field Amount',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAccountCreateCommit: missing field Amount',
)
})
it('throws w/ invalid Amount', function () {
tx.Amount = { currency: 'ETH' }
assert.throws(
() => validateXChainAccountCreateCommit(tx),
ValidationError,
'XChainAccountCreateCommit: invalid field Amount',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAccountCreateCommit: invalid field Amount',
)
})
})

View File

@@ -0,0 +1,381 @@
import { assert } from 'chai'
import { validate, ValidationError } from '../../src'
import { validateXChainAddAccountCreateAttestation } from '../../src/models/transactions/XChainAddAccountCreateAttestation'
/**
* XChainAddAccountCreateAttestation Transaction Verification Testing.
*
* Providing runtime verification testing for each specific transaction type.
*/
describe('XChainAddAccountCreateAttestation', function () {
let tx
beforeEach(function () {
tx = {
Account: 'r9cYxdjQsoXAEz3qQJc961SNLaXRkWXCvT',
Amount: '10000000',
AttestationRewardAccount: 'r9cYxdjQsoXAEz3qQJc961SNLaXRkWXCvT',
AttestationSignerAccount: 'r9cYxdjQsoXAEz3qQJc961SNLaXRkWXCvT',
Destination: 'rJdTJRJZ6GXCCRaamHJgEqVzB7Zy4557Pi',
Fee: '20',
LastLedgerSequence: 13,
OtherChainSource: 'raFcdz1g8LWJDJWJE2ZKLRGdmUmsTyxaym',
PublicKey:
'ED1F4A024ACFEBDB6C7AA88DEDE3364E060487EA31B14CC9E0D610D152B31AADC2',
Sequence: 5,
Signature:
'EEFCFA3DC2AB4AB7C4D2EBBC168CB621A11B82BABD86534DFC8EFA72439A496' +
'62D744073CD848E7A587A95B35162CDF9A69BB237E72C9537A987F5B8C394F30D',
SignatureReward: '100',
TransactionType: 'XChainAddAccountCreateAttestation',
WasLockingChainSend: 1,
XChainAccountCreateCount: '0000000000000006',
XChainBridge: {
IssuingChainDoor: 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh',
IssuingChainIssue: {
currency: 'XRP',
},
LockingChainDoor: 'rDJVtEuDKr4rj1B3qtW7R5TVWdXV2DY7Qg',
LockingChainIssue: {
currency: 'XRP',
},
},
} as any
})
it('verifies valid XChainAddAccountCreateAttestation', function () {
assert.doesNotThrow(() => validateXChainAddAccountCreateAttestation(tx))
assert.doesNotThrow(() => validate(tx))
})
it('throws w/ missing Amount', function () {
delete tx.Amount
assert.throws(
() => validateXChainAddAccountCreateAttestation(tx),
ValidationError,
'XChainAddAccountCreateAttestation: missing field Amount',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddAccountCreateAttestation: missing field Amount',
)
})
it('throws w/ invalid Amount', function () {
tx.Amount = { currency: 'ETH' }
assert.throws(
() => validateXChainAddAccountCreateAttestation(tx),
ValidationError,
'XChainAddAccountCreateAttestation: invalid field Amount',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddAccountCreateAttestation: invalid field Amount',
)
})
it('throws w/ missing AttestationRewardAccount', function () {
delete tx.AttestationRewardAccount
assert.throws(
() => validateXChainAddAccountCreateAttestation(tx),
ValidationError,
'XChainAddAccountCreateAttestation: missing field AttestationRewardAccount',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddAccountCreateAttestation: missing field AttestationRewardAccount',
)
})
it('throws w/ invalid AttestationRewardAccount', function () {
tx.AttestationRewardAccount = 123
assert.throws(
() => validateXChainAddAccountCreateAttestation(tx),
ValidationError,
'XChainAddAccountCreateAttestation: invalid field AttestationRewardAccount',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddAccountCreateAttestation: invalid field AttestationRewardAccount',
)
})
it('throws w/ missing AttestationSignerAccount', function () {
delete tx.AttestationSignerAccount
assert.throws(
() => validateXChainAddAccountCreateAttestation(tx),
ValidationError,
'XChainAddAccountCreateAttestation: missing field AttestationSignerAccount',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddAccountCreateAttestation: missing field AttestationSignerAccount',
)
})
it('throws w/ invalid AttestationSignerAccount', function () {
tx.AttestationSignerAccount = 123
assert.throws(
() => validateXChainAddAccountCreateAttestation(tx),
ValidationError,
'XChainAddAccountCreateAttestation: invalid field AttestationSignerAccount',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddAccountCreateAttestation: invalid field AttestationSignerAccount',
)
})
it('throws w/ missing Destination', function () {
delete tx.Destination
assert.throws(
() => validateXChainAddAccountCreateAttestation(tx),
ValidationError,
'XChainAddAccountCreateAttestation: missing field Destination',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddAccountCreateAttestation: missing field Destination',
)
})
it('throws w/ invalid Destination', function () {
tx.Destination = 123
assert.throws(
() => validateXChainAddAccountCreateAttestation(tx),
ValidationError,
'XChainAddAccountCreateAttestation: invalid field Destination',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddAccountCreateAttestation: invalid field Destination',
)
})
it('throws w/ missing OtherChainSource', function () {
delete tx.OtherChainSource
assert.throws(
() => validateXChainAddAccountCreateAttestation(tx),
ValidationError,
'XChainAddAccountCreateAttestation: missing field OtherChainSource',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddAccountCreateAttestation: missing field OtherChainSource',
)
})
it('throws w/ invalid OtherChainSource', function () {
tx.OtherChainSource = 123
assert.throws(
() => validateXChainAddAccountCreateAttestation(tx),
ValidationError,
'XChainAddAccountCreateAttestation: invalid field OtherChainSource',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddAccountCreateAttestation: invalid field OtherChainSource',
)
})
it('throws w/ missing PublicKey', function () {
delete tx.PublicKey
assert.throws(
() => validateXChainAddAccountCreateAttestation(tx),
ValidationError,
'XChainAddAccountCreateAttestation: missing field PublicKey',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddAccountCreateAttestation: missing field PublicKey',
)
})
it('throws w/ invalid PublicKey', function () {
tx.PublicKey = 123
assert.throws(
() => validateXChainAddAccountCreateAttestation(tx),
ValidationError,
'XChainAddAccountCreateAttestation: invalid field PublicKey',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddAccountCreateAttestation: invalid field PublicKey',
)
})
it('throws w/ missing Signature', function () {
delete tx.Signature
assert.throws(
() => validateXChainAddAccountCreateAttestation(tx),
ValidationError,
'XChainAddAccountCreateAttestation: missing field Signature',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddAccountCreateAttestation: missing field Signature',
)
})
it('throws w/ invalid Signature', function () {
tx.Signature = 123
assert.throws(
() => validateXChainAddAccountCreateAttestation(tx),
ValidationError,
'XChainAddAccountCreateAttestation: invalid field Signature',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddAccountCreateAttestation: invalid field Signature',
)
})
it('throws w/ missing SignatureReward', function () {
delete tx.SignatureReward
assert.throws(
() => validateXChainAddAccountCreateAttestation(tx),
ValidationError,
'XChainAddAccountCreateAttestation: missing field SignatureReward',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddAccountCreateAttestation: missing field SignatureReward',
)
})
it('throws w/ invalid SignatureReward', function () {
tx.SignatureReward = { currency: 'ETH' }
assert.throws(
() => validateXChainAddAccountCreateAttestation(tx),
ValidationError,
'XChainAddAccountCreateAttestation: invalid field SignatureReward',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddAccountCreateAttestation: invalid field SignatureReward',
)
})
it('throws w/ missing WasLockingChainSend', function () {
delete tx.WasLockingChainSend
assert.throws(
() => validateXChainAddAccountCreateAttestation(tx),
ValidationError,
'XChainAddAccountCreateAttestation: missing field WasLockingChainSend',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddAccountCreateAttestation: missing field WasLockingChainSend',
)
})
it('throws w/ invalid WasLockingChainSend', function () {
tx.WasLockingChainSend = 2
assert.throws(
() => validateXChainAddAccountCreateAttestation(tx),
ValidationError,
'XChainAddAccountCreateAttestation: invalid field WasLockingChainSend',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddAccountCreateAttestation: invalid field WasLockingChainSend',
)
})
it('throws w/ missing XChainAccountCreateCount', function () {
delete tx.XChainAccountCreateCount
assert.throws(
() => validateXChainAddAccountCreateAttestation(tx),
ValidationError,
'XChainAddAccountCreateAttestation: missing field XChainAccountCreateCount',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddAccountCreateAttestation: missing field XChainAccountCreateCount',
)
})
it('throws w/ invalid XChainAccountCreateCount', function () {
tx.XChainAccountCreateCount = { currency: 'ETH' }
assert.throws(
() => validateXChainAddAccountCreateAttestation(tx),
ValidationError,
'XChainAddAccountCreateAttestation: invalid field XChainAccountCreateCount',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddAccountCreateAttestation: invalid field XChainAccountCreateCount',
)
})
it('throws w/ missing XChainBridge', function () {
delete tx.XChainBridge
assert.throws(
() => validateXChainAddAccountCreateAttestation(tx),
ValidationError,
'XChainAddAccountCreateAttestation: missing field XChainBridge',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddAccountCreateAttestation: missing field XChainBridge',
)
})
it('throws w/ invalid XChainBridge', function () {
tx.XChainBridge = { XChainDoor: 'test' }
assert.throws(
() => validateXChainAddAccountCreateAttestation(tx),
ValidationError,
'XChainAddAccountCreateAttestation: invalid field XChainBridge',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddAccountCreateAttestation: invalid field XChainBridge',
)
})
})

View File

@@ -0,0 +1,334 @@
import { assert } from 'chai'
import { validate, ValidationError } from '../../src'
import { validateXChainAddClaimAttestation } from '../../src/models/transactions/XChainAddClaimAttestation'
/**
* XChainAddClaimAttestation Transaction Verification Testing.
*
* Providing runtime verification testing for each specific transaction type.
*/
describe('XChainAddClaimAttestation', function () {
let tx
beforeEach(function () {
tx = {
Account: 'rsqvD8WFFEBBv4nztpoW9YYXJ7eRzLrtc3',
Amount: '10000000',
AttestationRewardAccount: 'rsqvD8WFFEBBv4nztpoW9YYXJ7eRzLrtc3',
AttestationSignerAccount: 'rsqvD8WFFEBBv4nztpoW9YYXJ7eRzLrtc3',
Destination: 'rJdTJRJZ6GXCCRaamHJgEqVzB7Zy4557Pi',
Fee: '20',
LastLedgerSequence: 19,
OtherChainSource: 'raFcdz1g8LWJDJWJE2ZKLRGdmUmsTyxaym',
PublicKey:
'ED7541DEC700470F54276C90C333A13CDBB5D341FD43C60CEA12170F6D6D4E1136',
Sequence: 9,
Signature:
'7C175050B08000AD35EEB2D87E16CD3F95A0AEEBF2A049474275153D9D4DD44528FE99AA50E71660A15B0B768E1B90E609BBD5DC7AFAFD45D9705D72D40EA10C',
TransactionType: 'XChainAddClaimAttestation',
WasLockingChainSend: 1,
XChainBridge: {
IssuingChainDoor: 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh',
IssuingChainIssue: {
currency: 'XRP',
},
LockingChainDoor: 'rDJVtEuDKr4rj1B3qtW7R5TVWdXV2DY7Qg',
LockingChainIssue: {
currency: 'XRP',
},
},
XChainClaimID: '0000000000000001',
} as any
})
it('verifies valid XChainAddClaimAttestation', function () {
assert.doesNotThrow(() => validateXChainAddClaimAttestation(tx))
assert.doesNotThrow(() => validate(tx))
})
it('throws w/ missing Amount', function () {
delete tx.Amount
assert.throws(
() => validateXChainAddClaimAttestation(tx),
ValidationError,
'XChainAddClaimAttestation: missing field Amount',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddClaimAttestation: missing field Amount',
)
})
it('throws w/ invalid Amount', function () {
tx.Amount = { currency: 'ETH' }
assert.throws(
() => validateXChainAddClaimAttestation(tx),
ValidationError,
'XChainAddClaimAttestation: invalid field Amount',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddClaimAttestation: invalid field Amount',
)
})
it('throws w/ missing AttestationRewardAccount', function () {
delete tx.AttestationRewardAccount
assert.throws(
() => validateXChainAddClaimAttestation(tx),
ValidationError,
'XChainAddClaimAttestation: missing field AttestationRewardAccount',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddClaimAttestation: missing field AttestationRewardAccount',
)
})
it('throws w/ invalid AttestationRewardAccount', function () {
tx.AttestationRewardAccount = 123
assert.throws(
() => validateXChainAddClaimAttestation(tx),
ValidationError,
'XChainAddClaimAttestation: invalid field AttestationRewardAccount',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddClaimAttestation: invalid field AttestationRewardAccount',
)
})
it('throws w/ missing AttestationSignerAccount', function () {
delete tx.AttestationSignerAccount
assert.throws(
() => validateXChainAddClaimAttestation(tx),
ValidationError,
'XChainAddClaimAttestation: missing field AttestationSignerAccount',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddClaimAttestation: missing field AttestationSignerAccount',
)
})
it('throws w/ invalid AttestationSignerAccount', function () {
tx.AttestationSignerAccount = 123
assert.throws(
() => validateXChainAddClaimAttestation(tx),
ValidationError,
'XChainAddClaimAttestation: invalid field AttestationSignerAccount',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddClaimAttestation: invalid field AttestationSignerAccount',
)
})
it('throws w/ invalid Destination', function () {
tx.Destination = 123
assert.throws(
() => validateXChainAddClaimAttestation(tx),
ValidationError,
'XChainAddClaimAttestation: invalid field Destination',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddClaimAttestation: invalid field Destination',
)
})
it('throws w/ missing OtherChainSource', function () {
delete tx.OtherChainSource
assert.throws(
() => validateXChainAddClaimAttestation(tx),
ValidationError,
'XChainAddClaimAttestation: missing field OtherChainSource',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddClaimAttestation: missing field OtherChainSource',
)
})
it('throws w/ invalid OtherChainSource', function () {
tx.OtherChainSource = 123
assert.throws(
() => validateXChainAddClaimAttestation(tx),
ValidationError,
'XChainAddClaimAttestation: invalid field OtherChainSource',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddClaimAttestation: invalid field OtherChainSource',
)
})
it('throws w/ missing PublicKey', function () {
delete tx.PublicKey
assert.throws(
() => validateXChainAddClaimAttestation(tx),
ValidationError,
'XChainAddClaimAttestation: missing field PublicKey',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddClaimAttestation: missing field PublicKey',
)
})
it('throws w/ invalid PublicKey', function () {
tx.PublicKey = 123
assert.throws(
() => validateXChainAddClaimAttestation(tx),
ValidationError,
'XChainAddClaimAttestation: invalid field PublicKey',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddClaimAttestation: invalid field PublicKey',
)
})
it('throws w/ missing Signature', function () {
delete tx.Signature
assert.throws(
() => validateXChainAddClaimAttestation(tx),
ValidationError,
'XChainAddClaimAttestation: missing field Signature',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddClaimAttestation: missing field Signature',
)
})
it('throws w/ invalid Signature', function () {
tx.Signature = 123
assert.throws(
() => validateXChainAddClaimAttestation(tx),
ValidationError,
'XChainAddClaimAttestation: invalid field Signature',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddClaimAttestation: invalid field Signature',
)
})
it('throws w/ missing WasLockingChainSend', function () {
delete tx.WasLockingChainSend
assert.throws(
() => validateXChainAddClaimAttestation(tx),
ValidationError,
'XChainAddClaimAttestation: missing field WasLockingChainSend',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddClaimAttestation: missing field WasLockingChainSend',
)
})
it('throws w/ invalid WasLockingChainSend', function () {
tx.WasLockingChainSend = 2
assert.throws(
() => validateXChainAddClaimAttestation(tx),
ValidationError,
'XChainAddClaimAttestation: invalid field WasLockingChainSend',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddClaimAttestation: invalid field WasLockingChainSend',
)
})
it('throws w/ missing XChainBridge', function () {
delete tx.XChainBridge
assert.throws(
() => validateXChainAddClaimAttestation(tx),
ValidationError,
'XChainAddClaimAttestation: missing field XChainBridge',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddClaimAttestation: missing field XChainBridge',
)
})
it('throws w/ invalid XChainBridge', function () {
tx.XChainBridge = { XChainDoor: 'test' }
assert.throws(
() => validateXChainAddClaimAttestation(tx),
ValidationError,
'XChainAddClaimAttestation: invalid field XChainBridge',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddClaimAttestation: invalid field XChainBridge',
)
})
it('throws w/ missing XChainClaimID', function () {
delete tx.XChainClaimID
assert.throws(
() => validateXChainAddClaimAttestation(tx),
ValidationError,
'XChainAddClaimAttestation: missing field XChainClaimID',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddClaimAttestation: missing field XChainClaimID',
)
})
it('throws w/ invalid XChainClaimID', function () {
tx.XChainClaimID = { currency: 'ETH' }
assert.throws(
() => validateXChainAddClaimAttestation(tx),
ValidationError,
'XChainAddClaimAttestation: invalid field XChainClaimID',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainAddClaimAttestation: invalid field XChainClaimID',
)
})
})

View File

@@ -0,0 +1,176 @@
import { assert } from 'chai'
import { validate, ValidationError } from '../../src'
import { validateXChainClaim } from '../../src/models/transactions/XChainClaim'
/**
* XChainClaim Transaction Verification Testing.
*
* Providing runtime verification testing for each specific transaction type.
*/
describe('XChainClaim', function () {
let tx
beforeEach(function () {
tx = {
Account: 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh',
Amount: '10000',
XChainBridge: {
LockingChainDoor: 'rGzx83BVoqTYbGn7tiVAnFw7cbxjin13jL',
LockingChainIssue: {
currency: 'XRP',
},
IssuingChainDoor: 'r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV',
IssuingChainIssue: {
currency: 'XRP',
},
},
Destination: 'r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV',
Fee: '10',
Flags: 2147483648,
Sequence: 1,
TransactionType: 'XChainClaim',
XChainClaimID: '0000000000000001',
} as any
})
it('verifies valid XChainClaim', function () {
assert.doesNotThrow(() => validateXChainClaim(tx))
assert.doesNotThrow(() => validate(tx))
})
it('throws w/ missing XChainBridge', function () {
delete tx.XChainBridge
assert.throws(
() => validateXChainClaim(tx),
ValidationError,
'XChainClaim: missing field XChainBridge',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainClaim: missing field XChainBridge',
)
})
it('throws w/ invalid XChainBridge', function () {
tx.XChainBridge = { XChainDoor: 'test' }
assert.throws(
() => validateXChainClaim(tx),
ValidationError,
'XChainClaim: invalid field XChainBridge',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainClaim: invalid field XChainBridge',
)
})
it('throws w/ missing XChainClaimID', function () {
delete tx.XChainClaimID
assert.throws(
() => validateXChainClaim(tx),
ValidationError,
'XChainClaim: missing field XChainClaimID',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainClaim: missing field XChainClaimID',
)
})
it('throws w/ invalid XChainClaimID', function () {
tx.XChainClaimID = { currency: 'ETH' }
assert.throws(
() => validateXChainClaim(tx),
ValidationError,
'XChainClaim: invalid field XChainClaimID',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainClaim: invalid field XChainClaimID',
)
})
it('throws w/ missing Destination', function () {
delete tx.Destination
assert.throws(
() => validateXChainClaim(tx),
ValidationError,
'XChainClaim: missing field Destination',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainClaim: missing field Destination',
)
})
it('throws w/ invalid Destination', function () {
tx.Destination = 123
assert.throws(
() => validateXChainClaim(tx),
ValidationError,
'XChainClaim: invalid field Destination',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainClaim: invalid field Destination',
)
})
it('throws w/ invalid DestinationTag', function () {
tx.DestinationTag = 'number'
assert.throws(
() => validateXChainClaim(tx),
ValidationError,
'XChainClaim: invalid field DestinationTag',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainClaim: invalid field DestinationTag',
)
})
it('throws w/ missing Amount', function () {
delete tx.Amount
assert.throws(
() => validateXChainClaim(tx),
ValidationError,
'XChainClaim: missing field Amount',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainClaim: missing field Amount',
)
})
it('throws w/ invalid Amount', function () {
tx.Amount = { currency: 'ETH' }
assert.throws(
() => validateXChainClaim(tx),
ValidationError,
'XChainClaim: invalid field Amount',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainClaim: invalid field Amount',
)
})
})

View File

@@ -0,0 +1,145 @@
import { assert } from 'chai'
import { validate, ValidationError } from '../../src'
import { validateXChainCommit } from '../../src/models/transactions/XChainCommit'
/**
* XChainCommit Transaction Verification Testing.
*
* Providing runtime verification testing for each specific transaction type.
*/
describe('XChainCommit', function () {
let tx
beforeEach(function () {
tx = {
Account: 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh',
Amount: '10000',
XChainBridge: {
LockingChainDoor: 'rGzx83BVoqTYbGn7tiVAnFw7cbxjin13jL',
LockingChainIssue: {
currency: 'XRP',
},
IssuingChainDoor: 'r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV',
IssuingChainIssue: {
currency: 'XRP',
},
},
Fee: '10',
Flags: 2147483648,
Sequence: 1,
TransactionType: 'XChainCommit',
XChainClaimID: '0000000000000001',
} as any
})
it('verifies valid XChainCommit', function () {
assert.doesNotThrow(() => validateXChainCommit(tx))
assert.doesNotThrow(() => validate(tx))
})
it('throws w/ missing XChainBridge', function () {
delete tx.XChainBridge
assert.throws(
() => validateXChainCommit(tx),
ValidationError,
'XChainCommit: missing field XChainBridge',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainCommit: missing field XChainBridge',
)
})
it('throws w/ invalid XChainBridge', function () {
tx.XChainBridge = { XChainDoor: 'test' }
assert.throws(
() => validateXChainCommit(tx),
ValidationError,
'XChainCommit: invalid field XChainBridge',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainCommit: invalid field XChainBridge',
)
})
it('throws w/ missing XChainClaimID', function () {
delete tx.XChainClaimID
assert.throws(
() => validateXChainCommit(tx),
ValidationError,
'XChainCommit: missing field XChainClaimID',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainCommit: missing field XChainClaimID',
)
})
it('throws w/ invalid XChainClaimID', function () {
tx.XChainClaimID = { currency: 'ETH' }
assert.throws(
() => validateXChainCommit(tx),
ValidationError,
'XChainCommit: invalid field XChainClaimID',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainCommit: invalid field XChainClaimID',
)
})
it('throws w/ invalid OtherChainDestination', function () {
tx.OtherChainDestination = 123
assert.throws(
() => validateXChainCommit(tx),
ValidationError,
'XChainCommit: invalid field OtherChainDestination',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainCommit: invalid field OtherChainDestination',
)
})
it('throws w/ missing Amount', function () {
delete tx.Amount
assert.throws(
() => validateXChainCommit(tx),
ValidationError,
'XChainCommit: missing field Amount',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainCommit: missing field Amount',
)
})
it('throws w/ invalid Amount', function () {
tx.Amount = { currency: 'ETH' }
assert.throws(
() => validateXChainCommit(tx),
ValidationError,
'XChainCommit: invalid field Amount',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainCommit: invalid field Amount',
)
})
})

View File

@@ -0,0 +1,115 @@
import { assert } from 'chai'
import { validate, ValidationError } from '../../src'
import { validateXChainCreateBridge } from '../../src/models/transactions/XChainCreateBridge'
/**
* XChainCreateBridge Transaction Verification Testing.
*
* Providing runtime verification testing for each specific transaction type.
*/
describe('XChainCreateBridge', function () {
let tx
beforeEach(function () {
tx = {
Account: 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh',
XChainBridge: {
LockingChainDoor: 'rGzx83BVoqTYbGn7tiVAnFw7cbxjin13jL',
LockingChainIssue: {
currency: 'XRP',
},
IssuingChainDoor: 'r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV',
IssuingChainIssue: {
currency: 'XRP',
},
},
Fee: '10',
Flags: 0,
MinAccountCreateAmount: '10000',
Sequence: 1,
SignatureReward: '1000',
TransactionType: 'XChainCreateBridge',
} as any
})
it('verifies valid XChainCreateBridge', function () {
assert.doesNotThrow(() => validateXChainCreateBridge(tx))
assert.doesNotThrow(() => validate(tx))
})
it('throws w/ missing XChainBridge', function () {
delete tx.XChainBridge
assert.throws(
() => validateXChainCreateBridge(tx),
ValidationError,
'XChainCreateBridge: missing field XChainBridge',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainCreateBridge: missing field XChainBridge',
)
})
it('throws w/ invalid XChainBridge', function () {
tx.XChainBridge = { XChainDoor: 'test' }
assert.throws(
() => validateXChainCreateBridge(tx),
ValidationError,
'XChainCreateBridge: invalid field XChainBridge',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainCreateBridge: invalid field XChainBridge',
)
})
it('throws w/ missing SignatureReward', function () {
delete tx.SignatureReward
assert.throws(
() => validateXChainCreateBridge(tx),
ValidationError,
'XChainCreateBridge: missing field SignatureReward',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainCreateBridge: missing field SignatureReward',
)
})
it('throws w/ invalid SignatureReward', function () {
tx.SignatureReward = { currency: 'ETH' }
assert.throws(
() => validateXChainCreateBridge(tx),
ValidationError,
'XChainCreateBridge: invalid field SignatureReward',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainCreateBridge: invalid field SignatureReward',
)
})
it('throws w/ invalid MinAccountCreateAmount', function () {
tx.MinAccountCreateAmount = { currency: 'ETH' }
assert.throws(
() => validateXChainCreateBridge(tx),
ValidationError,
'XChainCreateBridge: invalid field MinAccountCreateAmount',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainCreateBridge: invalid field MinAccountCreateAmount',
)
})
})

View File

@@ -0,0 +1,130 @@
import { assert } from 'chai'
import { validate, ValidationError } from '../../src'
import { validateXChainCreateClaimID } from '../../src/models/transactions/XChainCreateClaimID'
/**
* XChainCreateClaimID Transaction Verification Testing.
*
* Providing runtime verification testing for each specific transaction type.
*/
describe('XChainCreateClaimID', function () {
let tx
beforeEach(function () {
tx = {
Account: 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh',
XChainBridge: {
LockingChainDoor: 'rGzx83BVoqTYbGn7tiVAnFw7cbxjin13jL',
LockingChainIssue: {
currency: 'XRP',
},
IssuingChainDoor: 'r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV',
IssuingChainIssue: {
currency: 'XRP',
},
},
Fee: '10',
Flags: 2147483648,
OtherChainSource: 'rGzx83BVoqTYbGn7tiVAnFw7cbxjin13jL',
Sequence: 1,
SignatureReward: '10000',
TransactionType: 'XChainCreateClaimID',
} as any
})
it('verifies valid XChainCreateClaimID', function () {
assert.doesNotThrow(() => validateXChainCreateClaimID(tx))
assert.doesNotThrow(() => validate(tx))
})
it('throws w/ missing XChainBridge', function () {
delete tx.XChainBridge
assert.throws(
() => validateXChainCreateClaimID(tx),
ValidationError,
'XChainCreateClaimID: missing field XChainBridge',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainCreateClaimID: missing field XChainBridge',
)
})
it('throws w/ invalid XChainBridge', function () {
tx.XChainBridge = { XChainDoor: 'test' }
assert.throws(
() => validateXChainCreateClaimID(tx),
ValidationError,
'XChainCreateClaimID: invalid field XChainBridge',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainCreateClaimID: invalid field XChainBridge',
)
})
it('throws w/ missing SignatureReward', function () {
delete tx.SignatureReward
assert.throws(
() => validateXChainCreateClaimID(tx),
ValidationError,
'XChainCreateClaimID: missing field SignatureReward',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainCreateClaimID: missing field SignatureReward',
)
})
it('throws w/ invalid SignatureReward', function () {
tx.SignatureReward = { currency: 'ETH' }
assert.throws(
() => validateXChainCreateClaimID(tx),
ValidationError,
'XChainCreateClaimID: invalid field SignatureReward',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainCreateClaimID: invalid field SignatureReward',
)
})
it('throws w/ missing OtherChainSource', function () {
delete tx.OtherChainSource
assert.throws(
() => validateXChainCreateClaimID(tx),
ValidationError,
'XChainCreateClaimID: missing field OtherChainSource',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainCreateClaimID: missing field OtherChainSource',
)
})
it('throws w/ invalid OtherChainSource', function () {
tx.OtherChainSource = 123
assert.throws(
() => validateXChainCreateClaimID(tx),
ValidationError,
'XChainCreateClaimID: invalid field OtherChainSource',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainCreateClaimID: invalid field OtherChainSource',
)
})
})

View File

@@ -0,0 +1,100 @@
import { assert } from 'chai'
import { validate, ValidationError } from '../../src'
import { validateXChainModifyBridge } from '../../src/models/transactions/XChainModifyBridge'
/**
* XChainModifyBridge Transaction Verification Testing.
*
* Providing runtime verification testing for each specific transaction type.
*/
describe('XChainModifyBridge', function () {
let tx
beforeEach(function () {
tx = {
Account: 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh',
XChainBridge: {
LockingChainDoor: 'rGzx83BVoqTYbGn7tiVAnFw7cbxjin13jL',
LockingChainIssue: {
currency: 'XRP',
},
IssuingChainDoor: 'r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV',
IssuingChainIssue: {
currency: 'XRP',
},
},
Fee: '10',
Flags: 0,
MinAccountCreateAmount: '10000',
Sequence: 1,
SignatureReward: '1000',
TransactionType: 'XChainModifyBridge',
} as any
})
it('verifies valid XChainModifyBridge', function () {
assert.doesNotThrow(() => validateXChainModifyBridge(tx))
assert.doesNotThrow(() => validate(tx))
})
it('throws w/ missing XChainBridge', function () {
delete tx.XChainBridge
assert.throws(
() => validateXChainModifyBridge(tx),
ValidationError,
'XChainModifyBridge: missing field XChainBridge',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainModifyBridge: missing field XChainBridge',
)
})
it('throws w/ invalid XChainBridge', function () {
tx.XChainBridge = { XChainDoor: 'test' }
assert.throws(
() => validateXChainModifyBridge(tx),
ValidationError,
'XChainModifyBridge: invalid field XChainBridge',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainModifyBridge: invalid field XChainBridge',
)
})
it('throws w/ invalid SignatureReward', function () {
tx.SignatureReward = { currency: 'ETH' }
assert.throws(
() => validateXChainModifyBridge(tx),
ValidationError,
'XChainModifyBridge: invalid field SignatureReward',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainModifyBridge: invalid field SignatureReward',
)
})
it('throws w/ invalid MinAccountCreateAmount', function () {
tx.MinAccountCreateAmount = { currency: 'ETH' }
assert.throws(
() => validateXChainModifyBridge(tx),
ValidationError,
'XChainModifyBridge: invalid field MinAccountCreateAmount',
)
assert.throws(
() => validate(tx),
ValidationError,
'XChainModifyBridge: invalid field MinAccountCreateAmount',
)
})
})

View File

@@ -78,7 +78,7 @@ describe('BaseTransaction', function () {
assert.throws(
() => validateBaseTransaction(invalidFee),
ValidationError,
'BaseTransaction: invalid Fee',
'Payment: invalid field Fee',
)
})
@@ -92,7 +92,7 @@ describe('BaseTransaction', function () {
assert.throws(
() => validateBaseTransaction(invalidSeq),
ValidationError,
'BaseTransaction: invalid Sequence',
'Payment: invalid field Sequence',
)
})
@@ -106,7 +106,7 @@ describe('BaseTransaction', function () {
assert.throws(
() => validateBaseTransaction(invalidID),
ValidationError,
'BaseTransaction: invalid AccountTxnID',
'Payment: invalid field AccountTxnID',
)
})
@@ -120,7 +120,7 @@ describe('BaseTransaction', function () {
assert.throws(
() => validateBaseTransaction(invalidLastLedgerSequence),
ValidationError,
'BaseTransaction: invalid LastLedgerSequence',
'Payment: invalid field LastLedgerSequence',
)
})
@@ -134,7 +134,7 @@ describe('BaseTransaction', function () {
assert.throws(
() => validateBaseTransaction(invalidSourceTag),
ValidationError,
'BaseTransaction: invalid SourceTag',
'Payment: invalid field SourceTag',
)
})
@@ -148,7 +148,7 @@ describe('BaseTransaction', function () {
assert.throws(
() => validateBaseTransaction(invalidSigningPubKey),
ValidationError,
'BaseTransaction: invalid SigningPubKey',
'Payment: invalid field SigningPubKey',
)
})
@@ -162,7 +162,7 @@ describe('BaseTransaction', function () {
assert.throws(
() => validateBaseTransaction(invalidTicketSequence),
ValidationError,
'BaseTransaction: invalid TicketSequence',
'Payment: invalid field TicketSequence',
)
})
@@ -176,7 +176,7 @@ describe('BaseTransaction', function () {
assert.throws(
() => validateBaseTransaction(invalidTxnSignature),
ValidationError,
'BaseTransaction: invalid TxnSignature',
'Payment: invalid field TxnSignature',
)
})
@@ -242,7 +242,7 @@ describe('BaseTransaction', function () {
assert.throws(
() => validateBaseTransaction(invalidNetworkID),
ValidationError,
'BaseTransaction: invalid NetworkID',
'Payment: invalid field NetworkID',
)
})
})

View File

@@ -1,19 +1,18 @@
import { assert } from 'chai'
import { getNFTokenID } from '../../src'
import * as NFTokenResponse from '../fixtures/rippled/mintNFTMeta.json'
import * as NFTokenResponse2 from '../fixtures/rippled/mintNFTMeta2.json'
import fixtures from '../fixtures/rippled'
describe('getNFTokenID', function () {
it('decode a valid NFTokenID', function () {
const result = getNFTokenID(NFTokenResponse.meta)
const result = getNFTokenID(fixtures.tx.NFTokenMint.meta)
const expectedNFTokenID =
'00081388DC1AB4937C899037B2FDFC3CB20F6F64E73120BB5F8AA66A00000228'
assert.equal(result, expectedNFTokenID)
})
it('decode a different valid NFTokenID', function () {
const result = getNFTokenID(NFTokenResponse2.meta)
const result = getNFTokenID(fixtures.tx.NFTokenMint2.meta)
const expectedNFTokenID =
'0008125CBE4B401B2F62ED35CC67362165AA813CCA06316FFA766254000003EE'
assert.equal(result, expectedNFTokenID)
@@ -21,8 +20,8 @@ describe('getNFTokenID', function () {
it('fails with nice error when given raw response instead of meta', function () {
assert.throws(() => {
// @ts-expect-error - Validating error for javascript users
const _ = getNFTokenID(NFTokenResponse)
// @ts-expect-error -- on purpose, to check the error
const _ = getNFTokenID(fixtures.tx.NFTokenMint)
}, /^Unable to parse the parameter given to getNFTokenID.*/u)
})
})

View File

@@ -0,0 +1,25 @@
import { assert } from 'chai'
import { getXChainClaimID } from '../../src'
import fixtures from '../fixtures/rippled'
describe('getXChainClaimID', function () {
it('decode a valid XChainClaimID', function () {
const result = getXChainClaimID(fixtures.tx.XChainCreateClaimID.meta)
const expectedXChainClaimID = 'b0'
assert.equal(result, expectedXChainClaimID)
})
it('decode a different valid XChainClaimID', function () {
const result = getXChainClaimID(fixtures.tx.XChainCreateClaimID2.meta)
const expectedXChainClaimID = 'ac'
assert.equal(result, expectedXChainClaimID)
})
it('fails with nice error when given raw response instead of meta', function () {
assert.throws(() => {
// @ts-expect-error -- on purpose, to check the error
const _ = getXChainClaimID(fixtures.tx.XChainCreateClaimID)
}, /^Unable to parse the parameter given to getXChainClaimID.*/u)
})
})

View File

@@ -0,0 +1,147 @@
/* eslint-disable no-continue -- unneeded here */
/**
* This file writes the `validate` function for a transaction, when provided the model name in the `src/models/transactions`
* folder.
*/
const fs = require('fs')
const NORMAL_TYPES = ['number', 'string']
const NUMBERS = ['0', '1']
// TODO: rewrite this to use regex
async function main() {
if (process.argv.length < 3) {
console.log(`Usage: ${process.argv[0]} ${process.argv[1]} TxName`)
process.exit(1)
}
const modelName = process.argv[2]
const filename = `./src/models/transactions/${modelName}.ts`
const [model, txName] = await getModel(filename)
return processModel(model, txName)
}
async function getModel(filename) {
let model = ''
let started = false
let ended = false
const data = await fs.promises.readFile(filename, { encoding: 'utf8' })
const lines = data.split('\n')
for (const line of lines) {
if (ended) {
continue
}
if (!started && !line.startsWith('export')) {
continue
}
if (!started && line.includes('Flags')) {
continue
}
if (!started) {
started = true
}
model += `${line}\n`
if (line === '}') {
ended = true
}
}
const name_line = model.split('\n')[0].split(' ')
const txName = name_line[2]
return [model, txName]
}
function getValidationFunction(paramType) {
if (NORMAL_TYPES.includes(paramType)) {
const paramTypeCapitalized =
paramType.substring(0, 1).toUpperCase() + paramType.substring(1)
return `is${paramTypeCapitalized}(inp)`
}
if (NUMBERS.includes(paramType)) {
return `inp === ${paramType}`
}
return `is${paramType}(inp)`
}
function getValidationLine(validationFns) {
if (validationFns.length === 1) {
if (!validationFns[0].includes('===')) {
// Example: `validateRequiredFields(tx, 'Amount', isAmount)`
const validationFn = validationFns[0]
// strip the `(inp)` in e.g. `isAmount(inp)`
return validationFn.substring(0, validationFn.length - 5)
}
}
// Example:
// `validateRequiredFields(tx, 'XChainAccountCreateCount',
// (inp) => isNumber(inp) || isString(inp)))`
return `(inp) => ${validationFns.join(' || ')}`
}
function processModel(model, txName) {
let output = ''
// process the TS model and get the types of each parameter
for (let line of model.split('\n')) {
if (line === '') {
continue
}
if (line.startsWith('export')) {
continue
}
if (line === '}') {
continue
}
line = line.trim()
if (line.startsWith('TransactionType')) {
continue
}
if (line.startsWith('Flags')) {
continue
}
if (line.startsWith('/**')) {
continue
}
if (line.startsWith('*')) {
continue
}
// process the line with a type
const split = line.split(' ')
const param = split[0].replace('?:', '').replace(':', '').trim()
const paramTypes = split.slice(1)
const optional = split[0].endsWith('?:')
const functionName = optional
? 'validateOptionalField'
: 'validateRequiredField'
// process the types and turn them into a validation function
let idx = 0
const if_outputs = []
while (idx < paramTypes.length) {
const paramType = paramTypes[idx]
if_outputs.push(getValidationFunction(paramType))
idx += 2
}
output += ` ${functionName}(tx, '${param}', ${getValidationLine(
if_outputs,
)})\n\n`
}
output = output.substring(0, output.length - 1)
output += '}\n'
// initial output content
output = `/**
* Verify the form and type of a ${txName} at runtime.
*
* @param tx - A ${txName} Transaction.
* @throws When the ${txName} is malformed.
*/
export function validate${txName}(tx: Record<string, unknown>): void {
validateBaseTransaction(tx)
${output}`
return output
}
main().then(console.log)

View File

@@ -6,6 +6,7 @@
"./test/**/*.json",
"./src/**/*.json",
"./snippets/src/**/*.ts",
".eslintrc.js"
".eslintrc.js",
],
"exclude": ["./tools/*.js"]
}