diff --git a/.vscode/settings.json b/.vscode/settings.json index ef2c734c23..6e4555a206 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,6 +3,8 @@ "cSpell.words": [ "altnet", "Autofills", + "Batchnet", + "bignumber", "Clawback", "hostid", "keypair", @@ -15,7 +17,8 @@ "secp256k1", "Setf", "Sidechains", - "xchain" + "xchain", + "xrplf" ], "[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode", diff --git a/packages/ripple-binary-codec/HISTORY.md b/packages/ripple-binary-codec/HISTORY.md index 85eb20b4e6..fd85599457 100644 --- a/packages/ripple-binary-codec/HISTORY.md +++ b/packages/ripple-binary-codec/HISTORY.md @@ -2,10 +2,13 @@ ## Unreleased +### Added +* Support for the `Batch` amendment (XLS-56). + ## 2.2.0 (2024-12-23) ### Added -* Support for the Multi-Purpose Token amendment (XLS-33) +* Support for the Multi-Purpose Token amendment (XLS-33). ## 2.1.0 (2024-06-03) diff --git a/packages/ripple-binary-codec/src/binary.ts b/packages/ripple-binary-codec/src/binary.ts index 8ca067d966..29dc1589e9 100644 --- a/packages/ripple-binary-codec/src/binary.ts +++ b/packages/ripple-binary-codec/src/binary.ts @@ -177,11 +177,50 @@ function multiSigningData( }) } +/** + * Interface describing fields required for a Batch signer + */ +interface BatchObject extends JsonObject { + flags: number + txIDs: string[] +} + +/** + * Serialize a signingClaim + * + * @param batch A Batch object to serialize + * @param opts.definitions Custom rippled types to use instead of the default. Used for sidechains and amendments. + * @returns the serialized object with appropriate prefix + */ +function signingBatchData(batch: BatchObject): Uint8Array { + if (batch.flags == null) { + throw Error("No field `flags'") + } + if (batch.txIDs == null) { + throw Error('No field `txIDs`') + } + const prefix = HashPrefix.batch + const flags = coreTypes.UInt32.from(batch.flags).toBytes() + const txIDsLength = coreTypes.UInt32.from(batch.txIDs.length).toBytes() + + const bytesList = new BytesList() + + bytesList.put(prefix) + bytesList.put(flags) + bytesList.put(txIDsLength) + batch.txIDs.forEach((txID: string) => { + bytesList.put(coreTypes.Hash256.from(txID).toBytes()) + }) + + return bytesList.toBytes() +} + export { BinaryParser, BinarySerializer, BytesList, ClaimObject, + BatchObject, makeParser, serializeObject, readJSON, @@ -191,4 +230,5 @@ export { binaryToJSON, sha512Half, transactionID, + signingBatchData, } diff --git a/packages/ripple-binary-codec/src/enums/definitions.json b/packages/ripple-binary-codec/src/enums/definitions.json index 1e8d094430..92eb5158f6 100644 --- a/packages/ripple-binary-codec/src/enums/definitions.json +++ b/packages/ripple-binary-codec/src/enums/definitions.json @@ -1,912 +1,1124 @@ { + "TYPES": { + "Done": -1, + "Unknown": -2, + "NotPresent": 0, + "UInt16": 1, + "UInt32": 2, + "UInt64": 3, + "Hash128": 4, + "Hash256": 5, + "Amount": 6, + "Blob": 7, + "AccountID": 8, + "Number": 9, + "STObject": 14, + "STArray": 15, + "UInt8": 16, + "Hash160": 17, + "PathSet": 18, + "Vector256": 19, + "UInt96": 20, + "Hash192": 21, + "UInt384": 22, + "UInt512": 23, + "Issue": 24, + "XChainBridge": 25, + "Currency": 26, + "Transaction": 10001, + "LedgerEntry": 10002, + "Validation": 10003, + "Metadata": 10004 + }, + "LEDGER_ENTRY_TYPES": { + "Any": -3, + "Child": -2, + "Invalid": -1, + "NFTokenOffer": 55, + "Check": 67, + "DID": 73, + "NegativeUNL": 78, + "NFTokenPage": 80, + "SignerList": 83, + "Ticket": 84, + "AccountRoot": 97, + "DirectoryNode": 100, + "Amendments": 102, + "LedgerHashes": 104, + "Bridge": 105, + "Offer": 111, + "DepositPreauth": 112, + "XChainOwnedClaimID": 113, + "RippleState": 114, + "FeeSettings": 115, + "XChainOwnedCreateAccountClaimID": 116, + "Escrow": 117, + "PayChannel": 120, + "AMM": 121, + "Oracle": 128, + "MPTokenIssuance": 126, + "MPToken": 127, + "Credential": 129, + "PermissionedDomain": 130 + }, "FIELDS": [ [ "Generic", { + "nth": 0, + "isVLEncoded": false, "isSerialized": false, "isSigningField": false, - "isVLEncoded": false, - "nth": 0, "type": "Unknown" } ], [ "Invalid", { + "nth": -1, + "isVLEncoded": false, "isSerialized": false, "isSigningField": false, - "isVLEncoded": false, - "nth": -1, "type": "Unknown" } ], [ "ObjectEndMarker", { + "nth": 1, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 1, "type": "STObject" } ], [ "ArrayEndMarker", { + "nth": 1, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 1, "type": "STArray" } ], [ - "taker_gets_funded", + "hash", { + "nth": 257, + "isVLEncoded": false, "isSerialized": false, "isSigningField": false, + "type": "Hash256" + } + ], + [ + "index", + { + "nth": 258, "isVLEncoded": false, + "isSerialized": false, + "isSigningField": false, + "type": "Hash256" + } + ], + [ + "taker_gets_funded", + { "nth": 258, + "isVLEncoded": false, + "isSerialized": false, + "isSigningField": false, "type": "Amount" } ], [ "taker_pays_funded", { + "nth": 259, + "isVLEncoded": false, "isSerialized": false, "isSigningField": false, - "isVLEncoded": false, - "nth": 259, "type": "Amount" } ], [ - "LedgerEntryType", + "LedgerEntry", { - "isSerialized": true, - "isSigningField": true, + "nth": 257, "isVLEncoded": false, - "nth": 1, - "type": "UInt16" + "isSerialized": false, + "isSigningField": false, + "type": "LedgerEntry" } ], [ - "TransactionType", + "Transaction", { - "isSerialized": true, - "isSigningField": true, + "nth": 257, "isVLEncoded": false, - "nth": 2, - "type": "UInt16" + "isSerialized": false, + "isSigningField": false, + "type": "Transaction" } ], [ - "SignerWeight", + "Validation", { - "isSerialized": true, - "isSigningField": true, + "nth": 257, "isVLEncoded": false, - "nth": 3, - "type": "UInt16" + "isSerialized": false, + "isSigningField": false, + "type": "Validation" } ], [ - "TransferFee", + "Metadata", { - "isSerialized": true, - "isSigningField": true, + "nth": 257, "isVLEncoded": false, - "nth": 4, - "type": "UInt16" + "isSerialized": false, + "isSigningField": false, + "type": "Metadata" } ], [ - "TradingFee", + "CloseResolution", { + "nth": 1, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 5, - "type": "UInt16" + "type": "UInt8" } ], [ - "DiscountedFee", + "Method", { + "nth": 2, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 6, - "type": "UInt16" + "type": "UInt8" } ], [ - "Version", + "TransactionResult", { + "nth": 3, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 16, - "type": "UInt16" + "type": "UInt8" } ], [ - "HookStateChangeCount", + "Scale", { + "nth": 4, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 17, - "type": "UInt16" + "type": "UInt8" } ], [ - "HookEmitCount", + "AssetScale", { + "nth": 5, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 18, - "type": "UInt16" + "type": "UInt8" } ], [ - "HookExecutionIndex", + "TickSize", { + "nth": 16, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 19, - "type": "UInt16" + "type": "UInt8" } ], [ - "HookApiVersion", + "UNLModifyDisabling", { + "nth": 17, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 20, - "type": "UInt16" + "type": "UInt8" } ], [ - "LedgerFixType", + "HookResult", { + "nth": 18, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 21, - "type": "UInt16" + "type": "UInt8" } ], [ - "NetworkID", + "WasLockingChainSend", { + "nth": 19, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 1, - "type": "UInt32" + "type": "UInt8" } ], [ - "Flags", + "LedgerEntryType", { + "nth": 1, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 2, - "type": "UInt32" + "type": "UInt16" } ], [ - "SourceTag", + "TransactionType", { + "nth": 2, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 3, - "type": "UInt32" + "type": "UInt16" } ], [ - "Sequence", + "SignerWeight", { + "nth": 3, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 4, - "type": "UInt32" + "type": "UInt16" } ], [ - "PreviousTxnLgrSeq", + "TransferFee", { + "nth": 4, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 5, - "type": "UInt32" + "type": "UInt16" } ], [ - "LedgerSequence", + "TradingFee", { + "nth": 5, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 6, - "type": "UInt32" + "type": "UInt16" } ], [ - "CloseTime", + "DiscountedFee", { + "nth": 6, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 7, - "type": "UInt32" + "type": "UInt16" } ], [ - "ParentCloseTime", + "Version", { + "nth": 16, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 8, - "type": "UInt32" + "type": "UInt16" } ], [ - "SigningTime", + "HookStateChangeCount", { + "nth": 17, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 9, - "type": "UInt32" + "type": "UInt16" } ], [ - "Expiration", + "HookEmitCount", { + "nth": 18, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 10, - "type": "UInt32" + "type": "UInt16" } ], [ - "TransferRate", + "HookExecutionIndex", { + "nth": 19, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 11, - "type": "UInt32" + "type": "UInt16" } ], [ - "WalletSize", + "HookApiVersion", { + "nth": 20, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 12, - "type": "UInt32" + "type": "UInt16" } ], [ - "OwnerCount", + "LedgerFixType", { + "nth": 21, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 13, - "type": "UInt32" + "type": "UInt16" } ], [ - "DestinationTag", + "NetworkID", { + "nth": 1, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 14, "type": "UInt32" } ], [ - "LastUpdateTime", + "Flags", { + "nth": 2, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 15, "type": "UInt32" } ], [ - "HighQualityIn", + "SourceTag", { + "nth": 3, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 16, "type": "UInt32" } ], [ - "HighQualityOut", + "Sequence", { + "nth": 4, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 17, "type": "UInt32" } ], [ - "LowQualityIn", + "PreviousTxnLgrSeq", { + "nth": 5, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 18, "type": "UInt32" } ], [ - "LowQualityOut", + "LedgerSequence", { + "nth": 6, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 19, "type": "UInt32" } ], [ - "QualityIn", + "CloseTime", { + "nth": 7, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 20, "type": "UInt32" } ], [ - "QualityOut", + "ParentCloseTime", { + "nth": 8, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 21, "type": "UInt32" } ], [ - "StampEscrow", + "SigningTime", { + "nth": 9, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 22, "type": "UInt32" } ], [ - "BondAmount", + "Expiration", { + "nth": 10, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 23, "type": "UInt32" } ], [ - "LoadFee", + "TransferRate", { + "nth": 11, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 24, "type": "UInt32" } ], [ - "OfferSequence", + "WalletSize", { + "nth": 12, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 25, "type": "UInt32" } ], [ - "FirstLedgerSequence", + "OwnerCount", { + "nth": 13, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 26, "type": "UInt32" } ], [ - "LastLedgerSequence", + "DestinationTag", { + "nth": 14, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 27, "type": "UInt32" } ], [ - "TransactionIndex", + "LastUpdateTime", { + "nth": 15, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 28, "type": "UInt32" } ], [ - "OperationLimit", + "HighQualityIn", { + "nth": 16, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 29, "type": "UInt32" } ], [ - "ReferenceFeeUnits", + "HighQualityOut", { + "nth": 17, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 30, "type": "UInt32" } ], [ - "ReserveBase", + "LowQualityIn", { + "nth": 18, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 31, "type": "UInt32" } ], [ - "ReserveIncrement", + "LowQualityOut", { + "nth": 19, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 32, "type": "UInt32" } ], [ - "SetFlag", + "QualityIn", { + "nth": 20, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 33, "type": "UInt32" } ], [ - "ClearFlag", + "QualityOut", { + "nth": 21, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 34, "type": "UInt32" } ], [ - "SignerQuorum", + "StampEscrow", { + "nth": 22, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 35, "type": "UInt32" } ], [ - "CancelAfter", + "BondAmount", { + "nth": 23, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 36, "type": "UInt32" } ], [ - "FinishAfter", + "LoadFee", { + "nth": 24, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 37, "type": "UInt32" } ], [ - "SignerListID", + "OfferSequence", { + "nth": 25, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 38, "type": "UInt32" } ], [ - "SettleDelay", + "FirstLedgerSequence", { + "nth": 26, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 39, "type": "UInt32" } ], [ - "TicketCount", + "LastLedgerSequence", { + "nth": 27, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 40, "type": "UInt32" } ], [ - "TicketSequence", + "TransactionIndex", { + "nth": 28, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 41, "type": "UInt32" } ], [ - "NFTokenTaxon", + "OperationLimit", { + "nth": 29, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 42, "type": "UInt32" } ], [ - "MintedNFTokens", + "ReferenceFeeUnits", { + "nth": 30, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 43, "type": "UInt32" } ], [ - "BurnedNFTokens", + "ReserveBase", { + "nth": 31, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 44, "type": "UInt32" } ], [ - "HookStateCount", + "ReserveIncrement", { + "nth": 32, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 45, "type": "UInt32" } ], [ - "EmitGeneration", + "SetFlag", { + "nth": 33, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 46, "type": "UInt32" } ], [ - "VoteWeight", + "ClearFlag", { + "nth": 34, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 48, "type": "UInt32" } ], [ - "FirstNFTokenSequence", + "SignerQuorum", { - "isSerialized": true, + "nth": 35, + "isVLEncoded": false, + "isSerialized": true, "isSigningField": true, + "type": "UInt32" + } + ], + [ + "CancelAfter", + { + "nth": 36, "isVLEncoded": false, - "nth": 50, + "isSerialized": true, + "isSigningField": true, "type": "UInt32" } ], [ - "OracleDocumentID", + "FinishAfter", { + "nth": 37, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "type": "UInt32" + } + ], + [ + "SignerListID", + { + "nth": 38, "isVLEncoded": false, - "nth": 51, + "isSerialized": true, + "isSigningField": true, "type": "UInt32" } ], [ - "IndexNext", + "SettleDelay", + { + "nth": 39, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "TicketCount", + { + "nth": 40, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "TicketSequence", + { + "nth": 41, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "NFTokenTaxon", + { + "nth": 42, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "MintedNFTokens", + { + "nth": 43, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "BurnedNFTokens", + { + "nth": 44, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "HookStateCount", + { + "nth": 45, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "EmitGeneration", + { + "nth": 46, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "VoteWeight", + { + "nth": 48, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "FirstNFTokenSequence", { + "nth": 50, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "type": "UInt32" + } + ], + [ + "OracleDocumentID", + { + "nth": 51, "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "IndexNext", + { "nth": 1, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, "type": "UInt64" } ], [ "IndexPrevious", { + "nth": 2, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 2, "type": "UInt64" } ], [ "BookNode", { + "nth": 3, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 3, "type": "UInt64" } ], [ "OwnerNode", { + "nth": 4, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 4, "type": "UInt64" } ], [ "BaseFee", { + "nth": 5, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 5, "type": "UInt64" } ], [ "ExchangeRate", { + "nth": 6, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 6, "type": "UInt64" } ], [ "LowNode", { + "nth": 7, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 7, "type": "UInt64" } ], [ "HighNode", { + "nth": 8, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 8, "type": "UInt64" } ], [ "DestinationNode", { + "nth": 9, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 9, "type": "UInt64" } ], [ "Cookie", { + "nth": 10, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 10, "type": "UInt64" } ], [ "ServerVersion", { + "nth": 11, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 11, "type": "UInt64" } ], [ "NFTokenOfferNode", { + "nth": 12, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 12, "type": "UInt64" } ], [ "EmitBurden", { + "nth": 13, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 13, "type": "UInt64" } ], [ "HookOn", { + "nth": 16, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 16, "type": "UInt64" } ], [ "HookInstructionCount", { + "nth": 17, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 17, "type": "UInt64" } ], [ "HookReturnCode", { + "nth": 18, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 18, "type": "UInt64" } ], [ "ReferenceCount", { + "nth": 19, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 19, "type": "UInt64" } ], [ "XChainClaimID", { + "nth": 20, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 20, "type": "UInt64" } ], [ "XChainAccountCreateCount", { + "nth": 21, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 21, "type": "UInt64" } ], [ "XChainAccountClaimCount", { + "nth": 22, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 22, "type": "UInt64" } ], [ "AssetPrice", { + "nth": 23, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 23, "type": "UInt64" } ], [ "MaximumAmount", { + "nth": 24, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 24, "type": "UInt64" } ], [ "OutstandingAmount", { + "nth": 25, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 25, "type": "UInt64" } ], [ "MPTAmount", { + "nth": 26, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 26, "type": "UInt64" } ], @@ -933,900 +1145,960 @@ [ "EmailHash", { + "nth": 1, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 1, "type": "Hash128" } ], [ - "LedgerHash", + "TakerPaysCurrency", { + "nth": 1, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 1, - "type": "Hash256" + "type": "Hash160" } ], [ - "ParentHash", + "TakerPaysIssuer", { + "nth": 2, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 2, - "type": "Hash256" + "type": "Hash160" } ], [ - "TransactionHash", + "TakerGetsCurrency", { + "nth": 3, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Hash160" + } + ], + [ + "TakerGetsIssuer", + { + "nth": 4, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Hash160" + } + ], + [ + "MPTokenIssuanceID", + { + "nth": 1, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Hash192" + } + ], + [ + "LedgerHash", + { + "nth": 1, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 3, "type": "Hash256" } ], [ - "AccountHash", + "ParentHash", { + "nth": 2, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "type": "Hash256" + } + ], + [ + "TransactionHash", + { + "nth": 3, "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Hash256" + } + ], + [ + "AccountHash", + { "nth": 4, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, "type": "Hash256" } ], [ "PreviousTxnID", { + "nth": 5, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 5, "type": "Hash256" } ], [ "LedgerIndex", { + "nth": 6, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 6, "type": "Hash256" } ], [ "WalletLocator", { + "nth": 7, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 7, "type": "Hash256" } ], [ "RootIndex", { + "nth": 8, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 8, "type": "Hash256" } ], [ "AccountTxnID", { + "nth": 9, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 9, "type": "Hash256" } ], [ "NFTokenID", { + "nth": 10, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 10, "type": "Hash256" } ], [ "EmitParentTxnID", { + "nth": 11, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 11, "type": "Hash256" } ], [ "EmitNonce", { + "nth": 12, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 12, "type": "Hash256" } ], [ "EmitHookHash", { + "nth": 13, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 13, "type": "Hash256" } ], [ "AMMID", { + "nth": 14, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 14, "type": "Hash256" } ], [ "BookDirectory", { + "nth": 16, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 16, "type": "Hash256" } ], [ "InvoiceID", { + "nth": 17, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 17, "type": "Hash256" } ], [ "Nickname", { + "nth": 18, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 18, "type": "Hash256" } ], [ "Amendment", { + "nth": 19, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 19, "type": "Hash256" } ], [ "Digest", { + "nth": 21, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 21, "type": "Hash256" } ], [ "Channel", { + "nth": 22, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 22, "type": "Hash256" } ], [ "ConsensusHash", { + "nth": 23, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 23, "type": "Hash256" } ], [ "CheckID", { + "nth": 24, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 24, "type": "Hash256" } ], [ "ValidatedHash", { + "nth": 25, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 25, "type": "Hash256" } ], [ "PreviousPageMin", { + "nth": 26, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 26, "type": "Hash256" } ], [ "NextPageMin", { + "nth": 27, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 27, "type": "Hash256" } ], [ "NFTokenBuyOffer", { + "nth": 28, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 28, "type": "Hash256" } ], [ "NFTokenSellOffer", { + "nth": 29, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 29, "type": "Hash256" } ], [ "HookStateKey", { + "nth": 30, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 30, "type": "Hash256" } ], [ "HookHash", { + "nth": 31, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 31, "type": "Hash256" } ], [ "HookNamespace", { + "nth": 32, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 32, "type": "Hash256" } ], [ "HookSetTxnID", { + "nth": 33, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 33, "type": "Hash256" } ], [ - "hash", + "DomainID", { - "isSerialized": false, - "isSigningField": false, + "nth": 34, "isVLEncoded": false, - "nth": 257, + "isSerialized": true, + "isSigningField": true, "type": "Hash256" } ], [ - "index", + "ParentBatchID", { - "isSerialized": false, - "isSigningField": false, + "nth": 35, "isVLEncoded": false, - "nth": 258, + "isSerialized": true, + "isSigningField": true, "type": "Hash256" } ], [ - "Amount", + "Number", { + "nth": 1, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, + "type": "Number" + } + ], + [ + "Amount", + { "nth": 1, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, "type": "Amount" } ], [ "Balance", { + "nth": 2, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 2, "type": "Amount" } ], [ "LimitAmount", { + "nth": 3, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 3, "type": "Amount" } ], [ "TakerPays", { + "nth": 4, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 4, "type": "Amount" } ], [ "TakerGets", { + "nth": 5, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 5, "type": "Amount" } ], [ "LowLimit", { + "nth": 6, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 6, "type": "Amount" } ], [ "HighLimit", { + "nth": 7, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 7, "type": "Amount" } ], [ "Fee", { + "nth": 8, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 8, "type": "Amount" } ], [ "SendMax", { + "nth": 9, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 9, "type": "Amount" } ], [ "DeliverMin", { + "nth": 10, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 10, "type": "Amount" } ], [ "Amount2", { + "nth": 11, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 11, "type": "Amount" } ], [ "BidMin", { + "nth": 12, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 12, "type": "Amount" } ], [ "BidMax", { + "nth": 13, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 13, "type": "Amount" } ], [ "MinimumOffer", { + "nth": 16, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 16, "type": "Amount" } ], [ "RippleEscrow", { + "nth": 17, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 17, "type": "Amount" } ], [ "DeliveredAmount", { + "nth": 18, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 18, "type": "Amount" } ], [ "NFTokenBrokerFee", { + "nth": 19, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 19, "type": "Amount" } ], [ "BaseFeeDrops", { + "nth": 22, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 22, "type": "Amount" } ], [ "ReserveBaseDrops", { - "isSerialized": true, - "isSigningField": true, - "isVLEncoded": false, "nth": 23, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, "type": "Amount" } ], [ "ReserveIncrementDrops", { + "nth": 24, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 24, "type": "Amount" } ], [ "LPTokenOut", { + "nth": 25, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 25, "type": "Amount" } ], [ "LPTokenIn", { + "nth": 26, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 26, "type": "Amount" } ], [ "EPrice", { + "nth": 27, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 27, "type": "Amount" } ], [ "Price", { + "nth": 28, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 28, "type": "Amount" } ], [ "SignatureReward", { + "nth": 29, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 29, "type": "Amount" } ], [ "MinAccountCreateAmount", { + "nth": 30, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 30, "type": "Amount" } ], [ "LPTokenBalance", { + "nth": 31, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 31, "type": "Amount" } ], [ "PublicKey", { + "nth": 1, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 1, "type": "Blob" } ], [ "MessageKey", { + "nth": 2, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 2, "type": "Blob" } ], [ "SigningPubKey", { + "nth": 3, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 3, "type": "Blob" } ], [ "TxnSignature", { + "nth": 4, + "isVLEncoded": true, "isSerialized": true, "isSigningField": false, - "isVLEncoded": true, - "nth": 4, "type": "Blob" } ], [ "URI", { + "nth": 5, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 5, "type": "Blob" } ], [ "Signature", { + "nth": 6, + "isVLEncoded": true, "isSerialized": true, "isSigningField": false, - "isVLEncoded": true, - "nth": 6, "type": "Blob" } ], [ "Domain", { + "nth": 7, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 7, "type": "Blob" } ], [ "FundCode", { + "nth": 8, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 8, "type": "Blob" } ], [ "RemoveCode", { + "nth": 9, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 9, "type": "Blob" } ], [ "ExpireCode", { + "nth": 10, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 10, "type": "Blob" } ], [ "CreateCode", { + "nth": 11, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 11, "type": "Blob" } ], [ "MemoType", { + "nth": 12, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 12, "type": "Blob" } ], [ "MemoData", { + "nth": 13, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 13, "type": "Blob" } ], [ "MemoFormat", { + "nth": 14, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 14, "type": "Blob" } ], [ "Fulfillment", { + "nth": 16, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 16, "type": "Blob" } ], [ "Condition", { + "nth": 17, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 17, "type": "Blob" } ], [ "MasterSignature", { + "nth": 18, + "isVLEncoded": true, "isSerialized": true, "isSigningField": false, - "isVLEncoded": true, - "nth": 18, "type": "Blob" } ], [ "UNLModifyValidator", { + "nth": 19, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 19, "type": "Blob" } ], [ "ValidatorToDisable", { + "nth": 20, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 20, "type": "Blob" } ], [ "ValidatorToReEnable", { + "nth": 21, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 21, "type": "Blob" } ], [ "HookStateData", { + "nth": 22, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 22, "type": "Blob" } ], [ "HookReturnString", { + "nth": 23, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 23, "type": "Blob" } ], [ "HookParameterName", { + "nth": 24, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 24, "type": "Blob" } ], [ "HookParameterValue", { + "nth": 25, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 25, "type": "Blob" } ], [ "DIDDocument", { + "nth": 26, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 26, "type": "Blob" } ], [ "Data", { + "nth": 27, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 27, "type": "Blob" } ], [ "AssetClass", { + "nth": 28, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 28, "type": "Blob" } ], [ "Provider", { + "nth": 29, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 29, "type": "Blob" } ], [ "MPTokenMetadata", { + "nth": 30, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 30, "type": "Blob" } ], @@ -1843,170 +2115,170 @@ [ "Account", { + "nth": 1, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 1, "type": "AccountID" } ], [ "Owner", { + "nth": 2, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 2, "type": "AccountID" } ], [ "Destination", { + "nth": 3, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 3, "type": "AccountID" } ], [ "Issuer", { + "nth": 4, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 4, "type": "AccountID" } ], [ "Authorize", { + "nth": 5, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 5, "type": "AccountID" } ], [ "Unauthorize", { + "nth": 6, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 6, "type": "AccountID" } ], [ "RegularKey", { + "nth": 8, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 8, "type": "AccountID" } ], [ "NFTokenMinter", { + "nth": 9, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 9, "type": "AccountID" } ], [ "EmitCallback", { + "nth": 10, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 10, "type": "AccountID" } ], [ "Holder", { + "nth": 11, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 11, "type": "AccountID" } ], [ "HookAccount", { + "nth": 16, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 16, "type": "AccountID" } ], [ "OtherChainSource", { + "nth": 18, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 18, "type": "AccountID" } ], [ "OtherChainDestination", { + "nth": 19, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 19, "type": "AccountID" } ], [ "AttestationSignerAccount", { + "nth": 20, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 20, "type": "AccountID" } ], [ "AttestationRewardAccount", { + "nth": 21, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 21, "type": "AccountID" } ], [ "LockingChainDoor", { + "nth": 22, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 22, "type": "AccountID" } ], [ "IssuingChainDoor", { + "nth": 23, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 23, "type": "AccountID" } ], @@ -2021,299 +2293,299 @@ } ], [ - "TransactionMetaData", + "Indexes", { + "nth": 1, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 2, - "type": "STObject" + "type": "Vector256" } ], [ - "CreatedNode", + "Hashes", { + "nth": 2, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 3, - "type": "STObject" + "type": "Vector256" } ], [ - "DeletedNode", + "Amendments", { + "nth": 3, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 4, - "type": "STObject" + "type": "Vector256" } ], [ - "ModifiedNode", + "NFTokenOffers", { + "nth": 4, + "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 5, - "type": "STObject" + "type": "Vector256" } ], [ - "PreviousFields", + "CredentialIDs", { - "isSerialized": true, + "nth": 5, + "isVLEncoded": true, + "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 6, - "type": "STObject" + "type": "Vector256" } ], [ - "FinalFields", + "Paths", { + "nth": 1, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 7, - "type": "STObject" + "type": "PathSet" } ], [ - "NewFields", + "BaseAsset", { + "nth": 1, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 8, - "type": "STObject" + "type": "Currency" } ], [ - "TemplateEntry", + "QuoteAsset", { + "nth": 2, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 9, - "type": "STObject" + "type": "Currency" } ], [ - "Memo", + "LockingChainIssue", { + "nth": 1, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 10, - "type": "STObject" + "type": "Issue" } ], [ - "SignerEntry", + "IssuingChainIssue", { + "nth": 2, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 11, - "type": "STObject" + "type": "Issue" } ], [ - "NFToken", + "Asset", { + "nth": 3, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 12, - "type": "STObject" + "type": "Issue" } ], [ - "EmitDetails", + "Asset2", { + "nth": 4, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 13, - "type": "STObject" + "type": "Issue" } ], [ - "Hook", + "XChainBridge", { + "nth": 1, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 14, - "type": "STObject" + "type": "XChainBridge" } ], [ - "Signer", + "TransactionMetaData", { + "nth": 2, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 16, "type": "STObject" } ], [ - "Majority", + "CreatedNode", { + "nth": 3, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 18, "type": "STObject" } ], [ - "DisabledValidator", + "DeletedNode", { + "nth": 4, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 19, "type": "STObject" } ], [ - "EmittedTxn", + "ModifiedNode", { + "nth": 5, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 20, "type": "STObject" } ], [ - "HookExecution", + "PreviousFields", { + "nth": 6, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 21, "type": "STObject" } ], [ - "HookDefinition", + "FinalFields", { + "nth": 7, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 22, "type": "STObject" } ], [ - "HookParameter", + "NewFields", { + "nth": 8, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 23, "type": "STObject" } ], [ - "HookGrant", + "TemplateEntry", { + "nth": 9, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 24, "type": "STObject" } ], [ - "VoteEntry", + "Memo", { + "nth": 10, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 25, "type": "STObject" } ], [ - "AuctionSlot", + "SignerEntry", { + "nth": 11, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 26, "type": "STObject" } ], [ - "AuthAccount", + "NFToken", { + "nth": 12, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 27, "type": "STObject" } ], [ - "XChainClaimProofSig", + "EmitDetails", { + "nth": 13, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 28, "type": "STObject" } ], [ - "XChainCreateAccountProofSig", + "Hook", { + "nth": 14, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 29, "type": "STObject" } ], [ - "XChainClaimAttestationCollectionElement", + "Signer", { + "nth": 16, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 30, "type": "STObject" } ], [ - "XChainCreateAccountAttestationCollectionElement", + "Majority", { + "nth": 18, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 31, "type": "STObject" } ], [ - "PriceData", + "DisabledValidator", { + "nth": 19, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 32, "type": "STObject" } ], [ - "Credential", + "EmittedTxn", { - "nth": 33, + "nth": 20, "isVLEncoded": false, "isSerialized": true, "isSigningField": true, @@ -2321,199 +2593,189 @@ } ], [ - "Signers", + "HookExecution", { - "isSerialized": true, - "isSigningField": false, + "nth": 21, "isVLEncoded": false, - "nth": 3, - "type": "STArray" + "isSerialized": true, + "isSigningField": true, + "type": "STObject" } ], [ - "SignerEntries", + "HookDefinition", { + "nth": 22, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 4, - "type": "STArray" + "type": "STObject" } ], [ - "Template", + "HookParameter", { + "nth": 23, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 5, - "type": "STArray" + "type": "STObject" } ], [ - "Necessary", + "HookGrant", { + "nth": 24, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 6, - "type": "STArray" + "type": "STObject" } ], [ - "Sufficient", + "VoteEntry", { + "nth": 25, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 7, - "type": "STArray" + "type": "STObject" } ], [ - "AffectedNodes", + "AuctionSlot", { + "nth": 26, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 8, - "type": "STArray" + "type": "STObject" } ], [ - "Memos", + "AuthAccount", { + "nth": 27, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 9, - "type": "STArray" + "type": "STObject" } ], [ - "NFTokens", + "XChainClaimProofSig", { + "nth": 28, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 10, - "type": "STArray" + "type": "STObject" } ], [ - "Hooks", + "XChainCreateAccountProofSig", { + "nth": 29, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 11, - "type": "STArray" + "type": "STObject" } ], [ - "VoteSlots", + "XChainClaimAttestationCollectionElement", { + "nth": 30, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 12, - "type": "STArray" + "type": "STObject" } ], [ - "Majorities", + "XChainCreateAccountAttestationCollectionElement", { - "isSerialized": true, - "isSigningField": true, + "nth": 31, "isVLEncoded": false, - "nth": 16, - "type": "STArray" - } - ], - [ - "DisabledValidators", - { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 17, - "type": "STArray" + "type": "STObject" } ], [ - "HookExecutions", + "PriceData", { + "nth": 32, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 18, - "type": "STArray" + "type": "STObject" } ], [ - "HookParameters", + "Credential", { + "nth": 33, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 19, - "type": "STArray" + "type": "STObject" } ], [ - "HookGrants", + "RawTransaction", { + "nth": 34, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 20, - "type": "STArray" + "type": "STObject" } ], [ - "XChainClaimAttestations", + "BatchSigner", { + "nth": 35, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 21, - "type": "STArray" + "type": "STObject" } ], [ - "XChainCreateAccountAttestations", + "Signers", { - "isSerialized": true, - "isSigningField": true, + "nth": 3, "isVLEncoded": false, - "nth": 22, + "isSerialized": true, + "isSigningField": false, "type": "STArray" } ], [ - "PriceDataSeries", + "SignerEntries", { + "nth": 4, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 24, "type": "STArray" } ], [ - "AuthAccounts", + "Template", { + "nth": 5, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 25, "type": "STArray" } ], [ - "AuthorizeCredentials", + "Necessary", { - "nth": 26, + "nth": 6, "isVLEncoded": false, "isSerialized": true, "isSigningField": true, @@ -2521,9 +2783,9 @@ } ], [ - "UnauthorizeCredentials", + "Sufficient", { - "nth": 27, + "nth": 7, "isVLEncoded": false, "isSerialized": true, "isSigningField": true, @@ -2531,476 +2793,225 @@ } ], [ - "CloseResolution", + "AffectedNodes", { - "isSerialized": true, - "isSigningField": true, + "nth": 8, "isVLEncoded": false, - "nth": 1, - "type": "UInt8" - } - ], - [ - "Method", - { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 2, - "type": "UInt8" + "type": "STArray" } ], [ - "TransactionResult", + "Memos", { - "isSerialized": true, - "isSigningField": true, + "nth": 9, "isVLEncoded": false, - "nth": 3, - "type": "UInt8" - } - ], - [ - "Scale", - { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 4, - "type": "UInt8" + "type": "STArray" } ], [ - "AssetScale", + "NFTokens", { - "isSerialized": true, - "isSigningField": true, + "nth": 10, "isVLEncoded": false, - "nth": 5, - "type": "UInt8" - } - ], - [ - "TickSize", - { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 16, - "type": "UInt8" + "type": "STArray" } ], [ - "UNLModifyDisabling", + "Hooks", { - "isSerialized": true, - "isSigningField": true, + "nth": 11, "isVLEncoded": false, - "nth": 17, - "type": "UInt8" - } - ], - [ - "HookResult", - { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 18, - "type": "UInt8" + "type": "STArray" } ], [ - "WasLockingChainSend", + "VoteSlots", { - "isSerialized": true, - "isSigningField": true, + "nth": 12, "isVLEncoded": false, - "nth": 19, - "type": "UInt8" - } - ], - [ - "TakerPaysCurrency", - { "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 1, - "type": "Hash160" + "type": "STArray" } ], [ - "TakerPaysIssuer", + "Majorities", { + "nth": 16, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 2, - "type": "Hash160" + "type": "STArray" } ], [ - "TakerGetsCurrency", + "DisabledValidators", { + "nth": 17, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 3, - "type": "Hash160" + "type": "STArray" } ], [ - "TakerGetsIssuer", + "HookExecutions", { + "nth": 18, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 4, - "type": "Hash160" + "type": "STArray" } ], [ - "Paths", + "HookParameters", { + "nth": 19, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 1, - "type": "PathSet" + "type": "STArray" } ], [ - "Indexes", + "HookGrants", { + "nth": 20, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 1, - "type": "Vector256" + "type": "STArray" } ], [ - "Hashes", + "XChainClaimAttestations", { + "nth": 21, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 2, - "type": "Vector256" + "type": "STArray" } ], [ - "Amendments", + "XChainCreateAccountAttestations", { + "nth": 22, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 3, - "type": "Vector256" + "type": "STArray" } ], [ - "NFTokenOffers", + "PriceDataSeries", { + "nth": 24, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": true, - "nth": 4, - "type": "Vector256" + "type": "STArray" } ], [ - "CredentialIDs", + "AuthAccounts", { - "nth": 5, - "isVLEncoded": true, + "nth": 25, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "Vector256" + "type": "STArray" } ], [ - "MPTokenIssuanceID", + "AuthorizeCredentials", { + "nth": 26, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 1, - "type": "Hash192" + "type": "STArray" } ], [ - "LockingChainIssue", + "UnauthorizeCredentials", { + "nth": 27, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 1, - "type": "Issue" + "type": "STArray" } ], [ - "IssuingChainIssue", + "AcceptedCredentials", { + "nth": 28, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 2, - "type": "Issue" + "type": "STArray" } ], [ - "Asset", + "RawTransactions", { + "nth": 29, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 3, - "type": "Issue" + "type": "STArray" } ], [ - "Asset2", + "BatchSigners", { + "nth": 30, + "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "isVLEncoded": false, - "nth": 4, - "type": "Issue" - } - ], - [ - "XChainBridge", - { - "isSerialized": true, - "isSigningField": true, - "isVLEncoded": false, - "nth": 1, - "type": "XChainBridge" - } - ], - [ - "BaseAsset", - { - "isSerialized": true, - "isSigningField": true, - "isVLEncoded": false, - "nth": 1, - "type": "Currency" - } - ], - [ - "QuoteAsset", - { - "isSerialized": true, - "isSigningField": true, - "isVLEncoded": false, - "nth": 2, - "type": "Currency" - } - ], - [ - "Transaction", - { - "isSerialized": false, - "isSigningField": false, - "isVLEncoded": false, - "nth": 257, - "type": "Transaction" - } - ], - [ - "LedgerEntry", - { - "isSerialized": false, - "isSigningField": false, - "isVLEncoded": false, - "nth": 257, - "type": "LedgerEntry" - } - ], - [ - "Validation", - { - "isSerialized": false, - "isSigningField": false, - "isVLEncoded": false, - "nth": 257, - "type": "Validation" - } - ], - [ - "Metadata", - { - "isSerialized": false, - "isSigningField": false, - "isVLEncoded": false, - "nth": 257, - "type": "Metadata" + "type": "STArray" } ] ], - "LEDGER_ENTRY_TYPES": { - "AMM": 121, - "AccountRoot": 97, - "Amendments": 102, - "Bridge": 105, - "Check": 67, - "DID": 73, - "DepositPreauth": 112, - "DirectoryNode": 100, - "Escrow": 117, - "FeeSettings": 115, - "Invalid": -1, - "LedgerHashes": 104, - "MPToken": 127, - "MPTokenIssuance": 126, - "NFTokenOffer": 55, - "NFTokenPage": 80, - "NegativeUNL": 78, - "Offer": 111, - "Oracle": 128, - "Credential": 129, - "PayChannel": 120, - "RippleState": 114, - "SignerList": 83, - "Ticket": 84, - "XChainOwnedClaimID": 113, - "XChainOwnedCreateAccountClaimID": 116 - }, "TRANSACTION_RESULTS": { - "tecAMM_ACCOUNT": 168, - "tecAMM_BALANCE": 163, - "tecAMM_EMPTY": 166, - "tecAMM_FAILED": 164, - "tecAMM_INVALID_TOKENS": 165, - "tecAMM_NOT_EMPTY": 167, - "tecARRAY_EMPTY": 190, - "tecARRAY_TOO_LARGE": 191, - "tecBAD_CREDENTIALS": 193, - "tecCANT_ACCEPT_OWN_NFTOKEN_OFFER": 158, - "tecCLAIM": 100, - "tecCRYPTOCONDITION_ERROR": 146, - "tecDIR_FULL": 121, - "tecDST_TAG_NEEDED": 143, - "tecDUPLICATE": 149, - "tecEMPTY_DID": 187, - "tecEXPIRED": 148, - "tecFAILED_PROCESSING": 105, - "tecFROZEN": 137, - "tecHAS_OBLIGATIONS": 151, - "tecINCOMPLETE": 169, - "tecINSUFFICIENT_FUNDS": 159, - "tecINSUFFICIENT_PAYMENT": 161, - "tecINSUFFICIENT_RESERVE": 141, - "tecINSUFF_FEE": 136, - "tecINSUF_RESERVE_LINE": 122, - "tecINSUF_RESERVE_OFFER": 123, - "tecINTERNAL": 144, - "tecINVALID_UPDATE_TIME": 188, - "tecINVARIANT_FAILED": 147, - "tecKILLED": 150, - "tecLOCKED": 192, - "tecMAX_SEQUENCE_REACHED": 154, - "tecNEED_MASTER_KEY": 142, - "tecNFTOKEN_BUY_SELL_MISMATCH": 156, - "tecNFTOKEN_OFFER_TYPE_MISMATCH": 157, - "tecNO_ALTERNATIVE_KEY": 130, - "tecNO_AUTH": 134, - "tecNO_DST": 124, - "tecNO_DST_INSUF_XRP": 125, - "tecNO_ENTRY": 140, - "tecNO_ISSUER": 133, - "tecNO_LINE": 135, - "tecNO_LINE_INSUF_RESERVE": 126, - "tecNO_LINE_REDUNDANT": 127, - "tecNO_PERMISSION": 139, - "tecNO_REGULAR_KEY": 131, - "tecNO_SUITABLE_NFTOKEN_PAGE": 155, - "tecNO_TARGET": 138, - "tecOBJECT_NOT_FOUND": 160, - "tecOVERSIZE": 145, - "tecOWNERS": 132, - "tecPATH_DRY": 128, - "tecPATH_PARTIAL": 101, - "tecTOKEN_PAIR_NOT_FOUND": 189, - "tecTOO_SOON": 152, - "tecUNFUNDED": 129, - "tecUNFUNDED_ADD": 102, - "tecUNFUNDED_AMM": 162, - "tecUNFUNDED_OFFER": 103, - "tecUNFUNDED_PAYMENT": 104, - "tecXCHAIN_ACCOUNT_CREATE_PAST": 181, - "tecXCHAIN_ACCOUNT_CREATE_TOO_MANY": 182, - "tecXCHAIN_BAD_CLAIM_ID": 172, - "tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR": 185, - "tecXCHAIN_BAD_TRANSFER_ISSUE": 170, - "tecXCHAIN_CLAIM_NO_QUORUM": 173, - "tecXCHAIN_CREATE_ACCOUNT_DISABLED": 186, - "tecXCHAIN_CREATE_ACCOUNT_NONXRP_ISSUE": 175, - "tecXCHAIN_INSUFF_CREATE_AMOUNT": 180, - "tecXCHAIN_NO_CLAIM_ID": 171, - "tecXCHAIN_NO_SIGNERS_LIST": 178, - "tecXCHAIN_PAYMENT_FAILED": 183, - "tecXCHAIN_PROOF_UNKNOWN_KEY": 174, - "tecXCHAIN_REWARD_MISMATCH": 177, - "tecXCHAIN_SELF_COMMIT": 184, - "tecXCHAIN_SENDING_ACCOUNT_MISMATCH": 179, - "tecXCHAIN_WRONG_CHAIN": 176, - "tefALREADY": -198, - "tefBAD_ADD_AUTH": -197, - "tefBAD_AUTH": -196, - "tefBAD_AUTH_MASTER": -183, - "tefBAD_LEDGER": -195, - "tefBAD_QUORUM": -185, - "tefBAD_SIGNATURE": -186, - "tefCREATED": -194, - "tefEXCEPTION": -193, - "tefFAILURE": -199, - "tefINTERNAL": -192, - "tefINVALID_LEDGER_FIX_TYPE": -178, - "tefINVARIANT_FAILED": -182, - "tefMASTER_DISABLED": -188, - "tefMAX_LEDGER": -187, - "tefNFTOKEN_IS_NOT_TRANSFERABLE": -179, - "tefNOT_MULTI_SIGNING": -184, - "tefNO_AUTH_REQUIRED": -191, - "tefNO_TICKET": -180, - "tefPAST_SEQ": -190, - "tefTOO_BIG": -181, - "tefWRONG_PRIOR": -189, + "telLOCAL_ERROR": -399, "telBAD_DOMAIN": -398, "telBAD_PATH_COUNT": -397, "telBAD_PUBLIC_KEY": -396, + "telFAILED_PROCESSING": -395, + "telINSUF_FEE_P": -394, + "telNO_DST_PARTIAL": -393, "telCAN_NOT_QUEUE": -392, "telCAN_NOT_QUEUE_BALANCE": -391, - "telCAN_NOT_QUEUE_BLOCKED": -389, "telCAN_NOT_QUEUE_BLOCKS": -390, + "telCAN_NOT_QUEUE_BLOCKED": -389, "telCAN_NOT_QUEUE_FEE": -388, "telCAN_NOT_QUEUE_FULL": -387, - "telENV_RPC_FAILED": -383, - "telFAILED_PROCESSING": -395, - "telINSUF_FEE_P": -394, - "telLOCAL_ERROR": -399, - "telNETWORK_ID_MAKES_TX_NON_CANONICAL": -384, - "telNO_DST_PARTIAL": -393, - "telREQUIRES_NETWORK_ID": -385, "telWRONG_NETWORK": -386, - "temARRAY_EMPTY": -253, - "temARRAY_TOO_LARGE": -252, - "temBAD_AMM_TOKENS": -261, + "telREQUIRES_NETWORK_ID": -385, + "telNETWORK_ID_MAKES_TX_NON_CANONICAL": -384, + "telENV_RPC_FAILED": -383, + + "temMALFORMED": -299, "temBAD_AMOUNT": -298, "temBAD_CURRENCY": -297, "temBAD_EXPIRATION": -296, "temBAD_FEE": -295, "temBAD_ISSUER": -294, "temBAD_LIMIT": -293, - "temBAD_NFTOKEN_TRANSFER_FEE": -262, "temBAD_OFFER": -292, "temBAD_PATH": -291, "temBAD_PATH_LOOP": -290, - "temBAD_QUORUM": -271, "temBAD_REGKEY": -289, "temBAD_SEND_XRP_LIMIT": -288, "temBAD_SEND_XRP_MAX": -287, @@ -3009,133 +3020,218 @@ "temBAD_SEND_XRP_PATHS": -284, "temBAD_SEQUENCE": -283, "temBAD_SIGNATURE": -282, - "temBAD_SIGNER": -272, "temBAD_SRC_ACCOUNT": -281, - "temBAD_TICK_SIZE": -269, - "temBAD_TRANSFER_FEE": -251, "temBAD_TRANSFER_RATE": -280, - "temBAD_WEIGHT": -270, - "temCANNOT_PREAUTH_SELF": -267, - "temDISABLED": -273, "temDST_IS_SRC": -279, "temDST_NEEDED": -278, - "temEMPTY_DID": -254, "temINVALID": -277, - "temINVALID_ACCOUNT_ID": -268, - "temINVALID_COUNT": -266, "temINVALID_FLAG": -276, - "temMALFORMED": -299, "temREDUNDANT": -275, "temRIPPLE_EMPTY": -274, - "temSEQ_AND_TICKET": -263, + "temDISABLED": -273, + "temBAD_SIGNER": -272, + "temBAD_QUORUM": -271, + "temBAD_WEIGHT": -270, + "temBAD_TICK_SIZE": -269, + "temINVALID_ACCOUNT_ID": -268, + "temCANNOT_PREAUTH_SELF": -267, + "temINVALID_COUNT": -266, "temUNCERTAIN": -265, "temUNKNOWN": -264, + "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, - "temXCHAIN_BRIDGE_NONDOOR_OWNER": -257, - "temXCHAIN_EQUAL_DOOR_ACCOUNTS": -260, + "temEMPTY_DID": -254, + "temARRAY_EMPTY": -253, + "temARRAY_TOO_LARGE": -252, + "temBAD_TRANSFER_FEE": -251, + "temINVALID_BATCH": -250, + + "tefFAILURE": -199, + "tefALREADY": -198, + "tefBAD_ADD_AUTH": -197, + "tefBAD_AUTH": -196, + "tefBAD_LEDGER": -195, + "tefCREATED": -194, + "tefEXCEPTION": -193, + "tefINTERNAL": -192, + "tefNO_AUTH_REQUIRED": -191, + "tefPAST_SEQ": -190, + "tefWRONG_PRIOR": -189, + "tefMASTER_DISABLED": -188, + "tefMAX_LEDGER": -187, + "tefBAD_SIGNATURE": -186, + "tefBAD_QUORUM": -185, + "tefNOT_MULTI_SIGNING": -184, + "tefBAD_AUTH_MASTER": -183, + "tefINVARIANT_FAILED": -182, + "tefTOO_BIG": -181, + "tefNO_TICKET": -180, + "tefNFTOKEN_IS_NOT_TRANSFERABLE": -179, + "tefINVALID_LEDGER_FIX_TYPE": -178, + + "terRETRY": -99, "terFUNDS_SPENT": -98, "terINSUF_FEE_B": -97, - "terLAST": -91, "terNO_ACCOUNT": -96, - "terNO_AMM": -87, "terNO_AUTH": -95, "terNO_LINE": -94, - "terNO_RIPPLE": -90, "terOWNERS": -93, "terPRE_SEQ": -92, - "terPRE_TICKET": -88, + "terLAST": -91, + "terNO_RIPPLE": -90, "terQUEUED": -89, - "terRETRY": -99, - "tesSUCCESS": 0 + "terPRE_TICKET": -88, + "terNO_AMM": -87, + + "tesSUCCESS": 0, + + "tecCLAIM": 100, + "tecPATH_PARTIAL": 101, + "tecUNFUNDED_ADD": 102, + "tecUNFUNDED_OFFER": 103, + "tecUNFUNDED_PAYMENT": 104, + "tecFAILED_PROCESSING": 105, + "tecDIR_FULL": 121, + "tecINSUF_RESERVE_LINE": 122, + "tecINSUF_RESERVE_OFFER": 123, + "tecNO_DST": 124, + "tecNO_DST_INSUF_XRP": 125, + "tecNO_LINE_INSUF_RESERVE": 126, + "tecNO_LINE_REDUNDANT": 127, + "tecPATH_DRY": 128, + "tecUNFUNDED": 129, + "tecNO_ALTERNATIVE_KEY": 130, + "tecNO_REGULAR_KEY": 131, + "tecOWNERS": 132, + "tecNO_ISSUER": 133, + "tecNO_AUTH": 134, + "tecNO_LINE": 135, + "tecINSUFF_FEE": 136, + "tecFROZEN": 137, + "tecNO_TARGET": 138, + "tecNO_PERMISSION": 139, + "tecNO_ENTRY": 140, + "tecINSUFFICIENT_RESERVE": 141, + "tecNEED_MASTER_KEY": 142, + "tecDST_TAG_NEEDED": 143, + "tecINTERNAL": 144, + "tecOVERSIZE": 145, + "tecCRYPTOCONDITION_ERROR": 146, + "tecINVARIANT_FAILED": 147, + "tecEXPIRED": 148, + "tecDUPLICATE": 149, + "tecKILLED": 150, + "tecHAS_OBLIGATIONS": 151, + "tecTOO_SOON": 152, + "tecHOOK_REJECTED": 153, + "tecMAX_SEQUENCE_REACHED": 154, + "tecNO_SUITABLE_NFTOKEN_PAGE": 155, + "tecNFTOKEN_BUY_SELL_MISMATCH": 156, + "tecNFTOKEN_OFFER_TYPE_MISMATCH": 157, + "tecCANT_ACCEPT_OWN_NFTOKEN_OFFER": 158, + "tecINSUFFICIENT_FUNDS": 159, + "tecOBJECT_NOT_FOUND": 160, + "tecINSUFFICIENT_PAYMENT": 161, + "tecUNFUNDED_AMM": 162, + "tecAMM_BALANCE": 163, + "tecAMM_FAILED": 164, + "tecAMM_INVALID_TOKENS": 165, + "tecAMM_EMPTY": 166, + "tecAMM_NOT_EMPTY": 167, + "tecAMM_ACCOUNT": 168, + "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, + "tecEMPTY_DID": 187, + "tecINVALID_UPDATE_TIME": 188, + "tecTOKEN_PAIR_NOT_FOUND": 189, + "tecARRAY_EMPTY": 190, + "tecARRAY_TOO_LARGE": 191, + "tecLOCKED": 192, + "tecBAD_CREDENTIALS": 193 }, "TRANSACTION_TYPES": { - "AMMBid": 39, - "AMMCreate": 35, - "AMMDelete": 40, - "AMMDeposit": 36, - "AMMVote": 38, - "AMMWithdraw": 37, - "AccountDelete": 21, - "AccountSet": 3, - "CheckCancel": 18, - "CheckCash": 17, - "CheckCreate": 16, - "Clawback": 30, - "CredentialCreate": 58, - "CredentialAccept": 59, - "CredentialDelete": 60, - "DIDDelete": 50, - "DIDSet": 49, - "DepositPreauth": 19, - "EnableAmendment": 100, - "EscrowCancel": 4, + "Invalid": -1, + "Payment": 0, "EscrowCreate": 1, "EscrowFinish": 2, - "Invalid": -1, - "LedgerStateFix": 53, - "MPTokenAuthorize": 57, - "MPTokenIssuanceCreate": 54, - "MPTokenIssuanceDestroy": 55, - "MPTokenIssuanceSet": 56, - "NFTokenAcceptOffer": 29, - "NFTokenBurn": 26, - "NFTokenCancelOffer": 28, - "NFTokenCreateOffer": 27, - "NFTokenMint": 25, - "OfferCancel": 8, + "AccountSet": 3, + "EscrowCancel": 4, + "SetRegularKey": 5, "OfferCreate": 7, - "OracleDelete": 52, - "OracleSet": 51, - "Payment": 0, - "PaymentChannelClaim": 15, + "OfferCancel": 8, + "TicketCreate": 10, + "SignerListSet": 12, "PaymentChannelCreate": 13, "PaymentChannelFund": 14, - "SetFee": 101, - "SetRegularKey": 5, - "SignerListSet": 12, - "TicketCreate": 10, + "PaymentChannelClaim": 15, + "CheckCreate": 16, + "CheckCash": 17, + "CheckCancel": 18, + "DepositPreauth": 19, "TrustSet": 20, - "UNLModify": 102, + "AccountDelete": 21, + "NFTokenMint": 25, + "NFTokenBurn": 26, + "NFTokenCreateOffer": 27, + "NFTokenCancelOffer": 28, + "NFTokenAcceptOffer": 29, + "Clawback": 30, + "AMMClawback": 31, + "AMMCreate": 35, + "AMMDeposit": 36, + "AMMWithdraw": 37, + "AMMVote": 38, + "AMMBid": 39, + "AMMDelete": 40, + "XChainCreateClaimID": 41, + "XChainCommit": 42, + "XChainClaim": 43, "XChainAccountCreateCommit": 44, - "XChainAddAccountCreateAttestation": 46, "XChainAddClaimAttestation": 45, - "XChainClaim": 43, - "XChainCommit": 42, + "XChainAddAccountCreateAttestation": 46, + "XChainModifyBridge": 47, "XChainCreateBridge": 48, - "XChainCreateClaimID": 41, - "XChainModifyBridge": 47 - }, - "TYPES": { - "AccountID": 8, - "Amount": 6, - "Blob": 7, - "Currency": 26, - "Done": -1, - "Hash128": 4, - "Hash160": 17, - "Hash192": 21, - "Hash256": 5, - "Issue": 24, - "LedgerEntry": 10002, - "Metadata": 10004, - "NotPresent": 0, - "PathSet": 18, - "STArray": 15, - "STObject": 14, - "Transaction": 10001, - "UInt16": 1, - "UInt32": 2, - "UInt384": 22, - "UInt512": 23, - "UInt64": 3, - "UInt8": 16, - "UInt96": 20, - "Unknown": -2, - "Validation": 10003, - "Vector256": 19, - "XChainBridge": 25 + "DIDSet": 49, + "DIDDelete": 50, + "OracleSet": 51, + "OracleDelete": 52, + "LedgerStateFix": 53, + "MPTokenIssuanceCreate": 54, + "MPTokenIssuanceDestroy": 55, + "MPTokenIssuanceSet": 56, + "MPTokenAuthorize": 57, + "CredentialCreate": 58, + "CredentialAccept": 59, + "CredentialDelete": 60, + "NFTokenModify": 61, + "PermissionedDomainSet": 62, + "PermissionedDomainDelete": 63, + "Batch": 64, + "EnableAmendment": 100, + "SetFee": 101, + "UNLModify": 102 } } diff --git a/packages/ripple-binary-codec/src/hash-prefixes.ts b/packages/ripple-binary-codec/src/hash-prefixes.ts index 98035167bc..edfa6e97ad 100644 --- a/packages/ripple-binary-codec/src/hash-prefixes.ts +++ b/packages/ripple-binary-codec/src/hash-prefixes.ts @@ -35,6 +35,8 @@ const HashPrefix: Record = { proposal: bytes(0x50525000), // payment channel claim paymentChannelClaim: bytes(0x434c4d00), + // batch + batch: bytes(0x42434800), } export { HashPrefix } diff --git a/packages/ripple-binary-codec/src/index.ts b/packages/ripple-binary-codec/src/index.ts index d0e44b5bae..4ee74396c0 100644 --- a/packages/ripple-binary-codec/src/index.ts +++ b/packages/ripple-binary-codec/src/index.ts @@ -1,6 +1,6 @@ import { quality, binary, HashPrefix } from './coretypes' import { decodeLedgerData } from './ledger-hashes' -import { ClaimObject } from './binary' +import { ClaimObject, BatchObject } from './binary' import { JsonObject } from './types/serialized-type' import { XrplDefinitionsBase, @@ -15,6 +15,7 @@ const { signingData, signingClaimData, multiSigningData, + signingBatchData, binaryToJSON, serializeObject, } = binary @@ -110,6 +111,13 @@ function encodeForMultisigning( ) } +function encodeForSigningBatch(json: object): string { + if (typeof json !== 'object') { + throw new Error() + } + return bytesToHex(signingBatchData(json as BatchObject)) +} + /** * Encode a quality value * @@ -142,6 +150,7 @@ export { encodeForSigning, encodeForSigningClaim, encodeForMultisigning, + encodeForSigningBatch, encodeQuality, decodeQuality, decodeLedgerData, diff --git a/packages/ripple-binary-codec/test/signing-data-encoding.test.ts b/packages/ripple-binary-codec/test/signing-data-encoding.test.ts index 22bab766cb..f7f9126c10 100644 --- a/packages/ripple-binary-codec/test/signing-data-encoding.test.ts +++ b/packages/ripple-binary-codec/test/signing-data-encoding.test.ts @@ -3,6 +3,7 @@ const { encodeForSigning, encodeForSigningClaim, encodeForMultisigning, + encodeForSigningBatch, } = require('../src') const normalDefinitions = require('../src/enums/definitions.json') @@ -244,4 +245,27 @@ describe('Signing data', function () { ].join(''), ) }) + + it('can create batch blob', function () { + const flags = 1 + const txIDs = [ + 'ABE4871E9083DF66727045D49DEEDD3A6F166EB7F8D1E92FE868F02E76B2C5CA', + '795AAC88B59E95C3497609749127E69F12958BC016C600C770AEEB1474C840B4', + ] + const json = { flags, txIDs } + const actual = encodeForSigningBatch(json) + expect(actual).toBe( + [ + // hash prefix + '42434800', + // flags + '00000001', + // txIds length + '00000002', + // txIds + 'ABE4871E9083DF66727045D49DEEDD3A6F166EB7F8D1E92FE868F02E76B2C5CA', + '795AAC88B59E95C3497609749127E69F12958BC016C600C770AEEB1474C840B4', + ].join(''), + ) + }) }) diff --git a/packages/xrpl/.eslintrc.js b/packages/xrpl/.eslintrc.js index 2321616dfa..9e4eabd1db 100644 --- a/packages/xrpl/.eslintrc.js +++ b/packages/xrpl/.eslintrc.js @@ -66,6 +66,7 @@ module.exports = { 'tsdoc/syntax': 'off', 'jsdoc/require-description-complete-sentence': 'off', 'import/prefer-default-export': 'off', + 'max-depth': ['warn', 3], }, overrides: [ { @@ -155,7 +156,6 @@ module.exports = { 'max-lines-per-function': ['off'], 'max-statements': ['off'], complexity: ['off'], - 'max-depth': ['warn', 3], }, }, ], diff --git a/packages/xrpl/HISTORY.md b/packages/xrpl/HISTORY.md index b834707f69..f30e604af2 100644 --- a/packages/xrpl/HISTORY.md +++ b/packages/xrpl/HISTORY.md @@ -4,6 +4,9 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr ## Unreleased Changes +### Added +* Add support for `Batch` amendment (XLS-56). + ## 4.1.0 (2024-12-23) ### Added diff --git a/packages/xrpl/package.json b/packages/xrpl/package.json index beb5408ec8..88e485aed0 100644 --- a/packages/xrpl/package.json +++ b/packages/xrpl/package.json @@ -64,7 +64,7 @@ "test": "jest --config=jest.config.unit.js --verbose false --silent=false", "test:integration": "TS_NODE_PROJECT=tsconfig.build.json jest --config=jest.config.integration.js --verbose false --silent=false --runInBand", "test:browser": "npm run build && npm run build:browserTests && karma start ./karma.config.js", - "test:watch": "jest --watch --verbose false --silent=false --runInBand ./test/**/*.test.ts --testPathIgnorePatterns=./test/integration --testPathIgnorePatterns=./test/fixtures", + "test:watch": "jest --watch --config=jest.config.unit.js --verbose false --silent=false", "format": "prettier --write '{src,test}/**/*.ts'", "lint": "eslint . --ext .ts --max-warnings 0", "perf": "./scripts/perf_test.sh", diff --git a/packages/xrpl/src/Wallet/batchSigner.ts b/packages/xrpl/src/Wallet/batchSigner.ts new file mode 100644 index 0000000000..e4fe4f5b7d --- /dev/null +++ b/packages/xrpl/src/Wallet/batchSigner.ts @@ -0,0 +1,222 @@ +import { bytesToHex } from '@xrplf/isomorphic/utils' +import BigNumber from 'bignumber.js' +import { decodeAccountID } from 'ripple-address-codec' +import { decode, encode, encodeForSigningBatch } from 'ripple-binary-codec' +import { sign } from 'ripple-keypairs' + +import { ValidationError } from '../errors' +import { Batch, Transaction, validate } from '../models' +import { BatchSigner, validateBatch } from '../models/transactions/batch' +import { hashSignedTx } from '../utils/hashes' + +import { Wallet } from '.' + +/** + * Sign a multi-account Batch transaction. + * + * @param wallet - Wallet instance. + * @param transaction - The Batch transaction to sign. + * @param opts - Additional options for regular key and multi-signing complexity. + * @param opts.batchAccount - The account submitting the inner Batch transaction, on behalf of which is this signature. + * @param opts.multisign - Specify true/false to use multisign or actual address (classic/x-address) to make multisign tx request. + * The actual address is only needed in the case of regular key usage. + * @throws ValidationError if the transaction is malformed. + */ +// eslint-disable-next-line max-lines-per-function -- TODO: refactor +export function signMultiBatch( + wallet: Wallet, + transaction: Batch, + opts: { batchAccount?: string; multisign?: boolean | string } = {}, +): void { + const batchAccount = opts.batchAccount ?? wallet.classicAddress + let multisignAddress: boolean | string = false + if (typeof opts.multisign === 'string') { + multisignAddress = opts.multisign + } else if (opts.multisign) { + multisignAddress = wallet.classicAddress + } + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- needed for JS + if (transaction.TransactionType !== 'Batch') { + throw new ValidationError('Must be a Batch transaction.') + } + + const involvedAccounts = transaction.RawTransactions.map( + (raw) => raw.RawTransaction.Account, + ) + if (!involvedAccounts.includes(batchAccount)) { + throw new ValidationError( + 'Must be signing for an address included in the Batch.', + ) + } + /* + * This will throw a more clear error for JS users if the supplied transaction has incorrect formatting + */ + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- validate does not accept Transaction type + validate(transaction as unknown as Record) + const fieldsToSign = { + flags: transaction.Flags, + txIDs: transaction.RawTransactions.map((rawTx) => + hashSignedTx(rawTx.RawTransaction), + ), + } + let batchSigner: BatchSigner + if (multisignAddress) { + batchSigner = { + BatchSigner: { + Account: batchAccount, + Signers: [ + { + Signer: { + Account: multisignAddress, + SigningPubKey: wallet.publicKey, + TxnSignature: sign( + encodeForSigningBatch(fieldsToSign), + wallet.privateKey, + ), + }, + }, + ], + }, + } + } else { + batchSigner = { + BatchSigner: { + Account: batchAccount, + SigningPubKey: wallet.publicKey, + TxnSignature: sign( + encodeForSigningBatch(fieldsToSign), + wallet.privateKey, + ), + }, + } + } + + // eslint-disable-next-line no-param-reassign -- okay for signing + transaction.BatchSigners = [batchSigner] +} + +/** + * Takes several transactions with BatchSigners fields (in object or blob form) and creates a + * single transaction with all BatchSigners that then gets signed and returned. + * + * @param transactions The transactions to combine `BatchSigners` values on. + * @returns A single signed Transaction which has all BatchSigners from transactions within it. + * @throws ValidationError if: + * - There were no transactions given to sign + * @category Signing + */ +export function combineBatchSigners( + transactions: Array, +): string { + if (transactions.length === 0) { + throw new ValidationError('There are 0 transactions to combine.') + } + + const decodedTransactions: Transaction[] = transactions.map((txOrBlob) => { + return getDecodedTransaction(txOrBlob) + }) + + decodedTransactions.forEach((tx) => { + if (tx.TransactionType !== 'Batch') { + throw new ValidationError('TransactionType must be `Batch`.') + } + /* + * This will throw a more clear error for JS users if any of the supplied transactions has incorrect formatting + */ + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- validate does not accept Transaction type + validateBatch(tx as unknown as Record) + if (tx.BatchSigners == null || tx.BatchSigners.length === 0) { + throw new ValidationError( + 'For combining Batch transaction signatures, all transactions must include a BatchSigners field containing an array of signatures.', + ) + } + + if (tx.TxnSignature != null || tx.Signers != null) { + throw new ValidationError('Batch transaction must be unsigned.') + } + }) + + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- checked above + const batchTransactions = decodedTransactions as Batch[] + + validateBatchTransactionEquivalence(batchTransactions) + + return encode(getTransactionWithAllBatchSigners(batchTransactions)) +} + +/** + * The transactions should all be equal except for the 'Signers' field. + * + * @param transactions - An array of Transactions which are expected to be equal other than 'Signers'. + * @throws ValidationError if the transactions are not equal in any field other than 'Signers'. + */ +function validateBatchTransactionEquivalence(transactions: Batch[]): void { + const exampleTransaction = JSON.stringify({ + flags: transactions[0].Flags, + transactionIDs: transactions[0].RawTransactions.map((rawTx) => + hashSignedTx(rawTx.RawTransaction), + ), + }) + if ( + transactions.slice(1).some( + (tx) => + JSON.stringify({ + flags: tx.Flags, + transactionIDs: tx.RawTransactions.map((rawTx) => + hashSignedTx(rawTx.RawTransaction), + ), + }) !== exampleTransaction, + ) + ) { + throw new ValidationError( + 'Flags and transaction hashes are not the same for all provided transactions.', + ) + } +} + +function getTransactionWithAllBatchSigners(transactions: Batch[]): Batch { + // Signers must be sorted in the combined transaction - See compareSigners' documentation for more details + const sortedSigners: BatchSigner[] = transactions + .flatMap((tx) => tx.BatchSigners ?? []) + .filter((signer) => signer.BatchSigner.Account !== transactions[0].Account) + .sort(compareBatchSigners) + + return { ...transactions[0], BatchSigners: sortedSigners } +} + +/** + * If presented in binary form, the BatchSigners array must be sorted based on + * the numeric value of the signer addresses, with the lowest value first. + * (If submitted as JSON, the submit_multisigned method handles this automatically.) + * https://xrpl.org/multi-signing.html. + * + * @param left - A BatchSigner to compare with. + * @param right - A second BatchSigner to compare with. + * @returns 1 if left \> right, 0 if left = right, -1 if left \< right, and null if left or right are NaN. + */ +function compareBatchSigners(left: BatchSigner, right: BatchSigner): number { + return addressToBigNumber(left.BatchSigner.Account).comparedTo( + addressToBigNumber(right.BatchSigner.Account), + ) +} + +// copied from signer.ts +// TODO: refactor +const NUM_BITS_IN_HEX = 16 + +function addressToBigNumber(address: string): BigNumber { + const hex = bytesToHex(decodeAccountID(address)) + return new BigNumber(hex, NUM_BITS_IN_HEX) +} + +function getDecodedTransaction(txOrBlob: Transaction | string): Transaction { + if (typeof txOrBlob === 'object') { + // We need this to handle X-addresses in multisigning + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- We are casting here to get strong typing + return decode(encode(txOrBlob)) as unknown as Transaction + } + + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- We are casting here to get strong typing + return decode(txOrBlob) as unknown as Transaction +} diff --git a/packages/xrpl/src/Wallet/defaultFaucets.ts b/packages/xrpl/src/Wallet/defaultFaucets.ts index bf5c38ae51..9c8c079f52 100644 --- a/packages/xrpl/src/Wallet/defaultFaucets.ts +++ b/packages/xrpl/src/Wallet/defaultFaucets.ts @@ -14,11 +14,13 @@ export interface FaucetWallet { export enum FaucetNetwork { Testnet = 'faucet.altnet.rippletest.net', Devnet = 'faucet.devnet.rippletest.net', + Batchnet = 'batch.faucet.nerdnest.xyz', } export const FaucetNetworkPaths: Record = { [FaucetNetwork.Testnet]: '/accounts', [FaucetNetwork.Devnet]: '/accounts', + [FaucetNetwork.Batchnet]: '/accounts', } /** @@ -36,6 +38,10 @@ export function getFaucetHost(client: Client): FaucetNetwork | undefined { return FaucetNetwork.Testnet } + if (connectionUrl.includes('batchnet')) { + return FaucetNetwork.Batchnet + } + if (connectionUrl.includes('sidechain-net2')) { throw new XRPLFaucetError( 'Cannot fund an account on an issuing chain. Accounts must be created via the bridge.', diff --git a/packages/xrpl/src/Wallet/index.ts b/packages/xrpl/src/Wallet/index.ts index c5ce5baca5..db1f4a313b 100644 --- a/packages/xrpl/src/Wallet/index.ts +++ b/packages/xrpl/src/Wallet/index.ts @@ -24,6 +24,8 @@ import { import ECDSA from '../ECDSA' import { ValidationError } from '../errors' import { Transaction, validate } from '../models/transactions' +import { GlobalFlags } from '../models/transactions/common' +import { hasFlag } from '../models/utils/flags' import { ensureClassicAddress } from '../sugar/utils' import { omitBy } from '../utils/collections' import { hashSignedTx } from '../utils/hashes/hashLedger' @@ -367,6 +369,7 @@ export class Wallet { * @param this - Wallet instance. * @param transaction - A transaction to be signed offline. * @param multisign - Specify true/false to use multisign or actual address (classic/x-address) to make multisign tx request. + * The actual address is only needed in the case of regular key usage. * @returns A signed transaction. * @throws ValidationError if the transaction is already signed or does not encode/decode to same result. * @throws XrplError if the issued currency being signed is XRP ignoring case. @@ -381,7 +384,7 @@ export class Wallet { hash: string } { let multisignAddress: boolean | string = false - if (typeof multisign === 'string' && multisign.startsWith('X')) { + if (typeof multisign === 'string') { multisignAddress = multisign } else if (multisign) { multisignAddress = this.classicAddress @@ -407,6 +410,9 @@ export class Wallet { */ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- validate does not accept Transaction type validate(tx as unknown as Record) + if (hasFlag(tx, GlobalFlags.tfInnerBatchTxn)) { + throw new ValidationError('Cannot sign a Batch inner transaction.') + } const txToSignAndEncode = { ...tx } diff --git a/packages/xrpl/src/client/RequestManager.ts b/packages/xrpl/src/client/RequestManager.ts index 79712140ac..f91ef9c55f 100644 --- a/packages/xrpl/src/client/RequestManager.ts +++ b/packages/xrpl/src/client/RequestManager.ts @@ -175,6 +175,7 @@ export default class RequestManager { * @param response - The response to handle. * @throws ResponseFormatError if the response format is invalid, RippledError if rippled returns an error. */ + // eslint-disable-next-line complexity -- handling a response is complex public handleResponse( response: Partial | ErrorResponse>, ): void { @@ -195,7 +196,9 @@ export default class RequestManager { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- We know this must be true const errorResponse = response as Partial const error = new RippledError( - errorResponse.error_message ?? errorResponse.error, + errorResponse.error_message ?? + errorResponse.error_exception ?? + errorResponse.error, errorResponse, ) this.reject(response.id, error) diff --git a/packages/xrpl/src/client/connection.ts b/packages/xrpl/src/client/connection.ts index 3214b557e2..7173bc45c3 100644 --- a/packages/xrpl/src/client/connection.ts +++ b/packages/xrpl/src/client/connection.ts @@ -349,7 +349,6 @@ export class Connection extends EventEmitter { try { this.requestManager.handleResponse(data) } catch (error) { - // eslint-disable-next-line max-depth -- okay here if (error instanceof Error) { this.emit('error', 'badMessage', error.message, message) } else { diff --git a/packages/xrpl/src/client/index.ts b/packages/xrpl/src/client/index.ts index 722b169f2a..5cd3947c80 100644 --- a/packages/xrpl/src/client/index.ts +++ b/packages/xrpl/src/client/index.ts @@ -62,6 +62,8 @@ import { setLatestValidatedLedgerSequence, checkAccountDeleteBlockers, txNeedsNetworkID, + autofillBatchTxn, + handleDeliverMax, } from '../sugar/autofill' import { formatBalances } from '../sugar/balances' import { @@ -657,7 +659,6 @@ class Client extends EventEmitter { * @throws ValidationError If Amount and DeliverMax fields are not identical in a Payment Transaction */ - // eslint-disable-next-line complexity -- handling Payment transaction API v2 requires more logic public async autofill( transaction: T, signersCount?: number, @@ -683,33 +684,11 @@ class Client extends EventEmitter { if (tx.TransactionType === 'AccountDelete') { promises.push(checkAccountDeleteBlockers(this, tx)) } - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment -- ignore type-assertions on the DeliverMax property - // @ts-expect-error -- DeliverMax property exists only at the RPC level, not at the protocol level - if (tx.TransactionType === 'Payment' && tx.DeliverMax != null) { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- This is a valid null check for Amount - if (tx.Amount == null) { - // If only DeliverMax is provided, use it to populate the Amount field - // eslint-disable-next-line @typescript-eslint/ban-ts-comment -- ignore type-assertions on the DeliverMax property - // @ts-expect-error -- DeliverMax property exists only at the RPC level, not at the protocol level - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- DeliverMax is a known RPC-level property - tx.Amount = tx.DeliverMax - } - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment -- ignore type-assertions on the DeliverMax property - // @ts-expect-error -- DeliverMax property exists only at the RPC level, not at the protocol level - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- This is a valid null check for Amount - if (tx.Amount != null && tx.Amount !== tx.DeliverMax) { - return Promise.reject( - new ValidationError( - 'PaymentTransaction: Amount and DeliverMax fields must be identical when both are provided', - ), - ) - } - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment -- ignore type-assertions on the DeliverMax property - // @ts-expect-error -- DeliverMax property exists only at the RPC level, not at the protocol level - delete tx.DeliverMax + if (tx.TransactionType === 'Batch') { + promises.push(autofillBatchTxn(this, tx)) + } + if (tx.TransactionType === 'Payment') { + handleDeliverMax(tx) } return Promise.all(promises).then(() => tx) diff --git a/packages/xrpl/src/models/ledger/Credential.ts b/packages/xrpl/src/models/ledger/Credential.ts index 7716409ece..50cf83ba0b 100644 --- a/packages/xrpl/src/models/ledger/Credential.ts +++ b/packages/xrpl/src/models/ledger/Credential.ts @@ -1,8 +1,8 @@ -import { GlobalFlags } from '../transactions/common' +import { GlobalFlagsInterface } from '../transactions/common' import { BaseLedgerEntry, HasPreviousTxnID } from './BaseLedgerEntry' -export interface CredentialFlags extends GlobalFlags { +export interface CredentialFlags extends GlobalFlagsInterface { lsfAccepted?: boolean } diff --git a/packages/xrpl/src/models/methods/baseMethod.ts b/packages/xrpl/src/models/methods/baseMethod.ts index 85dcf90efb..f264c2bd32 100644 --- a/packages/xrpl/src/models/methods/baseMethod.ts +++ b/packages/xrpl/src/models/methods/baseMethod.ts @@ -53,6 +53,7 @@ export interface ErrorResponse { error: string error_code?: string error_message?: string + error_exception?: string request: Request api_version?: number } diff --git a/packages/xrpl/src/models/transactions/AMMDeposit.ts b/packages/xrpl/src/models/transactions/AMMDeposit.ts index 2dd8d27e39..884d06ac4c 100644 --- a/packages/xrpl/src/models/transactions/AMMDeposit.ts +++ b/packages/xrpl/src/models/transactions/AMMDeposit.ts @@ -3,7 +3,7 @@ import { Amount, Currency, IssuedCurrencyAmount } from '../common' import { BaseTransaction, - GlobalFlags, + GlobalFlagsInterface, isAmount, isCurrency, isIssuedCurrency, @@ -24,7 +24,7 @@ export enum AMMDepositFlags { tfTwoAssetIfEmpty = 0x00800000, } -export interface AMMDepositFlagsInterface extends GlobalFlags { +export interface AMMDepositFlagsInterface extends GlobalFlagsInterface { tfLPToken?: boolean tfSingleAsset?: boolean tfTwoAsset?: boolean diff --git a/packages/xrpl/src/models/transactions/AMMWithdraw.ts b/packages/xrpl/src/models/transactions/AMMWithdraw.ts index fcce5912b3..bc668afff1 100644 --- a/packages/xrpl/src/models/transactions/AMMWithdraw.ts +++ b/packages/xrpl/src/models/transactions/AMMWithdraw.ts @@ -3,7 +3,7 @@ import { Amount, Currency, IssuedCurrencyAmount } from '../common' import { BaseTransaction, - GlobalFlags, + GlobalFlagsInterface, isAmount, isCurrency, isIssuedCurrency, @@ -25,7 +25,7 @@ export enum AMMWithdrawFlags { tfLimitLPToken = 0x00400000, } -export interface AMMWithdrawFlagsInterface extends GlobalFlags { +export interface AMMWithdrawFlagsInterface extends GlobalFlagsInterface { tfLPToken?: boolean tfWithdrawAll?: boolean tfOneAssetWithdrawAll?: boolean diff --git a/packages/xrpl/src/models/transactions/MPTokenAuthorize.ts b/packages/xrpl/src/models/transactions/MPTokenAuthorize.ts index 4453f571e7..c796677492 100644 --- a/packages/xrpl/src/models/transactions/MPTokenAuthorize.ts +++ b/packages/xrpl/src/models/transactions/MPTokenAuthorize.ts @@ -6,7 +6,7 @@ import { Account, validateOptionalField, isAccount, - GlobalFlags, + GlobalFlagsInterface, } from './common' /** @@ -32,7 +32,7 @@ export enum MPTokenAuthorizeFlags { * * @category Transaction Flags */ -export interface MPTokenAuthorizeFlagsInterface extends GlobalFlags { +export interface MPTokenAuthorizeFlagsInterface extends GlobalFlagsInterface { tfMPTUnauthorize?: boolean } diff --git a/packages/xrpl/src/models/transactions/MPTokenIssuanceCreate.ts b/packages/xrpl/src/models/transactions/MPTokenIssuanceCreate.ts index 6566e4ce05..c84f7ffa33 100644 --- a/packages/xrpl/src/models/transactions/MPTokenIssuanceCreate.ts +++ b/packages/xrpl/src/models/transactions/MPTokenIssuanceCreate.ts @@ -3,7 +3,7 @@ import { isHex, INTEGER_SANITY_CHECK, isFlagEnabled } from '../utils' import { BaseTransaction, - GlobalFlags, + GlobalFlagsInterface, validateBaseTransaction, validateOptionalField, isString, @@ -58,7 +58,8 @@ export enum MPTokenIssuanceCreateFlags { * * @category Transaction Flags */ -export interface MPTokenIssuanceCreateFlagsInterface extends GlobalFlags { +export interface MPTokenIssuanceCreateFlagsInterface + extends GlobalFlagsInterface { tfMPTCanLock?: boolean tfMPTRequireAuth?: boolean tfMPTCanEscrow?: boolean diff --git a/packages/xrpl/src/models/transactions/MPTokenIssuanceSet.ts b/packages/xrpl/src/models/transactions/MPTokenIssuanceSet.ts index 7e1c723382..fcfc5dd1ea 100644 --- a/packages/xrpl/src/models/transactions/MPTokenIssuanceSet.ts +++ b/packages/xrpl/src/models/transactions/MPTokenIssuanceSet.ts @@ -9,7 +9,7 @@ import { Account, validateOptionalField, isAccount, - GlobalFlags, + GlobalFlagsInterface, } from './common' /** @@ -34,7 +34,7 @@ export enum MPTokenIssuanceSetFlags { * * @category Transaction Flags */ -export interface MPTokenIssuanceSetFlagsInterface extends GlobalFlags { +export interface MPTokenIssuanceSetFlagsInterface extends GlobalFlagsInterface { tfMPTLock?: boolean tfMPTUnlock?: boolean } diff --git a/packages/xrpl/src/models/transactions/NFTokenCreateOffer.ts b/packages/xrpl/src/models/transactions/NFTokenCreateOffer.ts index 9575d1b6be..ae42ca4874 100644 --- a/packages/xrpl/src/models/transactions/NFTokenCreateOffer.ts +++ b/packages/xrpl/src/models/transactions/NFTokenCreateOffer.ts @@ -4,7 +4,7 @@ import { isFlagEnabled } from '../utils' import { BaseTransaction, - GlobalFlags, + GlobalFlagsInterface, validateBaseTransaction, isAmount, parseAmountValue, @@ -33,7 +33,7 @@ export enum NFTokenCreateOfferFlags { * * @category Transaction Flags */ -export interface NFTokenCreateOfferFlagsInterface extends GlobalFlags { +export interface NFTokenCreateOfferFlagsInterface extends GlobalFlagsInterface { tfSellNFToken?: boolean } diff --git a/packages/xrpl/src/models/transactions/NFTokenMint.ts b/packages/xrpl/src/models/transactions/NFTokenMint.ts index 2630a6b9c6..6fe03e0241 100644 --- a/packages/xrpl/src/models/transactions/NFTokenMint.ts +++ b/packages/xrpl/src/models/transactions/NFTokenMint.ts @@ -4,7 +4,7 @@ import { isHex } from '../utils' import { Account, BaseTransaction, - GlobalFlags, + GlobalFlagsInterface, isAccount, validateBaseTransaction, validateOptionalField, @@ -46,7 +46,7 @@ export enum NFTokenMintFlags { * * @category Transaction Flags */ -export interface NFTokenMintFlagsInterface extends GlobalFlags { +export interface NFTokenMintFlagsInterface extends GlobalFlagsInterface { tfBurnable?: boolean tfOnlyXRP?: boolean tfTrustLine?: boolean diff --git a/packages/xrpl/src/models/transactions/XChainModifyBridge.ts b/packages/xrpl/src/models/transactions/XChainModifyBridge.ts index 841960b8f0..4825106b79 100644 --- a/packages/xrpl/src/models/transactions/XChainModifyBridge.ts +++ b/packages/xrpl/src/models/transactions/XChainModifyBridge.ts @@ -2,7 +2,7 @@ import { Amount, XChainBridge } from '../common' import { BaseTransaction, - GlobalFlags, + GlobalFlagsInterface, isAmount, isXChainBridge, validateBaseTransaction, @@ -26,7 +26,7 @@ export enum XChainModifyBridgeFlags { * * @category Transaction Flags */ -export interface XChainModifyBridgeFlagsInterface extends GlobalFlags { +export interface XChainModifyBridgeFlagsInterface extends GlobalFlagsInterface { /** Clears the MinAccountCreateAmount of the bridge. */ tfClearAccountCreateAmount?: boolean } diff --git a/packages/xrpl/src/models/transactions/accountSet.ts b/packages/xrpl/src/models/transactions/accountSet.ts index 1d4c9078a5..8a74c51e6e 100644 --- a/packages/xrpl/src/models/transactions/accountSet.ts +++ b/packages/xrpl/src/models/transactions/accountSet.ts @@ -3,6 +3,7 @@ import { ValidationError } from '../../errors' import { Account, BaseTransaction, + GlobalFlagsInterface, isAccount, validateBaseTransaction, validateOptionalField, @@ -112,7 +113,7 @@ export enum AccountSetTfFlags { * // } * ``` */ -export interface AccountSetFlagsInterface { +export interface AccountSetFlagsInterface extends GlobalFlagsInterface { tfRequireDestTag?: boolean tfOptionalDestTag?: boolean tfRequireAuth?: boolean diff --git a/packages/xrpl/src/models/transactions/batch.ts b/packages/xrpl/src/models/transactions/batch.ts new file mode 100644 index 0000000000..6448e5a0e8 --- /dev/null +++ b/packages/xrpl/src/models/transactions/batch.ts @@ -0,0 +1,147 @@ +import { ValidationError } from '../../errors' +import { Signer } from '../common' + +import { + BaseTransaction, + GlobalFlagsInterface, + isArray, + isObject, + isString, + validateBaseTransaction, + validateOptionalField, + validateRequiredField, +} from './common' +import type { SubmittableTransaction } from './transaction' + +/** + * Enum representing values of {@link Batch} transaction flags. + * + * @category Transaction Flags + */ +export enum BatchFlags { + tfAllOrNothing = 0x00010000, + tfOnlyOne = 0x00020000, + tfUntilFailure = 0x00040000, + tfIndependent = 0x00080000, +} + +/** + * Map of flags to boolean values representing {@link Batch} transaction + * flags. + * + * @category Transaction Flags + */ +export interface BatchFlagsInterface extends GlobalFlagsInterface { + tfAllOrNothing?: boolean + tfOnlyOne?: boolean + tfUntilFailure?: boolean + tfIndependent?: boolean +} + +export type BatchInnerTransaction = SubmittableTransaction & { + Fee?: '0' + + SigningPubKey?: '' + + TxnSignature?: never + + Signers?: never + + LastLedgerSequence?: never +} + +export interface BatchSigner { + BatchSigner: { + Account: string + + SigningPubKey?: string + + TxnSignature?: string + + Signers?: Signer[] + } +} + +/** + * @category Transaction Models + */ +export interface Batch extends BaseTransaction { + TransactionType: 'Batch' + + BatchSigners?: BatchSigner[] + + RawTransactions: Array<{ + RawTransaction: BatchInnerTransaction + }> +} + +/** + * Verify the form and type of a Batch at runtime. + * + * @param tx - A Batch Transaction. + * @throws When the Batch is malformed. + */ +// eslint-disable-next-line max-lines-per-function -- needed here due to the complexity +export function validateBatch(tx: Record): void { + validateBaseTransaction(tx) + + validateRequiredField(tx, 'RawTransactions', isArray) + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- checked above + const rawTransactions = tx.RawTransactions as unknown[] + rawTransactions.forEach((rawTxObj, index) => { + if (!isObject(rawTxObj)) { + throw new ValidationError( + `Batch: RawTransactions[${index}] is not object.`, + ) + } + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- checked above + const rawTxRecord = rawTxObj as Record + validateRequiredField(rawTxRecord, 'RawTransaction', isObject, { + paramName: `RawTransactions[${index}].RawTransaction`, + txType: 'Batch', + }) + + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- checked above + const rawTx = rawTxRecord.RawTransaction as Record + if (rawTx.TransactionType === 'Batch') { + throw new ValidationError( + `Batch: RawTransactions[${index}] is a Batch transaction. Cannot nest Batch transactions.`, + ) + } + }) + // Full validation of each `RawTransaction` object is done in `validate` to avoid dependency cycles + + validateOptionalField(tx, 'BatchSigners', isArray) + + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- checked above + const batchSigners = tx.BatchSigners as unknown[] | undefined + batchSigners?.forEach((signerObj, index) => { + if (!isObject(signerObj)) { + throw new ValidationError(`Batch: BatchSigners[${index}] is not object.`) + } + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- checked above + const signerRecord = signerObj as Record + validateRequiredField(signerRecord, 'BatchSigner', isObject, { + paramName: `BatchSigners[${index}].BatchSigner`, + txType: 'Batch', + }) + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- checked above + const signer = signerRecord.BatchSigner as Record + validateRequiredField(signer, 'Account', isString, { + paramName: `BatchSigners[${index}].Account`, + txType: 'Batch', + }) + validateOptionalField(signer, 'SigningPubKey', isString, { + paramName: `BatchSigners[${index}].SigningPubKey`, + txType: 'Batch', + }) + validateOptionalField(signer, 'TxnSignature', isString, { + paramName: `BatchSigners[${index}].TxnSignature`, + txType: 'Batch', + }) + validateOptionalField(signer, 'Signers', isArray, { + paramName: `BatchSigners[${index}].Signers`, + txType: 'Batch', + }) + }) +} diff --git a/packages/xrpl/src/models/transactions/common.ts b/packages/xrpl/src/models/transactions/common.ts index d82625355f..a2bcf6a858 100644 --- a/packages/xrpl/src/models/transactions/common.ts +++ b/packages/xrpl/src/models/transactions/common.ts @@ -208,31 +208,57 @@ export function isXChainBridge(input: unknown): input is XChainBridge { ) } +/** + * Verify the form and type of an Object at runtime. + * + * @param input - The object to check the form and type of. + * @returns Whether the Object is properly formed. + */ +export function isObject(input: unknown): input is object { + return typeof input === 'object' +} + +/** + * Verify the form and type of an Array at runtime. + * + * @param input - The object to check the form and type of. + * @returns Whether the Array is properly formed. + */ +export function isArray(input: unknown): boolean { + return Array.isArray(input) +} + /* 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 tx - The object input to check the form and type of. + * @param param - The object parameter. * @param checkValidity - The function to use to check the type. + * @param errorOpts - Extra values to make the error message easier to understand. + * @param errorOpts.txType - The transaction type throwing the error. + * @param errorOpts.paramName - The name of the parameter in the transaction with the error. * @throws */ +// eslint-disable-next-line max-params -- helper function export function validateRequiredField( tx: Record, - paramName: string, + param: string, checkValidity: (inp: unknown) => boolean, + errorOpts: { + txType?: string + paramName?: string + } = {}, ): void { - if (tx[paramName] == null) { - throw new ValidationError( - `${tx.TransactionType}: missing field ${paramName}`, - ) + const paramNameStr = errorOpts.paramName ?? param + const txType = errorOpts.txType ?? tx.TransactionType + if (tx[param] == null) { + throw new ValidationError(`${txType}: missing field ${paramNameStr}`) } - if (!checkValidity(tx[paramName])) { - throw new ValidationError( - `${tx.TransactionType}: invalid field ${paramName}`, - ) + if (!checkValidity(tx[param])) { + throw new ValidationError(`${txType}: invalid field ${paramNameStr}`) } } @@ -240,26 +266,39 @@ export function validateRequiredField( * 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 param - The object parameter. * @param checkValidity - The function to use to check the type. + * @param errorOpts - Extra values to make the error message easier to understand. + * @param errorOpts.txType - The transaction type throwing the error. + * @param errorOpts.paramName - The name of the parameter in the transaction with the error. * @throws */ +// eslint-disable-next-line max-params -- helper function export function validateOptionalField( tx: Record, - paramName: string, + param: string, checkValidity: (inp: unknown) => boolean, + errorOpts: { + txType?: string + paramName?: string + } = {}, ): void { - if (tx[paramName] !== undefined && !checkValidity(tx[paramName])) { - throw new ValidationError( - `${tx.TransactionType}: invalid field ${paramName}`, - ) + const paramNameStr = errorOpts.paramName ?? param + const txType = errorOpts.txType ?? tx.TransactionType + if (tx[param] !== undefined && !checkValidity(tx[param])) { + throw new ValidationError(`${txType}: invalid field ${paramNameStr}`) } } /* 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 {} +export enum GlobalFlags { + tfInnerBatchTxn = 0x40000000, +} + +export interface GlobalFlagsInterface { + tfInnerBatchTxn?: boolean +} /** * Every transaction has the same set of common fields. @@ -292,7 +331,7 @@ export interface BaseTransaction { */ AccountTxnID?: string /** Set of bit-flags for this transaction. */ - Flags?: number | GlobalFlags + Flags?: number | GlobalFlagsInterface /** * Highest ledger index this transaction can appear in. Specifying this field * places a strict upper limit on how long the transaction can wait to be @@ -355,7 +394,9 @@ export function validateBaseTransaction(common: Record): void { } if (!TRANSACTION_TYPES.includes(common.TransactionType)) { - throw new ValidationError('BaseTransaction: Unknown TransactionType') + throw new ValidationError( + `BaseTransaction: Unknown TransactionType ${common.TransactionType}`, + ) } validateRequiredField(common, 'Account', isString) diff --git a/packages/xrpl/src/models/transactions/index.ts b/packages/xrpl/src/models/transactions/index.ts index d7e7effadb..dd2c71b13d 100644 --- a/packages/xrpl/src/models/transactions/index.ts +++ b/packages/xrpl/src/models/transactions/index.ts @@ -105,3 +105,4 @@ export { XChainModifyBridgeFlags, XChainModifyBridgeFlagsInterface, } from './XChainModifyBridge' +export { Batch } from './batch' diff --git a/packages/xrpl/src/models/transactions/metadata.ts b/packages/xrpl/src/models/transactions/metadata.ts index 75551d1a06..3d6a6db53c 100644 --- a/packages/xrpl/src/models/transactions/metadata.ts +++ b/packages/xrpl/src/models/transactions/metadata.ts @@ -88,6 +88,8 @@ export interface TransactionMetadataBase { delivered_amount?: Amount | MPTAmount | 'unavailable' TransactionIndex: number TransactionResult: string + + ParentBatchID?: string } export type TransactionMetadata = diff --git a/packages/xrpl/src/models/transactions/offerCreate.ts b/packages/xrpl/src/models/transactions/offerCreate.ts index 782e635499..5870730e6a 100644 --- a/packages/xrpl/src/models/transactions/offerCreate.ts +++ b/packages/xrpl/src/models/transactions/offerCreate.ts @@ -3,7 +3,7 @@ import { Amount } from '../common' import { BaseTransaction, - GlobalFlags, + GlobalFlagsInterface, validateBaseTransaction, isAmount, } from './common' @@ -78,7 +78,7 @@ export enum OfferCreateFlags { * // } * ``` */ -export interface OfferCreateFlagsInterface extends GlobalFlags { +export interface OfferCreateFlagsInterface extends GlobalFlagsInterface { tfPassive?: boolean tfImmediateOrCancel?: boolean tfFillOrKill?: boolean diff --git a/packages/xrpl/src/models/transactions/payment.ts b/packages/xrpl/src/models/transactions/payment.ts index 25f3dc8974..d8e1bdf24d 100644 --- a/packages/xrpl/src/models/transactions/payment.ts +++ b/packages/xrpl/src/models/transactions/payment.ts @@ -5,7 +5,7 @@ import { isFlagEnabled } from '../utils' import { BaseTransaction, isAmount, - GlobalFlags, + GlobalFlagsInterface, validateBaseTransaction, isAccount, validateRequiredField, @@ -83,7 +83,7 @@ export enum PaymentFlags { * // } * ``` */ -export interface PaymentFlagsInterface extends GlobalFlags { +export interface PaymentFlagsInterface extends GlobalFlagsInterface { /** * Do not use the default path; only use paths included in the Paths field. * This is intended to force the transaction to take arbitrage opportunities. diff --git a/packages/xrpl/src/models/transactions/paymentChannelClaim.ts b/packages/xrpl/src/models/transactions/paymentChannelClaim.ts index f99368b388..78a9c3b570 100644 --- a/packages/xrpl/src/models/transactions/paymentChannelClaim.ts +++ b/packages/xrpl/src/models/transactions/paymentChannelClaim.ts @@ -2,7 +2,7 @@ import { ValidationError } from '../../errors' import { BaseTransaction, - GlobalFlags, + GlobalFlagsInterface, validateBaseTransaction, validateCredentialsList, } from './common' @@ -72,7 +72,8 @@ export enum PaymentChannelClaimFlags { * // } * ``` */ -export interface PaymentChannelClaimFlagsInterface extends GlobalFlags { +export interface PaymentChannelClaimFlagsInterface + extends GlobalFlagsInterface { /** * Clear the channel's Expiration time. (Expiration is different from the * channel's immutable CancelAfter time.) Only the source address of the diff --git a/packages/xrpl/src/models/transactions/transaction.ts b/packages/xrpl/src/models/transactions/transaction.ts index 44840187bb..1d947fde40 100644 --- a/packages/xrpl/src/models/transactions/transaction.ts +++ b/packages/xrpl/src/models/transactions/transaction.ts @@ -14,6 +14,7 @@ import { AMMDelete, validateAMMDelete } from './AMMDelete' import { AMMDeposit, validateAMMDeposit } from './AMMDeposit' import { AMMVote, validateAMMVote } from './AMMVote' import { AMMWithdraw, validateAMMWithdraw } from './AMMWithdraw' +import { Batch, validateBatch } from './batch' import { CheckCancel, validateCheckCancel } from './checkCancel' import { CheckCash, validateCheckCash } from './checkCash' import { CheckCreate, validateCheckCreate } from './checkCreate' @@ -121,6 +122,7 @@ export type SubmittableTransaction = | AMMWithdraw | AccountDelete | AccountSet + | Batch | CheckCancel | CheckCash | CheckCreate @@ -289,6 +291,19 @@ export function validate(transaction: Record): void { validateAccountSet(tx) break + case 'Batch': + validateBatch(tx) + // This is done here to avoid issues with dependency cycles + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment -- okay here + // @ts-expect-error -- already checked + // eslint-disable-next-line @typescript-eslint/no-unsafe-call -- already checked above + tx.RawTransactions.forEach((innerTx: Record) => { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- already checked above + validate(innerTx.RawTransaction as Record) + }) + break + case 'CheckCancel': validateCheckCancel(tx) break diff --git a/packages/xrpl/src/models/transactions/trustSet.ts b/packages/xrpl/src/models/transactions/trustSet.ts index f584261af3..e05d091e3e 100644 --- a/packages/xrpl/src/models/transactions/trustSet.ts +++ b/packages/xrpl/src/models/transactions/trustSet.ts @@ -3,7 +3,7 @@ import { IssuedCurrencyAmount } from '../common' import { BaseTransaction, - GlobalFlags, + GlobalFlagsInterface, isAmount, validateBaseTransaction, } from './common' @@ -72,7 +72,7 @@ export enum TrustSetFlags { * // } * ``` */ -export interface TrustSetFlagsInterface extends GlobalFlags { +export interface TrustSetFlagsInterface extends GlobalFlagsInterface { /** * Authorize the other party to hold currency issued by this account. (No * effect unless using the asfRequireAuth AccountSet flag.) Cannot be unset. diff --git a/packages/xrpl/src/models/utils/flags.ts b/packages/xrpl/src/models/utils/flags.ts index 0d04b0ecad..a386c5f903 100644 --- a/packages/xrpl/src/models/utils/flags.ts +++ b/packages/xrpl/src/models/utils/flags.ts @@ -8,7 +8,8 @@ import { import { AccountSetTfFlags } from '../transactions/accountSet' import { AMMDepositFlags } from '../transactions/AMMDeposit' import { AMMWithdrawFlags } from '../transactions/AMMWithdraw' -import { GlobalFlags } from '../transactions/common' +import { BatchFlags } from '../transactions/batch' +import { GlobalFlags, GlobalFlagsInterface } from '../transactions/common' import { MPTokenAuthorizeFlags } from '../transactions/MPTokenAuthorize' import { MPTokenIssuanceCreateFlags } from '../transactions/MPTokenIssuanceCreate' import { MPTokenIssuanceSetFlags } from '../transactions/MPTokenIssuanceSet' @@ -51,6 +52,7 @@ const txToFlag = { AccountSet: AccountSetTfFlags, AMMDeposit: AMMDepositFlags, AMMWithdraw: AMMWithdrawFlags, + Batch: BatchFlags, MPTokenAuthorize: MPTokenAuthorizeFlags, MPTokenIssuanceCreate: MPTokenIssuanceCreateFlags, MPTokenIssuanceSet: MPTokenIssuanceSetFlags, @@ -82,16 +84,17 @@ export function setTransactionFlagsToNumber(tx: Transaction): void { : 0 } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -- added ValidationError check for flagEnum -function convertFlagsToNumber(flags: GlobalFlags, flagEnum: any): number { +function convertFlagsToNumber( + flags: GlobalFlagsInterface, + flagEnum: object, +): number { return Object.keys(flags).reduce((resultFlags, flag) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- safe member access if (flagEnum[flag] == null) { throw new ValidationError( `flag ${flag} doesn't exist in flagEnum: ${JSON.stringify(flagEnum)}`, ) } - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- safe member access + return flags[flag] ? resultFlags | flagEnum[flag] : resultFlags }, 0) } @@ -122,3 +125,24 @@ export function parseTransactionFlags(tx: Transaction): object { return flagsMap } + +/** + * Determines whether a transaction has a certain flag enabled. + * + * @param tx The transaction. + * @param flag The flag to check. + * @returns Whether `flag` is enabled on `tx`. + */ +export function hasFlag(tx: Transaction, flag: number): boolean { + if (tx.Flags == null) { + return false + } + if (typeof tx.Flags === 'number') { + return isFlagEnabled(tx.Flags, flag) + } + const txFlagNum = convertFlagsToNumber( + tx.Flags, + txToFlag[tx.TransactionType] ?? GlobalFlags, + ) + return isFlagEnabled(txFlagNum, flag) +} diff --git a/packages/xrpl/src/sugar/autofill.ts b/packages/xrpl/src/sugar/autofill.ts index ca0757611d..3352a74da5 100644 --- a/packages/xrpl/src/sugar/autofill.ts +++ b/packages/xrpl/src/sugar/autofill.ts @@ -1,10 +1,12 @@ +/* eslint-disable max-lines -- lots of helper functions needed for autofill */ import BigNumber from 'bignumber.js' import { xAddressToClassicAddress, isValidXAddress } from 'ripple-address-codec' import { type Client } from '..' import { ValidationError, XrplError } from '../errors' import { AccountInfoRequest, AccountObjectsRequest } from '../models/methods' -import { Transaction } from '../models/transactions' +import { Batch, Payment, Transaction } from '../models/transactions' +import { GlobalFlags } from '../models/transactions/common' import { xrpToDrops } from '../utils' import getFeeXrp from './getFeeXrp' @@ -207,6 +209,20 @@ function convertToClassicAddress(tx: Transaction, fieldName: string): void { } } +// Helper function to get the next valid sequence number for an account. +async function getNextValidSequenceNumber( + client: Client, + account: string, +): Promise { + const request: AccountInfoRequest = { + command: 'account_info', + account, + ledger_index: 'current', + } + const data = await client.request(request) + return data.result.account_data.Sequence +} + /** * Sets the next valid sequence number for a transaction. * @@ -219,14 +235,8 @@ export async function setNextValidSequenceNumber( client: Client, tx: Transaction, ): Promise { - const request: AccountInfoRequest = { - command: 'account_info', - account: tx.Account, - ledger_index: 'current', - } - const data = await client.request(request) // eslint-disable-next-line no-param-reassign, require-atomic-updates -- param reassign is safe with no race condition - tx.Sequence = data.result.account_data.Sequence + tx.Sequence = await getNextValidSequenceNumber(client, tx.Account) } /** @@ -274,13 +284,16 @@ export async function calculateFeePerTransactionType( scaleValue(netFeeDrops, 33 + fulfillmentBytesSize / 16), ) baseFee = product.dp(0, BigNumber.ROUND_CEIL) - } - - if ( + } else if ( tx.TransactionType === 'AccountDelete' || tx.TransactionType === 'AMMCreate' ) { baseFee = await fetchAccountDeleteFee(client) + } else if (tx.TransactionType === 'Batch') { + baseFee = BigNumber.sum( + baseFee.times(2), + baseFee.times(tx.RawTransactions.length + (tx.BatchSigners?.length ?? 0)), + ) } /* @@ -359,3 +372,119 @@ export async function checkAccountDeleteBlockers( resolve() }) } +/** + * Replaces Amount with DeliverMax if needed. + * + * @param tx - The transaction object. + * @throws ValidationError if Amount and DeliverMax are both provided but do not match. + */ +export function handleDeliverMax(tx: Payment): void { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment -- ignore type-assertions on the DeliverMax property + // @ts-expect-error -- DeliverMax property exists only at the RPC level, not at the protocol level + if (tx.DeliverMax != null) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- needed here + if (tx.Amount == null) { + // If only DeliverMax is provided, use it to populate the Amount field + // eslint-disable-next-line @typescript-eslint/ban-ts-comment -- ignore type-assertions on the DeliverMax property + // @ts-expect-error -- DeliverMax property exists only at the RPC level, not at the protocol level + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, no-param-reassign -- known RPC-level property + tx.Amount = tx.DeliverMax + } + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment -- ignore type-assertions on the DeliverMax property + // @ts-expect-error -- DeliverMax property exists only at the RPC level, not at the protocol level + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- needed here + if (tx.Amount != null && tx.Amount !== tx.DeliverMax) { + throw new ValidationError( + 'PaymentTransaction: Amount and DeliverMax fields must be identical when both are provided', + ) + } + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment -- ignore type-assertions on the DeliverMax property + // @ts-expect-error -- DeliverMax property exists only at the RPC level, not at the protocol level + // eslint-disable-next-line no-param-reassign -- needed here + delete tx.DeliverMax + } +} + +/** + * Autofills all the relevant `x` fields. + * + * @param client - The client object. + * @param tx - The transaction object. + * @returns A promise that resolves with void if there are no blockers, or rejects with an XrplError if there are blockers. + */ +// eslint-disable-next-line complexity, max-lines-per-function, max-statements -- needed here, lots to check +export async function autofillBatchTxn( + client: Client, + tx: Batch, +): Promise { + const accountSequences: Record = {} + + for await (const rawTxn of tx.RawTransactions) { + const txn = rawTxn.RawTransaction + + // Flag processing + /* eslint-disable no-bitwise -- needed here for flag parsing */ + if (txn.Flags == null) { + txn.Flags = GlobalFlags.tfInnerBatchTxn + } else if (typeof txn.Flags === 'number') { + if (!((txn.Flags & GlobalFlags.tfInnerBatchTxn) === 0)) { + txn.Flags |= GlobalFlags.tfInnerBatchTxn + } + } else if (!txn.Flags.tfInnerBatchTxn) { + txn.Flags.tfInnerBatchTxn = true + } + /* eslint-enable no-bitwise */ + + // Sequence processing + if (txn.Sequence == null && txn.TicketSequence == null) { + if (txn.Account in accountSequences) { + txn.Sequence = accountSequences[txn.Account] + accountSequences[txn.Account] += 1 + } else { + const nextSequence = await getNextValidSequenceNumber( + client, + txn.Account, + ) + const sequence = + txn.Account === tx.Account ? nextSequence + 1 : nextSequence + accountSequences[txn.Account] = sequence + 1 + txn.Sequence = sequence + } + } + + if (txn.Fee == null) { + txn.Fee = '0' + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- JS check + } else if (txn.Fee !== '0') { + throw new XrplError('Must have `Fee of "0" in inner Batch transaction.') + } + + if (txn.SigningPubKey == null) { + txn.SigningPubKey = '' + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- JS check + } else if (txn.SigningPubKey !== '') { + throw new XrplError( + 'Must have `SigningPubKey` of "" in inner Batch transaction.', + ) + } + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- needed for JS + if (txn.TxnSignature != null) { + throw new XrplError( + 'Must not have `TxnSignature` in inner Batch transaction.', + ) + } + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- needed for JS + if (txn.Signers != null) { + throw new XrplError('Must not have `Signers` in inner Batch transaction.') + } + + if (txn.NetworkID == null) { + txn.NetworkID = txNeedsNetworkID(client) ? client.networkID : undefined + } + } +} diff --git a/packages/xrpl/src/sugar/submit.ts b/packages/xrpl/src/sugar/submit.ts index 423a863cf8..547dad77c5 100644 --- a/packages/xrpl/src/sugar/submit.ts +++ b/packages/xrpl/src/sugar/submit.ts @@ -176,7 +176,6 @@ function isSigned(transaction: SubmittableTransaction | string): boolean { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- we know that tx.Signers is an array of Signers const signers = tx.Signers as Signer[] for (const signer of signers) { - // eslint-disable-next-line max-depth -- necessary for checking if signer is signed if ( // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- necessary check signer.Signer.SigningPubKey == null || @@ -284,7 +283,7 @@ export function getLastLedgerSequence( transaction: Transaction | string, ): number | null { const tx = typeof transaction === 'string' ? decode(transaction) : transaction - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- converts LastLedgSeq to number if present. + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- converts LastLedgerSeq to number if present. return tx.LastLedgerSequence as number | null } diff --git a/packages/xrpl/src/utils/hashes/hashLedger.ts b/packages/xrpl/src/utils/hashes/hashLedger.ts index e9cb9329b6..0788e1ee4d 100644 --- a/packages/xrpl/src/utils/hashes/hashLedger.ts +++ b/packages/xrpl/src/utils/hashes/hashLedger.ts @@ -12,6 +12,8 @@ import { APIVersion } from '../../models' import { LedgerEntry } from '../../models/ledger' import { LedgerVersionMap } from '../../models/ledger/Ledger' import { Transaction, TransactionMetadata } from '../../models/transactions' +import { GlobalFlags } from '../../models/transactions/common' +import { hasFlag } from '../../models/utils/flags' import HashPrefix from './HashPrefix' import sha512Half from './sha512Half' @@ -66,7 +68,7 @@ function addLengthPrefix(hex: string): string { * * @param tx - A transaction to hash. Tx may be in binary blob form. Tx must be signed. * @returns A hash of tx. - * @throws ValidationError if the Transaction is unsigned.\ + * @throws ValidationError if the Transaction is unsigned. * @category Utilities */ export function hashSignedTx(tx: Transaction | string): string { @@ -84,7 +86,8 @@ export function hashSignedTx(tx: Transaction | string): string { if ( txObject.TxnSignature === undefined && txObject.Signers === undefined && - txObject.SigningPubKey === undefined + txObject.SigningPubKey === undefined && + !hasFlag(txObject, GlobalFlags.tfInnerBatchTxn) ) { throw new ValidationError('The transaction must be signed to hash it.') } diff --git a/packages/xrpl/test/client/autofill.test.ts b/packages/xrpl/test/client/autofill.test.ts index 8c2d9b5ec8..653c010711 100644 --- a/packages/xrpl/test/client/autofill.test.ts +++ b/packages/xrpl/test/client/autofill.test.ts @@ -6,6 +6,7 @@ import { EscrowFinish, Payment, Transaction, + Batch, } from '../../src' import { ValidationError } from '../../src/errors' import rippled from '../fixtures/rippled' @@ -435,4 +436,86 @@ describe('client.autofill', function () { assert.strictEqual(txResult.Sequence, 23) assert.strictEqual(txResult.LastLedgerSequence, 9038234) }) + + it('should autofill Batch transaction with single account', async function () { + const tx: Batch = { + TransactionType: 'Batch', + Account: 'rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf', + RawTransactions: [ + { + RawTransaction: { + TransactionType: 'DepositPreauth', + Flags: 0x40000000, + Account: 'rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf', + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', + }, + }, + { + RawTransaction: { + TransactionType: 'DepositPreauth', + Flags: 0x40000000, + Account: 'rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf', + Authorize: 'rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn', + }, + }, + ], + Fee, + Sequence, + LastLedgerSequence, + } + testContext.mockRippled!.addResponse('account_info', { + status: 'success', + type: 'response', + result: { + account_data: { + Sequence: 23, + }, + }, + }) + const txResult = await testContext.client.autofill(tx) + txResult.RawTransactions.forEach((rawTxOuter, index) => { + const rawTx = rawTxOuter.RawTransaction + assert.strictEqual(rawTx.Sequence, 23 + index + 1) + }) + }) + + it('should autofill Batch transaction with single account', async function () { + const tx: Transaction = { + TransactionType: 'Batch', + Account: 'rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf', + RawTransactions: [ + { + RawTransaction: { + TransactionType: 'DepositPreauth', + Flags: 0x40000000, + Account: 'rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf', + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', + }, + }, + { + RawTransaction: { + TransactionType: 'DepositPreauth', + Flags: 0x40000000, + Account: 'rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn', + Authorize: 'rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo', + }, + }, + ], + Fee, + Sequence, + LastLedgerSequence, + } + testContext.mockRippled!.addResponse('account_info', { + status: 'success', + type: 'response', + result: { + account_data: { + Sequence: 23, + }, + }, + }) + const txResult = await testContext.client.autofill(tx) + assert.strictEqual(txResult.RawTransactions[0].RawTransaction.Sequence, 24) + assert.strictEqual(txResult.RawTransactions[1].RawTransaction.Sequence, 23) + }) }) diff --git a/packages/xrpl/test/integration/submitAndWait.test.ts b/packages/xrpl/test/integration/submitAndWait.test.ts index bae32f7d0c..7d1b3a3982 100644 --- a/packages/xrpl/test/integration/submitAndWait.test.ts +++ b/packages/xrpl/test/integration/submitAndWait.test.ts @@ -59,7 +59,6 @@ describe('client.submitAndWait', function () { retries = 0 break } catch (err) { - // eslint-disable-next-line max-depth -- Necessary if (!(err instanceof Error)) { throw err } @@ -69,7 +68,7 @@ describe('client.submitAndWait', function () { const errorCode = matches?.groups?.errorCode // Retry if another transaction finished before this one - // eslint-disable-next-line max-depth -- Testing + if (['tefPAST_SEQ', 'tefMAX_LEDGER'].includes(errorCode || '')) { // eslint-disable-next-line no-await-in-loop, no-promise-executor-return -- We are waiting on retries await new Promise((resolve) => setTimeout(resolve, 1000)) diff --git a/packages/xrpl/test/integration/transactions/batch.test.ts b/packages/xrpl/test/integration/transactions/batch.test.ts new file mode 100644 index 0000000000..e5318fbc8e --- /dev/null +++ b/packages/xrpl/test/integration/transactions/batch.test.ts @@ -0,0 +1,114 @@ +import { Batch, Wallet } from '../../../src' +import { BatchFlags } from '../../../src/models/transactions/batch' +import { signMultiBatch } from '../../../src/Wallet/batchSigner' +import serverUrl from '../serverUrl' +import { + setupClient, + teardownClient, + type XrplIntegrationTestContext, +} from '../setup' +import { + generateFundedWallet, + testTransaction, + verifySubmittedTransaction, +} from '../utils' + +// how long before each test case times out +const TIMEOUT = 20000 + +describe('Batch', function () { + let testContext: XrplIntegrationTestContext + let destination: Wallet + let wallet2: Wallet + + async function testBatchTransaction( + batch: Batch, + wallet: Wallet, + retry?: { + count: number + delayMs: number + }, + ): Promise { + await testTransaction(testContext.client, batch, wallet, retry) + const promises: Array> = [] + for (const rawTx of batch.RawTransactions) { + promises.push( + verifySubmittedTransaction(testContext.client, rawTx.RawTransaction), + ) + } + await Promise.all(promises) + } + + beforeAll(async () => { + testContext = await setupClient(serverUrl) + wallet2 = await generateFundedWallet(testContext.client) + destination = await generateFundedWallet(testContext.client) + }, TIMEOUT) + afterAll(async () => teardownClient(testContext)) + + it( + 'base', + async () => { + const tx: Batch = { + TransactionType: 'Batch', + Account: testContext.wallet.classicAddress, + Flags: BatchFlags.tfAllOrNothing, + RawTransactions: [ + { + RawTransaction: { + TransactionType: 'Payment', + Account: testContext.wallet.classicAddress, + Destination: destination.classicAddress, + Amount: '10000000', + }, + }, + { + RawTransaction: { + TransactionType: 'Payment', + Account: testContext.wallet.classicAddress, + Destination: destination.classicAddress, + Amount: '10000000', + }, + }, + ], + } + const autofilled = await testContext.client.autofill(tx) + await testBatchTransaction(autofilled, testContext.wallet) + }, + TIMEOUT, + ) + + it( + 'batch multisign', + async () => { + const tx: Batch = { + TransactionType: 'Batch', + Account: testContext.wallet.classicAddress, + Flags: BatchFlags.tfAllOrNothing, + Fee: '50', + RawTransactions: [ + { + RawTransaction: { + TransactionType: 'Payment', + Account: testContext.wallet.classicAddress, + Destination: destination.classicAddress, + Amount: '10000000', + }, + }, + { + RawTransaction: { + TransactionType: 'Payment', + Account: wallet2.classicAddress, + Destination: destination.classicAddress, + Amount: '10000000', + }, + }, + ], + } + const autofilled = await testContext.client.autofill(tx) + signMultiBatch(wallet2, autofilled) + await testBatchTransaction(autofilled, testContext.wallet) + }, + TIMEOUT, + ) +}) diff --git a/packages/xrpl/test/models/Batch.test.ts b/packages/xrpl/test/models/Batch.test.ts new file mode 100644 index 0000000000..e2e8618985 --- /dev/null +++ b/packages/xrpl/test/models/Batch.test.ts @@ -0,0 +1,163 @@ +import { assert } from 'chai' + +import { validate } from '../../src' +import { validateBatch } from '../../src/models/transactions/batch' +import { assertTxValidationError } from '../testUtils' + +/** + * Batch Transaction Verification Testing. + * + * Providing runtime verification testing for each specific transaction type. + */ +describe('Batch', function () { + let tx: any + + beforeEach(function () { + tx = { + Account: 'rJCxK2hX9tDMzbnn3cg1GU2g19Kfmhzxkp', + BatchSigners: [ + { + BatchSigner: { + Account: 'rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK', + SigningPubKey: + '02691AC5AE1C4C333AE5DF8A93BDC495F0EEBFC6DB0DA7EB6EF808F3AFC006E3FE', + TxnSignature: + '30450221008E595499C334127A23190F61FB9ADD8B8C501D543E37945B11FABB66B097A6130220138C908E8C4929B47E994A46D611FAC17AB295CFB8D9E0828B32F2947B97394B', + }, + }, + ], + Flags: 1, + RawTransactions: [ + { + RawTransaction: { + Account: 'rJCxK2hX9tDMzbnn3cg1GU2g19Kfmhzxkp', + Amount: '5000000', + Destination: 'rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK', + Fee: '0', + NetworkID: 21336, + Sequence: 0, + SigningPubKey: '', + TransactionType: 'Payment', + }, + }, + { + RawTransaction: { + Account: 'rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK', + Amount: '1000000', + Destination: 'rJCxK2hX9tDMzbnn3cg1GU2g19Kfmhzxkp', + Fee: '0', + NetworkID: 21336, + Sequence: 0, + SigningPubKey: '', + TransactionType: 'Payment', + }, + }, + ], + TransactionType: 'Batch', + } + }) + + it('verifies valid Batch', function () { + assert.doesNotThrow(() => validateBatch(tx)) + assert.doesNotThrow(() => validate(tx)) + }) + + it('verifies single-account Batch', function () { + tx = { + Account: 'rJCxK2hX9tDMzbnn3cg1GU2g19Kfmhzxkp', + Flags: 1, + RawTransactions: [ + { + RawTransaction: { + Account: 'rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK', + Amount: '5000000', + Destination: 'rJCxK2hX9tDMzbnn3cg1GU2g19Kfmhzxkp', + Fee: '0', + NetworkID: 21336, + Sequence: 0, + SigningPubKey: '', + TransactionType: 'Payment', + }, + }, + { + RawTransaction: { + Account: 'rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK', + Amount: '1000000', + Destination: 'rJCxK2hX9tDMzbnn3cg1GU2g19Kfmhzxkp', + Fee: '0', + NetworkID: 21336, + Sequence: 0, + SigningPubKey: '', + TransactionType: 'Payment', + }, + }, + ], + TransactionType: 'Batch', + } + assert.doesNotThrow(() => validateBatch(tx)) + assert.doesNotThrow(() => validate(tx)) + }) + + it('throws w/ invalid BatchSigners', function () { + tx.BatchSigners = 0 + assertTxValidationError( + tx, + validateBatch, + 'Batch: invalid field BatchSigners', + ) + }) + + it('throws w/ missing RawTransactions', function () { + delete tx.RawTransactions + assertTxValidationError( + tx, + validateBatch, + 'Batch: missing field RawTransactions', + ) + }) + + it('throws w/ invalid RawTransactions', function () { + tx.RawTransactions = 0 + assertTxValidationError( + tx, + validateBatch, + 'Batch: invalid field RawTransactions', + ) + }) + + it('throws w/ invalid RawTransactions object', function () { + tx.RawTransactions = [0] + assertTxValidationError( + tx, + validateBatch, + 'Batch: RawTransactions[0] is not object', + ) + }) + + it('throws w/ invalid RawTransactions.RawTransaction object', function () { + tx.RawTransactions = [{ RawTransaction: 0 }] + assertTxValidationError( + tx, + validateBatch, + 'Batch: invalid field RawTransactions[0].RawTransaction', + ) + }) + + it('throws w/ nested Batch', function () { + tx.RawTransactions = [{ RawTransaction: tx }] + assertTxValidationError( + tx, + validateBatch, + 'Batch: RawTransactions[0] is a Batch transaction. Cannot nest Batch transactions.', + ) + }) + + it('throws w/ non-object in BatchSigner list', function () { + tx.BatchSigners = [1] + assertTxValidationError( + tx, + validateBatch, + 'Batch: BatchSigners[0] is not object.', + ) + }) +}) diff --git a/packages/xrpl/test/testUtils.ts b/packages/xrpl/test/testUtils.ts index ee922a63e4..71342382de 100644 --- a/packages/xrpl/test/testUtils.ts +++ b/packages/xrpl/test/testUtils.ts @@ -5,7 +5,12 @@ import net from 'net' import { assert } from 'chai' import omit from 'lodash/omit' -import { rippleTimeToUnixTime, unixTimeToRippleTime } from '../src' +import { + rippleTimeToUnixTime, + unixTimeToRippleTime, + validate, + ValidationError, +} from '../src' import addresses from './fixtures/addresses.json' @@ -52,6 +57,22 @@ export function assertResultMatch( ) } +/** + * Check that a transaction error validation fails properly. + * + * @param tx The transaction that should fail validation. + * @param validateTx The transaction-specific validation function (e.g. `validatePayment`). + * @param errorMessage The error message that should be included in the error. + */ +export function assertTxValidationError( + tx: any, + validateTx: (tx: any) => void, + errorMessage: string, +): void { + assert.throws(() => validateTx(tx), ValidationError, errorMessage) + assert.throws(() => validate(tx), ValidationError, errorMessage) +} + /** * Check that the promise rejects with an expected error. * diff --git a/packages/xrpl/test/utils/hashes.test.ts b/packages/xrpl/test/utils/hashes.test.ts index daa42301d4..edc19575fa 100644 --- a/packages/xrpl/test/utils/hashes.test.ts +++ b/packages/xrpl/test/utils/hashes.test.ts @@ -10,6 +10,7 @@ import { Transaction, ValidationError, } from '../../src' +import { BatchInnerTransaction } from '../../src/models/transactions/batch' import { hashStateTree, hashTxTree, @@ -211,4 +212,22 @@ describe('Hashes', function () { 'CA4562711E4679FE9317DD767871E90A404C7A8B84FAFD35EC2CF0231F1F6DAF', ) }) + + it('hashSignedTx - batch transaction', function () { + const transaction: BatchInnerTransaction = { + Account: 'rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK', + Amount: '1000000', + Destination: 'rJCxK2hX9tDMzbnn3cg1GU2g19Kfmhzxkp', + Fee: '0', + Flags: 0x40000000, + Sequence: 470, + SigningPubKey: '', + TransactionType: 'Payment', + } + + assert.equal( + hashSignedTx(transaction), + '9EDF5DB29F536DD3919037F1E8A72B040D075571A10C9000294C57B5ECEEA791', + ) + }) }) diff --git a/packages/xrpl/test/wallet/authorizeChannel.test.ts b/packages/xrpl/test/wallet/authorizeChannel.test.ts index f8fd1cb9ca..9681dbe152 100644 --- a/packages/xrpl/test/wallet/authorizeChannel.test.ts +++ b/packages/xrpl/test/wallet/authorizeChannel.test.ts @@ -3,27 +3,29 @@ import { assert } from 'chai' import { ECDSA, Wallet } from '../../src' import { authorizeChannel } from '../../src/Wallet/authorizeChannel' -it('authorizeChannel succeeds with secp256k1 seed', function () { - const secpWallet = Wallet.fromSeed('snGHNrPbHrdUcszeuDEigMdC1Lyyd', { - algorithm: ECDSA.secp256k1, - }) - const channelId = - '5DB01B7FFED6B67E6B0414DED11E051D2EE2B7619CE0EAA6286D67A3A4D5BDB3' - const amount = '1000000' +describe('authorizeChannel', function () { + it('authorizeChannel succeeds with secp256k1 seed', function () { + const secpWallet = Wallet.fromSeed('snGHNrPbHrdUcszeuDEigMdC1Lyyd', { + algorithm: ECDSA.secp256k1, + }) + const channelId = + '5DB01B7FFED6B67E6B0414DED11E051D2EE2B7619CE0EAA6286D67A3A4D5BDB3' + const amount = '1000000' - assert.equal( - authorizeChannel(secpWallet, channelId, amount), - '304402204E7052F33DDAFAAA55C9F5B132A5E50EE95B2CF68C0902F61DFE77299BC893740220353640B951DCD24371C16868B3F91B78D38B6F3FD1E826413CDF891FA8250AAC', - ) -}) + assert.equal( + authorizeChannel(secpWallet, channelId, amount), + '304402204E7052F33DDAFAAA55C9F5B132A5E50EE95B2CF68C0902F61DFE77299BC893740220353640B951DCD24371C16868B3F91B78D38B6F3FD1E826413CDF891FA8250AAC', + ) + }) -it('authorizeChannel succeeds with ed25519 seed', function () { - const edWallet = Wallet.fromSeed('sEdSuqBPSQaood2DmNYVkwWTn1oQTj2') - const channelId = - '5DB01B7FFED6B67E6B0414DED11E051D2EE2B7619CE0EAA6286D67A3A4D5BDB3' - const amount = '1000000' - assert.equal( - authorizeChannel(edWallet, channelId, amount), - '7E1C217A3E4B3C107B7A356E665088B4FBA6464C48C58267BEF64975E3375EA338AE22E6714E3F5E734AE33E6B97AAD59058E1E196C1F92346FC1498D0674404', - ) + it('authorizeChannel succeeds with ed25519 seed', function () { + const edWallet = Wallet.fromSeed('sEdSuqBPSQaood2DmNYVkwWTn1oQTj2') + const channelId = + '5DB01B7FFED6B67E6B0414DED11E051D2EE2B7619CE0EAA6286D67A3A4D5BDB3' + const amount = '1000000' + assert.equal( + authorizeChannel(edWallet, channelId, amount), + '7E1C217A3E4B3C107B7A356E665088B4FBA6464C48C58267BEF64975E3375EA338AE22E6714E3F5E734AE33E6B97AAD59058E1E196C1F92346FC1498D0674404', + ) + }) }) diff --git a/packages/xrpl/test/wallet/batchSigner.test.ts b/packages/xrpl/test/wallet/batchSigner.test.ts new file mode 100644 index 0000000000..b431fada9f --- /dev/null +++ b/packages/xrpl/test/wallet/batchSigner.test.ts @@ -0,0 +1,370 @@ +import { assert } from 'chai' + +import { + Batch, + decode, + ECDSA, + encode, + ValidationError, + Wallet, +} from '../../src' +import { + BatchFlags, + BatchInnerTransaction, + BatchSigner, +} from '../../src/models/transactions/batch' +import { + combineBatchSigners, + signMultiBatch, +} from '../../src/Wallet/batchSigner' + +const secpWallet = Wallet.fromSeed('spkcsko6Ag3RbCSVXV2FJ8Pd4Zac1', { + algorithm: ECDSA.secp256k1, +}) +const edWallet = Wallet.fromSeed('spkcsko6Ag3RbCSVXV2FJ8Pd4Zac1', { + algorithm: ECDSA.ed25519, +}) +const submitWallet = Wallet.fromSeed('sEd7HmQFsoyj5TAm6d98gytM9LJA1MF', { + algorithm: ECDSA.ed25519, +}) +const regkeyWallet = Wallet.fromSeed('sEdStM1pngFcLQqVfH3RQcg2Qr6ov9e', { + algorithm: ECDSA.ed25519, +}) +const otherWallet = Wallet.generate() + +const nonBatchTx = { + TransactionType: 'Payment', + Account: 'rJy554HmWFFJQGnRfZuoo8nV97XSMq77h7', + Destination: 'rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK', + Amount: '1000', +} + +interface RawTransaction { + RawTransaction: BatchInnerTransaction +} + +describe('Wallet batch operations', function () { + describe('signMultiBatch', function () { + let transaction: Batch + + beforeEach(() => { + transaction = { + Account: 'rJCxK2hX9tDMzbnn3cg1GU2g19Kfmhzxkp', + Flags: 1, + RawTransactions: [ + { + RawTransaction: { + Account: 'rJy554HmWFFJQGnRfZuoo8nV97XSMq77h7', + Flags: 0x40000000, + Amount: '5000000', + Destination: 'rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK', + Fee: '0', + Sequence: 215, + SigningPubKey: '', + TransactionType: 'Payment', + }, + }, + { + RawTransaction: { + Account: 'rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK', + Flags: 0x40000000, + Amount: '1000000', + Destination: 'rJCxK2hX9tDMzbnn3cg1GU2g19Kfmhzxkp', + Fee: '0', + Sequence: 470, + SigningPubKey: '', + TransactionType: 'Payment', + }, + }, + ], + TransactionType: 'Batch', + } + }) + it('succeeds with secp256k1 seed', function () { + signMultiBatch(secpWallet, transaction) + const expected = [ + { + BatchSigner: { + Account: 'rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK', + SigningPubKey: + '02691AC5AE1C4C333AE5DF8A93BDC495F0EEBFC6DB0DA7EB6EF808F3AFC006E3FE', + TxnSignature: + '304402207E8238D3D2B24B98BA925D69DDAFA3E7D07F85C8ABF1C040B3D1BEBE2C36E92B02200C122F7F3F86AB8FF89207539CAFB4613D665FF336796F99283ED94C66FB3094', + }, + }, + ] + assert.property(transaction, 'BatchSigners') + assert.strictEqual( + JSON.stringify(transaction.BatchSigners), + JSON.stringify(expected), + ) + }) + + it('succeeds with ed25519 seed', function () { + signMultiBatch(edWallet, transaction) + const expected = [ + { + BatchSigner: { + Account: 'rJy554HmWFFJQGnRfZuoo8nV97XSMq77h7', + SigningPubKey: + 'ED3CC3D14FD80C213BC92A98AFE13A405A030F845EDCFD5E395286A6E9E62BA638', + TxnSignature: + '744FF09C11399F3AC1484F909A92F2D836EA979CB7655BC8F6BC3793F18892F92A16FE41C60EDCD6C2B757FF85D179F1589824ECA397EEA208B94C9D108CDF0A', + }, + }, + ] + assert.property(transaction, 'BatchSigners') + assert.strictEqual( + JSON.stringify(transaction.BatchSigners), + JSON.stringify(expected), + ) + }) + + it('succeeds with a different account', function () { + signMultiBatch(regkeyWallet, transaction, { + batchAccount: edWallet.address, + }) + const expected = [ + { + BatchSigner: { + Account: 'rJy554HmWFFJQGnRfZuoo8nV97XSMq77h7', + SigningPubKey: + 'ED37D3F048B7F1E680B0A97F70C7843160B9F25D6398D07E68B9A2C83AA8E1B156', + TxnSignature: + 'E53E2821CE46C98638E46CA0E6DB712CE45CEC45A697830A5028873D2BA51E1FA008F20526AC16B609401E2F1F8938AE60603223BC9D82A0221CFA5E58C90807', + }, + }, + ] + assert.property(transaction, 'BatchSigners') + assert.strictEqual( + JSON.stringify(transaction.BatchSigners), + JSON.stringify(expected), + ) + }) + + it('succeeds with multisign', function () { + signMultiBatch(regkeyWallet, transaction, { + batchAccount: edWallet.address, + multisign: true, + }) + const expected = [ + { + BatchSigner: { + Account: 'rJy554HmWFFJQGnRfZuoo8nV97XSMq77h7', + Signers: [ + { + Signer: { + Account: 'rwRNeznwHzdfYeKWpevYmax2NSDioyeEtT', + SigningPubKey: + 'ED37D3F048B7F1E680B0A97F70C7843160B9F25D6398D07E68B9A2C83AA8E1B156', + TxnSignature: + 'E53E2821CE46C98638E46CA0E6DB712CE45CEC45A697830A5028873D2BA51E1FA008F20526AC16B609401E2F1F8938AE60603223BC9D82A0221CFA5E58C90807', + }, + }, + ], + }, + }, + ] + assert.property(transaction, 'BatchSigners') + assert.strictEqual( + JSON.stringify(transaction.BatchSigners), + JSON.stringify(expected), + ) + }) + + it('succeeds with multisign + regular key', function () { + signMultiBatch(regkeyWallet, transaction, { + batchAccount: edWallet.address, + multisign: submitWallet.address, + }) + const expected = [ + { + BatchSigner: { + Account: 'rJy554HmWFFJQGnRfZuoo8nV97XSMq77h7', + Signers: [ + { + Signer: { + Account: 'rJCxK2hX9tDMzbnn3cg1GU2g19Kfmhzxkp', + SigningPubKey: + 'ED37D3F048B7F1E680B0A97F70C7843160B9F25D6398D07E68B9A2C83AA8E1B156', + TxnSignature: + 'E53E2821CE46C98638E46CA0E6DB712CE45CEC45A697830A5028873D2BA51E1FA008F20526AC16B609401E2F1F8938AE60603223BC9D82A0221CFA5E58C90807', + }, + }, + ], + }, + }, + ] + assert.property(transaction, 'BatchSigners') + assert.strictEqual( + JSON.stringify(transaction.BatchSigners), + JSON.stringify(expected), + ) + }) + + it('fails with not-included account', function () { + assert.throws( + () => signMultiBatch(otherWallet, transaction), + ValidationError, + 'Must be signing for an address included in the Batch.', + ) + }) + + it('fails with non-Batch transaction', function () { + assert.throws( + // @ts-expect-error - needed for JS/codecov + () => signMultiBatch(otherWallet, nonBatchTx), + ValidationError, + 'Must be a Batch transaction.', + ) + }) + }) + + describe('combineBatchSigners', function () { + let tx1: Batch + let tx2: Batch + const originalTx: Batch = { + Account: 'rJCxK2hX9tDMzbnn3cg1GU2g19Kfmhzxkp', + Flags: BatchFlags.tfAllOrNothing, + LastLedgerSequence: 14973, + NetworkID: 21336, + RawTransactions: [ + { + RawTransaction: { + Account: 'rJy554HmWFFJQGnRfZuoo8nV97XSMq77h7', + Amount: '5000000', + Destination: 'rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK', + Fee: '0', + Sequence: 215, + SigningPubKey: '', + TransactionType: 'Payment', + }, + }, + { + RawTransaction: { + Account: 'rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK', + Amount: '1000000', + Destination: 'rJCxK2hX9tDMzbnn3cg1GU2g19Kfmhzxkp', + Fee: '0', + Sequence: 470, + SigningPubKey: '', + TransactionType: 'Payment', + }, + }, + ], + Sequence: 215, + TransactionType: 'Batch', + } + let expectedValid: BatchSigner[] + + beforeEach(() => { + tx1 = { ...originalTx } + tx2 = { ...originalTx } + signMultiBatch(edWallet, tx1) + signMultiBatch(secpWallet, tx2) + expectedValid = (tx1.BatchSigners ?? []).concat(tx2.BatchSigners ?? []) + }) + + it('combines valid transactions', function () { + const result = combineBatchSigners([tx1, tx2]) + assert.deepEqual(decode(result).BatchSigners, expectedValid) + }) + + it('combines valid serialized transactions', function () { + const result = combineBatchSigners([encode(tx1), encode(tx2)]) + assert.deepEqual(decode(result).BatchSigners, expectedValid) + }) + + it('sorts the signers', function () { + const result = combineBatchSigners([tx2, tx1]) + assert.deepEqual(decode(result).BatchSigners, expectedValid) + }) + + it('removes signer for Batch submitter', function () { + // add a third inner transaction from the transaction submitter + const rawTx3: RawTransaction = { + RawTransaction: { + Account: 'rJCxK2hX9tDMzbnn3cg1GU2g19Kfmhzxkp', + Amount: '1000000', + Destination: 'rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK', + Fee: '0', + Flags: 0x40000000, + Sequence: 470, + SigningPubKey: '', + TransactionType: 'Payment', + }, + } + const rawTxs = originalTx.RawTransactions.concat(rawTx3) + + // set up all the transactions again (repeat what's done in `beforeEach`) + const newTx = { + ...originalTx, + RawTransactions: rawTxs, + } + tx1 = { ...newTx } + tx2 = { ...newTx } + const tx3 = { ...newTx } + signMultiBatch(edWallet, tx1) + signMultiBatch(secpWallet, tx2) + signMultiBatch(submitWallet, tx3) + + // run test + const result = combineBatchSigners([tx1, tx2, tx3]) + const expected = (tx1.BatchSigners ?? []).concat(tx2.BatchSigners ?? []) + assert.deepEqual(decode(result).BatchSigners, expected) + }) + + it('fails with no transactions provided', function () { + assert.throws( + () => combineBatchSigners([]), + ValidationError, + 'There are 0 transactions to combine.', + ) + }) + + it('fails with non-Batch transaction provided', function () { + assert.throws( + // @ts-expect-error - needed for JS/codecov + () => combineBatchSigners([tx1, tx2, nonBatchTx]), + ValidationError, + 'TransactionType must be `Batch`.', + ) + }) + + it('fails with no BatchSigners provided in a transaction', function () { + const badTx1 = { ...tx1 } + delete badTx1.BatchSigners + assert.throws( + () => combineBatchSigners([badTx1, tx2]), + ValidationError, + 'For combining Batch transaction signatures, all transactions must include a BatchSigners field containing an array of signatures.', + ) + + badTx1.BatchSigners = [] + assert.throws( + () => combineBatchSigners([badTx1, tx2]), + ValidationError, + 'For combining Batch transaction signatures, all transactions must include a BatchSigners field containing an array of signatures.', + ) + }) + + it('fails with signed inner transaction', function () { + assert.throws( + () => combineBatchSigners([secpWallet.sign(tx1).tx_blob, tx2]), + ValidationError, + 'Batch transaction must be unsigned.', + ) + }) + + it('fails with different inner transactions', function () { + const badTx2 = { ...tx2 } + badTx2.Flags = BatchFlags.tfIndependent + signMultiBatch(secpWallet, tx2) + assert.throws( + () => combineBatchSigners([tx1, badTx2]), + ValidationError, + 'Flags and transaction hashes are not the same for all provided transactions.', + ) + }) + }) +}) diff --git a/packages/xrpl/test/wallet/index.test.ts b/packages/xrpl/test/wallet/index.test.ts index bbb454108f..5647c8df93 100644 --- a/packages/xrpl/test/wallet/index.test.ts +++ b/packages/xrpl/test/wallet/index.test.ts @@ -598,6 +598,17 @@ describe('Wallet', function () { }) }) + it('sign with regular address for multisignAddress', async function () { + const signature = wallet.sign( + REQUEST_FIXTURES.signAs as Transaction, + wallet.address, + ) + assert.deepEqual(signature, { + tx_blob: RESPONSE_FIXTURES.signAs.signedTransaction, + hash: 'D8CF5FC93CFE5E131A34599AFB7CE186A5B8D1B9F069E35F4634AD3B27837E35', + }) + }) + it('sign with X Address and tag for multisignAddress', async function () { const signature = wallet.sign( REQUEST_FIXTURES.signAs as Transaction,