diff --git a/package-lock.json b/package-lock.json index 00fca07b24..6e0345b1c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19120,7 +19120,7 @@ } }, "packages/ripple-binary-codec": { - "version": "2.7.0", + "version": "2.8.0-smartcontract.0", "license": "ISC", "dependencies": { "@xrplf/isomorphic": "^1.0.1", @@ -19153,7 +19153,7 @@ } }, "packages/xrpl": { - "version": "4.6.0", + "version": "4.7.0-smartcontract.0", "license": "ISC", "dependencies": { "@scure/bip32": "^2.0.1", @@ -19164,7 +19164,7 @@ "eventemitter3": "^5.0.1", "fast-json-stable-stringify": "^2.1.0", "ripple-address-codec": "^5.0.0", - "ripple-binary-codec": "^2.7.0", + "ripple-binary-codec": "^2.8.0-smartcontract.0", "ripple-keypairs": "^2.0.0" }, "devDependencies": { diff --git a/packages/ripple-binary-codec/package.json b/packages/ripple-binary-codec/package.json index 75ceedd86f..125f9c13e7 100644 --- a/packages/ripple-binary-codec/package.json +++ b/packages/ripple-binary-codec/package.json @@ -1,6 +1,6 @@ { "name": "ripple-binary-codec", - "version": "2.7.0", + "version": "2.8.0-smartcontract.0", "description": "XRP Ledger binary codec", "files": [ "dist/*", diff --git a/packages/ripple-binary-codec/src/enums/definitions.json b/packages/ripple-binary-codec/src/enums/definitions.json index ccd7b7990f..fe853ab157 100644 --- a/packages/ripple-binary-codec/src/enums/definitions.json +++ b/packages/ripple-binary-codec/src/enums/definitions.json @@ -130,46 +130,6 @@ "type": "UInt16" } ], - [ - "HookStateChangeCount", - { - "isSerialized": true, - "isSigningField": true, - "isVLEncoded": false, - "nth": 17, - "type": "UInt16" - } - ], - [ - "HookEmitCount", - { - "isSerialized": true, - "isSigningField": true, - "isVLEncoded": false, - "nth": 18, - "type": "UInt16" - } - ], - [ - "HookExecutionIndex", - { - "isSerialized": true, - "isSigningField": true, - "isVLEncoded": false, - "nth": 19, - "type": "UInt16" - } - ], - [ - "HookApiVersion", - { - "isSerialized": true, - "isSigningField": true, - "isVLEncoded": false, - "nth": 20, - "type": "UInt16" - } - ], [ "LedgerFixType", { @@ -630,26 +590,6 @@ "type": "UInt32" } ], - [ - "HookStateCount", - { - "isSerialized": true, - "isSigningField": true, - "isVLEncoded": false, - "nth": 45, - "type": "UInt32" - } - ], - [ - "EmitGeneration", - { - "isSerialized": true, - "isSigningField": true, - "isVLEncoded": false, - "nth": 46, - "type": "UInt32" - } - ], [ "VoteWeight", { @@ -850,6 +790,66 @@ "type": "UInt32" } ], + [ + "ExtensionComputeLimit", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 69, + "type": "UInt32" + } + ], + [ + "ExtensionSizeLimit", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 70, + "type": "UInt32" + } + ], + [ + "GasPrice", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 71, + "type": "UInt32" + } + ], + [ + "ComputationAllowance", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 72, + "type": "UInt32" + } + ], + [ + "GasUsed", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 73, + "type": "UInt32" + } + ], + [ + "ParameterFlag", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 74, + "type": "UInt32" + } + ], [ "IndexNext", { @@ -980,36 +980,6 @@ "type": "UInt64" } ], - [ - "HookOn", - { - "isSerialized": true, - "isSigningField": true, - "isVLEncoded": false, - "nth": 16, - "type": "UInt64" - } - ], - [ - "HookInstructionCount", - { - "isSerialized": true, - "isSigningField": true, - "isVLEncoded": false, - "nth": 17, - "type": "UInt64" - } - ], - [ - "HookReturnCode", - { - "isSerialized": true, - "isSigningField": true, - "isVLEncoded": false, - "nth": 18, - "type": "UInt64" - } - ], [ "ReferenceCount", { @@ -1421,92 +1391,72 @@ } ], [ - "HookStateKey", - { - "isSerialized": true, - "isSigningField": true, - "isVLEncoded": false, - "nth": 30, - "type": "Hash256" - } - ], - [ - "HookHash", - { - "isSerialized": true, - "isSigningField": true, - "isVLEncoded": false, - "nth": 31, - "type": "Hash256" - } - ], - [ - "HookNamespace", + "DomainID", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 32, + "nth": 34, "type": "Hash256" } ], [ - "HookSetTxnID", + "VaultID", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 33, + "nth": 35, "type": "Hash256" } ], [ - "DomainID", + "ParentBatchID", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 34, + "nth": 36, "type": "Hash256" } ], [ - "VaultID", + "LoanBrokerID", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 35, + "nth": 37, "type": "Hash256" } ], [ - "ParentBatchID", + "LoanID", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 36, + "nth": 38, "type": "Hash256" } ], [ - "LoanBrokerID", + "ContractHash", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 37, + "nth": 39, "type": "Hash256" } ], [ - "LoanID", + "ContractID", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 38, + "nth": 40, "type": "Hash256" } ], @@ -2001,102 +1951,92 @@ } ], [ - "HookStateData", - { - "isSerialized": true, - "isSigningField": true, - "isVLEncoded": true, - "nth": 22, - "type": "Blob" - } - ], - [ - "HookReturnString", + "DIDDocument", { "isSerialized": true, "isSigningField": true, "isVLEncoded": true, - "nth": 23, + "nth": 26, "type": "Blob" } ], [ - "HookParameterName", + "Data", { "isSerialized": true, "isSigningField": true, "isVLEncoded": true, - "nth": 24, + "nth": 27, "type": "Blob" } ], [ - "HookParameterValue", + "AssetClass", { "isSerialized": true, "isSigningField": true, "isVLEncoded": true, - "nth": 25, + "nth": 28, "type": "Blob" } ], [ - "DIDDocument", + "Provider", { "isSerialized": true, "isSigningField": true, "isVLEncoded": true, - "nth": 26, + "nth": 29, "type": "Blob" } ], [ - "Data", + "MPTokenMetadata", { "isSerialized": true, "isSigningField": true, "isVLEncoded": true, - "nth": 27, + "nth": 30, "type": "Blob" } ], [ - "AssetClass", + "CredentialType", { "isSerialized": true, "isSigningField": true, "isVLEncoded": true, - "nth": 28, + "nth": 31, "type": "Blob" } ], [ - "Provider", + "FinishFunction", { "isSerialized": true, "isSigningField": true, "isVLEncoded": true, - "nth": 29, + "nth": 32, "type": "Blob" } ], [ - "MPTokenMetadata", + "ContractCode", { "isSerialized": true, "isSigningField": true, "isVLEncoded": true, - "nth": 30, + "nth": 33, "type": "Blob" } ], [ - "CredentialType", + "FunctionName", { "isSerialized": true, "isSigningField": true, "isVLEncoded": true, - "nth": 31, + "nth": 34, "type": "Blob" } ], @@ -2210,16 +2150,6 @@ "type": "AccountID" } ], - [ - "HookAccount", - { - "isSerialized": true, - "isSigningField": true, - "isVLEncoded": true, - "nth": 16, - "type": "AccountID" - } - ], [ "OtherChainSource", { @@ -2310,6 +2240,16 @@ "type": "AccountID" } ], + [ + "ContractAccount", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": true, + "nth": 27, + "type": "AccountID" + } + ], [ "Number", { @@ -2490,6 +2430,16 @@ "type": "Int32" } ], + [ + "WasmReturnCode", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 2, + "type": "Int32" + } + ], [ "TransactionMetaData", { @@ -2610,16 +2560,6 @@ "type": "STObject" } ], - [ - "Hook", - { - "isSerialized": true, - "isSigningField": true, - "isVLEncoded": false, - "nth": 14, - "type": "STObject" - } - ], [ "Permission", { @@ -2661,182 +2601,172 @@ } ], [ - "EmittedTxn", + "VoteEntry", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 20, + "nth": 25, "type": "STObject" } ], [ - "HookExecution", + "AuctionSlot", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 21, + "nth": 26, "type": "STObject" } ], [ - "HookDefinition", + "AuthAccount", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 22, + "nth": 27, "type": "STObject" } ], [ - "HookParameter", + "XChainClaimProofSig", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 23, + "nth": 28, "type": "STObject" } ], [ - "HookGrant", + "XChainCreateAccountProofSig", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 24, + "nth": 29, "type": "STObject" } ], [ - "VoteEntry", + "XChainClaimAttestationCollectionElement", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 25, + "nth": 30, "type": "STObject" } ], [ - "AuctionSlot", + "XChainCreateAccountAttestationCollectionElement", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 26, + "nth": 31, "type": "STObject" } ], [ - "AuthAccount", + "PriceData", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 27, + "nth": 32, "type": "STObject" } ], [ - "XChainClaimProofSig", + "Credential", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 28, + "nth": 33, "type": "STObject" } ], [ - "XChainCreateAccountProofSig", + "RawTransaction", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 29, + "nth": 34, "type": "STObject" } ], [ - "XChainClaimAttestationCollectionElement", + "BatchSigner", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 30, + "nth": 35, "type": "STObject" } ], [ - "XChainCreateAccountAttestationCollectionElement", + "Book", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 31, + "nth": 36, "type": "STObject" } ], [ - "PriceData", + "CounterpartySignature", { "isSerialized": true, - "isSigningField": true, + "isSigningField": false, "isVLEncoded": false, - "nth": 32, + "nth": 37, "type": "STObject" } ], [ - "Credential", + "Function", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 33, + "nth": 38, "type": "STObject" } ], [ - "RawTransaction", + "InstanceParameter", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 34, + "nth": 39, "type": "STObject" } ], [ - "BatchSigner", + "InstanceParameterValue", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 35, + "nth": 40, "type": "STObject" } ], [ - "Book", + "Parameter", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 36, - "type": "STObject" - } - ], - [ - "CounterpartySignature", - { - "isSerialized": true, - "isSigningField": false, - "isVLEncoded": false, - "nth": 37, + "nth": 41, "type": "STObject" } ], @@ -2920,16 +2850,6 @@ "type": "STArray" } ], - [ - "Hooks", - { - "isSerialized": true, - "isSigningField": true, - "isVLEncoded": false, - "nth": 11, - "type": "STArray" - } - ], [ "VoteSlots", { @@ -2971,132 +2891,142 @@ } ], [ - "HookExecutions", + "XChainClaimAttestations", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 18, + "nth": 21, "type": "STArray" } ], [ - "HookParameters", + "XChainCreateAccountAttestations", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 19, + "nth": 22, "type": "STArray" } ], [ - "HookGrants", + "PriceDataSeries", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 20, + "nth": 24, "type": "STArray" } ], [ - "XChainClaimAttestations", + "AuthAccounts", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 21, + "nth": 25, "type": "STArray" } ], [ - "XChainCreateAccountAttestations", + "AuthorizeCredentials", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 22, + "nth": 26, "type": "STArray" } ], [ - "PriceDataSeries", + "UnauthorizeCredentials", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 24, + "nth": 27, "type": "STArray" } ], [ - "AuthAccounts", + "AcceptedCredentials", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 25, + "nth": 28, "type": "STArray" } ], [ - "AuthorizeCredentials", + "Permissions", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 26, + "nth": 29, "type": "STArray" } ], [ - "UnauthorizeCredentials", + "RawTransactions", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 27, + "nth": 30, "type": "STArray" } ], [ - "AcceptedCredentials", + "BatchSigners", + { + "isSerialized": true, + "isSigningField": false, + "isVLEncoded": false, + "nth": 31, + "type": "STArray" + } + ], + [ + "Functions", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 28, + "nth": 32, "type": "STArray" } ], [ - "Permissions", + "InstanceParameters", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 29, + "nth": 33, "type": "STArray" } ], [ - "RawTransactions", + "InstanceParameterValues", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 30, + "nth": 34, "type": "STArray" } ], [ - "BatchSigners", + "Parameters", { "isSerialized": true, - "isSigningField": false, + "isSigningField": true, "isVLEncoded": false, - "nth": 31, + "nth": 35, "type": "STArray" } ], @@ -3171,32 +3101,32 @@ } ], [ - "HookResult", + "WasLockingChainSend", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 18, + "nth": 19, "type": "UInt8" } ], [ - "WasLockingChainSend", + "WithdrawalPolicy", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 19, + "nth": 20, "type": "UInt8" } ], [ - "WithdrawalPolicy", + "ContractResult", { "isSerialized": true, "isSigningField": true, "isVLEncoded": false, - "nth": 20, + "nth": 21, "type": "UInt8" } ], @@ -3390,6 +3320,36 @@ "type": "Currency" } ], + [ + "ParameterValue", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 1, + "type": "Data" + } + ], + [ + "ParameterType", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 1, + "type": "DataType" + } + ], + [ + "ContractJson", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 1, + "type": "Json" + } + ], [ "Transaction", { @@ -3437,6 +3397,9 @@ "Amendments": 102, "Bridge": 105, "Check": 67, + "Contract": 134, + "ContractData": 135, + "ContractSource": 133, "Credential": 129, "DID": 73, "Delegate": 131, @@ -3494,6 +3457,7 @@ "tecINSUF_RESERVE_LINE": 122, "tecINSUF_RESERVE_OFFER": 123, "tecINTERNAL": 144, + "tecINVALID_PARAMETERS": 200, "tecINVALID_UPDATE_TIME": 188, "tecINVARIANT_FAILED": 147, "tecKILLED": 150, @@ -3531,6 +3495,7 @@ "tecUNFUNDED_AMM": 162, "tecUNFUNDED_OFFER": 103, "tecUNFUNDED_PAYMENT": 104, + "tecWASM_REJECTED": 199, "tecWRONG_ASSET": 194, "tecXCHAIN_ACCOUNT_CREATE_PAST": 181, "tecXCHAIN_ACCOUNT_CREATE_TOO_MANY": 182, @@ -3569,8 +3534,10 @@ "tefNOT_MULTI_SIGNING": -184, "tefNO_AUTH_REQUIRED": -191, "tefNO_TICKET": -180, + "tefNO_WASM": -177, "tefPAST_SEQ": -190, "tefTOO_BIG": -181, + "tefWASM_FIELD_NOT_INCLUDED": -176, "tefWRONG_PRIOR": -189, "telBAD_DOMAIN": -398, @@ -3618,6 +3585,7 @@ "temBAD_TICK_SIZE": -269, "temBAD_TRANSFER_FEE": -251, "temBAD_TRANSFER_RATE": -280, + "temBAD_WASM": -249, "temBAD_WEIGHT": -270, "temCANNOT_PREAUTH_SELF": -267, "temDISABLED": -273, @@ -3633,6 +3601,7 @@ "temREDUNDANT": -275, "temRIPPLE_EMPTY": -274, "temSEQ_AND_TICKET": -263, + "temTEMP_DISABLED": -248, "temUNCERTAIN": -265, "temUNKNOWN": -264, "temXCHAIN_BAD_PROOF": -259, @@ -3675,6 +3644,12 @@ "CheckCash": 17, "CheckCreate": 16, "Clawback": 30, + "ContractCall": 90, + "ContractClawback": 88, + "ContractCreate": 85, + "ContractDelete": 87, + "ContractModify": 86, + "ContractUserDelete": 89, "CredentialAccept": 59, "CredentialCreate": 58, "CredentialDelete": 60, @@ -3743,6 +3718,8 @@ "Amount": 6, "Blob": 7, "Currency": 26, + "Data": 27, + "DataType": 28, "Done": -1, "Hash128": 4, "Hash160": 17, @@ -3751,6 +3728,7 @@ "Int32": 10, "Int64": 11, "Issue": 24, + "Json": 29, "LedgerEntry": 10002, "Metadata": 10004, "NotPresent": 0, diff --git a/packages/ripple-binary-codec/src/serdes/binary-serializer.ts b/packages/ripple-binary-codec/src/serdes/binary-serializer.ts index 08de8ad093..cb5d62211e 100644 --- a/packages/ripple-binary-codec/src/serdes/binary-serializer.ts +++ b/packages/ripple-binary-codec/src/serdes/binary-serializer.ts @@ -99,7 +99,7 @@ class BinarySerializer { * * @param length the length of the bytes */ - private encodeVariableLength(length: number): Uint8Array { + static encodeVariableLength(length: number): Uint8Array { const lenBytes = new Uint8Array(3) if (length <= 192) { lenBytes[0] = length @@ -158,7 +158,7 @@ class BinarySerializer { // this part doesn't happen for the Account field in a UNLModify transaction value.toBytesSink(bytes) } - this.put(this.encodeVariableLength(bytes.getLength())) + this.put(BinarySerializer.encodeVariableLength(bytes.getLength())) this.writeBytesList(bytes) } } diff --git a/packages/ripple-binary-codec/src/types/account-id.ts b/packages/ripple-binary-codec/src/types/account-id.ts index 8bfa2c59dd..37d7b6b750 100644 --- a/packages/ripple-binary-codec/src/types/account-id.ts +++ b/packages/ripple-binary-codec/src/types/account-id.ts @@ -6,6 +6,7 @@ import { } from 'ripple-address-codec' import { Hash160 } from './hash-160' import { hexToBytes } from '@xrplf/isomorphic/utils' +import { SerializedTypeID } from './serialized-type' const HEX_REGEX = /^[A-F0-9]{40}$/ @@ -81,6 +82,10 @@ class AccountID extends Hash160 { toBase58(): string { return encodeAccountID(this.bytes) } + + getSType(): SerializedTypeID { + return SerializedTypeID.STI_ACCOUNT + } } export { AccountID } diff --git a/packages/ripple-binary-codec/src/types/amount.ts b/packages/ripple-binary-codec/src/types/amount.ts index c09884475e..87c6b4b9c4 100644 --- a/packages/ripple-binary-codec/src/types/amount.ts +++ b/packages/ripple-binary-codec/src/types/amount.ts @@ -2,7 +2,7 @@ import { BinaryParser } from '../serdes/binary-parser' import { AccountID } from './account-id' import { Currency } from './currency' -import { JsonObject, SerializedType } from './serialized-type' +import { JsonObject, SerializedType, SerializedTypeID } from './serialized-type' import BigNumber from 'bignumber.js' import { bytesToHex, concat, hexToBytes } from '@xrplf/isomorphic/utils' import { readUInt32BE, writeUInt32BE } from '../utils' @@ -372,6 +372,10 @@ class Amount extends SerializedType { private isIOU(): boolean { return (this.bytes[0] & 0x80) !== 0 } + + getSType(): SerializedTypeID { + return SerializedTypeID.STI_AMOUNT + } } export { Amount, AmountObject } diff --git a/packages/ripple-binary-codec/src/types/blob.ts b/packages/ripple-binary-codec/src/types/blob.ts index 36bd93f94a..19a4b35679 100644 --- a/packages/ripple-binary-codec/src/types/blob.ts +++ b/packages/ripple-binary-codec/src/types/blob.ts @@ -1,4 +1,4 @@ -import { SerializedType } from './serialized-type' +import { SerializedType, SerializedTypeID } from './serialized-type' import { BinaryParser } from '../serdes/binary-parser' import { hexToBytes } from '@xrplf/isomorphic/utils' @@ -41,6 +41,10 @@ class Blob extends SerializedType { throw new Error('Cannot construct Blob from value given') } + + getSType(): SerializedTypeID { + return SerializedTypeID.STI_VL + } } export { Blob } diff --git a/packages/ripple-binary-codec/src/types/currency.ts b/packages/ripple-binary-codec/src/types/currency.ts index d6c5a1c1ed..e8cdbc9740 100644 --- a/packages/ripple-binary-codec/src/types/currency.ts +++ b/packages/ripple-binary-codec/src/types/currency.ts @@ -1,5 +1,6 @@ import { Hash160 } from './hash-160' import { bytesToHex, hexToBytes, hexToString } from '@xrplf/isomorphic/utils' +import { SerializedTypeID } from './serialized-type' const XRP_HEX_REGEX = /^0{40}$/ const ISO_REGEX = /^[A-Z0-9a-z?!@#$%^&*(){}[\]|]{3}$/ @@ -135,6 +136,10 @@ class Currency extends Hash160 { } return bytesToHex(this.bytes) } + + getSType(): SerializedTypeID { + return SerializedTypeID.STI_CURRENCY + } } export { Currency } diff --git a/packages/ripple-binary-codec/src/types/data.ts b/packages/ripple-binary-codec/src/types/data.ts new file mode 100644 index 0000000000..c77ab25012 --- /dev/null +++ b/packages/ripple-binary-codec/src/types/data.ts @@ -0,0 +1,294 @@ +import { BinaryParser } from '../serdes/binary-parser' +import { + JsonObject, + SerializedType, + SerializedTypeID, + TYPE_ID_TO_STRING, + TYPE_STRING_TO_ID, + TYPE_NUMBER_TO_ID, +} from './serialized-type' +import { readUInt16BE, writeUInt16BE } from '../utils' +import { bytesToHex, concat } from '@xrplf/isomorphic/utils' +import { Hash128 } from './hash-128' +import { Hash160 } from './hash-160' +import { Hash192 } from './hash-192' +import { Hash256 } from './hash-256' +import { AccountID } from './account-id' +import { Amount } from './amount' +import { Blob } from './blob' +import { Currency } from './currency' +import { STNumber } from './st-number' +import { Issue } from './issue' +import { UInt8 } from './uint-8' +import { UInt16 } from './uint-16' +import { UInt32 } from './uint-32' +import { UInt64 } from './uint-64' +import { BinarySerializer } from '../binary' + +/** + * Interface for Data JSON representation + */ +interface DataJSON extends JsonObject { + type: string + value: string | number | JsonObject +} + +/** + * Map from SerializedTypeID to the corresponding type class. + * Types listed here use standard from()/fromParser()/toBytes() without + * any extra framing (unlike VL and Account which need length prefixes). + */ +const SIMPLE_TYPE_MAP: Partial< + Record +> = { + [SerializedTypeID.STI_UINT8]: UInt8, + [SerializedTypeID.STI_UINT16]: UInt16, + [SerializedTypeID.STI_UINT32]: UInt32, + [SerializedTypeID.STI_UINT64]: UInt64, + [SerializedTypeID.STI_UINT128]: Hash128, + [SerializedTypeID.STI_UINT160]: Hash160, + [SerializedTypeID.STI_UINT192]: Hash192, + [SerializedTypeID.STI_UINT256]: Hash256, + [SerializedTypeID.STI_AMOUNT]: Amount, + [SerializedTypeID.STI_ISSUE]: Issue, + [SerializedTypeID.STI_CURRENCY]: Currency, + [SerializedTypeID.STI_NUMBER]: STNumber, +} + +/** + * Types whose from() method expects a numeric argument. + * For these, json.value is coerced to a number before calling from(). + */ +const NUMERIC_TYPES = new Set([ + SerializedTypeID.STI_UINT8, + SerializedTypeID.STI_UINT16, + SerializedTypeID.STI_UINT32, +]) + +/** + * STData: Encodes XRPL's "Data" type. + * + * This type wraps both a SerializedTypeID and the actual data value. + * It's encoded as a 2-byte type ID followed by the serialized data. + * + * Usage: + * Data.from({ type: "Amount", value: "1000000" }) + * Data.from({ type: "UInt64", value: "123456789" }) + * Data.fromParser(parser) + */ +class Data extends SerializedType { + static readonly ZERO_DATA: Data = new Data( + concat([ + new Uint8Array([0x00, 0x01]), // Type ID for UINT16 (SerializedTypeID.STI_UINT16 = 1) as uint16 + new Uint8Array([0x00, 0x00]), // Value: two zero bytes for UINT16 + ]), + ) + + /** + * Construct Data from bytes + * @param bytes - Uint8Array containing type ID and data + */ + constructor(bytes: Uint8Array) { + super(bytes ?? Data.ZERO_DATA.bytes) + } + + /** + * Create Data from various input types + * + * @param value - Can be: + * - Data instance (returns as-is) + * - DataJSON object with 'type' and 'value' fields + * @returns Data instance + * @throws Error if value type is not supported + */ + static from(value: unknown): Data { + if (value instanceof Data) { + return value + } + + if ( + typeof value === 'object' && + value !== null && + 'type' in value && + 'value' in value + ) { + const json = value as DataJSON + return Data.fromJSON(json) + } + + throw new Error('Data.from: value must be Data instance or DataJSON object') + } + + /** + * Create Data from JSON representation + * + * @param json - Object with 'type' and 'value' fields + * @returns Data instance + * @throws Error if type is not supported + */ + static fromJSON(json: DataJSON): Data { + const typeId = TYPE_STRING_TO_ID[json.type] + if (typeId === undefined) { + throw new Error(`Data: unsupported type string: ${json.type}`) + } + + let dataBytes: Uint8Array + + const TypeClass = SIMPLE_TYPE_MAP[typeId] + if (TypeClass) { + // For UInt8/16/32, coerce value to number; all others pass through + const coercedValue = NUMERIC_TYPES.has(typeId) + ? typeof json.value === 'string' + ? parseInt(json.value, 10) + : Number(json.value) + : json.value + dataBytes = TypeClass.from(coercedValue).toBytes() + } else if (typeId === SerializedTypeID.STI_VL) { + const val = + typeof json.value === 'string' ? json.value : json.value.toString() + dataBytes = Blob.from(val).toBytes() + dataBytes = concat([ + BinarySerializer.encodeVariableLength(dataBytes.length), + dataBytes, + ]) + } else if (typeId === SerializedTypeID.STI_ACCOUNT) { + const val = + typeof json.value === 'string' ? json.value : json.value.toString() + dataBytes = concat([ + new Uint8Array([0x14]), + AccountID.from(val).toBytes(), + ]) + } else { + throw new Error(`Data.fromJSON(): unsupported type ID: ${typeId}`) + } + + // Combine type header with data bytes + const typeBytes = new Uint8Array(2) + writeUInt16BE(typeBytes, typeId, 0) + return new Data(concat([typeBytes, dataBytes])) + } + + /** + * Read Data from a BinaryParser stream + * + * @param parser - BinaryParser positioned at the start of Data + * @returns Data instance + */ + static fromParser(parser: BinaryParser): Data { + // Read the 2-byte type ID + const typeBytes = parser.read(2) + const typeId = TYPE_NUMBER_TO_ID[readUInt16BE(typeBytes, 0)] + + let dataBytes: Uint8Array + + const TypeClass = SIMPLE_TYPE_MAP[typeId] + if (TypeClass) { + dataBytes = TypeClass.fromParser(parser).toBytes() + } else if (typeId === SerializedTypeID.STI_VL) { + const valueVL = parser.readVariableLength() + dataBytes = concat([ + BinarySerializer.encodeVariableLength(valueVL.length), + valueVL, + ]) + } else if (typeId === SerializedTypeID.STI_ACCOUNT) { + parser.skip(1) + dataBytes = concat([ + new Uint8Array([0x14]), + AccountID.fromParser(parser).toBytes(), + ]) + } else { + throw new Error(`Data: unsupported type ID when parsing: ${typeId}`) + } + + return new Data(concat([typeBytes, dataBytes])) + } + + /** + * Get the inner SerializedTypeID + * + * @returns The inner type ID + */ + getInnerType(): SerializedTypeID { + return TYPE_NUMBER_TO_ID[readUInt16BE(this.bytes, 0)] + } + + /** + * Get the string representation of the inner type + * + * @returns String name of the type + */ + getInnerTypeString(): string { + const innerType = this.getInnerType() + return TYPE_ID_TO_STRING[innerType] || innerType.toString() + } + + /** + * Get the data value + * + * @returns The stored data value + */ + getValue(): SerializedType { + const innerType = this.getInnerType() + const parser = new BinaryParser(bytesToHex(this.bytes.slice(2))) + + const TypeClass = SIMPLE_TYPE_MAP[innerType] + if (TypeClass) { + return TypeClass.fromParser(parser) + } + + if (innerType === SerializedTypeID.STI_VL) { + const vlLength = parser.readVariableLengthLength() + return Blob.fromParser(parser, vlLength) + } + + if (innerType === SerializedTypeID.STI_ACCOUNT) { + parser.skip(1) + return AccountID.fromParser(parser) + } + + throw new Error(`Data.getValue(): unsupported type ID: ${typeof innerType}`) + } + + /** + * Convert to JSON representation + * + * @returns JSON object with 'type' and 'value' fields + */ + toJSON(): DataJSON { + return { + type: this.getInnerTypeString(), + value: this.getValue().toJSON() as string | number | JsonObject, + } + } + + /** + * Compare with another Data for equality + * + * @param other - Another Data to compare with + * @returns true if both have the same inner type and data + */ + equals(other: Data): boolean { + if (!(other instanceof Data)) { + return false + } + + // Compare bytes directly + if (this.bytes.length !== other.bytes.length) { + return false + } + + for (let i = 0; i < this.bytes.length; i++) { + if (this.bytes[i] !== other.bytes[i]) { + return false + } + } + + return true + } + + getSType(): SerializedTypeID { + return SerializedTypeID.STI_DATA + } +} + +export { Data } diff --git a/packages/ripple-binary-codec/src/types/dataType.ts b/packages/ripple-binary-codec/src/types/dataType.ts new file mode 100644 index 0000000000..907d425451 --- /dev/null +++ b/packages/ripple-binary-codec/src/types/dataType.ts @@ -0,0 +1,178 @@ +import { BinaryParser } from '../serdes/binary-parser' +import { + JsonObject, + SerializedType, + SerializedTypeID, + TYPE_ID_TO_STRING, + TYPE_STRING_TO_ID, +} from './serialized-type' +import { readUInt16BE, writeUInt16BE } from '../utils' + +/** + * Interface for DataType JSON representation + */ +interface DataTypeJSON extends JsonObject { + type: string +} + +/** + * STDataType: Encodes XRPL's "DataType" type. + * + * This type wraps an inner SerializedTypeID to indicate what type of data + * a field contains. It's encoded as a 2-byte unsigned integer representing + * the inner type. + * + * Usage: + * DataType.from({ type: "Amount" }) + * DataType.from("UInt64") + * DataType.fromParser(parser) + */ +class DataType extends SerializedType { + private innerType: SerializedTypeID + + /** + * Default bytes for DataType (STI_NOTPRESENT) + */ + static readonly defaultBytes = new Uint8Array([0x00, 0x01]) + + /** + * Construct a DataType from bytes + * @param bytes - 2-byte Uint8Array containing the inner type ID + * @param innerType - Optional explicit inner type (used when constructing from value) + * @throws Error if bytes is not a 2-byte Uint8Array + */ + constructor(bytes?: Uint8Array, innerType?: SerializedTypeID) { + const used = bytes ?? DataType.defaultBytes + if (!(used instanceof Uint8Array) || used.length !== 2) { + throw new Error( + `DataType must be constructed from a 2-byte Uint8Array, got ${used?.length} bytes`, + ) + } + super(used) + + // If innerType is explicitly provided, use it; otherwise read from bytes + if (innerType !== undefined) { + this.innerType = innerType + } else { + this.innerType = readUInt16BE(used, 0) as unknown as SerializedTypeID + } + } + + /** + * Construct from various input types + * + * @param value - Can be: + * - DataType instance (returns as-is) + * - DataTypeJSON object with 'type' field + * - String type name (e.g., "Amount", "UInt64") + * - SerializedTypeID enum value + * @returns DataType instance + * @throws Error if value type is not supported or type string is unknown + */ + static from(value: unknown): DataType { + if (value instanceof DataType) { + return value + } + + if (typeof value === 'object' && value !== null && 'type' in value) { + const json = value as DataTypeJSON + return DataType.fromTypeString(json.type) + } + + if (typeof value === 'string') { + return DataType.fromTypeString(value) + } + + if (typeof value === 'number') { + return DataType.fromTypeId(value as SerializedTypeID) + } + + throw new Error( + 'DataType.from: value must be DataType, DataTypeJSON, string, or SerializedTypeID', + ) + } + + /** + * Construct from a type string + * + * @param typeStr - Type string like "Amount", "UInt64", etc. + * @returns DataType instance + * @throws Error if type string is not recognized + */ + static fromTypeString(typeStr: string): DataType { + const typeId = TYPE_STRING_TO_ID[typeStr] + if (typeId === undefined) { + throw new Error(`DataType: unsupported type string: ${typeStr}`) + } + return DataType.fromTypeId(typeId) + } + + /** + * Construct from a SerializedTypeID + * + * @param typeId - The SerializedTypeID enum value + * @returns DataType instance + */ + static fromTypeId(typeId: SerializedTypeID): DataType { + const bytes = new Uint8Array(2) + writeUInt16BE(bytes, typeId, 0) + return new DataType(bytes, typeId) + } + + /** + * Read a DataType from a BinaryParser stream (2 bytes) + * + * @param parser - BinaryParser positioned at the start of a DataType + * @returns DataType instance + */ + static fromParser(parser: BinaryParser): DataType { + const bytes = parser.read(2) + return new DataType(bytes) + } + + /** + * Get the inner SerializedTypeID + * + * @returns The inner type ID + */ + getInnerType(): SerializedTypeID { + return this.innerType + } + + /** + * Set the inner SerializedTypeID + * + * @param typeId - The new inner type ID + */ + setInnerType(typeId: SerializedTypeID): void { + this.innerType = typeId + writeUInt16BE(this.bytes, typeId, 0) + } + + /** + * Get the string representation of the inner type + * + * @returns String name of the type, or numeric string if unknown + */ + getInnerTypeString(): string { + return TYPE_ID_TO_STRING[this.innerType] || this.innerType.toString() + } + + /** + * Convert to JSON representation + * + * @returns JSON object with 'type' field + */ + toJSON(): DataTypeJSON { + return { + type: this.getInnerTypeString(), + } + } + + getSType(): SerializedTypeID { + return SerializedTypeID.STI_DATATYPE + } +} + +// Export the DataType class for external use +export { DataType } diff --git a/packages/ripple-binary-codec/src/types/hash-128.ts b/packages/ripple-binary-codec/src/types/hash-128.ts index 52790ffeaa..83393219b4 100644 --- a/packages/ripple-binary-codec/src/types/hash-128.ts +++ b/packages/ripple-binary-codec/src/types/hash-128.ts @@ -1,5 +1,6 @@ import { Hash } from './hash' import { bytesToHex } from '@xrplf/isomorphic/utils' +import { SerializedTypeID } from './serialized-type' /** * Hash with a width of 128 bits @@ -28,6 +29,10 @@ class Hash128 extends Hash { } return hex } + + getSType(): SerializedTypeID { + return SerializedTypeID.STI_UINT128 + } } export { Hash128 } diff --git a/packages/ripple-binary-codec/src/types/hash-160.ts b/packages/ripple-binary-codec/src/types/hash-160.ts index 053a27dd02..f49adb28a4 100644 --- a/packages/ripple-binary-codec/src/types/hash-160.ts +++ b/packages/ripple-binary-codec/src/types/hash-160.ts @@ -1,4 +1,5 @@ import { Hash } from './hash' +import { SerializedTypeID } from './serialized-type' /** * Hash with a width of 160 bits @@ -14,6 +15,10 @@ class Hash160 extends Hash { super(bytes ?? Hash160.ZERO_160.bytes) } + + getSType(): SerializedTypeID { + return SerializedTypeID.STI_UINT160 + } } export { Hash160 } diff --git a/packages/ripple-binary-codec/src/types/hash-192.ts b/packages/ripple-binary-codec/src/types/hash-192.ts index 2f0d2b4867..5b2d117dcb 100644 --- a/packages/ripple-binary-codec/src/types/hash-192.ts +++ b/packages/ripple-binary-codec/src/types/hash-192.ts @@ -1,4 +1,5 @@ import { Hash } from './hash' +import { SerializedTypeID } from './serialized-type' /** * Hash with a width of 192 bits @@ -14,6 +15,10 @@ class Hash192 extends Hash { super(bytes ?? Hash192.ZERO_192.bytes) } + + getSType(): SerializedTypeID { + return SerializedTypeID.STI_UINT192 + } } export { Hash192 } diff --git a/packages/ripple-binary-codec/src/types/hash-256.ts b/packages/ripple-binary-codec/src/types/hash-256.ts index a8a48ff245..3fb981b29a 100644 --- a/packages/ripple-binary-codec/src/types/hash-256.ts +++ b/packages/ripple-binary-codec/src/types/hash-256.ts @@ -1,4 +1,5 @@ import { Hash } from './hash' +import { SerializedTypeID } from './serialized-type' /** * Hash with a width of 256 bits @@ -10,6 +11,10 @@ class Hash256 extends Hash { constructor(bytes: Uint8Array) { super(bytes ?? Hash256.ZERO_256.bytes) } + + getSType(): SerializedTypeID { + return SerializedTypeID.STI_UINT256 + } } export { Hash256 } diff --git a/packages/ripple-binary-codec/src/types/index.ts b/packages/ripple-binary-codec/src/types/index.ts index 890d726862..c7bc575b4f 100644 --- a/packages/ripple-binary-codec/src/types/index.ts +++ b/packages/ripple-binary-codec/src/types/index.ts @@ -2,6 +2,8 @@ import { AccountID } from './account-id' import { Amount } from './amount' import { Blob } from './blob' import { Currency } from './currency' +import { Data } from './data' +import { DataType } from './dataType' import { Hash128 } from './hash-128' import { Hash160 } from './hash-160' import { Hash192 } from './hash-192' @@ -26,6 +28,8 @@ const coreTypes: Record = { Amount, Blob, Currency, + Data, + DataType, Hash128, Hash160, Hash192, @@ -55,6 +59,8 @@ export { Amount, Blob, Currency, + Data, + DataType, Hash128, Hash160, Hash192, diff --git a/packages/ripple-binary-codec/src/types/issue.ts b/packages/ripple-binary-codec/src/types/issue.ts index 011b8d05ae..0fc15fd9dc 100644 --- a/packages/ripple-binary-codec/src/types/issue.ts +++ b/packages/ripple-binary-codec/src/types/issue.ts @@ -3,7 +3,7 @@ import { BinaryParser } from '../serdes/binary-parser' import { AccountID } from './account-id' import { Currency } from './currency' -import { JsonObject, SerializedType } from './serialized-type' +import { JsonObject, SerializedType, SerializedTypeID } from './serialized-type' import { Hash192 } from './hash-192' import { readUInt32BE, writeUInt32BE } from '../utils' @@ -158,6 +158,10 @@ class Issue extends SerializedType { issuer: issuer.toJSON(), } } + + getSType(): SerializedTypeID { + return SerializedTypeID.STI_ISSUE + } } export { Issue, IssueObject } diff --git a/packages/ripple-binary-codec/src/types/json.ts b/packages/ripple-binary-codec/src/types/json.ts new file mode 100644 index 0000000000..448f643634 --- /dev/null +++ b/packages/ripple-binary-codec/src/types/json.ts @@ -0,0 +1,650 @@ +/* eslint-disable max-lines */ +import { BinaryParser } from '../serdes/binary-parser' +import { JsonObject, SerializedType, SerializedTypeID } from './serialized-type' +import { bytesToHex } from '@xrplf/isomorphic/utils' +import { BinarySerializer, BytesList } from '../serdes/binary-serializer' + +/** + * STJson: Serialized Type for JSON-like structures (objects or arrays). + * + * Supports two modes: + * - Object: Key-value pairs where keys are VL-encoded strings + * - Array: Ordered list of values + * + * Values are [SType marker][VL-encoded SType serialization]. + * Values can be any SType, including nested STJson. + * + * Serialization format: [type_byte][VL_length][data...] + * - type_byte: 0x00 = Object, 0x01 = Array + * + * Depth constraint: Maximum nesting depth of 1 level + */ +class STJson extends SerializedType { + private static readonly JsonType = { + Object: 0x00, + Array: 0x01, + } + + private data: Map | (SerializedType | null)[] + private jsonType: number + private default_ = false + + /** + * Construct STJson from bytes + * @param bytes - Uint8Array containing serialized JSON + */ + constructor(bytes: Uint8Array) { + super(bytes) + this.data = new Map() + this.jsonType = STJson.JsonType.Object + } + + /** + * Create an empty STJson with the given type + */ + private static createEmpty( + type: number, + initialData?: + | Map + | (SerializedType | null)[], + ): STJson { + const json = new STJson(new Uint8Array()) + json.data = initialData ?? (type === STJson.JsonType.Array ? [] : new Map()) + json.jsonType = type + return json + } + + /** + * Parse STJson from BinaryParser + * + * @param parser - BinaryParser positioned at the start of STJson + * @returns STJson instance + */ + static fromParser(parser: BinaryParser): STJson { + const dataLength = parser.readVariableLengthLength() + + if (dataLength < 0) { + throw new Error('Invalid STJson length') + } + + if (dataLength === 0) { + const json = new STJson(new Uint8Array()) + json.data = new Map() + return json + } + + // Read type byte + const typeByte = parser.read(1)[0] + const type = typeByte + const initialBytesLeft = parser.size() + + if (type === STJson.JsonType.Array) { + const array: (SerializedType | null)[] = [] + while ( + parser.size() > 0 && + initialBytesLeft - parser.size() < dataLength + ) { + const valueVL = parser.readVariableLength() + if (valueVL.length > 0) { + const valueSit = new BinaryParser(bytesToHex(valueVL)) + const value = STJson.makeValueFromVLWithType(valueSit) + array.push(value) + } else { + array.push(null) + } + } + + const json = new STJson(new Uint8Array()) + json.data = array + json.jsonType = STJson.JsonType.Array + return json + } else { + // JsonType.Object + const map = new Map() + while ( + parser.size() > 0 && + initialBytesLeft - parser.size() < dataLength + ) { + const [key, value] = STJson.parsePair(parser) + map.set(key, value) + } + + const json = new STJson(new Uint8Array()) + json.data = map + json.jsonType = STJson.JsonType.Object + return json + } + } + + /** + * Parse a single key-value pair from the parser + */ + private static parsePair( + parser: BinaryParser, + ): [string, SerializedType | null] { + const keyVL = parser.readVariableLength() + const key = new TextDecoder().decode(keyVL) + + const valueVL = parser.readVariableLength() + let value: SerializedType | null = null + + if (valueVL.length > 0) { + const valueSit = new BinaryParser(bytesToHex(valueVL)) + value = STJson.makeValueFromVLWithType(valueSit) + } + + return [key, value] + } + + /** + * Factory for SType value from VL blob (with SType marker) + */ + private static makeValueFromVLWithType(parser: BinaryParser): SerializedType { + if (parser.size() === 0) { + throw new Error('Empty data when parsing STJson value') + } + + const typeId = parser.read(1)[0] + + // Delegate to appropriate type's fromParser + // This is a placeholder - actual implementation would dispatch to concrete types + // For now, we create an STJson if type is Object or Array + if (typeId === STJson.JsonType.Object || typeId === STJson.JsonType.Array) { + return STJson.fromParser(parser) + } + + throw new Error(`Unsupported type ID in STJson: ${typeId}`) + } + + /** + * Check if this is an array type + */ + isArray(): boolean { + return this.jsonType === STJson.JsonType.Array + } + + /** + * Check if this is an object type + */ + isObject(): boolean { + return this.jsonType === STJson.JsonType.Object + } + + /** + * Get the JSON type + */ + getType(): number { + return this.jsonType + } + + /** + * Get nesting depth (0 = no nesting, 1 = one level of nesting) + */ + getDepth(): number { + if (this.isArray()) { + const array = this.data as (SerializedType | null)[] + for (const value of array) { + if (value && value instanceof STJson) { + return 1 + value.getDepth() + } + } + return 0 + } else { + // isObject() + const map = this.data as Map + for (const value of map.values()) { + if (value && value instanceof STJson) { + return 1 + value.getDepth() + } + } + return 0 + } + } + + /** + * Validate nesting depth (max 1 level) + */ + private validateDepth( + value: SerializedType | null, + currentDepth: number, + ): void { + if (!value) { + return + } + + if (!(value instanceof STJson)) { + return + } + + const valueDepth = value.getDepth() + if (currentDepth + valueDepth > 1) { + throw new Error('STJson nesting depth exceeds maximum of 1') + } + } + + /** + * Set a field in an object + */ + setObjectField(key: string, value: SerializedType | null): void { + if (!this.isObject()) { + throw new Error('STJson::setObjectField called on non-object') + } + this.validateDepth(value, 0) + ;(this.data as Map).set(key, value) + } + + /** + * Get a field from an object + */ + getObjectField(key: string): SerializedType | null | undefined { + if (!this.isObject()) { + return undefined + } + return (this.data as Map).get(key) + } + + /** + * Set a nested object field (one level deep) + */ + setNestedObjectField( + key: string, + nestedKey: string, + value: SerializedType | null, + ): void { + if (!this.isObject()) { + throw new Error('STJson::setNestedObjectField called on non-object') + } + + const map = this.data as Map + let nestedObj = map.get(key) + + if (!nestedObj || !(nestedObj instanceof STJson) || !nestedObj.isObject()) { + const newNested = STJson.createEmpty(STJson.JsonType.Object) + map.set(key, newNested as SerializedType) + nestedObj = newNested + } + + if (nestedObj instanceof STJson) { + nestedObj.setObjectField(nestedKey, value) + } + } + + /** + * Get a nested object field + */ + getNestedObjectField( + key: string, + nestedKey: string, + ): SerializedType | null | undefined { + if (!this.isObject()) { + return undefined + } + + const nestedObj = (this.data as Map).get(key) + if (nestedObj instanceof STJson && nestedObj.isObject()) { + return nestedObj.getObjectField(nestedKey) + } + return undefined + } + + /** + * Get the inner data as a Map (for objects) + */ + getMap(): Map { + if (!this.isObject()) { + throw new Error('STJson is not an object type') + } + return this.data as Map + } + + /** + * Get the inner data as an array + */ + getArray(): (SerializedType | null)[] { + if (!this.isArray()) { + throw new Error('STJson is not an array type') + } + return this.data as (SerializedType | null)[] + } + + /** + * Push an element to an array + */ + pushArrayElement(value: SerializedType | null): void { + if (!this.isArray()) { + throw new Error('STJson::pushArrayElement called on non-array') + } + this.validateDepth(value, 0) + ;(this.data as (SerializedType | null)[]).push(value) + } + + /** + * Get an array element by index + */ + getArrayElement(index: number): SerializedType | null | undefined { + if (!this.isArray()) { + return undefined + } + const array = this.data as (SerializedType | null)[] + return array[index] + } + + /** + * Set an array element by index + */ + setArrayElement(index: number, value: SerializedType | null): void { + if (!this.isArray()) { + throw new Error('STJson::setArrayElement called on non-array') + } + this.validateDepth(value, 0) + + const array = this.data as (SerializedType | null)[] + // Auto-resize with nulls if needed + if (index >= array.length) { + array.length = index + 1 + array.fill(null) + } + array[index] = value + } + + /** + * Set a field within an array element (element must be an object) + */ + setArrayElementField( + index: number, + key: string, + value: SerializedType | null, + ): void { + if (!this.isArray()) { + throw new Error('STJson::setArrayElementField called on non-array') + } + + this.validateDepth(value, 1) + + const array = this.data as (SerializedType | null)[] + // Auto-resize with nulls if needed + if (index >= array.length) { + array.length = index + 1 + array.fill(null) + } + + let element = array[index] + if (!element || !(element instanceof STJson) || !element.isObject()) { + const newElement = STJson.createEmpty(STJson.JsonType.Object) + array[index] = newElement as SerializedType + element = newElement + } + + if (element instanceof STJson) { + element.setObjectField(key, value) + } + } + + /** + * Get a field within an array element + */ + getArrayElementField( + index: number, + key: string, + ): SerializedType | null | undefined { + if (!this.isArray()) { + return undefined + } + + const array = this.data as (SerializedType | null)[] + if (index >= array.length) { + return undefined + } + + const element = array[index] + if (element instanceof STJson && element.isObject()) { + return element.getObjectField(key) + } + return undefined + } + + /** + * Get the size of the array + */ + arraySize(): number { + if (!this.isArray()) { + return 0 + } + return (this.data as (SerializedType | null)[]).length + } + + /** + * Set a nested array element (array stored in object field) + */ + setNestedArrayElement( + key: string, + index: number, + value: SerializedType | null, + ): void { + if (!this.isObject()) { + throw new Error('STJson::setNestedArrayElement called on non-object') + } + + this.validateDepth(value, 1) + + const map = this.data as Map + let arrayJson = map.get(key) + + if (!arrayJson || !(arrayJson instanceof STJson) || !arrayJson.isArray()) { + const newArray = STJson.createEmpty(STJson.JsonType.Array) + map.set(key, newArray as SerializedType) + arrayJson = newArray + } + + if (arrayJson instanceof STJson) { + arrayJson.setArrayElement(index, value) + } + } + + /** + * Set a field within a nested array element + */ + // eslint-disable-next-line max-params -- all 4 params are needed to address a nested array element field + setNestedArrayElementField( + key: string, + index: number, + nestedKey: string, + value: SerializedType | null, + ): void { + if (!this.isObject()) { + throw new Error('STJson::setNestedArrayElementField called on non-object') + } + + this.validateDepth(value, 1) + + const map = this.data as Map + let arrayJson = map.get(key) + + if (!arrayJson || !(arrayJson instanceof STJson) || !arrayJson.isArray()) { + const newArray = STJson.createEmpty(STJson.JsonType.Array) + map.set(key, newArray as SerializedType) + arrayJson = newArray + } + + if (arrayJson instanceof STJson) { + arrayJson.setArrayElementField(index, nestedKey, value) + } + } + + /** + * Get a nested array element + */ + getNestedArrayElement( + key: string, + index: number, + ): SerializedType | null | undefined { + if (!this.isObject()) { + return undefined + } + + const arrayJson = (this.data as Map).get(key) + if (arrayJson instanceof STJson && arrayJson.isArray()) { + return arrayJson.getArrayElement(index) + } + return undefined + } + + /** + * Get a field within a nested array element + */ + getNestedArrayElementField( + key: string, + index: number, + nestedKey: string, + ): SerializedType | null | undefined { + if (!this.isObject()) { + return undefined + } + + const arrayJson = (this.data as Map).get(key) + if (arrayJson instanceof STJson && arrayJson.isArray()) { + return arrayJson.getArrayElementField(index, nestedKey) + } + return undefined + } + + /** + * Serialize to binary + */ + add(s: BinarySerializer): void { + const bytesList = new BytesList() + const tmp = new BinarySerializer(bytesList) + + // Add type byte + tmp.put(new Uint8Array([this.jsonType])) + + if (this.isArray()) { + const array = this.data as (SerializedType | null)[] + for (const value of array) { + STJson.addVLValue(tmp, value) + } + } else { + // isObject() + const map = this.data as Map + for (const [key, value] of map.entries()) { + STJson.addVLKey(tmp, key) + STJson.addVLValue(tmp, value) + } + } + + const innerBytes = bytesList.toBytes() + const lengthBytes = BinarySerializer.encodeVariableLength(innerBytes.length) + s.put(lengthBytes) + s.put(innerBytes) + } + + /** + * Encode a key as VL + */ + private static addVLKey(s: BinarySerializer, str: string): void { + const keyBytes = new TextEncoder().encode(str) + const lengthBytes = BinarySerializer.encodeVariableLength(keyBytes.length) + s.put(lengthBytes) + s.put(keyBytes) + } + + /** + * Encode a value as [SType marker][VL] + */ + private static addVLValue( + s: BinarySerializer, + value: SerializedType | null, + ): void { + if (!value) { + s.put(BinarySerializer.encodeVariableLength(0)) + return + } + + const bytesList = new BytesList() + const tmp = new BinarySerializer(bytesList) + tmp.put(new Uint8Array([value.getSType()])) + value.toBytesSink(bytesList) + + const innerBytes = bytesList.toBytes() + const lengthBytes = BinarySerializer.encodeVariableLength(innerBytes.length) + s.put(lengthBytes) + s.put(innerBytes) + } + + /** + * Convert to JSON representation + */ + toJSON(): JsonObject | JsonObject[] { + if (this.isArray()) { + const array = this.data as (SerializedType | null)[] + return array.map((item) => { + return item ? item.toJSON() : null + }) as JsonObject[] + } else { + // isObject() + const map = this.data as Map + const result: JsonObject = {} + for (const [key, value] of map.entries()) { + result[key] = value ? value.toJSON() : null + } + return result + } + } + + /** + * Compare with another STJson for equivalence + */ + isEquivalent(t: SerializedType): boolean { + if (!(t instanceof STJson)) { + return false + } + return bytesToHex(this.bytes) === bytesToHex(t.bytes) + } + + /** + * Check if this is the default value + */ + isDefault(): boolean { + return this.default_ + } + + /** + * Get blob representation + */ + toBlob(): Uint8Array { + const bytesList = new BytesList() + const s = new BinarySerializer(bytesList) + this.add(s) + return bytesList.toBytes() + } + + /** + * Get the size (number of bytes in serialized form) + */ + size(): number { + const bytesList = new BytesList() + const s = new BinarySerializer(bytesList) + this.add(s) + return bytesList.getLength() + } + + /** + * Set the value from another STJson + */ + setValue(v: STJson): void { + if (!(v instanceof STJson)) { + throw new Error('setValue: value must be STJson') + } + this.data = v.data + this.jsonType = v.jsonType + } + + /** + * Get serialized type ID + */ + getSType(): SerializedTypeID { + return SerializedTypeID.STI_JSON + } +} + +export { STJson } diff --git a/packages/ripple-binary-codec/src/types/path-set.ts b/packages/ripple-binary-codec/src/types/path-set.ts index 4359255df9..603571fa56 100644 --- a/packages/ripple-binary-codec/src/types/path-set.ts +++ b/packages/ripple-binary-codec/src/types/path-set.ts @@ -1,7 +1,7 @@ import { AccountID } from './account-id' import { Currency } from './currency' import { BinaryParser } from '../serdes/binary-parser' -import { SerializedType, JsonObject } from './serialized-type' +import { SerializedType, JsonObject, SerializedTypeID } from './serialized-type' import { bytesToHex, concat } from '@xrplf/isomorphic/utils' /** @@ -285,6 +285,10 @@ class PathSet extends SerializedType { return json } + + getSType(): SerializedTypeID { + return SerializedTypeID.STI_PATHSET + } } export { PathSet } diff --git a/packages/ripple-binary-codec/src/types/serialized-type.ts b/packages/ripple-binary-codec/src/types/serialized-type.ts index 2f039f0cd1..76f7efeed4 100644 --- a/packages/ripple-binary-codec/src/types/serialized-type.ts +++ b/packages/ripple-binary-codec/src/types/serialized-type.ts @@ -2,6 +2,69 @@ import { BytesList } from '../serdes/binary-serializer' import { BinaryParser } from '../serdes/binary-parser' import { XrplDefinitionsBase } from '../enums' import { bytesToHex } from '@xrplf/isomorphic/utils' +import definitions from '../enums/definitions.json' + +/** + * Enum for SerializedTypeID values used in XRPL + * These match the C++ implementation's STI_ constants + */ +export enum SerializedTypeID { + STI_NOTPRESENT = 0, + STI_UINT16 = 1, + STI_UINT32 = 2, + STI_UINT64 = 3, + STI_UINT128 = 4, + STI_UINT256 = 5, + STI_AMOUNT = 6, + STI_VL = 7, + STI_ACCOUNT = 8, + STI_NUMBER = 9, + STI_INT32 = 10, + STI_INT64 = 11, + + STI_OBJECT = 14, + STI_ARRAY = 15, + + STI_UINT8 = 16, + STI_UINT160 = 17, + STI_PATHSET = 18, + STI_VECTOR256 = 19, + STI_UINT96 = 20, + STI_UINT192 = 21, + STI_UINT384 = 22, + STI_UINT512 = 23, + STI_ISSUE = 24, + STI_XCHAIN_BRIDGE = 25, + STI_CURRENCY = 26, + STI_DATA = 27, + STI_DATATYPE = 28, + STI_JSON = 29, +} + +/** + * Maps built dynamically from definitions.json TYPES. + * This ensures type string names (e.g. "Hash256", "Hash128") stay in sync + * with the canonical definitions rather than using hardcoded uppercase variants. + */ + +// Map of type name strings to SerializedTypeID values +export const TYPE_STRING_TO_ID: Record = {} + +// Map of numeric type codes to SerializedTypeID values +export const TYPE_NUMBER_TO_ID: Record = {} + +// Map of SerializedTypeID values to type name strings +export const TYPE_ID_TO_STRING: Record = {} + +// Populate all three maps from definitions.json TYPES +for (const [name, id] of Object.entries(definitions.TYPES)) { + if (id >= 0) { + const typeId = id as SerializedTypeID + TYPE_STRING_TO_ID[name] = typeId + TYPE_NUMBER_TO_ID[id] = typeId + TYPE_ID_TO_STRING[typeId] = name + } +} type JSON = string | number | boolean | null | undefined | JSON[] | JsonObject @@ -77,6 +140,10 @@ class SerializedType { toString(): string { return this.toHex() } + + getSType(): SerializedTypeID { + return this.getSType() + } } /** diff --git a/packages/ripple-binary-codec/src/types/st-array.ts b/packages/ripple-binary-codec/src/types/st-array.ts index c705d4d864..5edc4db9eb 100644 --- a/packages/ripple-binary-codec/src/types/st-array.ts +++ b/packages/ripple-binary-codec/src/types/st-array.ts @@ -1,5 +1,5 @@ import { DEFAULT_DEFINITIONS, XrplDefinitionsBase } from '../enums' -import { SerializedType, JsonObject } from './serialized-type' +import { SerializedType, JsonObject, SerializedTypeID } from './serialized-type' import { STObject } from './st-object' import { BinaryParser } from '../serdes/binary-parser' import { concat } from '@xrplf/isomorphic/utils' @@ -108,6 +108,10 @@ class STArray extends SerializedType { return result } + + getSType(): SerializedTypeID { + return SerializedTypeID.STI_ARRAY + } } export { STArray } diff --git a/packages/ripple-binary-codec/src/types/st-number.ts b/packages/ripple-binary-codec/src/types/st-number.ts index 2e432bf374..efea789a23 100644 --- a/packages/ripple-binary-codec/src/types/st-number.ts +++ b/packages/ripple-binary-codec/src/types/st-number.ts @@ -1,6 +1,6 @@ /* eslint-disable complexity -- required for various checks */ import { BinaryParser } from '../serdes/binary-parser' -import { SerializedType } from './serialized-type' +import { SerializedType, SerializedTypeID } from './serialized-type' import { writeInt32BE, writeInt64BE, readInt32BE, readInt64BE } from '../utils' /** @@ -307,4 +307,8 @@ export class STNumber extends SerializedType { fractionPart ? '.' + fractionPart : '' }` } + + getSType(): SerializedTypeID { + return SerializedTypeID.STI_NUMBER + } } diff --git a/packages/ripple-binary-codec/src/types/st-object.ts b/packages/ripple-binary-codec/src/types/st-object.ts index f17bafc62d..0b507cdfd0 100644 --- a/packages/ripple-binary-codec/src/types/st-object.ts +++ b/packages/ripple-binary-codec/src/types/st-object.ts @@ -4,7 +4,7 @@ import { Bytes, XrplDefinitionsBase, } from '../enums' -import { SerializedType, JsonObject } from './serialized-type' +import { SerializedType, JsonObject, SerializedTypeID } from './serialized-type' import { xAddressToClassicAddress, isValidXAddress } from 'ripple-address-codec' import { BinaryParser } from '../serdes/binary-parser' import { BinarySerializer, BytesList } from '../serdes/binary-serializer' @@ -151,7 +151,13 @@ class STObject extends SerializedType { ? STArray.from(xAddressDecoded[field.name], definitions) : field.type.name === 'UInt64' ? UInt64.from(xAddressDecoded[field.name], field.name) - : field.associatedType.from(xAddressDecoded[field.name]) + : field.associatedType?.from + ? field.associatedType.from(xAddressDecoded[field.name]) + : (() => { + throw new Error( + `Type ${field.type.name} for field ${field.name} is missing associatedType.from`, + ) + })() if (associatedValue == undefined) { throw new TypeError( @@ -201,6 +207,10 @@ class STObject extends SerializedType { return accumulator } + + getSType(): SerializedTypeID { + return SerializedTypeID.STI_OBJECT + } } export { STObject } diff --git a/packages/ripple-binary-codec/src/types/uint-16.ts b/packages/ripple-binary-codec/src/types/uint-16.ts index a217333d5b..5e2c68f6e6 100644 --- a/packages/ripple-binary-codec/src/types/uint-16.ts +++ b/packages/ripple-binary-codec/src/types/uint-16.ts @@ -1,6 +1,7 @@ import { UInt } from './uint' import { BinaryParser } from '../serdes/binary-parser' import { readUInt16BE, writeUInt16BE } from '../utils' +import { SerializedTypeID } from './serialized-type' /** * Derived UInt class for serializing/deserializing 16 bit UInt @@ -48,6 +49,10 @@ class UInt16 extends UInt { valueOf(): number { return parseInt(readUInt16BE(this.bytes, 0)) } + + getSType(): SerializedTypeID { + return SerializedTypeID.STI_UINT16 + } } export { UInt16 } diff --git a/packages/ripple-binary-codec/src/types/uint-32.ts b/packages/ripple-binary-codec/src/types/uint-32.ts index e188ae910c..d9c47a2b31 100644 --- a/packages/ripple-binary-codec/src/types/uint-32.ts +++ b/packages/ripple-binary-codec/src/types/uint-32.ts @@ -1,6 +1,7 @@ import { UInt } from './uint' import { BinaryParser } from '../serdes/binary-parser' import { readUInt32BE, writeUInt32BE } from '../utils' +import { SerializedTypeID } from './serialized-type' /** * Derived UInt class for serializing/deserializing 32 bit UInt @@ -54,6 +55,10 @@ class UInt32 extends UInt { valueOf(): number { return parseInt(readUInt32BE(this.bytes, 0), 10) } + + getSType(): SerializedTypeID { + return SerializedTypeID.STI_UINT32 + } } export { UInt32 } diff --git a/packages/ripple-binary-codec/src/types/uint-64.ts b/packages/ripple-binary-codec/src/types/uint-64.ts index 2b232f39c0..88a2d6c59e 100644 --- a/packages/ripple-binary-codec/src/types/uint-64.ts +++ b/packages/ripple-binary-codec/src/types/uint-64.ts @@ -3,6 +3,7 @@ import { BinaryParser } from '../serdes/binary-parser' import { bytesToHex, concat, hexToBytes } from '@xrplf/isomorphic/utils' import { readUInt32BE, writeUInt32BE } from '../utils' import { DEFAULT_DEFINITIONS, XrplDefinitionsBase } from '../enums' +import { SerializedTypeID } from './serialized-type' const HEX_REGEX = /^[a-fA-F0-9]{1,16}$/ const BASE10_REGEX = /^[0-9]{1,20}$/ @@ -131,6 +132,10 @@ class UInt64 extends UInt { toBytes(): Uint8Array { return this.bytes } + + getSType(): SerializedTypeID { + return SerializedTypeID.STI_UINT64 + } } export { UInt64 } diff --git a/packages/ripple-binary-codec/src/types/uint-8.ts b/packages/ripple-binary-codec/src/types/uint-8.ts index 7d7ae9753f..ca805ffc6c 100644 --- a/packages/ripple-binary-codec/src/types/uint-8.ts +++ b/packages/ripple-binary-codec/src/types/uint-8.ts @@ -2,6 +2,7 @@ import { UInt } from './uint' import { BinaryParser } from '../serdes/binary-parser' import { bytesToHex } from '@xrplf/isomorphic/utils' import { writeUInt8 } from '../utils' +import { SerializedTypeID } from './serialized-type' /** * Derived UInt class for serializing/deserializing 8 bit UInt @@ -47,6 +48,10 @@ class UInt8 extends UInt { valueOf(): number { return parseInt(bytesToHex(this.bytes), 16) } + + getSType(): SerializedTypeID { + return SerializedTypeID.STI_UINT8 + } } export { UInt8 } diff --git a/packages/ripple-binary-codec/src/types/vector-256.ts b/packages/ripple-binary-codec/src/types/vector-256.ts index 0f7bc2ccb0..808f1bc120 100644 --- a/packages/ripple-binary-codec/src/types/vector-256.ts +++ b/packages/ripple-binary-codec/src/types/vector-256.ts @@ -1,4 +1,4 @@ -import { SerializedType } from './serialized-type' +import { SerializedType, SerializedTypeID } from './serialized-type' import { BinaryParser } from '../serdes/binary-parser' import { Hash256 } from './hash-256' import { BytesList } from '../serdes/binary-serializer' @@ -74,6 +74,10 @@ class Vector256 extends SerializedType { } return result } + + getSType(): SerializedTypeID { + return SerializedTypeID.STI_VECTOR256 + } } export { Vector256 } diff --git a/packages/ripple-binary-codec/src/types/xchain-bridge.ts b/packages/ripple-binary-codec/src/types/xchain-bridge.ts index 6bda43ae33..560747875f 100644 --- a/packages/ripple-binary-codec/src/types/xchain-bridge.ts +++ b/packages/ripple-binary-codec/src/types/xchain-bridge.ts @@ -1,7 +1,7 @@ import { BinaryParser } from '../serdes/binary-parser' import { AccountID } from './account-id' -import { JsonObject, SerializedType } from './serialized-type' +import { JsonObject, SerializedType, SerializedTypeID } from './serialized-type' import { Issue, IssueObject } from './issue' import { concat } from '@xrplf/isomorphic/utils' @@ -123,6 +123,10 @@ class XChainBridge extends SerializedType { }) return json as XChainBridgeObject } + + getSType(): SerializedTypeID { + return SerializedTypeID.STI_XCHAIN_BRIDGE + } } export { XChainBridge, XChainBridgeObject } diff --git a/packages/ripple-binary-codec/test/binary-serializer.test.ts b/packages/ripple-binary-codec/test/binary-serializer.test.ts index 5d690d5295..d39630b6b9 100644 --- a/packages/ripple-binary-codec/test/binary-serializer.test.ts +++ b/packages/ripple-binary-codec/test/binary-serializer.test.ts @@ -55,6 +55,13 @@ const Ticket = { }, } +const Contract = { + call: { + tx: require('./fixtures/contract-call-tx.json'), + binary: require('./fixtures/contract-call-binary.json'), + }, +} + let json_undefined = { TakerPays: '223174650', Account: 'rPk2dXr27rMw9G5Ej9ad2Tt7RJzGy8ycBp', @@ -280,6 +287,13 @@ function nfTokenTest() { } } +function ContractTest() { + it('can serialize ContractCall', () => { + expect(encode(Contract.call.tx)).toEqual(Contract.call.binary) + expect(decode(Contract.call.binary)).toEqual(Contract.call.tx) + }) +} + describe('Binary Serialization', function () { describe('nestedObjectTests', nestedObjectTests) describe('BytesList', bytesListTest) @@ -292,4 +306,5 @@ describe('Binary Serialization', function () { describe('OmitUndefined', omitUndefinedTest) describe('TicketTest', ticketTest) describe('NFToken', nfTokenTest) + describe('Contract', ContractTest) }) diff --git a/packages/ripple-binary-codec/test/fixtures/contract-call-binary.json b/packages/ripple-binary-codec/test/fixtures/contract-call-binary.json new file mode 100644 index 0000000000..93e398fd53 --- /dev/null +++ b/packages/ripple-binary-codec/test/fixtures/contract-call-binary.json @@ -0,0 +1 @@ +"12005A24000000052048000F42406840000000000000C870220F66756E6374696F6E5F706172616D738114AE123A8556F3CF91154711376AFB0F894F832B3D801B1478A28D084038E5C11F268DE3AAF7182805BE87BFF023E029204A00000000011B0010FFE1E029204A00000000011B0001FFFFE1E029204A00000000011B0002FFFFFFFFE1E029204A00000000011B00037FFFFFFFFFFFFFFFE1E029204A00000000011B000400000000000000000000000000000001E1E029204A00000000011B00110000000000000000000000000000000000000001E1E029204A00000000011B0015000000000000000000000000000000000000000000000001E1E029204A00000000011B0005D955DAC2E77519F05AD151A5D3C99FC8125FB39D58FF9F106F1ACA4491902C25E1E029204A00000000011B000704DEADBEEFE1E029204A00000000011B000814AE123A8556F3CF91154711376AFB0F894F832B3DE1E029204A00000000011B000640000000000F4240E1E029204A00000000011B0006D4844364C5BB00000000000000000000000000005553440000000000A407AF5856CCF3C42619DAA925813FC955C72983E1E029204A00000000011B000910A741A462780000FFFFFFEEE1F1" diff --git a/packages/ripple-binary-codec/test/fixtures/contract-call-tx.json b/packages/ripple-binary-codec/test/fixtures/contract-call-tx.json new file mode 100644 index 0000000000..16a8634a9a --- /dev/null +++ b/packages/ripple-binary-codec/test/fixtures/contract-call-tx.json @@ -0,0 +1,132 @@ +{ + "Account": "rG1QQv2nh2gr7RCZ1P8YYcBUKCCN633jCn", + "ComputationAllowance": 1000000, + "ContractAccount": "rBziFEmkBf1QaVKq36EHu9BXaLGmvDC6ww", + "Fee": "200", + "FunctionName": "66756E6374696F6E5F706172616D73", + "Parameters": [ + { + "Parameter": { + "ParameterFlag": 0, + "ParameterValue": { + "type": "UInt8", + "value": 255 + } + } + }, + { + "Parameter": { + "ParameterFlag": 0, + "ParameterValue": { + "type": "UInt16", + "value": 65535 + } + } + }, + { + "Parameter": { + "ParameterFlag": 0, + "ParameterValue": { + "type": "UInt32", + "value": 4294967295 + } + } + }, + { + "Parameter": { + "ParameterFlag": 0, + "ParameterValue": { + "type": "UInt64", + "value": "7FFFFFFFFFFFFFFF" + } + } + }, + { + "Parameter": { + "ParameterFlag": 0, + "ParameterValue": { + "type": "Hash128", + "value": "00000000000000000000000000000001" + } + } + }, + { + "Parameter": { + "ParameterFlag": 0, + "ParameterValue": { + "type": "Hash160", + "value": "0000000000000000000000000000000000000001" + } + } + }, + { + "Parameter": { + "ParameterFlag": 0, + "ParameterValue": { + "type": "Hash192", + "value": "000000000000000000000000000000000000000000000001" + } + } + }, + { + "Parameter": { + "ParameterFlag": 0, + "ParameterValue": { + "type": "Hash256", + "value": "D955DAC2E77519F05AD151A5D3C99FC8125FB39D58FF9F106F1ACA4491902C25" + } + } + }, + { + "Parameter": { + "ParameterFlag": 0, + "ParameterValue": { + "type": "Blob", + "value": "DEADBEEF" + } + } + }, + { + "Parameter": { + "ParameterFlag": 0, + "ParameterValue": { + "type": "AccountID", + "value": "rG1QQv2nh2gr7RCZ1P8YYcBUKCCN633jCn" + } + } + }, + { + "Parameter": { + "ParameterFlag": 0, + "ParameterValue": { + "type": "Amount", + "value": "1000000" + } + } + }, + { + "Parameter": { + "ParameterFlag": 0, + "ParameterValue": { + "type": "Amount", + "value": { + "currency": "USD", + "issuer": "rExKpRKXNz25UAjbckCRtQsJFcSfjL9Er3", + "value": "1.2" + } + } + } + }, + { + "Parameter": { + "ParameterFlag": 0, + "ParameterValue": { + "type": "Number", + "value": "1.2" + } + } + } + ], + "Sequence": 5, + "TransactionType": "ContractCall" +} diff --git a/packages/ripple-binary-codec/test/st-data.test.ts b/packages/ripple-binary-codec/test/st-data.test.ts new file mode 100644 index 0000000000..dc4026452c --- /dev/null +++ b/packages/ripple-binary-codec/test/st-data.test.ts @@ -0,0 +1,486 @@ +import { BinaryParser } from '../src/binary' +import { coreTypes } from '../src/types' +// import { bytesToHex, hexToBytes } from '@xrplf/isomorphic/utils' + +const { Data } = coreTypes + +describe('Data Type with all STTypes', () => { + describe('UInt8', () => { + it('should encode and decode UInt8', () => { + const data = Data.from({ type: 'UInt8', value: 255 }) + const hex = data.toHex() + expect(hex).toBe('0010FF') // 0010 = type ID for UInt8, FF = 255 + + const parser = new BinaryParser(hex) + const parsed = Data.fromParser(parser) + expect(parsed.toJSON()).toEqual({ type: 'UInt8', value: 255 }) + }) + + it('should handle UInt8 zero', () => { + const data = Data.from({ type: 'UInt8', value: 0 }) + expect(data.toHex()).toBe('001000') + expect(data.toJSON()).toEqual({ type: 'UInt8', value: 0 }) + }) + + it('should handle UInt8 from string', () => { + const data = Data.from({ type: 'UInt8', value: '128' }) + expect(data.toHex()).toBe('001080') + expect(data.toJSON()).toEqual({ type: 'UInt8', value: 128 }) + }) + }) + + describe('UInt16', () => { + it('should encode and decode UInt16', () => { + const data = Data.from({ type: 'UInt16', value: 65535 }) + const hex = data.toHex() + expect(hex).toBe('0001FFFF') // 0001 = type ID for UInt16, FFFF = 65535 + + const parser = new BinaryParser(hex) + const parsed = Data.fromParser(parser) + expect(parsed.toJSON()).toEqual({ type: 'UInt16', value: 65535 }) + }) + + it('should handle UInt16 zero', () => { + const data = Data.from({ type: 'UInt16', value: 0 }) + expect(data.toHex()).toBe('00010000') + }) + }) + + describe('UInt32', () => { + it('should encode and decode UInt32', () => { + const data = Data.from({ type: 'UInt32', value: 4294967295 }) + const hex = data.toHex() + expect(hex).toBe('0002FFFFFFFF') // 0002 = type ID for UInt32 + + const parser = new BinaryParser(hex) + const parsed = Data.fromParser(parser) + expect(parsed.toJSON()).toEqual({ type: 'UInt32', value: 4294967295 }) + }) + }) + + describe('UInt64', () => { + it('should encode and decode UInt64', () => { + const data = Data.from({ type: 'UInt64', value: '7fffffffffffffff' }) + const hex = data.toHex() + expect(hex).toBe('00037FFFFFFFFFFFFFFF') // 0003 = type ID for UInt64 + + const parser = new BinaryParser(hex) + const parsed = Data.fromParser(parser) + expect(parsed.toJSON()).toEqual({ + type: 'UInt64', + value: '7FFFFFFFFFFFFFFF', + }) + }) + + // it('should handle UInt64 as number string', () => { + // const data = Data.from({ type: 'UInt64', value: '123456789' }) + // const parser = new BinaryParser(data.toHex()) + // const parsed = Data.fromParser(parser) + // expect(parsed.getValue().toJSON()).toBe('123456789') + // }) + }) + + describe('Hash128', () => { + it('should encode and decode Hash128', () => { + const value = '00000000000000000000000000000001' + const data = Data.from({ type: 'Hash128', value }) + const hex = data.toHex() + expect(hex).toBe('0004' + value) // 0004 = type ID for Hash128 + + const parser = new BinaryParser(hex) + const parsed = Data.fromParser(parser) + expect(parsed.toJSON()).toEqual({ + type: 'Hash128', + value: value.toUpperCase(), + }) + }) + }) + + describe('Hash160', () => { + it('should encode and decode Hash160', () => { + const value = '0000000000000000000000000000000000000001' + const data = Data.from({ type: 'Hash160', value }) + const hex = data.toHex() + expect(hex).toBe('0011' + value) // 0011 = type ID for Hash160 + + const parser = new BinaryParser(hex) + const parsed = Data.fromParser(parser) + expect(parsed.toJSON()).toEqual({ + type: 'Hash160', + value: value.toUpperCase(), + }) + }) + }) + + describe('Hash192', () => { + it('should encode and decode Hash192', () => { + const value = '000000000000000000000000000000000000000000000001' + const data = Data.from({ type: 'Hash192', value }) + const hex = data.toHex() + expect(hex).toBe('0015' + value) // 0015 = type ID for Hash192 + + const parser = new BinaryParser(hex) + const parsed = Data.fromParser(parser) + expect(parsed.toJSON()).toEqual({ + type: 'Hash192', + value: value.toUpperCase(), + }) + }) + }) + + describe('Hash256', () => { + it('should encode and decode Hash256', () => { + const value = + 'D955DAC2E77519F05AD151A5D3C99FC8125FB39D58FF9F106F1ACA4491902C25' + const data = Data.from({ type: 'Hash256', value }) + const hex = data.toHex() + expect(hex).toBe('0005' + value) // 0005 = type ID for Hash256 + + const parser = new BinaryParser(hex) + const parsed = Data.fromParser(parser) + expect(parsed.toJSON()).toEqual({ type: 'Hash256', value }) + }) + }) + + describe('Blob (Variable Length)', () => { + it('should encode and decode Blob with hex string', () => { + const value = 'DEADBEEF' + const data = Data.from({ type: 'Blob', value }) + const hex = data.toHex() + + // Blob encoding: type ID (0007) + length prefix (04 for 4 bytes) + data + expect(hex).toBe('000704DEADBEEF') + + const parser = new BinaryParser(hex) + const parsed = Data.fromParser(parser) + expect(parsed.toJSON()).toEqual({ type: 'Blob', value }) + }) + + it('should handle empty Blob', () => { + const data = Data.from({ type: 'Blob', value: '' }) + const hex = data.toHex() + expect(hex).toBe('000700') // 00 = length 0 + + const parser = new BinaryParser(hex) + const parsed = Data.fromParser(parser) + expect(parsed.toJSON()).toEqual({ type: 'Blob', value: '' }) + }) + + it('should handle longer Blob data', () => { + const value = 'DEADBEEFCAFE' + '00'.repeat(100) // Long hex string + const data = Data.from({ type: 'Blob', value }) + const parser = new BinaryParser(data.toHex()) + const parsed = Data.fromParser(parser) + expect(parsed.toJSON()).toEqual({ type: 'Blob', value }) + }) + }) + + describe('AccountID', () => { + it('should encode and decode AccountID', () => { + const value = 'rG1QQv2nh2gr7RCZ1P8YYcBUKCCN633jCn' + const data = Data.from({ type: 'AccountID', value }) + const hex = data.toHex() + + // AccountID encoding: type ID (0008) + (14) + 20 bytes of account ID + expect(hex.substring(0, 4)).toBe('0008') + expect(hex.length).toBe(6 + 40) // 2 bytes type + 1 byte length + 20 bytes account + + const parser = new BinaryParser(hex) + const parsed = Data.fromParser(parser) + expect(parsed.toJSON()).toEqual({ type: 'AccountID', value }) + }) + + it('should handle different account format', () => { + const value = 'rExKpRKXNz25UAjbckCRtQsJFcSfjL9Er3' + const data = Data.from({ type: 'AccountID', value }) + const parser = new BinaryParser(data.toHex()) + const parsed = Data.fromParser(parser) + expect(parsed.toJSON()).toEqual({ type: 'AccountID', value }) + }) + }) + + // describe('AMOUNT', () => { + // it('should encode and decode XRP AMOUNT', () => { + // const value = '1000000' + // const data = Data.from({ type: 'AMOUNT', value }) + // const hex = data.toHex() + + // // XRP amount encoding: type ID (0006) + positive bit + amount + // expect(hex).toBe('000640000000000F4240') // 0F4240 = 1000000 in hex + + // const parser = new BinaryParser(hex) + // const parsed = Data.fromParser(parser) + // expect(parsed.toJSON()).toEqual({ type: 'AMOUNT', value }) + // }) + + // it('should encode and decode issued currency AMOUNT', () => { + // const value = { + // currency: 'USD', + // issuer: 'rExKpRKXNz25UAjbckCRtQsJFcSfjL9Er3', + // value: '1.2', + // } + // const data = Data.from({ type: 'AMOUNT', value }) + // const hex = data.toHex() + + // // Issued currency: type ID (0006) + 48 bytes (8 bytes amount + 20 bytes currency + 20 bytes issuer) + // expect(hex.substring(0, 4)).toBe('0006') + // expect(hex.length).toBe(4 + 96) // 2 bytes type + 48 bytes for issued currency + + // const parser = new BinaryParser(hex) + // const parsed = Data.fromParser(parser) + // const parsedValue = parsed.toJSON().value + + // // The parsed value should match the original structure + // expect(parsedValue).toMatchObject({ + // currency: 'USD', + // issuer: value.issuer, + // value: '1.2', + // }) + // }) + + // it('should handle negative issued currency amount', () => { + // const value = { + // currency: 'EUR', + // issuer: 'rExKpRKXNz25UAjbckCRtQsJFcSfjL9Er3', + // value: '-100.5', + // } + // const data = Data.from({ type: 'AMOUNT', value }) + // const parser = new BinaryParser(data.toHex()) + // const parsed = Data.fromParser(parser) + // const parsedValue = parsed.toJSON().value + + // expect(parsedValue).toMatchObject({ + // currency: 'EUR', + // value: '-100.5', + // }) + // }) + // }) + + // describe('CURRENCY', () => { + // it('should encode and decode standard CURRENCY', () => { + // const value = 'USD' + // const data = Data.from({ type: 'CURRENCY', value }) + // const hex = data.toHex() + + // // CURRENCY encoding: type ID (000A) + 20 bytes currency code + // expect(hex.substring(0, 4)).toBe('000A') + // expect(hex.length).toBe(4 + 40) // 2 bytes type + 20 bytes currency + + // const parser = new BinaryParser(hex) + // const parsed = Data.fromParser(parser) + // expect(parsed.toJSON()).toEqual({ type: 'CURRENCY', value }) + // }) + + // it('should handle non-standard currency code', () => { + // const value = '0158415500000000C1F76FF6ECB0BAC600000000' + // const data = Data.from({ type: 'CURRENCY', value }) + // const parser = new BinaryParser(data.toHex()) + // const parsed = Data.fromParser(parser) + // expect(parsed.toJSON()).toEqual({ + // type: 'CURRENCY', + // value: value.toUpperCase(), + // }) + // }) + // }) + + describe('Number (STNumber)', () => { + it('should encode and decode positive decimal Number', () => { + const value = '1.2' + const data = Data.from({ type: 'Number', value }) + const hex = data.toHex() + + // Number encoding: type ID (0009) + serialized number + expect(hex.substring(0, 4)).toBe('0009') + + const parser = new BinaryParser(hex) + const parsed = Data.fromParser(parser) + expect(parsed.toJSON()).toEqual({ type: 'Number', value }) + }) + + it('should handle integer Number', () => { + const value = '123456789' + const data = Data.from({ type: 'Number', value }) + const parser = new BinaryParser(data.toHex()) + const parsed = Data.fromParser(parser) + expect(parsed.toJSON()).toEqual({ type: 'Number', value }) + }) + + it('should handle negative Number', () => { + const value = '-987.654' + const data = Data.from({ type: 'Number', value }) + const parser = new BinaryParser(data.toHex()) + const parsed = Data.fromParser(parser) + expect(parsed.toJSON()).toEqual({ type: 'Number', value }) + }) + + it('should handle zero Number', () => { + const value = '0' + const data = Data.from({ type: 'Number', value }) + const parser = new BinaryParser(data.toHex()) + const parsed = Data.fromParser(parser) + expect(parsed.toJSON()).toEqual({ type: 'Number', value }) + }) + + it('should handle scientific notation Number', () => { + const value = '1.23e5' + const data = Data.from({ type: 'Number', value }) + const parser = new BinaryParser(data.toHex()) + const parsed = Data.fromParser(parser) + // STNumber normalizes scientific notation to decimal + expect(parsed.toJSON()).toEqual({ type: 'Number', value: '123000' }) + }) + }) + + // describe('ISSUE', () => { + // it('should encode and decode ISSUE with currency only', () => { + // const value = { currency: 'USD' } + // const data = Data.from({ type: 'ISSUE', value }) + // const hex = data.toHex() + + // // ISSUE encoding: type ID (000C) + currency (20 bytes) + issuer (20 bytes if present) + // expect(hex.substring(0, 4)).toBe('000C') + + // const parser = new BinaryParser(hex) + // const parsed = Data.fromParser(parser) + // expect(parsed.toJSON()).toEqual({ type: 'ISSUE', value }) + // }) + + // it('should encode and decode ISSUE with currency and issuer', () => { + // const value = { + // currency: 'EUR', + // issuer: 'rExKpRKXNz25UAjbckCRtQsJFcSfjL9Er3', + // } + // const data = Data.from({ type: 'ISSUE', value }) + // const parser = new BinaryParser(data.toHex()) + // const parsed = Data.fromParser(parser) + // expect(parsed.toJSON()).toEqual({ type: 'ISSUE', value }) + // }) + // }) + + // describe('Complex roundtrip tests', () => { + // it('should correctly serialize all parameter types from the contract call', () => { + // const parameters = [ + // { type: 'UINT8', value: 255 }, + // { type: 'UINT16', value: 65535 }, + // { type: 'UINT32', value: 4294967295 }, + // { type: 'UINT64', value: '7fffffffffffffff' }, + // { type: 'UINT128', value: '00000000000000000000000000000001' }, + // { type: 'UINT160', value: '0000000000000000000000000000000000000001' }, + // { + // type: 'UINT192', + // value: '000000000000000000000000000000000000000000000001', + // }, + // { + // type: 'UINT256', + // value: + // 'D955DAC2E77519F05AD151A5D3C99FC8125FB39D58FF9F106F1ACA4491902C25', + // }, + // { type: 'VL', value: 'DEADBEEF' }, + // { type: 'ACCOUNT', value: 'rG1QQv2nh2gr7RCZ1P8YYcBUKCCN633jCn' }, + // { type: 'AMOUNT', value: '1000000' }, + // { + // type: 'AMOUNT', + // value: { + // currency: 'USD', + // issuer: 'rExKpRKXNz25UAjbckCRtQsJFcSfjL9Er3', + // value: '1.2', + // }, + // }, + // { type: 'NUMBER', value: '1.2' }, + // ] + + // const results: string[] = [] + + // parameters.forEach((param, index) => { + // const data = Data.from(param) + // const hex = data.toHex() + // results.push(`Parameter ${index} (${param.type}): ${hex}`) + + // // Verify roundtrip + // const parser = new BinaryParser(hex) + // const parsed = Data.fromParser(parser) + // const json = parsed.toJSON() + + // // Log any mismatches + // if (JSON.stringify(json.value) !== JSON.stringify(param.value)) { + // console.log(`Mismatch at parameter ${index}:`) + // console.log(' Original:', param.value) + // console.log(' Parsed:', json.value) + // } + // }) + + // // Log all results for debugging + // console.log('All parameter encodings:') + // results.forEach((r) => console.log(r)) + // }) + // }) + + // describe('Error handling', () => { + // it('should throw on invalid type string', () => { + // expect(() => { + // Data.from({ type: 'INVALID_TYPE', value: '123' }) + // }).toThrow('Data: unsupported type string: INVALID_TYPE') + // }) + + // it('should throw on UINT8 out of range', () => { + // expect(() => { + // Data.from({ type: 'UINT8', value: 256 }) + // }).toThrow('UINT8 value out of range') + // }) + + // it('should throw on UINT16 out of range', () => { + // expect(() => { + // Data.from({ type: 'UINT16', value: 65536 }) + // }).toThrow('UINT16 value out of range') + // }) + + // it('should throw on invalid input format', () => { + // expect(() => { + // Data.from('invalid') + // }).toThrow('Data.from: value must be Data instance or DataJSON object') + // }) + // }) + + // describe('Data equality', () => { + // it('should correctly compare equal Data instances', () => { + // const data1 = Data.from({ type: 'UINT32', value: 12345 }) + // const data2 = Data.from({ type: 'UINT32', value: 12345 }) + // expect(data1.equals(data2)).toBe(true) + // }) + + // it('should correctly identify unequal Data instances', () => { + // const data1 = Data.from({ type: 'UINT32', value: 12345 }) + // const data2 = Data.from({ type: 'UINT32', value: 54321 }) + // expect(data1.equals(data2)).toBe(false) + // }) + + // it('should identify different types as unequal', () => { + // const data1 = Data.from({ type: 'UINT16', value: 100 }) + // const data2 = Data.from({ type: 'UINT32', value: 100 }) + // expect(data1.equals(data2)).toBe(false) + // }) + // }) + + // describe('Data getValue method', () => { + // it('should correctly retrieve UINT8 value', () => { + // const data = Data.from({ type: 'UINT8', value: 42 }) + // const value = data.getValue() + // expect(value).toBeInstanceOf(UInt8) + // expect(value.toJSON()).toBe(42) + // }) + + // it('should correctly retrieve VL value', () => { + // const data = Data.from({ type: 'VL', value: 'DEADBEEF' }) + // const value = data.getValue() + // expect(value).toBeInstanceOf(Blob) + // expect(value.toJSON()).toBe('DEADBEEF') + // }) + + // it('should correctly retrieve AMOUNT value', () => { + // const data = Data.from({ type: 'AMOUNT', value: '1000000' }) + // const value = data.getValue() + // expect(value).toBeInstanceOf(Amount) + // expect(value.toJSON()).toBe('1000000') + // }) + // }) +}) diff --git a/packages/ripple-binary-codec/tools/generateDefinitions.js b/packages/ripple-binary-codec/tools/generateDefinitions.js index 5f970f694b..21330a0af0 100644 --- a/packages/ripple-binary-codec/tools/generateDefinitions.js +++ b/packages/ripple-binary-codec/tools/generateDefinitions.js @@ -104,6 +104,7 @@ async function main() { VL: 'Blob', DIR_NODE: 'DirectoryNode', PAYCHAN: 'PayChannel', + DATATYPE: 'DataType', } if (nonstandardRenames[inp] != null) return nonstandardRenames[inp] diff --git a/packages/xrpl/package.json b/packages/xrpl/package.json index ee49748175..477f7d8adb 100644 --- a/packages/xrpl/package.json +++ b/packages/xrpl/package.json @@ -1,6 +1,6 @@ { "name": "xrpl", - "version": "4.6.0", + "version": "4.7.0-smartcontract.0", "license": "ISC", "description": "A TypeScript/JavaScript API for interacting with the XRP Ledger in Node.js and the browser", "files": [ @@ -30,7 +30,7 @@ "eventemitter3": "^5.0.1", "fast-json-stable-stringify": "^2.1.0", "ripple-address-codec": "^5.0.0", - "ripple-binary-codec": "^2.7.0", + "ripple-binary-codec": "^2.8.0-smartcontract.0", "ripple-keypairs": "^2.0.0" }, "devDependencies": { diff --git a/packages/xrpl/src/client/index.ts b/packages/xrpl/src/client/index.ts index 420d60058f..a6659f1494 100644 --- a/packages/xrpl/src/client/index.ts +++ b/packages/xrpl/src/client/index.ts @@ -69,6 +69,7 @@ import { autofillBatchTxn, handleDeliverMax, getTransactionFee, + getComputationAllowance, } from '../sugar/autofill' import { formatBalances } from '../sugar/balances' import { @@ -688,6 +689,7 @@ class Client extends EventEmitter { * @returns The autofilled transaction. * @throws ValidationError If Amount and DeliverMax fields are not identical in a Payment Transaction */ + // eslint-disable-next-line complexity -- ignore public async autofill( transaction: T, signersCount?: number, @@ -717,6 +719,9 @@ class Client extends EventEmitter { if (tx.TransactionType === 'Payment' && tx.DeliverMax != null) { handleDeliverMax(tx) } + if (tx.TransactionType === 'ContractCall' && !tx.ComputationAllowance) { + promises.push(getComputationAllowance(this, tx)) + } return Promise.all(promises).then(() => tx) } diff --git a/packages/xrpl/src/models/common/index.ts b/packages/xrpl/src/models/common/index.ts index 4297f47252..a9e9732d54 100644 --- a/packages/xrpl/src/models/common/index.ts +++ b/packages/xrpl/src/models/common/index.ts @@ -326,3 +326,42 @@ export interface MPTokenMetadataUri { */ title: string } + +export interface Function { + Function: { + FunctionName: string + Parameters?: Parameter[] + } +} + +export interface ParameterType { + type: string +} + +export interface ParameterValue { + type: string + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- value can be any type depending on the parameter + value: any +} + +export interface Parameter { + Parameter: { + ParameterFlag?: number + ParameterType?: ParameterType + ParameterValue?: ParameterValue + } +} + +export interface InstanceParameter { + InstanceParameter: { + ParameterFlag: number + ParameterType: ParameterType + } +} + +export interface InstanceParameterValue { + InstanceParameterValue: { + ParameterFlag: number + ParameterValue: ParameterValue + } +} diff --git a/packages/xrpl/src/models/ledger/AccountRoot.ts b/packages/xrpl/src/models/ledger/AccountRoot.ts index fdcc5e0f86..1a6249ad47 100644 --- a/packages/xrpl/src/models/ledger/AccountRoot.ts +++ b/packages/xrpl/src/models/ledger/AccountRoot.ts @@ -34,6 +34,12 @@ export default interface AccountRoot extends BaseLedgerEntry, HasPreviousTxnID { * If present, indicates that this is a special AMM AccountRoot; always omitted on non-AMM accounts. */ AMMID?: string + /** + * The ledger entry ID of the corresponding Contract ledger entry. + * Set during contract creation; cannot be modified. + * If present, indicates that this is a special Contract AccountRoot; always omitted on non-Contract accounts. + */ + ContractID?: string /** * A domain associated with this account. In JSON, this is the hexadecimal * for the ASCII representation of the domain. diff --git a/packages/xrpl/src/models/ledger/Contract.ts b/packages/xrpl/src/models/ledger/Contract.ts new file mode 100644 index 0000000000..26288f5b08 --- /dev/null +++ b/packages/xrpl/src/models/ledger/Contract.ts @@ -0,0 +1,26 @@ +import { InstanceParameterValue } from '../common' + +import { BaseLedgerEntry, HasPreviousTxnID } from './BaseLedgerEntry' + +/** + * + * + * @category Ledger Entries + */ +export default interface Contract extends BaseLedgerEntry, HasPreviousTxnID { + LedgerEntryType: 'Contract' + /** The sequence number of the next valid transaction for this account. */ + Sequence: number + /** The owner node for this contract. */ + OwnerNode: string + /** The account that owns this contract. */ + Owner: string + /** The account associated with this contract. */ + ContractAccount: string + /** The hash of the contract. */ + ContractHash: string + /** Instance parameter values for the contract. */ + InstanceParameterValues?: InstanceParameterValue[] + /** URI associated with the contract. */ + URI?: string +} diff --git a/packages/xrpl/src/models/ledger/ContractData.ts b/packages/xrpl/src/models/ledger/ContractData.ts new file mode 100644 index 0000000000..148043d3d1 --- /dev/null +++ b/packages/xrpl/src/models/ledger/ContractData.ts @@ -0,0 +1,20 @@ +import { BaseLedgerEntry, HasPreviousTxnID } from './BaseLedgerEntry' + +/** + * + * + * @category Ledger Entries + */ +export default interface ContractData + extends BaseLedgerEntry, HasPreviousTxnID { + LedgerEntryType: 'ContractData' + /** The owner node for this contract data. */ + OwnerNode: string + /** The account that owns this contract data. */ + Owner: string + /** The account associated with this contract. */ + ContractAccount: string + /** The JSON data for the contract. */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- contract JSON data can have any shape + ContractJson: Record +} diff --git a/packages/xrpl/src/models/ledger/ContractSource.ts b/packages/xrpl/src/models/ledger/ContractSource.ts new file mode 100644 index 0000000000..f22338f742 --- /dev/null +++ b/packages/xrpl/src/models/ledger/ContractSource.ts @@ -0,0 +1,67 @@ +import { Function, InstanceParameter } from '../common' + +import { BaseLedgerEntry, HasPreviousTxnID } from './BaseLedgerEntry' + +/** + * + * + * @category Ledger Entries + */ +export default interface ContractSource + extends BaseLedgerEntry, HasPreviousTxnID { + LedgerEntryType: 'ContractSource' + /** The hash of the contract. */ + ContractHash: string + /** The code for the contract. */ + ContractCode: string + /** The functions available in this contract. */ + Functions: Function[] + /** Instance parameters for the contract. */ + InstanceParameters?: InstanceParameter[] + /** Reference count for this contract source. */ + ReferenceCount: number + + Flags: number +} + +/** + * A boolean map of ContractFlags for simplified code checking Contract settings. + * For submitting settings flags to the ledger, use ContractFlags instead. + */ +export interface ContractFlagsInterface { + /** + * Indicates whether the contract is immutable. + */ + lsfImmutable?: boolean + /** + * Indicates whether the contract code is immutable. + */ + tfCodeImmutable?: boolean + /** + * Indicates whether the contract ABI is immutable. + */ + tfABIImmutable?: boolean + /** + * Indicates whether the contract is undeletable. + */ + tfUndeletable?: boolean +} + +export enum ContractFlags { + /** + * Indicates whether the contract is immutable. + */ + lsfImmutable = 0x00010000, + /** + * Indicates whether the contract code is immutable. + */ + tfCodeImmutable = 0x00020000, + /** + * Indicates whether the contract ABI is immutable. + */ + tfABIImmutable = 0x00040000, + /** + * Indicates whether the contract is undeletable. + */ + tfUndeletable = 0x00080000, +} diff --git a/packages/xrpl/src/models/methods/contractInfo.ts b/packages/xrpl/src/models/methods/contractInfo.ts new file mode 100644 index 0000000000..bb6f60ee47 --- /dev/null +++ b/packages/xrpl/src/models/methods/contractInfo.ts @@ -0,0 +1,60 @@ +import { AccountRoot } from '../ledger' + +import { BaseRequest, BaseResponse, LookupByLedgerRequest } from './baseMethod' + +/** + * The `contract_info` command retrieves information about a contract, its + * activity, and its XRP balance. All information retrieved is relative to a + * particular version of the ledger. Returns an {@link ContractInfoResponse}. + * + * @category Requests + */ +export interface ContractInfoRequest + extends BaseRequest, LookupByLedgerRequest { + command: 'contract_info' + /** A unique identifier for the contract, most commonly the contract's address. */ + contract_account: string + /** If you include an account we will return the contract data for that account. */ + account?: string + /** If you include a function we will return the contract data for that function. */ + function?: string +} + +export interface ContractInfoResponse extends BaseResponse { + result: { + // IDEA + // contract: LedgerObject Contract for the contract instance + // contract_source: LedgerObject ContractSource for the contract code + // contract_account: LedgerObject AccountRoot for pseudo-account of the contract + + contract_account: string + code: string + hash: string + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- function definitions can vary + functions: any[] + source_code_uri: string + + /** + * The AccountRoot ledger object with this account's information, as stored + * in the ledger. + */ + account_data: AccountRoot + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- contract data structure is flexible + contract_data: any + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- user data structure is flexible + user_data?: any + + /** + * The ledger index of the current in-progress ledger, which was used when + * retrieving this information. + */ + ledger_current_index?: number + /** + * The ledger index of the ledger version used when retrieving this + * information. The information does not contain any changes from ledger + * versions newer than this one. + */ + ledger_index?: number + validated?: boolean + } +} diff --git a/packages/xrpl/src/models/methods/index.ts b/packages/xrpl/src/models/methods/index.ts index 96e01dc992..127b67dc75 100644 --- a/packages/xrpl/src/models/methods/index.ts +++ b/packages/xrpl/src/models/methods/index.ts @@ -1,6 +1,6 @@ /* eslint-disable no-inline-comments -- Necessary for important note */ +/* eslint-disable max-len -- Conditional type indentation is deeply nested */ /* eslint-disable max-lines -- There is a lot to export */ -/* eslint-disable prettier/prettier -- Required here to keep formatting in line */ import type { APIVersion, DEFAULT_API_VERSION } from '../common' import { @@ -326,164 +326,164 @@ export type RequestResponseMap< > = T extends AccountChannelsRequest ? AccountChannelsResponse : T extends AccountCurrenciesRequest - ? AccountCurrenciesResponse - : T extends AccountInfoRequest - ? AccountInfoVersionResponseMap - : T extends AccountLinesRequest - ? AccountLinesResponse - : T extends AccountNFTsRequest - ? AccountNFTsResponse - : T extends AccountObjectsRequest - ? AccountObjectsResponse - : T extends AccountOffersRequest - ? AccountOffersResponse - : T extends AccountTxRequest - ? AccountTxVersionResponseMap - : T extends AMMInfoRequest - ? AMMInfoResponse - : T extends GatewayBalancesRequest - ? GatewayBalancesResponse - : T extends GetAggregatePriceRequest - ? GetAggregatePriceResponse - : T extends NoRippleCheckRequest - ? NoRippleCheckResponse - : // NOTE: The order of these LedgerRequest types is important - // to get the proper type matching overrides based on parameters set - // in the request. For example LedgerRequestExpandedTransactionsBinary - // should match LedgerRequestExpandedTransactionsOnly, but not - // LedgerRequestExpandedAccountsOnly. This is because the - // LedgerRequestExpandedTransactionsBinary type is a superset of - // LedgerRequestExpandedTransactionsOnly, but not of the other. - // This is why LedgerRequestExpandedTransactionsBinary is listed - // first in the type list. - // - // Here is an example using real data: - // LedgerRequestExpandedTransactionsBinary = { - // command: 'ledger', - // ledger_index: 'validated', - // expand: true, - // transactions: true, - // binary: true, - // } - // LedgerRequestExpandedTransactionsOnly = { - // command: 'ledger', - // ledger_index: 'validated', - // expand: true, - // transactions: true, - // } - // LedgerRequestExpandedAccountsOnly = { - // command: 'ledger', - // ledger_index: 'validated', - // accounts: true, - // expand: true, - // } - // LedgerRequest = { - // command: 'ledger', - // ledger_index: 'validated', - // } - // - // The type with the most parameters set should be listed first. In this - // case LedgerRequestExpandedTransactionsBinary has the most parameters (`expand`, `transactions`, and `binary`) - // set, so it is listed first. When TypeScript tries to match the type of - // a request to a response, it will try to match the request type to the - // response type in the order they are listed. So, if we have a request - // with the following parameters: - // { - // command: 'ledger', - // ledger_index: 'validated', - // expand: true, - // transactions: true, - // binary: true, - // } - // TypeScript will first try to match the request type to - // LedgerRequestExpandedTransactionsBinary, which will succeed. It will - // then try to match the response type to LedgerResponseExpanded, which - // will also succeed. If we had listed LedgerRequestExpandedTransactionsOnly - // first, TypeScript would have tried to match the request type to - // LedgerRequestExpandedTransactionsOnly, which would have succeeded, but - // then we'd get the wrong response type, LedgerResponse, instead of - // LedgerResponseExpanded. - T extends LedgerRequestExpandedTransactionsBinary - ? LedgerVersionResponseMap - : T extends LedgerRequestExpandedAccountsAndTransactions - ? LedgerResponseExpanded - : T extends LedgerRequestExpandedTransactionsOnly - ? LedgerResponseExpanded - : T extends LedgerRequestExpandedAccountsOnly - ? LedgerResponseExpanded - : T extends LedgerRequest - ? LedgerVersionResponseMap - : T extends LedgerClosedRequest - ? LedgerClosedResponse - : T extends LedgerCurrentRequest - ? LedgerCurrentResponse - : T extends LedgerDataRequest - ? LedgerDataResponse - : T extends LedgerEntryBinaryRequest - ? LedgerEntryBinaryResponse - : T extends LedgerEntryJsonRequest - ? LedgerEntryJsonResponse - : T extends LedgerEntryRequest - ? LedgerEntryJsonResponse - : T extends SimulateBinaryRequest - ? SimulateBinaryResponse - : T extends SimulateJsonRequest - ? SimulateJsonResponse - : T extends SimulateRequest - ? SimulateJsonResponse - : T extends SubmitRequest - ? SubmitResponse - : T extends SubmitMultisignedRequest - ? SubmitMultisignedVersionResponseMap - : T extends TransactionEntryRequest - ? TransactionEntryResponse - : T extends TxRequest - ? TxVersionResponseMap - : T extends BookOffersRequest - ? BookOffersResponse - : T extends DepositAuthorizedRequest - ? DepositAuthorizedResponse - : T extends PathFindRequest - ? PathFindResponse - : T extends RipplePathFindRequest - ? RipplePathFindResponse - : T extends ChannelVerifyRequest - ? ChannelVerifyResponse - : T extends SubscribeRequest - ? SubscribeResponse - : T extends UnsubscribeRequest - ? UnsubscribeResponse - : T extends FeeRequest - ? FeeResponse - : T extends ManifestRequest - ? ManifestResponse - : T extends ServerInfoRequest - ? ServerInfoResponse - : T extends ServerStateRequest - ? ServerStateResponse - : T extends ServerDefinitionsRequest - ? ServerDefinitionsResponse - : T extends FeatureAllRequest - ? FeatureAllResponse - : T extends FeatureOneRequest - ? FeatureOneResponse - : T extends PingRequest - ? PingResponse - : T extends RandomRequest - ? RandomResponse - : T extends NFTBuyOffersRequest - ? NFTBuyOffersResponse - : T extends NFTSellOffersRequest - ? NFTSellOffersResponse - : T extends NFTInfoRequest - ? NFTInfoResponse - : T extends NFTsByIssuerRequest - ? NFTsByIssuerResponse - : T extends NFTHistoryRequest - ? NFTHistoryResponse - : T extends VaultInfoRequest - ? VaultInfoResponse - : Response + ? AccountCurrenciesResponse + : T extends AccountInfoRequest + ? AccountInfoVersionResponseMap + : T extends AccountLinesRequest + ? AccountLinesResponse + : T extends AccountNFTsRequest + ? AccountNFTsResponse + : T extends AccountObjectsRequest + ? AccountObjectsResponse + : T extends AccountOffersRequest + ? AccountOffersResponse + : T extends AccountTxRequest + ? AccountTxVersionResponseMap + : T extends AMMInfoRequest + ? AMMInfoResponse + : T extends GatewayBalancesRequest + ? GatewayBalancesResponse + : T extends GetAggregatePriceRequest + ? GetAggregatePriceResponse + : T extends NoRippleCheckRequest + ? NoRippleCheckResponse + : // NOTE: The order of these LedgerRequest types is important + // to get the proper type matching overrides based on parameters set + // in the request. For example LedgerRequestExpandedTransactionsBinary + // should match LedgerRequestExpandedTransactionsOnly, but not + // LedgerRequestExpandedAccountsOnly. This is because the + // LedgerRequestExpandedTransactionsBinary type is a superset of + // LedgerRequestExpandedTransactionsOnly, but not of the other. + // This is why LedgerRequestExpandedTransactionsBinary is listed + // first in the type list. + // + // Here is an example using real data: + // LedgerRequestExpandedTransactionsBinary = { + // command: 'ledger', + // ledger_index: 'validated', + // expand: true, + // transactions: true, + // binary: true, + // } + // LedgerRequestExpandedTransactionsOnly = { + // command: 'ledger', + // ledger_index: 'validated', + // expand: true, + // transactions: true, + // } + // LedgerRequestExpandedAccountsOnly = { + // command: 'ledger', + // ledger_index: 'validated', + // accounts: true, + // expand: true, + // } + // LedgerRequest = { + // command: 'ledger', + // ledger_index: 'validated', + // } + // + // The type with the most parameters set should be listed first. In this + // case LedgerRequestExpandedTransactionsBinary has the most parameters (`expand`, `transactions`, and `binary`) + // set, so it is listed first. When TypeScript tries to match the type of + // a request to a response, it will try to match the request type to the + // response type in the order they are listed. So, if we have a request + // with the following parameters: + // { + // command: 'ledger', + // ledger_index: 'validated', + // expand: true, + // transactions: true, + // binary: true, + // } + // TypeScript will first try to match the request type to + // LedgerRequestExpandedTransactionsBinary, which will succeed. It will + // then try to match the response type to LedgerResponseExpanded, which + // will also succeed. If we had listed LedgerRequestExpandedTransactionsOnly + // first, TypeScript would have tried to match the request type to + // LedgerRequestExpandedTransactionsOnly, which would have succeeded, but + // then we'd get the wrong response type, LedgerResponse, instead of + // LedgerResponseExpanded. + T extends LedgerRequestExpandedTransactionsBinary + ? LedgerVersionResponseMap + : T extends LedgerRequestExpandedAccountsAndTransactions + ? LedgerResponseExpanded + : T extends LedgerRequestExpandedTransactionsOnly + ? LedgerResponseExpanded + : T extends LedgerRequestExpandedAccountsOnly + ? LedgerResponseExpanded + : T extends LedgerRequest + ? LedgerVersionResponseMap + : T extends LedgerClosedRequest + ? LedgerClosedResponse + : T extends LedgerCurrentRequest + ? LedgerCurrentResponse + : T extends LedgerDataRequest + ? LedgerDataResponse + : T extends LedgerEntryBinaryRequest + ? LedgerEntryBinaryResponse + : T extends LedgerEntryJsonRequest + ? LedgerEntryJsonResponse + : T extends LedgerEntryRequest + ? LedgerEntryJsonResponse + : T extends SimulateBinaryRequest + ? SimulateBinaryResponse + : T extends SimulateJsonRequest + ? SimulateJsonResponse + : T extends SimulateRequest + ? SimulateJsonResponse + : T extends SubmitRequest + ? SubmitResponse + : T extends SubmitMultisignedRequest + ? SubmitMultisignedVersionResponseMap + : T extends TransactionEntryRequest + ? TransactionEntryResponse + : T extends TxRequest + ? TxVersionResponseMap + : T extends BookOffersRequest + ? BookOffersResponse + : T extends DepositAuthorizedRequest + ? DepositAuthorizedResponse + : T extends PathFindRequest + ? PathFindResponse + : T extends RipplePathFindRequest + ? RipplePathFindResponse + : T extends ChannelVerifyRequest + ? ChannelVerifyResponse + : T extends SubscribeRequest + ? SubscribeResponse + : T extends UnsubscribeRequest + ? UnsubscribeResponse + : T extends FeeRequest + ? FeeResponse + : T extends ManifestRequest + ? ManifestResponse + : T extends ServerInfoRequest + ? ServerInfoResponse + : T extends ServerStateRequest + ? ServerStateResponse + : T extends ServerDefinitionsRequest + ? ServerDefinitionsResponse + : T extends FeatureAllRequest + ? FeatureAllResponse + : T extends FeatureOneRequest + ? FeatureOneResponse + : T extends PingRequest + ? PingResponse + : T extends RandomRequest + ? RandomResponse + : T extends NFTBuyOffersRequest + ? NFTBuyOffersResponse + : T extends NFTSellOffersRequest + ? NFTSellOffersResponse + : T extends NFTInfoRequest + ? NFTInfoResponse + : T extends NFTsByIssuerRequest + ? NFTsByIssuerResponse + : T extends NFTHistoryRequest + ? NFTHistoryResponse + : T extends VaultInfoRequest + ? VaultInfoResponse + : Response export type MarkerRequest = Request & { limit?: number @@ -504,18 +504,18 @@ export type RequestAllResponseMap< > = T extends AccountChannelsRequest ? AccountChannelsResponse : T extends AccountLinesRequest - ? AccountLinesResponse - : T extends AccountObjectsRequest - ? AccountObjectsResponse - : T extends AccountOffersRequest - ? AccountOffersResponse - : T extends AccountTxRequest - ? AccountTxVersionResponseMap - : T extends LedgerDataRequest - ? LedgerDataResponse - : T extends BookOffersRequest - ? BookOffersResponse - : MarkerResponse + ? AccountLinesResponse + : T extends AccountObjectsRequest + ? AccountObjectsResponse + : T extends AccountOffersRequest + ? AccountOffersResponse + : T extends AccountTxRequest + ? AccountTxVersionResponseMap + : T extends LedgerDataRequest + ? LedgerDataResponse + : T extends BookOffersRequest + ? BookOffersResponse + : MarkerResponse export { // Allow users to define their own requests and responses. This is useful for releasing experimental versions diff --git a/packages/xrpl/src/models/methods/ledger.ts b/packages/xrpl/src/models/methods/ledger.ts index c0dab28a71..90f65b90e0 100644 --- a/packages/xrpl/src/models/methods/ledger.ts +++ b/packages/xrpl/src/models/methods/ledger.ts @@ -144,7 +144,8 @@ export interface LedgerRequestExpandedAccountsOnly extends LedgerRequest { * * @category Requests */ -// eslint-disable-next-line max-len -- Disable for interface declaration. + +// eslint-disable-next-line max-len -- Interface name is descriptive and intentionally long export interface LedgerRequestExpandedAccountsAndTransactions extends LedgerRequest { expand: true accounts: true diff --git a/packages/xrpl/src/models/transactions/MPTokenIssuanceCreate.ts b/packages/xrpl/src/models/transactions/MPTokenIssuanceCreate.ts index 6a6b6f9e32..d6d2e3762c 100644 --- a/packages/xrpl/src/models/transactions/MPTokenIssuanceCreate.ts +++ b/packages/xrpl/src/models/transactions/MPTokenIssuanceCreate.ts @@ -58,12 +58,13 @@ export enum MPTokenIssuanceCreateFlags { } /** - * Map of flags to boolean values representing {@link MPTokenIssuanceCreate} transaction - * flags. + * Map of flags to boolean values representing {@link MPTokenIssuanceCreate} + * transaction flags. * * @category Transaction Flags */ -// eslint-disable-next-line max-len -- Disable for interface declaration. + +// eslint-disable-next-line max-len -- Interface name is descriptive and intentionally long export interface MPTokenIssuanceCreateFlagsInterface extends GlobalFlagsInterface { tfMPTCanLock?: boolean tfMPTRequireAuth?: boolean diff --git a/packages/xrpl/src/models/transactions/common.ts b/packages/xrpl/src/models/transactions/common.ts index 42dfe47cbf..639c5ac480 100644 --- a/packages/xrpl/src/models/transactions/common.ts +++ b/packages/xrpl/src/models/transactions/common.ts @@ -778,3 +778,26 @@ export function isDomainID(domainID: unknown): domainID is string { isHex(domainID) ) } + +/** + * Enum representing values of {@link ContractParameter} transaction flags. + * + * @category Transaction Flags + */ +export enum ContractParameterFlags { + tfSendAmount = 0x00010000, + tfSendNFToken = 0x00020000, + tfAuthorizeToken = 0x00040000, +} + +/** + * Map of flags to boolean values representing {@link ContractParameter} transaction + * flags. + * + * @category Transaction Flags + */ +export interface ContractParameterFlagsInterface extends GlobalFlagsInterface { + tfSendAmount?: boolean + tfSendNFToken?: boolean + tfAuthorizeToken?: boolean +} diff --git a/packages/xrpl/src/models/transactions/contractCall.ts b/packages/xrpl/src/models/transactions/contractCall.ts new file mode 100644 index 0000000000..dbd2cb9757 --- /dev/null +++ b/packages/xrpl/src/models/transactions/contractCall.ts @@ -0,0 +1,45 @@ +import { Parameter } from '../common' + +import { + BaseTransaction, + isArray, + isNumber, + isString, + validateBaseTransaction, + validateOptionalField, + validateRequiredField, +} from './common' + +/** + * @category Transaction Models + */ +export interface ContractCall extends BaseTransaction { + TransactionType: 'ContractCall' + + // Optional because autofill will add it if missing + ComputationAllowance?: number + + ContractAccount: string + + FunctionName: string + + Parameters?: Parameter[] +} + +/** + * Verify the form and type of a ContractCall at runtime. + * + * @param tx - A ContractCall Transaction. + * @throws When the ContractCall is malformed. + */ +export function validateContractCall(tx: Record): void { + validateBaseTransaction(tx) + + validateRequiredField(tx, 'ComputationAllowance', isNumber) + + validateRequiredField(tx, 'ContractAccount', isString) + + validateRequiredField(tx, 'FunctionName', isString) + + validateOptionalField(tx, 'Parameters', isArray) +} diff --git a/packages/xrpl/src/models/transactions/contractClawback.ts b/packages/xrpl/src/models/transactions/contractClawback.ts new file mode 100644 index 0000000000..91fe7c5794 --- /dev/null +++ b/packages/xrpl/src/models/transactions/contractClawback.ts @@ -0,0 +1,35 @@ +import { Amount } from '../common' + +import { + BaseTransaction, + isAmount, + isString, + validateBaseTransaction, + validateOptionalField, + validateRequiredField, +} from './common' + +/** + * @category Transaction Models + */ +export interface ContractClawback extends BaseTransaction { + TransactionType: 'ContractClawback' + + Amount: Amount + + ContractAccount?: string +} + +/** + * Verify the form and type of a ContractClawback at runtime. + * + * @param tx - A ContractClawback Transaction. + * @throws When the ContractClawback is malformed. + */ +export function validateContractClawback(tx: Record): void { + validateBaseTransaction(tx) + + validateRequiredField(tx, 'Amount', isAmount) + + validateOptionalField(tx, 'ContractAccount', isString) +} diff --git a/packages/xrpl/src/models/transactions/contractCreate.ts b/packages/xrpl/src/models/transactions/contractCreate.ts new file mode 100644 index 0000000000..aba9352cdf --- /dev/null +++ b/packages/xrpl/src/models/transactions/contractCreate.ts @@ -0,0 +1,76 @@ +import { Function, InstanceParameter, InstanceParameterValue } from '../common' + +import { + BaseTransaction, + GlobalFlagsInterface, + isArray, + isString, + validateBaseTransaction, + validateOptionalField, +} from './common' + +/** + * Enum representing values of {@link Contract} transaction flags. + * + * @category Transaction Flags + */ +export enum ContractFlags { + tfImmutable = 0x00010000, + tfCodeImmutable = 0x00020000, + tfABIImmutable = 0x00040000, + tfUndeletable = 0x00080000, +} + +/** + * Map of flags to boolean values representing {@link Contract} transaction + * flags. + * + * @category Transaction Flags + */ +export interface ContractFlagsInterface extends GlobalFlagsInterface { + tfImmutable?: boolean + tfCodeImmutable?: boolean + tfABIImmutable?: boolean + tfUndeletable?: boolean +} + +/** + * @category Transaction Models + */ +export interface ContractCreate extends BaseTransaction { + TransactionType: 'ContractCreate' + + ContractCode?: string + + ContractHash?: string + + Functions?: Function[] + + InstanceParameters?: InstanceParameter[] + + InstanceParameterValues?: InstanceParameterValue[] + + URI?: string +} + +/** + * Verify the form and type of a ContractCreate at runtime. + * + * @param tx - A ContractCreate Transaction. + * @throws When the ContractCreate is malformed. + */ +export function validateContractCreate(tx: Record): void { + validateBaseTransaction(tx) + + validateOptionalField(tx, 'ContractCode', isString) + + validateOptionalField(tx, 'ContractHash', isString) + + validateOptionalField(tx, 'Functions', isArray) + + validateOptionalField(tx, 'InstanceParameters', isArray) + + validateOptionalField(tx, 'InstanceParameterValues', isArray) + + validateOptionalField(tx, 'URI', isString) +} diff --git a/packages/xrpl/src/models/transactions/contractDelete.ts b/packages/xrpl/src/models/transactions/contractDelete.ts new file mode 100644 index 0000000000..d809b9f3b9 --- /dev/null +++ b/packages/xrpl/src/models/transactions/contractDelete.ts @@ -0,0 +1,27 @@ +import { + BaseTransaction, + isString, + validateBaseTransaction, + validateRequiredField, +} from './common' + +/** + * @category Transaction Models + */ +export interface ContractDelete extends BaseTransaction { + TransactionType: 'ContractDelete' + + ContractAccount: string +} + +/** + * Verify the form and type of a ContractDelete at runtime. + * + * @param tx - A ContractDelete Transaction. + * @throws When the ContractDelete is malformed. + */ +export function validateContractDelete(tx: Record): void { + validateBaseTransaction(tx) + + validateRequiredField(tx, 'ContractAccount', isString) +} diff --git a/packages/xrpl/src/models/transactions/contractModify.ts b/packages/xrpl/src/models/transactions/contractModify.ts new file mode 100644 index 0000000000..3a3d172a0f --- /dev/null +++ b/packages/xrpl/src/models/transactions/contractModify.ts @@ -0,0 +1,54 @@ +import { Function, InstanceParameter, InstanceParameterValue } from '../common' + +import { + BaseTransaction, + isArray, + isString, + validateBaseTransaction, + validateOptionalField, +} from './common' + +/** + * @category Transaction Models + */ +export interface ContractModify extends BaseTransaction { + TransactionType: 'ContractModify' + + ContractAccount?: string + + ContractCode?: string + + ContractHash?: string + + Functions?: Function[] + + InstanceParameters?: InstanceParameter[] + + InstanceParameterValues?: InstanceParameterValue[] + + URI?: string +} + +/** + * Verify the form and type of a ContractModify at runtime. + * + * @param tx - A ContractModify Transaction. + * @throws When the ContractModify is malformed. + */ +export function validateContractModify(tx: Record): void { + validateBaseTransaction(tx) + + validateOptionalField(tx, 'ContractAccount', isString) + + validateOptionalField(tx, 'ContractCode', isString) + + validateOptionalField(tx, 'ContractHash', isString) + + validateOptionalField(tx, 'Functions', isArray) + + validateOptionalField(tx, 'InstanceParameters', isArray) + + validateOptionalField(tx, 'InstanceParameterValues', isArray) + + validateOptionalField(tx, 'URI', isString) +} diff --git a/packages/xrpl/src/models/transactions/contractUserDelete.ts b/packages/xrpl/src/models/transactions/contractUserDelete.ts new file mode 100644 index 0000000000..8624265ccc --- /dev/null +++ b/packages/xrpl/src/models/transactions/contractUserDelete.ts @@ -0,0 +1,32 @@ +import { + BaseTransaction, + isNumber, + isString, + validateBaseTransaction, + validateRequiredField, +} from './common' + +/** + * @category Transaction Models + */ +export interface ContractUserDelete extends BaseTransaction { + TransactionType: 'ContractUserDelete' + + ComputationAllowance: number + + ContractAccount: string +} + +/** + * Verify the form and type of a ContractUserDelete at runtime. + * + * @param tx - A ContractUserDelete Transaction. + * @throws When the ContractUserDelete is malformed. + */ +export function validateContractUserDelete(tx: Record): void { + validateBaseTransaction(tx) + + validateRequiredField(tx, 'ComputationAllowance', isNumber) + + validateRequiredField(tx, 'ContractAccount', isString) +} diff --git a/packages/xrpl/src/models/transactions/index.ts b/packages/xrpl/src/models/transactions/index.ts index d7afb63012..a61071f187 100644 --- a/packages/xrpl/src/models/transactions/index.ts +++ b/packages/xrpl/src/models/transactions/index.ts @@ -43,6 +43,16 @@ export { CheckCancel } from './checkCancel' export { CheckCash } from './checkCash' export { CheckCreate } from './checkCreate' export { Clawback } from './clawback' +export { ContractCall } from './contractCall' +export { ContractClawback } from './contractClawback' +export { + ContractFlags, + ContractFlagsInterface, + ContractCreate, +} from './contractCreate' +export { ContractDelete } from './contractDelete' +export { ContractModify } from './contractModify' +export { ContractUserDelete } from './contractUserDelete' export { CredentialAccept } from './CredentialAccept' export { CredentialCreate } from './CredentialCreate' export { CredentialDelete } from './CredentialDelete' diff --git a/packages/xrpl/src/models/transactions/metadata.ts b/packages/xrpl/src/models/transactions/metadata.ts index 9a9d7b0604..39eac20388 100644 --- a/packages/xrpl/src/models/transactions/metadata.ts +++ b/packages/xrpl/src/models/transactions/metadata.ts @@ -90,6 +90,7 @@ export interface TransactionMetadataBase { TransactionResult: string ParentBatchID?: string + GasUsed?: number } export type TransactionMetadata = diff --git a/packages/xrpl/src/models/transactions/paymentChannelClaim.ts b/packages/xrpl/src/models/transactions/paymentChannelClaim.ts index d13f694dbe..e77280385e 100644 --- a/packages/xrpl/src/models/transactions/paymentChannelClaim.ts +++ b/packages/xrpl/src/models/transactions/paymentChannelClaim.ts @@ -73,7 +73,8 @@ export enum PaymentChannelClaimFlags { * // } * ``` */ -// eslint-disable-next-line max-len -- Disable for interface declaration. + +// eslint-disable-next-line max-len -- Interface name is descriptive and intentionally long export interface PaymentChannelClaimFlagsInterface extends GlobalFlagsInterface { /** * Clear the channel's Expiration time. (Expiration is different from the diff --git a/packages/xrpl/src/models/transactions/transaction.ts b/packages/xrpl/src/models/transactions/transaction.ts index 97f710a2ea..40e542861d 100644 --- a/packages/xrpl/src/models/transactions/transaction.ts +++ b/packages/xrpl/src/models/transactions/transaction.ts @@ -23,6 +23,15 @@ import { isIssuedCurrencyAmount, validateBaseTransaction, } from './common' +import { ContractCall, validateContractCall } from './contractCall' +import { ContractClawback, validateContractClawback } from './contractClawback' +import { ContractCreate, validateContractCreate } from './contractCreate' +import { ContractDelete, validateContractDelete } from './contractDelete' +import { ContractModify, validateContractModify } from './contractModify' +import { + ContractUserDelete, + validateContractUserDelete, +} from './contractUserDelete' import { CredentialAccept, validateCredentialAccept } from './CredentialAccept' import { CredentialCreate, validateCredentialCreate } from './CredentialCreate' import { CredentialDelete, validateCredentialDelete } from './CredentialDelete' @@ -165,6 +174,12 @@ export type SubmittableTransaction = | CheckCash | CheckCreate | Clawback + | ContractCall + | ContractClawback + | ContractCreate + | ContractDelete + | ContractModify + | ContractUserDelete | CredentialAccept | CredentialCreate | CredentialDelete @@ -346,6 +361,30 @@ export function validate(transaction: Record): void { validateClawback(tx) break + case 'ContractCall': + validateContractCall(tx) + break + + case 'ContractClawback': + validateContractClawback(tx) + break + + case 'ContractCreate': + validateContractCreate(tx) + break + + case 'ContractDelete': + validateContractDelete(tx) + break + + case 'ContractModify': + validateContractModify(tx) + break + + case 'ContractUserDelete': + validateContractUserDelete(tx) + break + case 'CredentialAccept': validateCredentialAccept(tx) break diff --git a/packages/xrpl/src/sugar/autofill.ts b/packages/xrpl/src/sugar/autofill.ts index bb7809ac98..7a4f0d8df5 100644 --- a/packages/xrpl/src/sugar/autofill.ts +++ b/packages/xrpl/src/sugar/autofill.ts @@ -2,6 +2,7 @@ /* eslint-disable max-lines -- lots of helper functions needed for autofill */ import BigNumber from 'bignumber.js' import { xAddressToClassicAddress, isValidXAddress } from 'ripple-address-codec' +import { encode } from 'ripple-binary-codec' import { type Client } from '..' import { ValidationError, XrplError } from '../errors' @@ -15,7 +16,7 @@ import { Batch, Payment, Transaction } from '../models/transactions' import { Account } from '../models/transactions/common' import { xrpToDrops } from '../utils' -import getFeeXrp from './getFeeXrp' +import getFeeXrp, { getGasEstimate } from './getFeeXrp' // Expire unconfirmed transactions after 20 ledger versions, approximately 1 minute, by default const LEDGER_OFFSET = 20 @@ -408,6 +409,27 @@ export async function getTransactionFee( tx.Fee = fee.toString(10) } +/** + * Estimates and sets the ComputationAllowance for a transaction by encoding + * a copy of the transaction with an empty SigningPubKey and zero Fee, then + * querying the gas estimate from the server. + * + * @param client - The client used to request the gas estimate. + * @param tx - The transaction object to set ComputationAllowance on. + * @returns A promise that resolves once ComputationAllowance has been set. + */ +export async function getComputationAllowance( + client: Client, + tx: Transaction, +): Promise { + const copyTx = { ...tx } + copyTx.ComputationAllowance = 100000000000 + delete copyTx.SigningPubKey + const tx_blob = encode(copyTx) + // eslint-disable-next-line require-atomic-updates, no-param-reassign -- ignore + tx.ComputationAllowance = await getGasEstimate(client, tx_blob) +} + /** * Scales the given value by multiplying it with the provided multiplier. * diff --git a/packages/xrpl/src/sugar/getFeeXrp.ts b/packages/xrpl/src/sugar/getFeeXrp.ts index 1526cc6c8b..6262488d6d 100644 --- a/packages/xrpl/src/sugar/getFeeXrp.ts +++ b/packages/xrpl/src/sugar/getFeeXrp.ts @@ -44,3 +44,38 @@ export default async function getFeeXrp( // Round fee to 6 decimal places return new BigNumber(fee.toFixed(NUM_DECIMAL_PLACES)).toString(BASE_10) } + +/** + * Estimates the gas required for a transaction by simulating the provided tx blob. + * + * @param client - The Client used to connect to the ledger. + * @param txBlob - The transaction blob to simulate. + * @returns The estimated gas as a string. + */ +export async function getGasEstimate( + client: Client, + txBlob: string, +): Promise { + const response = await client.request({ + command: 'simulate', + tx_blob: txBlob, + }) + + if (response.result.engine_result !== 'tesSUCCESS') { + throw new Error(response.result.engine_result_message) + } + + if (typeof response.result.meta !== 'object') { + throw new XrplError( + 'getGasEstimate: Could not get meta from simulate response', + ) + } + + const meta = response.result.meta + if (typeof meta.GasUsed !== 'number') { + throw new XrplError( + 'getGasEstimate: GasUsed in simulate response is not a number', + ) + } + return Number(meta.GasUsed) +} diff --git a/packages/xrpl/test/models/contractCall.test.ts b/packages/xrpl/test/models/contractCall.test.ts new file mode 100644 index 0000000000..2433d7ef90 --- /dev/null +++ b/packages/xrpl/test/models/contractCall.test.ts @@ -0,0 +1,64 @@ +import { validateContractCall } from '../../src/models/transactions/contractCall' +import { assertTxIsValid, assertTxValidationError } from '../testUtils' + +const assertValid = (tx: any): void => assertTxIsValid(tx, validateContractCall) +const assertInvalid = (tx: any, message: string): void => + assertTxValidationError(tx, validateContractCall, message) + +/** + * ContractCall Transaction Verification Testing. + * + * Providing runtime verification testing for each specific transaction type. + */ +describe('ContractCall', function () { + let tx + + beforeEach(function () { + tx = { + TransactionType: 'ContractCall', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + ComputationAllowance: 1000, + ContractAccount: 'rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe', + FunctionName: 'noop', + } as any + }) + + it('verifies valid ContractCall', function () { + assertValid(tx) + }) + + it('throws w/ missing ComputationAllowance', function () { + delete tx.ComputationAllowance + assertInvalid(tx, 'ContractCall: missing field ComputationAllowance') + }) + + it('throws w/ invalid ComputationAllowance', function () { + tx.ComputationAllowance = 'number' + assertInvalid(tx, 'ContractCall: invalid field ComputationAllowance') + }) + + it('throws w/ missing ContractAccount', function () { + delete tx.ContractAccount + assertInvalid(tx, 'ContractCall: missing field ContractAccount') + }) + + it('throws w/ invalid ContractAccount', function () { + tx.ContractAccount = 123 + assertInvalid(tx, 'ContractCall: invalid field ContractAccount') + }) + + it('throws w/ missing FunctionName', function () { + delete tx.FunctionName + assertInvalid(tx, 'ContractCall: missing field FunctionName') + }) + + it('throws w/ invalid FunctionName', function () { + tx.FunctionName = 123 + assertInvalid(tx, 'ContractCall: invalid field FunctionName') + }) + + it('throws w/ invalid Parameters', function () { + tx.Parameters = 'not_an_array' + assertInvalid(tx, 'ContractCall: invalid field Parameters') + }) +}) diff --git a/packages/xrpl/test/models/contractClawback.test.ts b/packages/xrpl/test/models/contractClawback.test.ts new file mode 100644 index 0000000000..29c78cb9c1 --- /dev/null +++ b/packages/xrpl/test/models/contractClawback.test.ts @@ -0,0 +1,47 @@ +import { validateContractClawback } from '../../src/models/transactions/contractClawback' +import { assertTxIsValid, assertTxValidationError } from '../testUtils' + +const assertValid = (tx: any): void => + assertTxIsValid(tx, validateContractClawback) +const assertInvalid = (tx: any, message: string): void => + assertTxValidationError(tx, validateContractClawback, message) + +/** + * ContractClawback Transaction Verification Testing. + * + * Providing runtime verification testing for each specific transaction type. + */ +describe('ContractClawback', function () { + let tx + + beforeEach(function () { + tx = { + TransactionType: 'ContractClawback', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + Amount: { + currency: 'USD', + value: '1000', + issuer: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + }, + } as any + }) + + it('verifies valid ContractClawback', function () { + assertValid(tx) + }) + + it('throws w/ missing Amount', function () { + delete tx.Amount + assertInvalid(tx, 'ContractClawback: missing field Amount') + }) + + it('throws w/ invalid Amount', function () { + tx.Amount = { currency: 'ETH' } + assertInvalid(tx, 'ContractClawback: invalid field Amount') + }) + + it('throws w/ invalid ContractAccount', function () { + tx.ContractAccount = 123 + assertInvalid(tx, 'ContractClawback: invalid field ContractAccount') + }) +}) diff --git a/packages/xrpl/test/models/contractCreate.test.ts b/packages/xrpl/test/models/contractCreate.test.ts new file mode 100644 index 0000000000..317a4fd481 --- /dev/null +++ b/packages/xrpl/test/models/contractCreate.test.ts @@ -0,0 +1,59 @@ +import { validateContractCreate } from '../../src/models/transactions/contractCreate' +import { assertTxIsValid, assertTxValidationError } from '../testUtils' + +const assertValid = (tx: any): void => + assertTxIsValid(tx, validateContractCreate) +const assertInvalid = (tx: any, message: string): void => + assertTxValidationError(tx, validateContractCreate, message) + +/** + * ContractCreate Transaction Verification Testing. + * + * Providing runtime verification testing for each specific transaction type. + */ +describe('ContractCreate', function () { + let tx + + beforeEach(function () { + tx = { + TransactionType: 'ContractCreate', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + ContractHash: + 'E5287A664638EDC1110BAF0FD3FF79013353FD797EF14FC970E552ED7097B721', + } as any + }) + + it('verifies valid ContractCreate', function () { + assertValid(tx) + }) + + it('throws w/ invalid ContractCode', function () { + tx.ContractCode = 123 + assertInvalid(tx, 'ContractCreate: invalid field ContractCode') + }) + + it('throws w/ invalid ContractHash', function () { + tx.ContractHash = 123 + assertInvalid(tx, 'ContractCreate: invalid field ContractHash') + }) + + it('throws w/ invalid Functions', function () { + tx.Functions = 'not_an_array' + assertInvalid(tx, 'ContractCreate: invalid field Functions') + }) + + it('throws w/ invalid InstanceParameters', function () { + tx.InstanceParameters = 'not_an_array' + assertInvalid(tx, 'ContractCreate: invalid field InstanceParameters') + }) + + it('throws w/ invalid InstanceParameterValues', function () { + tx.InstanceParameterValues = 'not_an_array' + assertInvalid(tx, 'ContractCreate: invalid field InstanceParameterValues') + }) + + it('throws w/ invalid URI', function () { + tx.URI = 123 + assertInvalid(tx, 'ContractCreate: invalid field URI') + }) +}) diff --git a/packages/xrpl/test/models/contractDelete.test.ts b/packages/xrpl/test/models/contractDelete.test.ts new file mode 100644 index 0000000000..effb1ea31e --- /dev/null +++ b/packages/xrpl/test/models/contractDelete.test.ts @@ -0,0 +1,38 @@ +import { validateContractDelete } from '../../src/models/transactions/contractDelete' +import { assertTxIsValid, assertTxValidationError } from '../testUtils' + +const assertValid = (tx: any): void => + assertTxIsValid(tx, validateContractDelete) +const assertInvalid = (tx: any, message: string): void => + assertTxValidationError(tx, validateContractDelete, message) + +/** + * ContractDelete Transaction Verification Testing. + * + * Providing runtime verification testing for each specific transaction type. + */ +describe('ContractDelete', function () { + let tx + + beforeEach(function () { + tx = { + TransactionType: 'ContractDelete', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + ContractAccount: 'rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe', + } as any + }) + + it('verifies valid ContractDelete', function () { + assertValid(tx) + }) + + it('throws w/ missing ContractAccount', function () { + delete tx.ContractAccount + assertInvalid(tx, 'ContractDelete: missing field ContractAccount') + }) + + it('throws w/ invalid ContractAccount', function () { + tx.ContractAccount = 123 + assertInvalid(tx, 'ContractDelete: invalid field ContractAccount') + }) +}) diff --git a/packages/xrpl/test/models/contractModify.test.ts b/packages/xrpl/test/models/contractModify.test.ts new file mode 100644 index 0000000000..87706a02cd --- /dev/null +++ b/packages/xrpl/test/models/contractModify.test.ts @@ -0,0 +1,65 @@ +import { validateContractModify } from '../../src/models/transactions/contractModify' +import { assertTxIsValid, assertTxValidationError } from '../testUtils' + +const assertValid = (tx: any): void => + assertTxIsValid(tx, validateContractModify) +const assertInvalid = (tx: any, message: string): void => + assertTxValidationError(tx, validateContractModify, message) + +/** + * ContractModify Transaction Verification Testing. + * + * Providing runtime verification testing for each specific transaction type. + */ +describe('ContractModify', function () { + let tx + + beforeEach(function () { + tx = { + TransactionType: 'ContractModify', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + ContractAccount: 'rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe', + ContractHash: + 'E5287A664638EDC1110BAF0FD3FF79013353FD797EF14FC970E552ED7097B721', + } as any + }) + + it('verifies valid ContractModify', function () { + assertValid(tx) + }) + + it('throws w/ invalid ContractAccount', function () { + tx.ContractAccount = 123 + assertInvalid(tx, 'ContractModify: invalid field ContractAccount') + }) + + it('throws w/ invalid ContractCode', function () { + tx.ContractCode = 123 + assertInvalid(tx, 'ContractModify: invalid field ContractCode') + }) + + it('throws w/ invalid ContractHash', function () { + tx.ContractHash = 123 + assertInvalid(tx, 'ContractModify: invalid field ContractHash') + }) + + it('throws w/ invalid Functions', function () { + tx.Functions = 'not_an_array' + assertInvalid(tx, 'ContractModify: invalid field Functions') + }) + + it('throws w/ invalid InstanceParameters', function () { + tx.InstanceParameters = 'not_an_array' + assertInvalid(tx, 'ContractModify: invalid field InstanceParameters') + }) + + it('throws w/ invalid InstanceParameterValues', function () { + tx.InstanceParameterValues = 'not_an_array' + assertInvalid(tx, 'ContractModify: invalid field InstanceParameterValues') + }) + + it('throws w/ invalid URI', function () { + tx.URI = 123 + assertInvalid(tx, 'ContractModify: invalid field URI') + }) +}) diff --git a/packages/xrpl/test/models/contractUserDelete.test.ts b/packages/xrpl/test/models/contractUserDelete.test.ts new file mode 100644 index 0000000000..4a144d88aa --- /dev/null +++ b/packages/xrpl/test/models/contractUserDelete.test.ts @@ -0,0 +1,49 @@ +import { validateContractUserDelete } from '../../src/models/transactions/contractUserDelete' +import { assertTxIsValid, assertTxValidationError } from '../testUtils' + +const assertValid = (tx: any): void => + assertTxIsValid(tx, validateContractUserDelete) +const assertInvalid = (tx: any, message: string): void => + assertTxValidationError(tx, validateContractUserDelete, message) + +/** + * ContractUserDelete Transaction Verification Testing. + * + * Providing runtime verification testing for each specific transaction type. + */ +describe('ContractUserDelete', function () { + let tx + + beforeEach(function () { + tx = { + TransactionType: 'ContractUserDelete', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + ComputationAllowance: 1000, + ContractAccount: 'rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe', + } as any + }) + + it('verifies valid ContractUserDelete', function () { + assertValid(tx) + }) + + it('throws w/ missing ComputationAllowance', function () { + delete tx.ComputationAllowance + assertInvalid(tx, 'ContractUserDelete: missing field ComputationAllowance') + }) + + it('throws w/ invalid ComputationAllowance', function () { + tx.ComputationAllowance = 'number' + assertInvalid(tx, 'ContractUserDelete: invalid field ComputationAllowance') + }) + + it('throws w/ missing ContractAccount', function () { + delete tx.ContractAccount + assertInvalid(tx, 'ContractUserDelete: missing field ContractAccount') + }) + + it('throws w/ invalid ContractAccount', function () { + tx.ContractAccount = 123 + assertInvalid(tx, 'ContractUserDelete: invalid field ContractAccount') + }) +}) diff --git a/packages/xrpl/tools/generateModels.js b/packages/xrpl/tools/generateModels.js index 0dd4ea67d1..1d1a601220 100644 --- a/packages/xrpl/tools/generateModels.js +++ b/packages/xrpl/tools/generateModels.js @@ -60,7 +60,7 @@ async function processRippledSource(folder) { 'include/xrpl/protocol/detail/transactions.macro', ) const txFormatsHits = transactionsMacroFile.matchAll( - /^ *TRANSACTION\(tt[A-Z_]+ *,* [0-9]+ *, *([A-Za-z]+)[ \n]*,[ \n]*Delegation::[A-Za-z]+[ \n]*,[ \n]*\({[ \n]*(({sf[A-Za-z0-9]+, soe(OPTIONAL|REQUIRED|DEFAULT)(, soeMPT(None|Supported|NotSupported))?},[ \n]+)*)}\)\)$/gm, + /^ *TRANSACTION\(tt[A-Z_]+ *,* [0-9]+ *, *([A-Za-z]+)[ \n]*,[ \n]*Delegation::[A-Za-z]+[ \n]*,[ \n]*[A-Za-z0-9_{}]+[ \n]*,[ \n]*[A-Za-z|]+[ \n]*,[ \n]*\({[ \n]*(({sf[A-Za-z0-9]+, soe(OPTIONAL|REQUIRED|DEFAULT)(, soeMPT(None|Supported|NotSupported))?},[ \n]+)*)}\)\)$/gm, ) const txFormats = {} for (const hit of txFormatsHits) { @@ -79,7 +79,12 @@ async function processRippledSource(folder) { .split('\n | ') .filter((value) => !value.includes('export type')) .map((value) => value.trim()) - existingLibraryTxs.push('EnableAmendment', 'SetFee', 'UNLModify') + existingLibraryTxs.push( + 'EnableAmendment', + 'SetFee', + 'UNLModify', + 'LedgerStateFix', + ) const txsToAdd = [] @@ -116,6 +121,9 @@ const typeMap = { XCHAIN_BRIDGE: 'XChainBridge', OBJECT: 'any', ARRAY: 'any[]', + DATA: 'any', + DATA_TYPE: 'any', + JSON: 'any', } const allCommonImports = ['Amount', 'Currency', 'Path', 'XChainBridge']