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

@@ -1,6 +1,9 @@
{
"editor.tabSize": 2,
"cSpell.words": [
"altnet",
"Autofills",
"Clawback",
"hostid",
"keypair",
"keypairs",
@@ -8,8 +11,11 @@
"multisigned",
"multisigning",
"preauthorization",
"rippletest",
"secp256k1",
"Setf"
"Setf",
"Sidechains",
"xchain"
],
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",

View File

@@ -1,6 +1,8 @@
# ripple-binary-codec Release History
## Unreleased
### Added
- Support for the XChainBridge amendment.
## 1.9.0 (2023-08-24)

View File

@@ -22,6 +22,7 @@
"UInt384": 22,
"UInt512": 23,
"Issue": 24,
"XChainBridge": 25,
"Transaction": 10001,
"LedgerEntry": 10002,
"Validation": 10003,
@@ -35,8 +36,11 @@
"Ticket": 84,
"SignerList": 83,
"Offer": 111,
"Bridge": 105,
"LedgerHashes": 104,
"Amendments": 102,
"XChainOwnedClaimID": 113,
"XChainOwnedCreateAccountClaimID": 116,
"FeeSettings": 115,
"Escrow": 117,
"PayChannel": 120,
@@ -233,6 +237,16 @@
"type": "UInt8"
}
],
[
"WasLockingChainSend",
{
"nth": 19,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
"type": "UInt8"
}
],
[
"LedgerEntryType",
{
@@ -983,6 +997,36 @@
"type": "UInt64"
}
],
[
"XChainClaimID",
{
"nth": 20,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
"type": "UInt64"
}
],
[
"XChainAccountCreateCount",
{
"nth": 21,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
"type": "UInt64"
}
],
[
"XChainAccountClaimCount",
{
"nth": 22,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
"type": "UInt64"
}
],
[
"EmailHash",
{
@@ -1583,6 +1627,26 @@
"type": "Amount"
}
],
[
"SignatureReward",
{
"nth": 29,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
"type": "Amount"
}
],
[
"MinAccountCreateAmount",
{
"nth": 30,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
"type": "Amount"
}
],
[
"LPTokenBalance",
{
@@ -1933,6 +1997,66 @@
"type": "AccountID"
}
],
[
"OtherChainSource",
{
"nth": 18,
"isVLEncoded": true,
"isSerialized": true,
"isSigningField": true,
"type": "AccountID"
}
],
[
"OtherChainDestination",
{
"nth": 19,
"isVLEncoded": true,
"isSerialized": true,
"isSigningField": true,
"type": "AccountID"
}
],
[
"AttestationSignerAccount",
{
"nth": 20,
"isVLEncoded": true,
"isSerialized": true,
"isSigningField": true,
"type": "AccountID"
}
],
[
"AttestationRewardAccount",
{
"nth": 21,
"isVLEncoded": true,
"isSerialized": true,
"isSigningField": true,
"type": "AccountID"
}
],
[
"LockingChainDoor",
{
"nth": 22,
"isVLEncoded": true,
"isSerialized": true,
"isSigningField": true,
"type": "AccountID"
}
],
[
"IssuingChainDoor",
{
"nth": 23,
"isVLEncoded": true,
"isSerialized": true,
"isSigningField": true,
"type": "AccountID"
}
],
[
"Indexes",
{
@@ -1983,6 +2107,26 @@
"type": "PathSet"
}
],
[
"LockingChainIssue",
{
"nth": 1,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
"type": "Issue"
}
],
[
"IssuingChainIssue",
{
"nth": 2,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
"type": "Issue"
}
],
[
"Asset",
{
@@ -2003,6 +2147,16 @@
"type": "Issue"
}
],
[
"XChainBridge",
{
"nth": 1,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
"type": "XChainBridge"
}
],
[
"TransactionMetaData",
{
@@ -2243,6 +2397,46 @@
"type": "STObject"
}
],
[
"XChainClaimProofSig",
{
"nth": 28,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
"type": "STObject"
}
],
[
"XChainCreateAccountProofSig",
{
"nth": 29,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
"type": "STObject"
}
],
[
"XChainClaimAttestationCollectionElement",
{
"nth": 30,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
"type": "STObject"
}
],
[
"XChainCreateAccountAttestationCollectionElement",
{
"nth": 31,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
"type": "STObject"
}
],
[
"Signers",
{
@@ -2393,6 +2587,26 @@
"type": "STArray"
}
],
[
"XChainClaimAttestations",
{
"nth": 21,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
"type": "STArray"
}
],
[
"XChainCreateAccountAttestations",
{
"nth": 22,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
"type": "STArray"
}
],
[
"AuthAccounts",
{
@@ -2461,6 +2675,12 @@
"temSEQ_AND_TICKET": -263,
"temBAD_NFTOKEN_TRANSFER_FEE": -262,
"temBAD_AMM_TOKENS": -261,
"temXCHAIN_EQUAL_DOOR_ACCOUNTS": -260,
"temXCHAIN_BAD_PROOF": -259,
"temXCHAIN_BRIDGE_BAD_ISSUES": -258,
"temXCHAIN_BRIDGE_NONDOOR_OWNER": -257,
"temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT": -256,
"temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT": -255,
"tefFAILURE": -199,
"tefALREADY": -198,
@@ -2497,6 +2717,7 @@
"terQUEUED": -89,
"terPRE_TICKET": -88,
"terNO_AMM": -87,
"terSUBMITTED": -86,
"tesSUCCESS": 0,
@@ -2538,6 +2759,7 @@
"tecKILLED": 150,
"tecHAS_OBLIGATIONS": 151,
"tecTOO_SOON": 152,
"tecHOOK_ERROR": 153,
"tecMAX_SEQUENCE_REACHED": 154,
"tecNO_SUITABLE_NFTOKEN_PAGE": 155,
"tecNFTOKEN_BUY_SELL_MISMATCH": 156,
@@ -2553,7 +2775,24 @@
"tecAMM_EMPTY": 166,
"tecAMM_NOT_EMPTY": 167,
"tecAMM_ACCOUNT": 168,
"tecINCOMPLETE": 169
"tecINCOMPLETE": 169,
"tecXCHAIN_BAD_TRANSFER_ISSUE": 170,
"tecXCHAIN_NO_CLAIM_ID": 171,
"tecXCHAIN_BAD_CLAIM_ID": 172,
"tecXCHAIN_CLAIM_NO_QUORUM": 173,
"tecXCHAIN_PROOF_UNKNOWN_KEY": 174,
"tecXCHAIN_CREATE_ACCOUNT_NONXRP_ISSUE": 175,
"tecXCHAIN_WRONG_CHAIN": 176,
"tecXCHAIN_REWARD_MISMATCH": 177,
"tecXCHAIN_NO_SIGNERS_LIST": 178,
"tecXCHAIN_SENDING_ACCOUNT_MISMATCH": 179,
"tecXCHAIN_INSUFF_CREATE_AMOUNT": 180,
"tecXCHAIN_ACCOUNT_CREATE_PAST": 181,
"tecXCHAIN_ACCOUNT_CREATE_TOO_MANY": 182,
"tecXCHAIN_PAYMENT_FAILED": 183,
"tecXCHAIN_SELF_COMMIT": 184,
"tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR": 185,
"tecXCHAIN_CREATE_ACCOUNT_DISABLED": 186
},
"TRANSACTION_TYPES": {
"Invalid": -1,
@@ -2592,6 +2831,14 @@
"AMMVote": 38,
"AMMBid": 39,
"AMMDelete": 40,
"XChainCreateClaimID": 41,
"XChainCommit": 42,
"XChainClaim": 43,
"XChainAccountCreateCommit": 44,
"XChainAddClaimAttestation": 45,
"XChainAddAccountCreateAttestation": 46,
"XChainModifyBridge": 47,
"XChainCreateBridge": 48,
"EnableAmendment": 100,
"SetFee": 101,
"UNLModify": 102

View File

@@ -14,6 +14,7 @@ import { UInt32 } from './uint-32'
import { UInt64 } from './uint-64'
import { UInt8 } from './uint-8'
import { Vector256 } from './vector-256'
import { XChainBridge } from './xchain-bridge'
import { type SerializedType } from './serialized-type'
import { DEFAULT_DEFINITIONS } from '../enums'
@@ -34,6 +35,7 @@ const coreTypes: Record<string, typeof SerializedType> = {
UInt32,
UInt64,
Vector256,
XChainBridge,
}
// Ensures that the DEFAULT_DEFINITIONS object connects these types to fields for serializing/deserializing

View File

@@ -0,0 +1,128 @@
import { BinaryParser } from '../serdes/binary-parser'
import { AccountID } from './account-id'
import { JsonObject, SerializedType } from './serialized-type'
import { Buffer } from 'buffer/'
import { Issue, IssueObject } from './issue'
/**
* Interface for JSON objects that represent cross-chain bridges
*/
interface XChainBridgeObject extends JsonObject {
LockingChainDoor: string
LockingChainIssue: IssueObject | string
IssuingChainDoor: string
IssuingChainIssue: IssueObject | string
}
/**
* Type guard for XChainBridgeObject
*/
function isXChainBridgeObject(arg): arg is XChainBridgeObject {
const keys = Object.keys(arg).sort()
return (
keys.length === 4 &&
keys[0] === 'IssuingChainDoor' &&
keys[1] === 'IssuingChainIssue' &&
keys[2] === 'LockingChainDoor' &&
keys[3] === 'LockingChainIssue'
)
}
/**
* Class for serializing/deserializing XChainBridges
*/
class XChainBridge extends SerializedType {
static readonly ZERO_XCHAIN_BRIDGE: XChainBridge = new XChainBridge(
Buffer.concat([
Buffer.from([0x14]),
Buffer.alloc(40),
Buffer.from([0x14]),
Buffer.alloc(40),
]),
)
static readonly TYPE_ORDER: { name: string; type: typeof SerializedType }[] =
[
{ name: 'LockingChainDoor', type: AccountID },
{ name: 'LockingChainIssue', type: Issue },
{ name: 'IssuingChainDoor', type: AccountID },
{ name: 'IssuingChainIssue', type: Issue },
]
constructor(bytes: Buffer) {
super(bytes ?? XChainBridge.ZERO_XCHAIN_BRIDGE.bytes)
}
/**
* Construct a cross-chain bridge from a JSON
*
* @param value XChainBridge or JSON to parse into an XChainBridge
* @returns An XChainBridge object
*/
static from<T extends XChainBridge | XChainBridgeObject>(
value: T,
): XChainBridge {
if (value instanceof XChainBridge) {
return value
}
if (isXChainBridgeObject(value)) {
const bytes: Array<Buffer> = []
this.TYPE_ORDER.forEach((item) => {
const { name, type } = item
if (type === AccountID) {
bytes.push(Buffer.from([0x14]))
}
const object = type.from(value[name])
bytes.push(object.toBytes())
})
return new XChainBridge(Buffer.concat(bytes))
}
throw new Error('Invalid type to construct an XChainBridge')
}
/**
* Read an XChainBridge from a BinaryParser
*
* @param parser BinaryParser to read the XChainBridge from
* @returns An XChainBridge object
*/
static fromParser(parser: BinaryParser): XChainBridge {
const bytes: Array<Buffer> = []
this.TYPE_ORDER.forEach((item) => {
const { type } = item
if (type === AccountID) {
parser.skip(1)
bytes.push(Buffer.from([0x14]))
}
const object = type.fromParser(parser)
bytes.push(object.toBytes())
})
return new XChainBridge(Buffer.concat(bytes))
}
/**
* Get the JSON representation of this XChainBridge
*
* @returns the JSON interpretation of this.bytes
*/
toJSON(): XChainBridgeObject {
const parser = new BinaryParser(this.toString())
const json = {}
XChainBridge.TYPE_ORDER.forEach((item) => {
const { name, type } = item
if (type === AccountID) {
parser.skip(1)
}
const object = type.fromParser(parser).toJSON()
json[name] = object
})
return json as XChainBridgeObject
}
}
export { XChainBridge, XChainBridgeObject }

View File

@@ -4435,7 +4435,8 @@
}
}
],
"transactions": [{
"transactions": [
{
"binary": "1200002200000000240000003E6140000002540BE40068400000000000000A7321034AADB09CFF4A4804073701EC53C3510CDC95917C2BB0150FB742D0C66E6CEE9E74473045022022EB32AECEF7C644C891C19F87966DF9C62B1F34BABA6BE774325E4BB8E2DD62022100A51437898C28C2B297112DF8131F2BB39EA5FE613487DDD611525F17962646398114550FC62003E785DC231A1058A05E56E3F09CF4E68314D4CC8AB5B21D86A82C3E9E8D0ECF2404B77FECBA",
"json": {
"Account": "r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV",
@@ -4449,6 +4450,191 @@
"Sequence": 62
}
},
{
"binary": "1200302200000000240000000168400000000000000A601D40000000000003E8601E400000000000271073210330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD020744630440220101BCA4B5B5A37C6F44480F9A34752C9AA8B2CDF5AD47E3CB424DEDC21C06DB702206EEB257E82A89B1F46A0A2C7F070B0BD181D980FF86FE4269E369F6FC7A270918114B5F762798A53D543A014CAF8B297CFF8F2F937E8011914AF80285F637EE4AF3C20378F9DFB12511ACB8D27000000000000000000000000000000000000000014550FC62003E785DC231A1058A05E56E3F09CF4E60000000000000000000000000000000000000000",
"json": {
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
"XChainBridge": {
"LockingChainDoor": "rGzx83BVoqTYbGn7tiVAnFw7cbxjin13jL",
"LockingChainIssue": {"currency": "XRP"},
"IssuingChainDoor": "r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV",
"IssuingChainIssue": {"currency": "XRP"}
},
"Fee": "10",
"Flags": 0,
"MinAccountCreateAmount": "10000",
"Sequence": 1,
"SignatureReward": "1000",
"SigningPubKey": "0330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD020",
"TransactionType": "XChainCreateBridge",
"TxnSignature": "30440220101BCA4B5B5A37C6F44480F9A34752C9AA8B2CDF5AD47E3CB424DEDC21C06DB702206EEB257E82A89B1F46A0A2C7F070B0BD181D980FF86FE4269E369F6FC7A27091"
}
},
{
"binary": "12002F2200000000240000000168400000000000000A601D40000000000003E8601E400000000000271073210330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD02074473045022100D2CABC1B0E0635A8EE2E6554F6D474C49BC292C995C5C9F83179F4A60634B04C02205D1DB569D9593136F2FBEA7140010C8F46794D653AFDBEA8D30B8750BA4805E58114B5F762798A53D543A014CAF8B297CFF8F2F937E8011914AF80285F637EE4AF3C20378F9DFB12511ACB8D27000000000000000000000000000000000000000014550FC62003E785DC231A1058A05E56E3F09CF4E60000000000000000000000000000000000000000",
"json": {
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
"XChainBridge": {
"LockingChainDoor": "rGzx83BVoqTYbGn7tiVAnFw7cbxjin13jL",
"LockingChainIssue": {"currency": "XRP"},
"IssuingChainDoor": "r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV",
"IssuingChainIssue": {"currency": "XRP"}
},
"Fee": "10",
"Flags": 0,
"MinAccountCreateAmount": "10000",
"Sequence": 1,
"SignatureReward": "1000",
"SigningPubKey": "0330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD020",
"TransactionType": "XChainModifyBridge",
"TxnSignature": "3045022100D2CABC1B0E0635A8EE2E6554F6D474C49BC292C995C5C9F83179F4A60634B04C02205D1DB569D9593136F2FBEA7140010C8F46794D653AFDBEA8D30B8750BA4805E5"
}
},
{
"binary": "1200292280000000240000000168400000000000000A601D400000000000271073210330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD020744630440220247B20A1B9C48E21A374CB9B3E1FE2A7C528151868DF8D307E9FBE15237E531A02207C20C092DDCC525E583EF4AB7CB91E862A6DED19426997D3F0A2C84E2BE8C5DD8114B5F762798A53D543A014CAF8B297CFF8F2F937E8801214AF80285F637EE4AF3C20378F9DFB12511ACB8D27011914AF80285F637EE4AF3C20378F9DFB12511ACB8D27000000000000000000000000000000000000000014550FC62003E785DC231A1058A05E56E3F09CF4E60000000000000000000000000000000000000000",
"json": {
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
"XChainBridge": {
"LockingChainDoor": "rGzx83BVoqTYbGn7tiVAnFw7cbxjin13jL",
"LockingChainIssue": {"currency": "XRP"},
"IssuingChainDoor": "r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV",
"IssuingChainIssue": {"currency": "XRP"}
},
"Fee": "10",
"Flags": 2147483648,
"OtherChainSource": "rGzx83BVoqTYbGn7tiVAnFw7cbxjin13jL",
"Sequence": 1,
"SignatureReward": "10000",
"SigningPubKey": "0330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD020",
"TransactionType": "XChainCreateClaimID",
"TxnSignature": "30440220247B20A1B9C48E21A374CB9B3E1FE2A7C528151868DF8D307E9FBE15237E531A02207C20C092DDCC525E583EF4AB7CB91E862A6DED19426997D3F0A2C84E2BE8C5DD"
}
},
{
"binary": "12002A228000000024000000013014000000000000000161400000000000271068400000000000000A73210330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD02074453043021F177323F0D93612C82A4393A99B23905A7E675753FD80C52997AFAB13F5F9D002203BFFAF457E90BDA65AABE8F8762BD96162FAD98A0C030CCD69B06EE9B12BBFFE8114B5F762798A53D543A014CAF8B297CFF8F2F937E8011914AF80285F637EE4AF3C20378F9DFB12511ACB8D27000000000000000000000000000000000000000014550FC62003E785DC231A1058A05E56E3F09CF4E60000000000000000000000000000000000000000",
"json": {
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
"Amount": "10000",
"XChainBridge": {
"LockingChainDoor": "rGzx83BVoqTYbGn7tiVAnFw7cbxjin13jL",
"LockingChainIssue": {"currency": "XRP"},
"IssuingChainDoor": "r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV",
"IssuingChainIssue": {"currency": "XRP"}
},
"Fee": "10",
"Flags": 2147483648,
"Sequence": 1,
"SigningPubKey": "0330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD020",
"TransactionType": "XChainCommit",
"TxnSignature": "3043021F177323F0D93612C82A4393A99B23905A7E675753FD80C52997AFAB13F5F9D002203BFFAF457E90BDA65AABE8F8762BD96162FAD98A0C030CCD69B06EE9B12BBFFE",
"XChainClaimID": "0000000000000001"
}
},
{
"binary": "12002B228000000024000000013014000000000000000161400000000000271068400000000000000A73210330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD020744630440220445F7469FDA401787D9EE8A9B6E24DFF81E94F4C09FD311D2C0A58FCC02C684A022029E2EF34A5EA35F50D5BB57AC6320AD3AE12C13C8D1379B255A486D72CED142E8114B5F762798A53D543A014CAF8B297CFF8F2F937E88314550FC62003E785DC231A1058A05E56E3F09CF4E6011914AF80285F637EE4AF3C20378F9DFB12511ACB8D27000000000000000000000000000000000000000014550FC62003E785DC231A1058A05E56E3F09CF4E60000000000000000000000000000000000000000",
"json": {
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
"Amount": "10000",
"XChainBridge": {
"LockingChainDoor": "rGzx83BVoqTYbGn7tiVAnFw7cbxjin13jL",
"LockingChainIssue": {"currency": "XRP"},
"IssuingChainDoor": "r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV",
"IssuingChainIssue": {"currency": "XRP"}
},
"Destination": "r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV",
"Fee": "10",
"Flags": 2147483648,
"Sequence": 1,
"SigningPubKey": "0330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD020",
"TransactionType": "XChainClaim",
"TxnSignature": "30440220445F7469FDA401787D9EE8A9B6E24DFF81E94F4C09FD311D2C0A58FCC02C684A022029E2EF34A5EA35F50D5BB57AC6320AD3AE12C13C8D1379B255A486D72CED142E",
"XChainClaimID": "0000000000000001"
}
},
{
"binary": "12002C228000000024000000016140000000000F424068400000000000000A601D400000000000271073210330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD0207446304402202984DDE7F0B566F081F7953D7212BF031ACBF8860FE114102E9512C4C8768C77022070113F4630B1DC3045E4A98DDD648CEBC31B12774F7B44A1B8123CD2C9F5CF188114B5F762798A53D543A014CAF8B297CFF8F2F937E88314AF80285F637EE4AF3C20378F9DFB12511ACB8D27011914AF80285F637EE4AF3C20378F9DFB12511ACB8D27000000000000000000000000000000000000000014550FC62003E785DC231A1058A05E56E3F09CF4E60000000000000000000000000000000000000000",
"json": {
"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",
"SigningPubKey": "0330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD020",
"TransactionType": "XChainAccountCreateCommit",
"TxnSignature": "304402202984DDE7F0B566F081F7953D7212BF031ACBF8860FE114102E9512C4C8768C77022070113F4630B1DC3045E4A98DDD648CEBC31B12774F7B44A1B8123CD2C9F5CF18"
}
},
{
"binary": "12002E2400000005201B0000000D30150000000000000006614000000000989680684000000000000014601D40000000000000647121ED1F4A024ACFEBDB6C7AA88DEDE3364E060487EA31B14CC9E0D610D152B31AADC27321EDF54108BA2E0A0D3DC2AE3897F8BE0EFE776AE8D0F9FB0D0B9D64233084A8DDD1744003E74AEF1F585F156786429D2FC87A89E5C6B5A56D68BFC9A6A329F3AC67CBF2B6958283C663A4522278CA162C69B23CF75149AF022B410EA0508C16F42058007640EEFCFA3DC2AB4AB7C4D2EBBC168CB621A11B82BABD86534DFC8EFA72439A49662D744073CD848E7A587A95B35162CDF9A69BB237E72C9537A987F5B8C394F30D81145E7A3E3D7200A794FA801C66CE3775B6416EE4128314C15F113E49BCC4B9FFF43CD0366C23ACD82F75638012143FD9ED9A79DEA67CB5D585111FEF0A29203FA0408014145E7A3E3D7200A794FA801C66CE3775B6416EE4128015145E7A3E3D7200A794FA801C66CE3775B6416EE4120010130101191486F0B1126CE1205E59FDFDD2661A9FB7505CA70F000000000000000000000000000000000000000014B5F762798A53D543A014CAF8B297CFF8F2F937E80000000000000000000000000000000000000000",
"json": {
"Account": "r9cYxdjQsoXAEz3qQJc961SNLaXRkWXCvT",
"Amount": "10000000",
"AttestationRewardAccount": "r9cYxdjQsoXAEz3qQJc961SNLaXRkWXCvT",
"AttestationSignerAccount": "r9cYxdjQsoXAEz3qQJc961SNLaXRkWXCvT",
"Destination": "rJdTJRJZ6GXCCRaamHJgEqVzB7Zy4557Pi",
"Fee": "20",
"LastLedgerSequence": 13,
"OtherChainSource": "raFcdz1g8LWJDJWJE2ZKLRGdmUmsTyxaym",
"PublicKey": "ED1F4A024ACFEBDB6C7AA88DEDE3364E060487EA31B14CC9E0D610D152B31AADC2",
"Sequence": 5,
"Signature": "EEFCFA3DC2AB4AB7C4D2EBBC168CB621A11B82BABD86534DFC8EFA72439A49662D744073CD848E7A587A95B35162CDF9A69BB237E72C9537A987F5B8C394F30D",
"SignatureReward": "100",
"SigningPubKey": "EDF54108BA2E0A0D3DC2AE3897F8BE0EFE776AE8D0F9FB0D0B9D64233084A8DDD1",
"TransactionType": "XChainAddAccountCreateAttestation",
"TxnSignature": "03E74AEF1F585F156786429D2FC87A89E5C6B5A56D68BFC9A6A329F3AC67CBF2B6958283C663A4522278CA162C69B23CF75149AF022B410EA0508C16F4205800",
"WasLockingChainSend": 1,
"XChainAccountCreateCount": "0000000000000006",
"XChainBridge": {
"IssuingChainDoor": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
"IssuingChainIssue": {
"currency": "XRP"
},
"LockingChainDoor": "rDJVtEuDKr4rj1B3qtW7R5TVWdXV2DY7Qg",
"LockingChainIssue": {
"currency": "XRP"
}
}
}
},
{
"binary": "12002D2400000009201B00000013301400000000000000016140000000009896806840000000000000147121ED7541DEC700470F54276C90C333A13CDBB5D341FD43C60CEA12170F6D6D4E11367321ED0406B134786FE0751717226657F7BF8AFE96442C05D28ACEC66FB64852BA604C7440D0423649E48A44F181262CF5FC08A68E7FA5CD9E55843E4F09014B76E602574741E8553383A4B43CABD194BB96713647FC0B885BE248E4FFA068FA3E6994CF0476407C175050B08000AD35EEB2D87E16CD3F95A0AEEBF2A049474275153D9D4DD44528FE99AA50E71660A15B0B768E1B90E609BBD5DC7AFAFD45D9705D72D40EA10C81141F30A4D728AB98B0950EC3B9815E6C8D43A7D5598314C15F113E49BCC4B9FFF43CD0366C23ACD82F75638012143FD9ED9A79DEA67CB5D585111FEF0A29203FA0408014141F30A4D728AB98B0950EC3B9815E6C8D43A7D5598015141F30A4D728AB98B0950EC3B9815E6C8D43A7D5590010130101191486F0B1126CE1205E59FDFDD2661A9FB7505CA70F000000000000000000000000000000000000000014B5F762798A53D543A014CAF8B297CFF8F2F937E80000000000000000000000000000000000000000",
"json": {
"Account": "rsqvD8WFFEBBv4nztpoW9YYXJ7eRzLrtc3",
"Amount": "10000000",
"AttestationRewardAccount": "rsqvD8WFFEBBv4nztpoW9YYXJ7eRzLrtc3",
"AttestationSignerAccount": "rsqvD8WFFEBBv4nztpoW9YYXJ7eRzLrtc3",
"Destination": "rJdTJRJZ6GXCCRaamHJgEqVzB7Zy4557Pi",
"Fee": "20",
"LastLedgerSequence": 19,
"OtherChainSource": "raFcdz1g8LWJDJWJE2ZKLRGdmUmsTyxaym",
"PublicKey": "ED7541DEC700470F54276C90C333A13CDBB5D341FD43C60CEA12170F6D6D4E1136",
"Sequence": 9,
"Signature": "7C175050B08000AD35EEB2D87E16CD3F95A0AEEBF2A049474275153D9D4DD44528FE99AA50E71660A15B0B768E1B90E609BBD5DC7AFAFD45D9705D72D40EA10C",
"SigningPubKey": "ED0406B134786FE0751717226657F7BF8AFE96442C05D28ACEC66FB64852BA604C",
"TransactionType": "XChainAddClaimAttestation",
"TxnSignature": "D0423649E48A44F181262CF5FC08A68E7FA5CD9E55843E4F09014B76E602574741E8553383A4B43CABD194BB96713647FC0B885BE248E4FFA068FA3E6994CF04",
"WasLockingChainSend": 1,
"XChainBridge": {
"IssuingChainDoor": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
"IssuingChainIssue": {
"currency": "XRP"
},
"LockingChainDoor": "rDJVtEuDKr4rj1B3qtW7R5TVWdXV2DY7Qg",
"LockingChainIssue": {
"currency": "XRP"
}
},
"XChainClaimID": "0000000000000001"
}
},
{
"binary": "12002315000A2200000000240015DAE161400000000000271068400000000000000A6BD5838D7EA4C680000000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C7321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B87440B3154D968314FCEB58001E1B0C3A4CFB33DF9FF6C73207E5EAEB9BD07E2747672168E1A2786D950495C38BD8DEE3391BF45F3008DD36F4B12E7C07D82CA5250E8114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A",
"json": {
@@ -4655,7 +4841,8 @@
"SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8",
"TxnSignature": "BC2F6E76969E3747E9BDE183C97573B086212F09D5387460E6EE2F32953E85EAEB9618FBBEF077276E30E59D619FCF7C7BDCDDDD9EB94D7CE1DD5CE9246B2107"
}
}],
}
],
"ledgerData": [{
"binary": "01E91435016340767BF1C4A3EACEB081770D8ADE216C85445DD6FB002C6B5A2930F2DECE006DA18150CB18F6DD33F6F0990754C962A7CCE62F332FF9C13939B03B864117F0BDA86B6E9B4F873B5C3E520634D343EF5D9D9A4246643D64DAD278BA95DC0EAC6EB5350CF970D521276CDE21276CE60A00",
"json": {

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"]
}

View File

@@ -0,0 +1,187 @@
const fs = require("fs");
const fixtures = require("../packages/ripple-binary-codec/test/fixtures/codec-fixtures.json");
const NORMAL_TYPES = ["number", "string"];
const NUMBERS = ["0", "1"];
function getTx(txName) {
transactions = fixtures.transactions;
const validTxs = fixtures.transactions
.filter((tx) => tx.json.TransactionType === txName)
.map((tx) => tx.json);
const validTx = validTxs[0];
delete validTx.TxnSignature;
delete validTx.SigningPubKey;
return JSON.stringify(validTx, null, 2);
}
function main() {
const modelName = process.argv[2];
const filename = `./packages/xrpl/src/models/transactions/${modelName}.ts`;
const [model, txName] = getModel(filename);
return processModel(model, txName);
}
// Extract just the model from the file
function getModel(filename) {
let model = "";
let started = false;
let ended = false;
const data = fs.readFileSync(filename, "utf8");
const lines = data.split("\n");
for (let 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 getInvalidValue(paramTypes) {
if (paramTypes.length === 1) {
const paramType = paramTypes[0];
if (paramType == "number") {
return "'number'";
} else if (paramType == "string") {
return 123;
} else if (paramType == "IssuedCurrency") {
return JSON.stringify({ test: "test" });
} else if (paramType == "Amount") {
return JSON.stringify({ currency: "ETH" });
} else if (paramType == "XChainBridge") {
return JSON.stringify({ XChainDoor: "test" });
} else {
throw Error(`${paramType} not supported yet`);
}
}
const simplifiedParamTypes = paramTypes.filter(
(_paramType, index) => index % 2 == 0
);
if (JSON.stringify(simplifiedParamTypes) === '["0","1"]') {
return 2;
} else if (JSON.stringify(simplifiedParamTypes) === '["number","string"]') {
return JSON.stringify({ currency: "ETH" });
} else {
throw Error(`${simplifiedParamTypes} not supported yet`);
}
}
// Process the model and build the tests
function processModel(model, txName) {
let output = "";
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")) {
// TODO: support flag checking
continue;
}
if (line.startsWith("/**")) {
continue;
}
if (line.startsWith("*")) {
continue;
}
const split = line.split(" ");
const param = split[0].replace("?:", "").replace(":", "").trim();
const paramTypes = split.slice(1);
const optional = split[0].endsWith("?:");
if (!optional) {
output += ` it("throws w/ missing ${param}", function () {
delete tx.${param}
assert.throws(
() => validate${txName}(tx),
ValidationError,
'${txName}: missing field ${param}',
)
assert.throws(
() => validate(tx),
ValidationError,
'${txName}: missing field ${param}',
)
})
`;
}
const fakeValue = getInvalidValue(paramTypes);
output += ` it('throws w/ invalid ${param}', function () {
tx.${param} = ${fakeValue}
assert.throws(
() => validate${txName}(tx),
ValidationError,
'${txName}: invalid field ${param}',
)
assert.throws(
() => validate(tx),
ValidationError,
'${txName}: invalid field ${param}',
)
})
`;
}
output = output.substring(0, output.length - 2);
output += "\n})\n";
output =
`import { assert } from 'chai'
import { validate, ValidationError } from '../../src'
import { validate${txName} } from '../../src/models/transactions/${txName}'
/**
* ${txName} Transaction Verification Testing.
*
* Providing runtime verification testing for each specific transaction type.
*/
describe('${txName}', function () {
let tx
beforeEach(function () {
tx = ${getTx(txName)} as any
})
it('verifies valid ${txName}', function () {
assert.doesNotThrow(() => validate${txName}(tx))
assert.doesNotThrow(() => validate(tx))
})
` + output;
return output;
}
console.log(main());