diff --git a/address-codec/address_codec.go b/address-codec/address_codec.go index 058cf24c0..f9f36b08f 100644 --- a/address-codec/address_codec.go +++ b/address-codec/address_codec.go @@ -133,6 +133,23 @@ func DecodeClassicAddressToAccountID(cAddress string) (typePrefix, accountID []b } +// Returns the classic address from accountID byte slice. +func EncodeAccountIDToClassicAddress(accountID []byte) (types.Address, error) { + if len(accountID) != AccountAddressLength { + return "", &EncodeLengthError{Instance: "AccountID", Input: len(accountID), Expected: AccountAddressLength} + } + + // Create the payload: [prefix(1 byte) + accountID(20 bytes)] + payload := make([]byte, 1+AccountAddressLength) + payload[0] = AccountAddressPrefix + copy(payload[1:], accountID) + + // Encode with Base58Check + classicAddress := Base58CheckEncode(payload) + + return types.Address(classicAddress), nil +} + func IsValidClassicAddress(cAddress string) bool { _, _, c := DecodeClassicAddressToAccountID(cAddress) diff --git a/address-codec/address_codec_test.go b/address-codec/address_codec_test.go index 57f755e6e..51d88cefd 100644 --- a/address-codec/address_codec_test.go +++ b/address-codec/address_codec_test.go @@ -307,6 +307,88 @@ func TestDecodeAddressToAccountID(t *testing.T) { } } +func TestEncodeAccountIDToClassicAddress(t *testing.T) { + tt := []struct { + description string + input []byte + expectedAddr types.Address + expectedErr error + }{ + { + description: "Successful encode - 1", + input: []byte{0x88, 0xa5, 0xa5, 0x7c, 0x82, 0x9f, 0x40, 0xf2, 0x5e, 0xa8, 0x33, 0x85, 0xbb, 0xde, 0x6c, 0x3d, 0x8b, 0x4c, 0xa0, 0x82}, + expectedAddr: "rDTXLQ7ZKZVKz33zJbHjgVShjsBnqMBhmN", + expectedErr: nil, + }, + { + description: "Successful encode - 2", + input: []byte{0xbd, 0xe4, 0x2b, 0xbd, 0x77, 0x5b, 0x46, 0x7e, 0x34, 0xfe, 0x48, 0x52, 0xe7, 0xce, 0x3d, 0xd2, 0x61, 0x03, 0xf7, 0x6c}, + expectedAddr: "rJKhsipKHooQbtS3v5Jro6N5Q7TMNPkoAs", + expectedErr: nil, + }, + { + description: "Length error - too short", + input: []byte{0x88, 0xa5, 0xa5, 0x7c, 0x82, 0x9f, 0x40, 0xf2, 0x5e, 0xa8, 0x33, 0x85, 0xbb, 0xde, 0x6c, 0x3d, 0x8b, 0x4c, 0xa0}, + expectedAddr: "", + expectedErr: &EncodeLengthError{Instance: "AccountID", Input: 19, Expected: 20}, + }, + { + description: "Length error - too long", + input: []byte{0x88, 0xa5, 0xa5, 0x7c, 0x82, 0x9f, 0x40, 0xf2, 0x5e, 0xa8, 0x33, 0x85, 0xbb, 0xde, 0x6c, 0x3d, 0x8b, 0x4c, 0xa0, 0x82, 0x00}, + expectedAddr: "", + expectedErr: &EncodeLengthError{Instance: "AccountID", Input: 21, Expected: 20}, + }, + } + + for _, tc := range tt { + t.Run(tc.description, func(t *testing.T) { + result, err := EncodeAccountIDToClassicAddress(tc.input) + + if tc.expectedErr != nil { + require.EqualError(t, err, tc.expectedErr.Error()) + require.Equal(t, tc.expectedAddr, result) + } else { + require.NoError(t, err) + require.Equal(t, tc.expectedAddr, result) + } + }) + } +} + +func TestEncodeDecodeClassicAddressRoundTrip(t *testing.T) { + tt := []struct { + description string + input string + }{ + { + description: "Round trip test - 1", + input: "rDTXLQ7ZKZVKz33zJbHjgVShjsBnqMBhmN", + }, + { + description: "Round trip test - 2", + input: "rJKhsipKHooQbtS3v5Jro6N5Q7TMNPkoAs", + }, + } + + for _, tc := range tt { + t.Run(tc.description, func(t *testing.T) { + // Decode classic address to accountID + typePrefix, accountID, err := DecodeClassicAddressToAccountID(tc.input) + require.NoError(t, err) + require.NotNil(t, typePrefix) + require.NotNil(t, accountID) + + // Encode accountID back to classic address + resultAddr, err := EncodeAccountIDToClassicAddress(accountID) + require.NoError(t, err) + require.NotEmpty(t, resultAddr) + + // Verify that the result matches the original input + require.Equal(t, tc.input, string(resultAddr)) + }) + } +} + func TestIsValidClassicAddress(t *testing.T) { tt := []struct { description string diff --git a/binary-codec/definitions/definitions.json b/binary-codec/definitions/definitions.json index b8fd9a8a1..037dc04d4 100644 --- a/binary-codec/definitions/definitions.json +++ b/binary-codec/definitions/definitions.json @@ -1,2871 +1,3395 @@ { - "TYPES": { - "Done": -1, - "Unknown": -2, - "NotPresent": 0, - "UInt16": 1, - "UInt32": 2, - "UInt64": 3, - "Hash128": 4, - "Hash256": 5, - "Amount": 6, - "Blob": 7, - "AccountID": 8, - "STObject": 14, - "STArray": 15, - "UInt8": 16, - "Hash160": 17, - "PathSet": 18, - "Vector256": 19, - "UInt96": 20, - "UInt192": 21, - "UInt384": 22, - "UInt512": 23, - "Issue": 24, - "XChainBridge": 25, - "Transaction": 10001, - "LedgerEntry": 10002, - "Validation": 10003, - "Metadata": 10004 - }, - "LEDGER_ENTRY_TYPES": { - "Invalid": -1, - "AccountRoot": 97, - "DirectoryNode": 100, - "RippleState": 114, - "Ticket": 84, - "SignerList": 83, - "Offer": 111, - "Bridge": 105, - "LedgerHashes": 104, - "Amendments": 102, - "XChainOwnedClaimID": 113, - "XChainOwnedCreateAccountClaimID": 116, - "FeeSettings": 115, - "Escrow": 117, - "PayChannel": 120, - "Check": 67, - "DepositPreauth": 112, - "NegativeUNL": 78, - "NFTokenPage": 80, - "NFTokenOffer": 55, - "AMM": 121, - "DID": 73, - "Any": -3, - "Child": -2, - "Nickname": 110, - "Contract": 99, - "GeneratorMap": 103 - }, "FIELDS": [ [ "Generic", { - "nth": 0, - "isVLEncoded": false, "isSerialized": false, "isSigningField": false, + "isVLEncoded": false, + "nth": 0, "type": "Unknown" } ], [ "Invalid", { - "nth": -1, - "isVLEncoded": false, "isSerialized": false, "isSigningField": false, + "isVLEncoded": false, + "nth": -1, "type": "Unknown" } ], [ "ObjectEndMarker", { - "nth": 1, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 1, "type": "STObject" } ], [ "ArrayEndMarker", { - "nth": 1, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 1, "type": "STArray" } ], [ - "hash", + "taker_gets_funded", { - "nth": 257, - "isVLEncoded": false, "isSerialized": false, "isSigningField": false, - "type": "Hash256" - } - ], - [ - "index", - { - "nth": 258, "isVLEncoded": false, - "isSerialized": false, - "isSigningField": false, - "type": "Hash256" - } - ], - [ - "taker_gets_funded", - { "nth": 258, - "isVLEncoded": false, - "isSerialized": false, - "isSigningField": false, "type": "Amount" } ], [ "taker_pays_funded", { - "nth": 259, - "isVLEncoded": false, "isSerialized": false, "isSigningField": false, + "isVLEncoded": false, + "nth": 259, "type": "Amount" } ], [ - "LedgerEntry", + "LedgerEntryType", { - "nth": 257, + "isSerialized": true, + "isSigningField": true, "isVLEncoded": false, - "isSerialized": false, - "isSigningField": false, - "type": "LedgerEntry" + "nth": 1, + "type": "UInt16" } ], [ - "Transaction", + "TransactionType", { - "nth": 257, + "isSerialized": true, + "isSigningField": true, "isVLEncoded": false, - "isSerialized": false, - "isSigningField": false, - "type": "Transaction" + "nth": 2, + "type": "UInt16" } ], [ - "Validation", + "SignerWeight", { - "nth": 257, + "isSerialized": true, + "isSigningField": true, "isVLEncoded": false, - "isSerialized": false, - "isSigningField": false, - "type": "Validation" + "nth": 3, + "type": "UInt16" } ], [ - "Metadata", + "TransferFee", { - "nth": 257, + "isSerialized": true, + "isSigningField": true, "isVLEncoded": false, - "isSerialized": false, - "isSigningField": false, - "type": "Metadata" + "nth": 4, + "type": "UInt16" } ], [ - "CloseResolution", + "TradingFee", { - "nth": 1, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "UInt8" + "isVLEncoded": false, + "nth": 5, + "type": "UInt16" } ], [ - "Method", + "DiscountedFee", { - "nth": 2, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "UInt8" + "isVLEncoded": false, + "nth": 6, + "type": "UInt16" } ], [ - "TransactionResult", + "Version", { - "nth": 3, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "UInt8" + "isVLEncoded": false, + "nth": 16, + "type": "UInt16" } ], [ - "TickSize", + "HookStateChangeCount", { - "nth": 16, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "UInt8" + "isVLEncoded": false, + "nth": 17, + "type": "UInt16" } ], [ - "UNLModifyDisabling", + "HookEmitCount", { - "nth": 17, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "UInt8" + "isVLEncoded": false, + "nth": 18, + "type": "UInt16" } ], [ - "HookResult", + "HookExecutionIndex", { - "nth": 18, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "UInt8" + "isVLEncoded": false, + "nth": 19, + "type": "UInt16" } ], [ - "WasLockingChainSend", + "HookApiVersion", { - "nth": 19, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "UInt8" + "isVLEncoded": false, + "nth": 20, + "type": "UInt16" } ], [ - "LedgerEntryType", + "LedgerFixType", { - "nth": 1, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 21, "type": "UInt16" } ], [ - "TransactionType", + "NetworkID", { - "nth": 2, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "UInt16" + "isVLEncoded": false, + "nth": 1, + "type": "UInt32" } ], [ - "SignerWeight", + "Flags", { - "nth": 3, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "UInt16" + "isVLEncoded": false, + "nth": 2, + "type": "UInt32" } ], [ - "TransferFee", + "SourceTag", { - "nth": 4, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "UInt16" + "isVLEncoded": false, + "nth": 3, + "type": "UInt32" } ], [ - "TradingFee", + "Sequence", { - "nth": 5, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "UInt16" + "isVLEncoded": false, + "nth": 4, + "type": "UInt32" } ], [ - "DiscountedFee", + "PreviousTxnLgrSeq", { - "nth": 6, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "UInt16" + "isVLEncoded": false, + "nth": 5, + "type": "UInt32" } ], [ - "Version", + "LedgerSequence", { - "nth": 16, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "UInt16" + "isVLEncoded": false, + "nth": 6, + "type": "UInt32" } ], [ - "HookStateChangeCount", + "CloseTime", { - "nth": 17, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "UInt16" + "isVLEncoded": false, + "nth": 7, + "type": "UInt32" } ], [ - "HookEmitCount", + "ParentCloseTime", { - "nth": 18, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "UInt16" - } - ], - [ - "HookExecutionIndex", - { - "nth": 19, - "isVLEncoded": false, - "isSerialized": true, - "isSigningField": true, - "type": "UInt16" - } - ], - [ - "HookApiVersion", - { - "nth": 20, - "isVLEncoded": false, - "isSerialized": true, - "isSigningField": true, - "type": "UInt16" - } - ], - [ - "NetworkID", - { - "nth": 1, "isVLEncoded": false, - "isSerialized": true, - "isSigningField": true, + "nth": 8, "type": "UInt32" } ], [ - "Flags", + "SigningTime", { - "nth": 2, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "UInt32" - } - ], - [ - "SourceTag", - { - "nth": 3, "isVLEncoded": false, - "isSerialized": true, - "isSigningField": true, + "nth": 9, "type": "UInt32" } ], [ - "Sequence", + "Expiration", { - "nth": 4, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "UInt32" - } - ], - [ - "PreviousTxnLgrSeq", - { - "nth": 5, "isVLEncoded": false, - "isSerialized": true, - "isSigningField": true, + "nth": 10, "type": "UInt32" } ], [ - "LedgerSequence", + "TransferRate", { - "nth": 6, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "UInt32" - } - ], - [ - "CloseTime", - { - "nth": 7, "isVLEncoded": false, - "isSerialized": true, - "isSigningField": true, + "nth": 11, "type": "UInt32" } ], [ - "ParentCloseTime", + "WalletSize", { - "nth": 8, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "UInt32" - } - ], - [ - "SigningTime", - { - "nth": 9, "isVLEncoded": false, - "isSerialized": true, - "isSigningField": true, + "nth": 12, "type": "UInt32" } ], [ - "Expiration", + "OwnerCount", { - "nth": 10, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "UInt32" - } - ], - [ - "TransferRate", - { - "nth": 11, "isVLEncoded": false, - "isSerialized": true, - "isSigningField": true, + "nth": 13, "type": "UInt32" } ], [ - "WalletSize", + "DestinationTag", { - "nth": 12, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "UInt32" - } - ], - [ - "OwnerCount", - { - "nth": 13, "isVLEncoded": false, - "isSerialized": true, - "isSigningField": true, + "nth": 14, "type": "UInt32" } ], [ - "DestinationTag", + "LastUpdateTime", { - "nth": 14, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 15, "type": "UInt32" } ], [ "HighQualityIn", { - "nth": 16, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 16, "type": "UInt32" } ], [ "HighQualityOut", { - "nth": 17, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 17, "type": "UInt32" } ], [ "LowQualityIn", { - "nth": 18, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 18, "type": "UInt32" } ], [ "LowQualityOut", { - "nth": 19, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 19, "type": "UInt32" } ], [ "QualityIn", { - "nth": 20, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 20, "type": "UInt32" } ], [ "QualityOut", { - "nth": 21, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 21, "type": "UInt32" } ], [ "StampEscrow", { - "nth": 22, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 22, "type": "UInt32" } ], [ "BondAmount", { - "nth": 23, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 23, "type": "UInt32" } ], [ "LoadFee", { - "nth": 24, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 24, "type": "UInt32" } ], [ "OfferSequence", { - "nth": 25, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 25, "type": "UInt32" } ], [ "FirstLedgerSequence", { - "nth": 26, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 26, "type": "UInt32" } ], [ "LastLedgerSequence", { - "nth": 27, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 27, "type": "UInt32" } ], [ "TransactionIndex", { - "nth": 28, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 28, "type": "UInt32" } ], [ "OperationLimit", { - "nth": 29, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 29, "type": "UInt32" } ], [ "ReferenceFeeUnits", { - "nth": 30, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 30, "type": "UInt32" } ], [ "ReserveBase", { - "nth": 31, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 31, "type": "UInt32" } ], [ "ReserveIncrement", { - "nth": 32, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 32, "type": "UInt32" } ], [ "SetFlag", { - "nth": 33, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 33, "type": "UInt32" } ], [ "ClearFlag", { - "nth": 34, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 34, "type": "UInt32" } ], [ "SignerQuorum", { - "nth": 35, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 35, "type": "UInt32" } ], [ "CancelAfter", { - "nth": 36, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 36, "type": "UInt32" } ], [ "FinishAfter", { - "nth": 37, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 37, "type": "UInt32" } ], [ "SignerListID", { - "nth": 38, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 38, "type": "UInt32" } ], [ "SettleDelay", { - "nth": 39, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 39, "type": "UInt32" } ], [ "TicketCount", { - "nth": 40, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 40, "type": "UInt32" } ], [ "TicketSequence", { - "nth": 41, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 41, "type": "UInt32" } ], [ "NFTokenTaxon", { - "nth": 42, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 42, "type": "UInt32" } ], [ "MintedNFTokens", { - "nth": 43, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 43, "type": "UInt32" } ], [ "BurnedNFTokens", { - "nth": 44, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 44, "type": "UInt32" } ], [ "HookStateCount", { - "nth": 45, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 45, "type": "UInt32" } ], [ "EmitGeneration", { - "nth": 46, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 46, "type": "UInt32" } ], [ "VoteWeight", { - "nth": 48, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 48, "type": "UInt32" } ], [ "FirstNFTokenSequence", { - "nth": 50, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 50, "type": "UInt32" } ], [ - "IndexNext", + "OracleDocumentID", { - "nth": 1, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "UInt64" + "isVLEncoded": false, + "nth": 51, + "type": "UInt32" } ], [ - "IndexPrevious", + "PermissionValue", { - "nth": 2, + "isSerialized": true, + "isSigningField": true, "isVLEncoded": false, + "nth": 52, + "type": "UInt32" + } + ], + [ + "IndexNext", + { "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 1, "type": "UInt64" } ], [ - "BookNode", + "IndexPrevious", { - "nth": 3, + "isSerialized": true, + "isSigningField": true, "isVLEncoded": false, + "nth": 2, + "type": "UInt64" + } + ], + [ + "BookNode", + { "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 3, "type": "UInt64" } ], [ "OwnerNode", { - "nth": 4, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 4, "type": "UInt64" } ], [ "BaseFee", { - "nth": 5, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 5, "type": "UInt64" } ], [ "ExchangeRate", { - "nth": 6, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 6, "type": "UInt64" } ], [ "LowNode", { - "nth": 7, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 7, "type": "UInt64" } ], [ "HighNode", { - "nth": 8, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 8, "type": "UInt64" } ], [ "DestinationNode", { - "nth": 9, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 9, "type": "UInt64" } ], [ "Cookie", { - "nth": 10, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 10, "type": "UInt64" } ], [ "ServerVersion", { - "nth": 11, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 11, "type": "UInt64" } ], [ "NFTokenOfferNode", { - "nth": 12, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 12, "type": "UInt64" } ], [ "EmitBurden", { - "nth": 13, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 13, "type": "UInt64" } ], [ "HookOn", { - "nth": 16, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 16, "type": "UInt64" } ], [ "HookInstructionCount", { - "nth": 17, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 17, "type": "UInt64" } ], [ "HookReturnCode", { - "nth": 18, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 18, "type": "UInt64" } ], [ "ReferenceCount", { - "nth": 19, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 19, "type": "UInt64" } ], [ "XChainClaimID", { - "nth": 20, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 20, "type": "UInt64" } ], [ "XChainAccountCreateCount", { - "nth": 21, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 21, "type": "UInt64" } ], [ "XChainAccountClaimCount", { - "nth": 22, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 22, "type": "UInt64" } ], [ - "EmailHash", + "AssetPrice", { - "nth": 1, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "Hash128" + "isVLEncoded": false, + "nth": 23, + "type": "UInt64" } ], [ - "TakerPaysCurrency", + "MaximumAmount", { - "nth": 1, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "Hash160" + "isVLEncoded": false, + "nth": 24, + "type": "UInt64" } ], [ - "TakerPaysIssuer", + "OutstandingAmount", { - "nth": 2, + "isSerialized": true, + "isSigningField": true, "isVLEncoded": false, + "nth": 25, + "type": "UInt64" + } + ], + [ + "MPTAmount", + { "isSerialized": true, "isSigningField": true, - "type": "Hash160" + "isVLEncoded": false, + "nth": 26, + "type": "UInt64" } ], [ - "TakerGetsCurrency", + "IssuerNode", { - "nth": 3, + "isSerialized": true, + "isSigningField": true, "isVLEncoded": false, + "nth": 27, + "type": "UInt64" + } + ], + [ + "SubjectNode", + { "isSerialized": true, "isSigningField": true, - "type": "Hash160" + "isVLEncoded": false, + "nth": 28, + "type": "UInt64" } ], [ - "TakerGetsIssuer", + "LockedAmount", { - "nth": 4, + "isSerialized": true, + "isSigningField": true, "isVLEncoded": false, + "nth": 29, + "type": "UInt64" + } + ], + [ + "EmailHash", + { "isSerialized": true, "isSigningField": true, - "type": "Hash160" + "isVLEncoded": false, + "nth": 1, + "type": "Hash128" } ], [ "LedgerHash", { - "nth": 1, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 1, "type": "Hash256" } ], [ "ParentHash", { - "nth": 2, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 2, "type": "Hash256" } ], [ "TransactionHash", { - "nth": 3, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 3, "type": "Hash256" } ], [ "AccountHash", { - "nth": 4, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 4, "type": "Hash256" } ], [ "PreviousTxnID", { - "nth": 5, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 5, "type": "Hash256" } ], [ "LedgerIndex", { - "nth": 6, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 6, "type": "Hash256" } ], [ "WalletLocator", { - "nth": 7, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 7, "type": "Hash256" } ], [ "RootIndex", { - "nth": 8, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 8, "type": "Hash256" } ], [ "AccountTxnID", { - "nth": 9, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 9, "type": "Hash256" } ], [ "NFTokenID", { - "nth": 10, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 10, "type": "Hash256" } ], [ "EmitParentTxnID", { - "nth": 11, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 11, "type": "Hash256" } ], [ "EmitNonce", { - "nth": 12, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 12, "type": "Hash256" } ], [ "EmitHookHash", { - "nth": 13, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 13, "type": "Hash256" } ], [ "AMMID", { - "nth": 14, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 14, "type": "Hash256" } ], [ "BookDirectory", { - "nth": 16, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 16, "type": "Hash256" } ], [ "InvoiceID", { - "nth": 17, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 17, "type": "Hash256" } ], [ "Nickname", { - "nth": 18, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 18, "type": "Hash256" } ], [ "Amendment", { - "nth": 19, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 19, "type": "Hash256" } ], [ "Digest", { - "nth": 21, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 21, "type": "Hash256" } ], [ "Channel", { - "nth": 22, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 22, "type": "Hash256" } ], [ "ConsensusHash", { - "nth": 23, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 23, "type": "Hash256" } ], [ "CheckID", { - "nth": 24, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 24, "type": "Hash256" } ], [ "ValidatedHash", { - "nth": 25, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 25, "type": "Hash256" } ], [ "PreviousPageMin", { - "nth": 26, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 26, "type": "Hash256" } ], [ "NextPageMin", { - "nth": 27, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 27, "type": "Hash256" } ], [ "NFTokenBuyOffer", { - "nth": 28, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 28, "type": "Hash256" } ], [ "NFTokenSellOffer", { - "nth": 29, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 29, "type": "Hash256" } ], [ "HookStateKey", { - "nth": 30, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 30, "type": "Hash256" } ], [ "HookHash", { - "nth": 31, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 31, "type": "Hash256" } ], [ "HookNamespace", { - "nth": 32, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 32, "type": "Hash256" } ], [ "HookSetTxnID", { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, "nth": 33, + "type": "Hash256" + } + ], + [ + "DomainID", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 34, + "type": "Hash256" + } + ], + [ + "VaultID", + { + "isSerialized": true, + "isSigningField": true, "isVLEncoded": false, + "nth": 35, + "type": "Hash256" + } + ], + [ + "ParentBatchID", + { "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 36, "type": "Hash256" } ], [ - "Amount", + "hash", { - "nth": 1, + "isSerialized": false, + "isSigningField": false, + "isVLEncoded": false, + "nth": 257, + "type": "Hash256" + } + ], + [ + "index", + { + "isSerialized": false, + "isSigningField": false, "isVLEncoded": false, + "nth": 258, + "type": "Hash256" + } + ], + [ + "Amount", + { "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 1, "type": "Amount" } ], [ "Balance", { - "nth": 2, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 2, "type": "Amount" } ], [ "LimitAmount", { - "nth": 3, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 3, "type": "Amount" } ], [ "TakerPays", { - "nth": 4, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 4, "type": "Amount" } ], [ "TakerGets", { - "nth": 5, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 5, "type": "Amount" } ], [ "LowLimit", { - "nth": 6, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 6, "type": "Amount" } ], [ "HighLimit", { - "nth": 7, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 7, "type": "Amount" } ], [ "Fee", { - "nth": 8, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 8, "type": "Amount" } ], [ "SendMax", { - "nth": 9, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 9, "type": "Amount" } ], [ "DeliverMin", { - "nth": 10, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 10, "type": "Amount" } ], [ "Amount2", { - "nth": 11, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 11, "type": "Amount" } ], [ "BidMin", { - "nth": 12, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 12, "type": "Amount" } ], [ "BidMax", { - "nth": 13, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 13, "type": "Amount" } ], [ "MinimumOffer", { - "nth": 16, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 16, "type": "Amount" } ], [ "RippleEscrow", { - "nth": 17, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 17, "type": "Amount" } ], [ "DeliveredAmount", { - "nth": 18, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 18, "type": "Amount" } ], [ "NFTokenBrokerFee", { - "nth": 19, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 19, "type": "Amount" } ], [ "BaseFeeDrops", { - "nth": 22, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 22, "type": "Amount" } ], [ "ReserveBaseDrops", { - "nth": 23, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 23, "type": "Amount" } ], [ "ReserveIncrementDrops", { - "nth": 24, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 24, "type": "Amount" } ], [ "LPTokenOut", { - "nth": 25, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 25, "type": "Amount" } ], [ "LPTokenIn", { - "nth": 26, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 26, "type": "Amount" } ], [ "EPrice", { - "nth": 27, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 27, "type": "Amount" } ], [ "Price", { - "nth": 28, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 28, "type": "Amount" } ], [ "SignatureReward", { - "nth": 29, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 29, "type": "Amount" } ], [ "MinAccountCreateAmount", { - "nth": 30, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 30, "type": "Amount" } ], [ "LPTokenBalance", { - "nth": 31, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 31, "type": "Amount" } ], [ "PublicKey", { - "nth": 1, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 1, "type": "Blob" } ], [ "MessageKey", { - "nth": 2, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 2, "type": "Blob" } ], [ "SigningPubKey", { - "nth": 3, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 3, "type": "Blob" } ], [ "TxnSignature", { - "nth": 4, - "isVLEncoded": true, "isSerialized": true, "isSigningField": false, + "isVLEncoded": true, + "nth": 4, "type": "Blob" } ], [ "URI", { - "nth": 5, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 5, "type": "Blob" } ], [ "Signature", { - "nth": 6, - "isVLEncoded": true, "isSerialized": true, "isSigningField": false, + "isVLEncoded": true, + "nth": 6, "type": "Blob" } ], [ "Domain", { - "nth": 7, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 7, "type": "Blob" } ], [ "FundCode", { - "nth": 8, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 8, "type": "Blob" } ], [ "RemoveCode", { - "nth": 9, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 9, "type": "Blob" } ], [ "ExpireCode", { - "nth": 10, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 10, "type": "Blob" } ], [ "CreateCode", { - "nth": 11, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 11, "type": "Blob" } ], [ "MemoType", { - "nth": 12, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 12, "type": "Blob" } ], [ "MemoData", { - "nth": 13, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 13, "type": "Blob" } ], [ "MemoFormat", { - "nth": 14, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 14, "type": "Blob" } ], [ "Fulfillment", { - "nth": 16, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 16, "type": "Blob" } ], [ "Condition", { - "nth": 17, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 17, "type": "Blob" } ], [ "MasterSignature", { - "nth": 18, - "isVLEncoded": true, "isSerialized": true, "isSigningField": false, + "isVLEncoded": true, + "nth": 18, "type": "Blob" } ], [ "UNLModifyValidator", { - "nth": 19, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 19, "type": "Blob" } ], [ "ValidatorToDisable", { - "nth": 20, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 20, "type": "Blob" } ], [ "ValidatorToReEnable", { - "nth": 21, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 21, "type": "Blob" } ], [ "HookStateData", { - "nth": 22, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 22, "type": "Blob" } ], [ "HookReturnString", { - "nth": 23, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 23, "type": "Blob" } ], [ "HookParameterName", { - "nth": 24, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 24, "type": "Blob" } ], [ "HookParameterValue", { - "nth": 25, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 25, "type": "Blob" } ], [ "DIDDocument", { - "nth": 26, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 26, "type": "Blob" } ], [ "Data", { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": true, "nth": 27, + "type": "Blob" + } + ], + [ + "AssetClass", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": true, + "nth": 28, + "type": "Blob" + } + ], + [ + "Provider", + { + "isSerialized": true, + "isSigningField": true, "isVLEncoded": true, + "nth": 29, + "type": "Blob" + } + ], + [ + "MPTokenMetadata", + { "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 30, "type": "Blob" } ], [ - "Account", + "CredentialType", { - "nth": 1, + "isSerialized": true, + "isSigningField": true, "isVLEncoded": true, + "nth": 31, + "type": "Blob" + } + ], + [ + "Account", + { "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 1, "type": "AccountID" } ], [ "Owner", { - "nth": 2, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 2, "type": "AccountID" } ], [ "Destination", { - "nth": 3, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 3, "type": "AccountID" } ], [ "Issuer", { - "nth": 4, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 4, "type": "AccountID" } ], [ "Authorize", { - "nth": 5, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 5, "type": "AccountID" } ], [ "Unauthorize", { - "nth": 6, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 6, "type": "AccountID" } ], [ "RegularKey", { - "nth": 8, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 8, "type": "AccountID" } ], [ "NFTokenMinter", { - "nth": 9, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 9, "type": "AccountID" } ], [ "EmitCallback", { - "nth": 10, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 10, "type": "AccountID" } ], [ - "HookAccount", + "Holder", { - "nth": 16, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 11, "type": "AccountID" } ], [ - "OtherChainSource", + "Delegate", { - "nth": 18, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 12, "type": "AccountID" } ], [ - "OtherChainDestination", + "HookAccount", { - "nth": 19, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 16, "type": "AccountID" } ], [ - "AttestationSignerAccount", + "OtherChainSource", { - "nth": 20, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 18, "type": "AccountID" } ], [ - "AttestationRewardAccount", + "OtherChainDestination", { - "nth": 21, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 19, "type": "AccountID" } ], [ - "LockingChainDoor", + "AttestationSignerAccount", { - "nth": 22, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 20, "type": "AccountID" } ], [ - "IssuingChainDoor", + "AttestationRewardAccount", { - "nth": 23, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, + "isVLEncoded": true, + "nth": 21, "type": "AccountID" } ], [ - "Indexes", + "LockingChainDoor", { - "nth": 1, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "type": "Vector256" - } - ], - [ - "Hashes", - { - "nth": 2, "isVLEncoded": true, - "isSerialized": true, - "isSigningField": true, - "type": "Vector256" + "nth": 22, + "type": "AccountID" } ], [ - "Amendments", + "IssuingChainDoor", { - "nth": 3, - "isVLEncoded": true, "isSerialized": true, "isSigningField": true, - "type": "Vector256" - } - ], - [ - "NFTokenOffers", - { - "nth": 4, "isVLEncoded": true, - "isSerialized": true, - "isSigningField": true, - "type": "Vector256" + "nth": 23, + "type": "AccountID" } ], [ - "Paths", + "Subject", { - "nth": 1, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "PathSet" + "isVLEncoded": true, + "nth": 24, + "type": "AccountID" } ], [ - "LockingChainIssue", + "Number", { - "nth": 1, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "Issue" + "isVLEncoded": false, + "nth": 1, + "type": "Number" } ], [ - "IssuingChainIssue", + "AssetsAvailable", { - "nth": 2, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "Issue" + "isVLEncoded": false, + "nth": 2, + "type": "Number" } ], [ - "Asset", + "AssetsMaximum", { - "nth": 3, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "Issue" + "isVLEncoded": false, + "nth": 3, + "type": "Number" } ], [ - "Asset2", + "AssetsTotal", { - "nth": 4, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "Issue" + "isVLEncoded": false, + "nth": 4, + "type": "Number" } ], [ - "XChainBridge", + "LossUnrealized", { - "nth": 1, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "XChainBridge" + "isVLEncoded": false, + "nth": 5, + "type": "Number" } ], [ "TransactionMetaData", { - "nth": 2, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 2, "type": "STObject" } ], [ "CreatedNode", { - "nth": 3, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 3, "type": "STObject" } ], [ "DeletedNode", { - "nth": 4, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 4, "type": "STObject" } ], [ "ModifiedNode", { - "nth": 5, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 5, "type": "STObject" } ], [ "PreviousFields", { - "nth": 6, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 6, "type": "STObject" } ], [ "FinalFields", { - "nth": 7, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 7, "type": "STObject" } ], [ "NewFields", { - "nth": 8, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 8, "type": "STObject" } ], [ "TemplateEntry", { - "nth": 9, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 9, "type": "STObject" } ], [ "Memo", { - "nth": 10, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 10, "type": "STObject" } ], [ "SignerEntry", { - "nth": 11, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 11, "type": "STObject" } ], [ "NFToken", { - "nth": 12, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 12, "type": "STObject" } ], [ "EmitDetails", { - "nth": 13, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 13, "type": "STObject" } ], [ "Hook", { - "nth": 14, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 14, "type": "STObject" } ], [ - "Signer", + "Permission", { - "nth": 16, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 15, "type": "STObject" } ], [ - "Majority", + "Signer", { - "nth": 18, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "STObject" + "isVLEncoded": false, + "nth": 16, + "type": "STObject" } ], [ - "DisabledValidator", + "Majority", { - "nth": 19, + "isSerialized": true, + "isSigningField": true, "isVLEncoded": false, + "nth": 18, + "type": "STObject" + } + ], + [ + "DisabledValidator", + { "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 19, "type": "STObject" } ], [ "EmittedTxn", { - "nth": 20, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 20, "type": "STObject" } ], [ "HookExecution", { - "nth": 21, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 21, "type": "STObject" } ], [ "HookDefinition", { - "nth": 22, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 22, "type": "STObject" } ], [ "HookParameter", { - "nth": 23, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 23, "type": "STObject" } ], [ "HookGrant", { - "nth": 24, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 24, "type": "STObject" } ], [ "VoteEntry", { - "nth": 25, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 25, "type": "STObject" } ], [ "AuctionSlot", { - "nth": 26, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 26, "type": "STObject" } ], [ "AuthAccount", { - "nth": 27, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 27, "type": "STObject" } ], [ "XChainClaimProofSig", { - "nth": 28, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 28, "type": "STObject" } ], [ "XChainCreateAccountProofSig", { - "nth": 29, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 29, "type": "STObject" } ], [ "XChainClaimAttestationCollectionElement", { - "nth": 30, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 30, "type": "STObject" } ], [ "XChainCreateAccountAttestationCollectionElement", { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, "nth": 31, + "type": "STObject" + } + ], + [ + "PriceData", + { + "isSerialized": true, + "isSigningField": true, "isVLEncoded": false, + "nth": 32, + "type": "STObject" + } + ], + [ + "Credential", + { "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 33, "type": "STObject" } ], [ - "Signers", + "RawTransaction", { - "nth": 3, + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 34, + "type": "STObject" + } + ], + [ + "BatchSigner", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 35, + "type": "STObject" + } + ], + [ + "Book", + { + "isSerialized": true, + "isSigningField": true, "isVLEncoded": false, + "nth": 36, + "type": "STObject" + } + ], + [ + "Signers", + { "isSerialized": true, "isSigningField": false, + "isVLEncoded": false, + "nth": 3, "type": "STArray" } ], [ "SignerEntries", { - "nth": 4, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 4, "type": "STArray" } ], [ "Template", { - "nth": 5, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 5, "type": "STArray" } ], [ "Necessary", { - "nth": 6, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 6, "type": "STArray" } ], [ "Sufficient", { - "nth": 7, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 7, "type": "STArray" } ], [ "AffectedNodes", { - "nth": 8, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 8, "type": "STArray" } ], [ "Memos", { - "nth": 9, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 9, "type": "STArray" } ], [ "NFTokens", { - "nth": 10, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 10, "type": "STArray" } ], [ "Hooks", { - "nth": 11, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 11, "type": "STArray" } ], [ "VoteSlots", { - "nth": 12, + "isSerialized": true, + "isSigningField": true, "isVLEncoded": false, + "nth": 12, + "type": "STArray" + } + ], + [ + "AdditionalBooks", + { "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 13, "type": "STArray" } ], [ "Majorities", { - "nth": 16, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 16, "type": "STArray" } ], [ "DisabledValidators", { - "nth": 17, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 17, "type": "STArray" } ], [ "HookExecutions", { - "nth": 18, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 18, "type": "STArray" } ], [ "HookParameters", { - "nth": 19, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 19, "type": "STArray" } ], [ "HookGrants", { - "nth": 20, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 20, "type": "STArray" } ], [ "XChainClaimAttestations", { - "nth": 21, - "isVLEncoded": false, "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 21, "type": "STArray" } ], [ "XChainCreateAccountAttestations", { - "nth": 22, + "isSerialized": true, + "isSigningField": true, "isVLEncoded": false, + "nth": 22, + "type": "STArray" + } + ], + [ + "PriceDataSeries", + { "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 24, "type": "STArray" } ], [ "AuthAccounts", { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, "nth": 25, + "type": "STArray" + } + ], + [ + "AuthorizeCredentials", + { + "isSerialized": true, + "isSigningField": true, "isVLEncoded": false, + "nth": 26, + "type": "STArray" + } + ], + [ + "UnauthorizeCredentials", + { "isSerialized": true, "isSigningField": true, + "isVLEncoded": false, + "nth": 27, "type": "STArray" } - ] - ], - "TRANSACTION_RESULTS": { - "telLOCAL_ERROR": -399, - "telBAD_DOMAIN": -398, - "telBAD_PATH_COUNT": -397, - "telBAD_PUBLIC_KEY": -396, - "telFAILED_PROCESSING": -395, - "telINSUF_FEE_P": -394, - "telNO_DST_PARTIAL": -393, - "telCAN_NOT_QUEUE": -392, - "telCAN_NOT_QUEUE_BALANCE": -391, - "telCAN_NOT_QUEUE_BLOCKS": -390, - "telCAN_NOT_QUEUE_BLOCKED": -389, - "telCAN_NOT_QUEUE_FEE": -388, - "telCAN_NOT_QUEUE_FULL": -387, - "telWRONG_NETWORK": -386, - "telREQUIRES_NETWORK_ID": -385, - "telNETWORK_ID_MAKES_TX_NON_CANONICAL": -384, - - "temMALFORMED": -299, - "temBAD_AMOUNT": -298, - "temBAD_CURRENCY": -297, - "temBAD_EXPIRATION": -296, - "temBAD_FEE": -295, - "temBAD_ISSUER": -294, - "temBAD_LIMIT": -293, - "temBAD_OFFER": -292, - "temBAD_PATH": -291, - "temBAD_PATH_LOOP": -290, - "temBAD_REGKEY": -289, - "temBAD_SEND_XRP_LIMIT": -288, - "temBAD_SEND_XRP_MAX": -287, - "temBAD_SEND_XRP_NO_DIRECT": -286, - "temBAD_SEND_XRP_PARTIAL": -285, - "temBAD_SEND_XRP_PATHS": -284, - "temBAD_SEQUENCE": -283, - "temBAD_SIGNATURE": -282, - "temBAD_SRC_ACCOUNT": -281, - "temBAD_TRANSFER_RATE": -280, - "temDST_IS_SRC": -279, - "temDST_NEEDED": -278, - "temINVALID": -277, - "temINVALID_FLAG": -276, - "temREDUNDANT": -275, - "temRIPPLE_EMPTY": -274, - "temDISABLED": -273, - "temBAD_SIGNER": -272, - "temBAD_QUORUM": -271, - "temBAD_WEIGHT": -270, + ], + [ + "AcceptedCredentials", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 28, + "type": "STArray" + } + ], + [ + "Permissions", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 29, + "type": "STArray" + } + ], + [ + "RawTransactions", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 30, + "type": "STArray" + } + ], + [ + "BatchSigners", + { + "isSerialized": true, + "isSigningField": false, + "isVLEncoded": false, + "nth": 31, + "type": "STArray" + } + ], + [ + "CloseResolution", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 1, + "type": "UInt8" + } + ], + [ + "Method", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 2, + "type": "UInt8" + } + ], + [ + "TransactionResult", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 3, + "type": "UInt8" + } + ], + [ + "Scale", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 4, + "type": "UInt8" + } + ], + [ + "AssetScale", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 5, + "type": "UInt8" + } + ], + [ + "TickSize", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 16, + "type": "UInt8" + } + ], + [ + "UNLModifyDisabling", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 17, + "type": "UInt8" + } + ], + [ + "HookResult", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 18, + "type": "UInt8" + } + ], + [ + "WasLockingChainSend", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 19, + "type": "UInt8" + } + ], + [ + "WithdrawalPolicy", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 20, + "type": "UInt8" + } + ], + [ + "TakerPaysCurrency", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 1, + "type": "Hash160" + } + ], + [ + "TakerPaysIssuer", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 2, + "type": "Hash160" + } + ], + [ + "TakerGetsCurrency", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 3, + "type": "Hash160" + } + ], + [ + "TakerGetsIssuer", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 4, + "type": "Hash160" + } + ], + [ + "Paths", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 1, + "type": "PathSet" + } + ], + [ + "Indexes", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": true, + "nth": 1, + "type": "Vector256" + } + ], + [ + "Hashes", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": true, + "nth": 2, + "type": "Vector256" + } + ], + [ + "Amendments", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": true, + "nth": 3, + "type": "Vector256" + } + ], + [ + "NFTokenOffers", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": true, + "nth": 4, + "type": "Vector256" + } + ], + [ + "CredentialIDs", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": true, + "nth": 5, + "type": "Vector256" + } + ], + [ + "MPTokenIssuanceID", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 1, + "type": "Hash192" + } + ], + [ + "ShareMPTID", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 2, + "type": "Hash192" + } + ], + [ + "LockingChainIssue", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 1, + "type": "Issue" + } + ], + [ + "IssuingChainIssue", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 2, + "type": "Issue" + } + ], + [ + "Asset", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 3, + "type": "Issue" + } + ], + [ + "Asset2", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 4, + "type": "Issue" + } + ], + [ + "XChainBridge", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 1, + "type": "XChainBridge" + } + ], + [ + "BaseAsset", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 1, + "type": "Currency" + } + ], + [ + "QuoteAsset", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 2, + "type": "Currency" + } + ], + [ + "Transaction", + { + "isSerialized": false, + "isSigningField": false, + "isVLEncoded": false, + "nth": 257, + "type": "Transaction" + } + ], + [ + "LedgerEntry", + { + "isSerialized": false, + "isSigningField": false, + "isVLEncoded": false, + "nth": 257, + "type": "LedgerEntry" + } + ], + [ + "Validation", + { + "isSerialized": false, + "isSigningField": false, + "isVLEncoded": false, + "nth": 257, + "type": "Validation" + } + ], + [ + "Metadata", + { + "isSerialized": false, + "isSigningField": false, + "isVLEncoded": false, + "nth": 257, + "type": "Metadata" + } + ] + ], + "LEDGER_ENTRY_TYPES": { + "AccountRoot": 97, + "Amendments": 102, + "AMM": 121, + "Any": -3, + "Bridge": 105, + "Check": 67, + "Child": -2, + "Contract": 99, + "Credential": 129, + "DID": 73, + "Delegate": 131, + "DepositPreauth": 112, + "DirectoryNode": 100, + "Escrow": 117, + "FeeSettings": 115, + "GeneratorMap": 103, + "Invalid": -1, + "LedgerHashes": 104, + "MPToken": 127, + "MPTokenIssuance": 126, + "NFTokenOffer": 55, + "NFTokenPage": 80, + "NegativeUNL": 78, + "Nickname": 110, + "Offer": 111, + "Oracle": 128, + "PayChannel": 120, + "PermissionedDomain": 130, + "RippleState": 114, + "SignerList": 83, + "Ticket": 84, + "Vault": 132, + "XChainOwnedClaimID": 113, + "XChainOwnedCreateAccountClaimID": 116 + }, + "TRANSACTION_RESULTS": { + "tecAMM_ACCOUNT": 168, + "tecAMM_BALANCE": 163, + "tecAMM_EMPTY": 166, + "tecAMM_FAILED": 164, + "tecAMM_INVALID_TOKENS": 165, + "tecAMM_NOT_EMPTY": 167, + "tecARRAY_EMPTY": 190, + "tecARRAY_TOO_LARGE": 191, + "tecBAD_CREDENTIALS": 193, + "tecCANT_ACCEPT_OWN_NFTOKEN_OFFER": 158, + "tecCLAIM": 100, + "tecCRYPTOCONDITION_ERROR": 146, + "tecDIR_FULL": 121, + "tecDST_TAG_NEEDED": 143, + "tecDUPLICATE": 149, + "tecEMPTY_DID": 187, + "tecEXPIRED": 148, + "tecFAILED_PROCESSING": 105, + "tecFROZEN": 137, + "tecHAS_OBLIGATIONS": 151, + "tecHOOK_REJECTED": 153, + "tecINCOMPLETE": 169, + "tecINSUFFICIENT_FUNDS": 159, + "tecINSUFFICIENT_PAYMENT": 161, + "tecINSUFFICIENT_RESERVE": 141, + "tecINSUFF_FEE": 136, + "tecINSUF_RESERVE_LINE": 122, + "tecINSUF_RESERVE_OFFER": 123, + "tecINTERNAL": 144, + "tecINVALID_UPDATE_TIME": 188, + "tecINVARIANT_FAILED": 147, + "tecKILLED": 150, + "tecLIMIT_EXCEEDED": 195, + "tecLOCKED": 192, + "tecMAX_SEQUENCE_REACHED": 154, + "tecNEED_MASTER_KEY": 142, + "tecNFTOKEN_BUY_SELL_MISMATCH": 156, + "tecNFTOKEN_OFFER_TYPE_MISMATCH": 157, + "tecNO_ALTERNATIVE_KEY": 130, + "tecNO_AUTH": 134, + "tecNO_DELEGATE_PERMISSION": 198, + "tecNO_DST": 124, + "tecNO_DST_INSUF_XRP": 125, + "tecNO_ENTRY": 140, + "tecNO_ISSUER": 133, + "tecNO_LINE": 135, + "tecNO_LINE_INSUF_RESERVE": 126, + "tecNO_LINE_REDUNDANT": 127, + "tecNO_PERMISSION": 139, + "tecNO_REGULAR_KEY": 131, + "tecNO_SUITABLE_NFTOKEN_PAGE": 155, + "tecNO_TARGET": 138, + "tecOBJECT_NOT_FOUND": 160, + "tecOVERSIZE": 145, + "tecOWNERS": 132, + "tecPATH_DRY": 128, + "tecPATH_PARTIAL": 101, + "tecPRECISION_LOSS": 197, + "tecPSEUDO_ACCOUNT": 196, + "tecTOKEN_PAIR_NOT_FOUND": 189, + "tecTOO_SOON": 152, + "tecUNFUNDED": 129, + "tecUNFUNDED_ADD": 102, + "tecUNFUNDED_AMM": 162, + "tecUNFUNDED_OFFER": 103, + "tecUNFUNDED_PAYMENT": 104, + "tecWRONG_ASSET": 194, + "tecXCHAIN_ACCOUNT_CREATE_PAST": 181, + "tecXCHAIN_ACCOUNT_CREATE_TOO_MANY": 182, + "tecXCHAIN_BAD_CLAIM_ID": 172, + "tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR": 185, + "tecXCHAIN_BAD_TRANSFER_ISSUE": 170, + "tecXCHAIN_CLAIM_NO_QUORUM": 173, + "tecXCHAIN_CREATE_ACCOUNT_DISABLED": 186, + "tecXCHAIN_CREATE_ACCOUNT_NONXRP_ISSUE": 175, + "tecXCHAIN_INSUFF_CREATE_AMOUNT": 180, + "tecXCHAIN_NO_CLAIM_ID": 171, + "tecXCHAIN_NO_SIGNERS_LIST": 178, + "tecXCHAIN_PAYMENT_FAILED": 183, + "tecXCHAIN_PROOF_UNKNOWN_KEY": 174, + "tecXCHAIN_REWARD_MISMATCH": 177, + "tecXCHAIN_SELF_COMMIT": 184, + "tecXCHAIN_SENDING_ACCOUNT_MISMATCH": 179, + "tecXCHAIN_WRONG_CHAIN": 176, + + "tefALREADY": -198, + "tefBAD_ADD_AUTH": -197, + "tefBAD_AUTH": -196, + "tefBAD_AUTH_MASTER": -183, + "tefBAD_LEDGER": -195, + "tefBAD_QUORUM": -185, + "tefBAD_SIGNATURE": -186, + "tefCREATED": -194, + "tefEXCEPTION": -193, + "tefFAILURE": -199, + "tefINTERNAL": -192, + "tefINVALID_LEDGER_FIX_TYPE": -178, + "tefINVARIANT_FAILED": -182, + "tefMASTER_DISABLED": -188, + "tefMAX_LEDGER": -187, + "tefNFTOKEN_IS_NOT_TRANSFERABLE": -179, + "tefNOT_MULTI_SIGNING": -184, + "tefNO_AUTH_REQUIRED": -191, + "tefNO_TICKET": -180, + "tefPAST_SEQ": -190, + "tefTOO_BIG": -181, + "tefWRONG_PRIOR": -189, + + "telBAD_DOMAIN": -398, + "telBAD_PATH_COUNT": -397, + "telBAD_PUBLIC_KEY": -396, + "telCAN_NOT_QUEUE": -392, + "telCAN_NOT_QUEUE_BALANCE": -391, + "telCAN_NOT_QUEUE_BLOCKED": -389, + "telCAN_NOT_QUEUE_BLOCKS": -390, + "telCAN_NOT_QUEUE_FEE": -388, + "telCAN_NOT_QUEUE_FULL": -387, + "telENV_RPC_FAILED": -383, + "telFAILED_PROCESSING": -395, + "telINSUF_FEE_P": -394, + "telLOCAL_ERROR": -399, + "telNETWORK_ID_MAKES_TX_NON_CANONICAL": -384, + "telNO_DST_PARTIAL": -393, + "telREQUIRES_NETWORK_ID": -385, + "telWRONG_NETWORK": -386, + + "temARRAY_EMPTY": -253, + "temARRAY_TOO_LARGE": -252, + "temBAD_AMM_TOKENS": -261, + "temBAD_AMOUNT": -298, + "temBAD_CURRENCY": -297, + "temBAD_EXPIRATION": -296, + "temBAD_FEE": -295, + "temBAD_ISSUER": -294, + "temBAD_LIMIT": -293, + "temBAD_NFTOKEN_TRANSFER_FEE": -262, + "temBAD_OFFER": -292, + "temBAD_PATH": -291, + "temBAD_PATH_LOOP": -290, + "temBAD_QUORUM": -271, + "temBAD_REGKEY": -289, + "temBAD_SEND_XRP_LIMIT": -288, + "temBAD_SEND_XRP_MAX": -287, + "temBAD_SEND_XRP_NO_DIRECT": -286, + "temBAD_SEND_XRP_PARTIAL": -285, + "temBAD_SEND_XRP_PATHS": -284, + "temBAD_SEQUENCE": -283, + "temBAD_SIGNATURE": -282, + "temBAD_SIGNER": -272, + "temBAD_SRC_ACCOUNT": -281, "temBAD_TICK_SIZE": -269, - "temINVALID_ACCOUNT_ID": -268, + "temBAD_TRANSFER_FEE": -251, + "temBAD_TRANSFER_RATE": -280, + "temBAD_WEIGHT": -270, "temCANNOT_PREAUTH_SELF": -267, + "temDISABLED": -273, + "temDST_IS_SRC": -279, + "temDST_NEEDED": -278, + "temEMPTY_DID": -254, + "temINVALID": -277, + "temINVALID_ACCOUNT_ID": -268, "temINVALID_COUNT": -266, + "temINVALID_FLAG": -276, + "temINVALID_INNER_BATCH": -250, + "temMALFORMED": -299, + "temREDUNDANT": -275, + "temRIPPLE_EMPTY": -274, + "temSEQ_AND_TICKET": -263, "temUNCERTAIN": -265, "temUNKNOWN": -264, - "temSEQ_AND_TICKET": -263, - "temBAD_NFTOKEN_TRANSFER_FEE": -262, - "temBAD_AMM_TOKENS": -261, - "temXCHAIN_EQUAL_DOOR_ACCOUNTS": -260, "temXCHAIN_BAD_PROOF": -259, "temXCHAIN_BRIDGE_BAD_ISSUES": -258, - "temXCHAIN_BRIDGE_NONDOOR_OWNER": -257, "temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT": -256, "temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT": -255, - "temEMPTY_DID": -254, - - "tefFAILURE": -199, - "tefALREADY": -198, - "tefBAD_ADD_AUTH": -197, - "tefBAD_AUTH": -196, - "tefBAD_LEDGER": -195, - "tefCREATED": -194, - "tefEXCEPTION": -193, - "tefINTERNAL": -192, - "tefNO_AUTH_REQUIRED": -191, - "tefPAST_SEQ": -190, - "tefWRONG_PRIOR": -189, - "tefMASTER_DISABLED": -188, - "tefMAX_LEDGER": -187, - "tefBAD_SIGNATURE": -186, - "tefBAD_QUORUM": -185, - "tefNOT_MULTI_SIGNING": -184, - "tefBAD_AUTH_MASTER": -183, - "tefINVARIANT_FAILED": -182, - "tefTOO_BIG": -181, - "tefNO_TICKET": -180, - "tefNFTOKEN_IS_NOT_TRANSFERABLE": -179, + "temXCHAIN_BRIDGE_NONDOOR_OWNER": -257, + "temXCHAIN_EQUAL_DOOR_ACCOUNTS": -260, - "terRETRY": -99, + "terADDRESS_COLLISION": -86, "terFUNDS_SPENT": -98, "terINSUF_FEE_B": -97, + "terLAST": -91, "terNO_ACCOUNT": -96, + "terNO_AMM": -87, "terNO_AUTH": -95, "terNO_LINE": -94, + "terNO_RIPPLE": -90, "terOWNERS": -93, "terPRE_SEQ": -92, - "terLAST": -91, - "terNO_RIPPLE": -90, - "terQUEUED": -89, "terPRE_TICKET": -88, - "terNO_AMM": -87, - "terSUBMITTED": -86, - - "tesSUCCESS": 0, + "terQUEUED": -89, + "terRETRY": -99, - "tecCLAIM": 100, - "tecPATH_PARTIAL": 101, - "tecUNFUNDED_ADD": 102, - "tecUNFUNDED_OFFER": 103, - "tecUNFUNDED_PAYMENT": 104, - "tecFAILED_PROCESSING": 105, - "tecDIR_FULL": 121, - "tecINSUF_RESERVE_LINE": 122, - "tecINSUF_RESERVE_OFFER": 123, - "tecNO_DST": 124, - "tecNO_DST_INSUF_XRP": 125, - "tecNO_LINE_INSUF_RESERVE": 126, - "tecNO_LINE_REDUNDANT": 127, - "tecPATH_DRY": 128, - "tecUNFUNDED": 129, - "tecNO_ALTERNATIVE_KEY": 130, - "tecNO_REGULAR_KEY": 131, - "tecOWNERS": 132, - "tecNO_ISSUER": 133, - "tecNO_AUTH": 134, - "tecNO_LINE": 135, - "tecINSUFF_FEE": 136, - "tecFROZEN": 137, - "tecNO_TARGET": 138, - "tecNO_PERMISSION": 139, - "tecNO_ENTRY": 140, - "tecINSUFFICIENT_RESERVE": 141, - "tecNEED_MASTER_KEY": 142, - "tecDST_TAG_NEEDED": 143, - "tecINTERNAL": 144, - "tecOVERSIZE": 145, - "tecCRYPTOCONDITION_ERROR": 146, - "tecINVARIANT_FAILED": 147, - "tecEXPIRED": 148, - "tecDUPLICATE": 149, - "tecKILLED": 150, - "tecHAS_OBLIGATIONS": 151, - "tecTOO_SOON": 152, - "tecHOOK_REJECTED": 153, - "tecMAX_SEQUENCE_REACHED": 154, - "tecNO_SUITABLE_NFTOKEN_PAGE": 155, - "tecNFTOKEN_BUY_SELL_MISMATCH": 156, - "tecNFTOKEN_OFFER_TYPE_MISMATCH": 157, - "tecCANT_ACCEPT_OWN_NFTOKEN_OFFER": 158, - "tecINSUFFICIENT_FUNDS": 159, - "tecOBJECT_NOT_FOUND": 160, - "tecINSUFFICIENT_PAYMENT": 161, - "tecUNFUNDED_AMM": 162, - "tecAMM_BALANCE": 163, - "tecAMM_FAILED": 164, - "tecAMM_INVALID_TOKENS": 165, - "tecAMM_EMPTY": 166, - "tecAMM_NOT_EMPTY": 167, - "tecAMM_ACCOUNT": 168, - "tecINCOMPLETE": 169, - "tecXCHAIN_BAD_TRANSFER_ISSUE": 170, - "tecXCHAIN_NO_CLAIM_ID": 171, - "tecXCHAIN_BAD_CLAIM_ID": 172, - "tecXCHAIN_CLAIM_NO_QUORUM": 173, - "tecXCHAIN_PROOF_UNKNOWN_KEY": 174, - "tecXCHAIN_CREATE_ACCOUNT_NONXRP_ISSUE": 175, - "tecXCHAIN_WRONG_CHAIN": 176, - "tecXCHAIN_REWARD_MISMATCH": 177, - "tecXCHAIN_NO_SIGNERS_LIST": 178, - "tecXCHAIN_SENDING_ACCOUNT_MISMATCH": 179, - "tecXCHAIN_INSUFF_CREATE_AMOUNT": 180, - "tecXCHAIN_ACCOUNT_CREATE_PAST": 181, - "tecXCHAIN_ACCOUNT_CREATE_TOO_MANY": 182, - "tecXCHAIN_PAYMENT_FAILED": 183, - "tecXCHAIN_SELF_COMMIT": 184, - "tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR": 185, - "tecXCHAIN_CREATE_ACCOUNT_DISABLED": 186, - "tecEMPTY_DID": 187 + "tesSUCCESS": 0 }, "TRANSACTION_TYPES": { - "Invalid": -1, - "Payment": 0, - "EscrowCreate": 1, - "EscrowFinish": 2, + "AMMBid": 39, + "AMMClawback": 31, + "AMMCreate": 35, + "AMMDelete": 40, + "AMMDeposit": 36, + "AMMVote": 38, + "AMMWithdraw": 37, + "AccountDelete": 21, "AccountSet": 3, + "Batch": 71, + "CheckCancel": 18, + "CheckCash": 17, + "CheckCreate": 16, + "Clawback": 30, + "CredentialAccept": 59, + "CredentialCreate": 58, + "CredentialDelete": 60, + "DIDDelete": 50, + "DIDSet": 49, + "DelegateSet": 64, + "DepositPreauth": 19, + "EnableAmendment": 100, "EscrowCancel": 4, - "SetRegularKey": 5, - "NickNameSet": 6, - "OfferCreate": 7, + "EscrowCreate": 1, + "EscrowFinish": 2, + "Invalid": -1, + "LedgerStateFix": 53, + "MPTokenAuthorize": 57, + "MPTokenIssuanceCreate": 54, + "MPTokenIssuanceDestroy": 55, + "MPTokenIssuanceSet": 56, + "NFTokenAcceptOffer": 29, + "NFTokenBurn": 26, + "NFTokenCancelOffer": 28, + "NFTokenCreateOffer": 27, + "NFTokenMint": 25, + "NFTokenModify": 61, "OfferCancel": 8, - "Contract": 9, - "TicketCreate": 10, - "TicketCancel": 11, - "SignerListSet": 12, + "OfferCreate": 7, + "OracleDelete": 52, + "OracleSet": 51, + "Payment": 0, + "PaymentChannelClaim": 15, "PaymentChannelCreate": 13, "PaymentChannelFund": 14, - "PaymentChannelClaim": 15, - "CheckCreate": 16, - "CheckCash": 17, - "CheckCancel": 18, - "DepositPreauth": 19, + "PermissionedDomainDelete": 63, + "PermissionedDomainSet": 62, + "SetFee": 101, + "SetRegularKey": 5, + "SignerListSet": 12, + "TicketCreate": 10, "TrustSet": 20, - "AccountDelete": 21, - "SetHook": 22, - "NFTokenMint": 25, - "NFTokenBurn": 26, - "NFTokenCreateOffer": 27, - "NFTokenCancelOffer": 28, - "NFTokenAcceptOffer": 29, - "Clawback": 30, - "AMMCreate": 35, - "AMMDeposit": 36, - "AMMWithdraw": 37, - "AMMVote": 38, - "AMMBid": 39, - "AMMDelete": 40, - "XChainCreateClaimID": 41, - "XChainCommit": 42, - "XChainClaim": 43, + "UNLModify": 102, + "VaultClawback": 70, + "VaultCreate": 65, + "VaultDelete": 67, + "VaultDeposit": 68, + "VaultSet": 66, + "VaultWithdraw": 69, "XChainAccountCreateCommit": 44, - "XChainAddClaimAttestation": 45, "XChainAddAccountCreateAttestation": 46, - "XChainModifyBridge": 47, + "XChainAddClaimAttestation": 45, + "XChainClaim": 43, + "XChainCommit": 42, "XChainCreateBridge": 48, - "DIDSet": 49, - "DIDDelete": 50, - "EnableAmendment": 100, - "SetFee": 101, - "UNLModify": 102 + "XChainCreateClaimID": 41, + "XChainModifyBridge": 47 + }, + "TYPES": { + "AccountID": 8, + "Amount": 6, + "Blob": 7, + "Currency": 26, + "Done": -1, + "Hash128": 4, + "Hash160": 17, + "Hash192": 21, + "Hash256": 5, + "Issue": 24, + "LedgerEntry": 10002, + "Metadata": 10004, + "NotPresent": 0, + "Number": 9, + "PathSet": 18, + "STArray": 15, + "STObject": 14, + "Transaction": 10001, + "UInt16": 1, + "UInt32": 2, + "UInt384": 22, + "UInt512": 23, + "UInt64": 3, + "UInt8": 16, + "UInt96": 20, + "Unknown": -2, + "Validation": 10003, + "Vector256": 19, + "XChainBridge": 25 } } diff --git a/binary-codec/types/amount.go b/binary-codec/types/amount.go index 0aafbf6b4..d28977943 100644 --- a/binary-codec/types/amount.go +++ b/binary-codec/types/amount.go @@ -26,9 +26,11 @@ const ( MaxIOUMantissa = 9999999999999999 NotXRPBitMask = 0x80 + MPTBitMask = 0x60 PosSignBitMask = 0x4000000000000000 ZeroCurrencyAmountHex = 0x8000000000000000 NativeAmountByteLength = 8 + MPTAmountByteLength = 33 // MPT amounts are 33 bytes: 1 byte leading + 8 bytes amount + 24 bytes MPT ID CurrencyAmountByteLength = 48 MinXRP = 1e-6 @@ -84,11 +86,50 @@ func (a *Amount) FromJson(value any) ([]byte, error) { return serializeXrpAmount(v) case types.IssuedCurrencyAmount: return serializeIssuedCurrencyAmount(v) + case types.MPTCurrencyAmount: + return serializeMPTCurrencyAmount(v) case map[string]interface{}: + // Check if this is an MPT transaction + if mpIssuanceID, hasMPT := v["mp_issuance_id"]; hasMPT { + var mpt types.MPTCurrencyAmount + if value, hasValue := v["value"]; hasValue { + if strValue, ok := value.(string); ok { + mpt.Value = strValue + } else { + return nil, fmt.Errorf("MPT value must be a string, got %T", value) + } + } + if strMPTIssuanceID, ok := mpIssuanceID.(string); ok { + mpt.MPTIssuanceID = types.Hash192(strMPTIssuanceID) + } else { + return nil, fmt.Errorf("MPT issuance ID must be a string, got %T", mpIssuanceID) + } + return serializeMPTCurrencyAmount(mpt) + } + + // Handle IssuedCurrencyAmount var cur types.IssuedCurrencyAmount - cur.Issuer = types.Address(v["issuer"].(string)) - cur.Currency = v["currency"].(string) - cur.Value = v["value"].(string) + if issuer, hasIssuer := v["issuer"]; hasIssuer && issuer != nil { + if strIssuer, ok := issuer.(string); ok { + cur.Issuer = types.Address(strIssuer) + } else { + return nil, fmt.Errorf("issuer must be a string, got %T", issuer) + } + } + if currency, hasCurrency := v["currency"]; hasCurrency && currency != nil { + if strCurrency, ok := currency.(string); ok { + cur.Currency = strCurrency + } else { + return nil, fmt.Errorf("currency must be a string, got %T", currency) + } + } + if value, hasValue := v["value"]; hasValue && value != nil { + if strValue, ok := value.(string); ok { + cur.Value = strValue + } else { + return nil, fmt.Errorf("value must be a string, got %T", value) + } + } return serializeIssuedCurrencyAmount(cur) case string: i64, e := strconv.ParseInt(v, 10, 64) @@ -120,8 +161,14 @@ func (a *Amount) ToJson(p *serdes.BinaryParser, opts ...int) (any, error) { xrpVal := binary.BigEndian.Uint64(xrp) xrpVal = xrpVal & 0x3FFFFFFFFFFFFFFF return sign + strconv.FormatUint(xrpVal, 10), nil + } else if isMPT(b) { + mptData, err := p.ReadBytes(MPTAmountByteLength) + if err != nil { + return nil, err + } + return deserializeMPT(mptData) } else { - token, err := p.ReadBytes(48) + token, err := p.ReadBytes(CurrencyAmountByteLength) if err != nil { return nil, err } @@ -129,6 +176,44 @@ func (a *Amount) ToJson(p *serdes.BinaryParser, opts ...int) (any, error) { } } +func deserializeMPT(data []byte) (map[string]any, error) { + if len(data) != 33 { + return nil, fmt.Errorf("MPT data must be exactly 33 bytes, got %d", len(data)) + } + + // First byte is the leading byte with flags + leadingByte := data[0] + + // Check if it's actually an MPT amount + if (leadingByte&NotXRPBitMask) != 0 || (leadingByte&MPTBitMask) == 0 { + return nil, fmt.Errorf("invalid MPT leading byte: %02x", leadingByte) + } + + // Determine sign from the second bit (0x40) + var sign string + if (leadingByte & 0x40) == 0 { + sign = "-" + } + + // Extract amount value from bytes 1-8 (big-endian) + amountBytes := data[1:9] + amountValue := binary.BigEndian.Uint64(amountBytes) + + // Clear the sign bit from the amount (bit 63 must be 0 for MPT) + if amountValue > 0x7FFFFFFFFFFFFFFF { + return nil, fmt.Errorf("MPT amount exceeds maximum allowed value") + } + + // Extract MPT issuance ID from bytes 9-32 (24 bytes) + mptIDBytes := data[9:MPTAmountByteLength] + mptID := strings.ToUpper(hex.EncodeToString(mptIDBytes)) + + return map[string]any{ + "value": sign + strconv.FormatUint(amountValue, 10), + "mp_issuance_id": mptID, + }, nil +} + func deserializeToken(data []byte) (map[string]any, error) { var value string @@ -431,8 +516,12 @@ func serializeIssuedCurrencyAmount(value types.IssuedCurrencyAmount) ([]byte, er // Returns true if this amount is a "native" XRP amount - first bit in first byte set to 0 for native XRP func isNative(value byte) bool { - x := value&NotXRPBitMask == 0 // & bitwise operator returns 1 if both first bits are 1, otherwise 0 - return x + return (value&NotXRPBitMask == 0) && (value&0x20 == 0) +} + +// Returns true if this amount is an MPT amount - first bit is 0 and 6th bit is 1 +func isMPT(value byte) bool { + return (value&NotXRPBitMask == 0) && (value&0x20 != 0) } // Determines if this AmountType is positive - 2nd bit in 1st byte is set to 1 for positive amounts @@ -448,3 +537,56 @@ func containsInvalidIOUCodeCharactersHex(currency []byte) bool { return len(m) != 1 } + +// serializeMPTCurrencyAmount serializes an MPT currency amount to its bytes representation. +func serializeMPTCurrencyAmount(value types.MPTCurrencyAmount) ([]byte, error) { + // Validate MPT value - should be a positive integer without decimal + if value.Value == "" { + return nil, errors.New("MPT value cannot be empty") + } + + // Check if value contains decimal point + if strings.Contains(value.Value, ".") { + return nil, errors.New("MPT value cannot contain decimal point") + } + + // Parse the value as integer + val, err := strconv.ParseInt(value.Value, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid MPT value: %w", err) + } + + // Check if value is negative + if val < 0 { + return nil, errors.New("MPT value cannot be negative") + } + + // Check if value exceeds the maximum allowed (bit 63 must be 0) + if val > 0x7FFFFFFFFFFFFFFF { + return nil, errors.New("MPT value exceeds maximum allowed") + } + + leadingByte := []byte{MPTBitMask} + + // Serialize the amount value as 8 bytes (big-endian) + amountBytes := make([]byte, 8) + binary.BigEndian.PutUint64(amountBytes, uint64(val)) + + // Serialize the MPT issuance ID (24 bytes) + mptIDBytes, err := hex.DecodeString(string(value.MPTIssuanceID)) + if err != nil { + return nil, fmt.Errorf("invalid MPT issuance ID format: %w", err) + } + + if len(mptIDBytes) != 24 { + return nil, fmt.Errorf("MPT issuance ID must be exactly 24 bytes, got %d", len(mptIDBytes)) + } + + // Combine: leading byte + amount + MPT ID + result := make([]byte, 0, MPTAmountByteLength) + result = append(result, leadingByte...) + result = append(result, amountBytes...) + result = append(result, mptIDBytes...) + + return result, nil +} diff --git a/binary-codec/types/amount_test.go b/binary-codec/types/amount_test.go index d40d00cf5..91544720a 100644 --- a/binary-codec/types/amount_test.go +++ b/binary-codec/types/amount_test.go @@ -1,6 +1,10 @@ package types import ( + "encoding/binary" + "encoding/hex" + "strconv" + "strings" "testing" "github.com/CreatureDev/xrpl-go/model/transactions/types" @@ -418,3 +422,364 @@ func TestIsPositive(t *testing.T) { }) } } + +func TestSerializeMPTCurrencyAmount(t *testing.T) { + amount := &Amount{} + + tests := []struct { + name string + input types.MPTCurrencyAmount + expectedHex string + expectErr bool + }{ + { + name: "Valid MPT amount with value 100", + input: types.MPTCurrencyAmount{ + Value: "100", + MPTIssuanceID: "00002403C84A0A28E0190E208E982C352BBD5006600555CF", + }, + expectedHex: "60000000000000006400002403C84A0A28E0190E208E982C352BBD5006600555CF", + expectErr: false, + }, + { + name: "Valid MPT amount with large value", + input: types.MPTCurrencyAmount{ + Value: "9223372036854775807", + MPTIssuanceID: "00002403C84A0A28E0190E208E982C352BBD5006600555CF", + }, + expectedHex: "607FFFFFFFFFFFFFFF00002403C84A0A28E0190E208E982C352BBD5006600555CF", + expectErr: false, + }, + { + name: "MPT amount with zero value", + input: types.MPTCurrencyAmount{ + Value: "0", + MPTIssuanceID: "00002403C84A0A28E0190E208E982C352BBD5006600555CF", + }, + expectedHex: "60000000000000000000002403C84A0A28E0190E208E982C352BBD5006600555CF", + expectErr: false, + }, + { + name: "MPT amount with empty value", + input: types.MPTCurrencyAmount{ + Value: "", + MPTIssuanceID: "00002403C84A0A28E0190E208E982C352BBD5006600555CF", + }, + expectErr: true, + }, + { + name: "MPT amount with decimal value", + input: types.MPTCurrencyAmount{ + Value: "100.5", + MPTIssuanceID: "00002403C84A0A28E0190E208E982C352BBD5006600555CF", + }, + expectErr: true, + }, + { + name: "MPT amount with negative value", + input: types.MPTCurrencyAmount{ + Value: "-100", + MPTIssuanceID: "00002403C84A0A28E0190E208E982C352BBD5006600555CF", + }, + expectErr: true, + }, + { + name: "MPT amount with invalid issuance ID", + input: types.MPTCurrencyAmount{ + Value: "100", + MPTIssuanceID: "invalid_hex", + }, + expectErr: true, + }, + { + name: "MPT amount with wrong length issuance ID", + input: types.MPTCurrencyAmount{ + Value: "100", + MPTIssuanceID: "00002403C84A0A28E0190E208E982C352BBD5006600555CF00", // 26 bytes + }, + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := amount.FromJson(tt.input) + + if tt.expectErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.NotNil(t, result) + + // Convert result to hex string for comparison + resultHex := strings.ToUpper(hex.EncodeToString(result)) + require.Equal(t, tt.expectedHex, resultHex) + + // Verify the length is correct (1 + 8 + 24 = 33 bytes) + require.Equal(t, 33, len(result)) + + // Verify the leading byte is 0x60 + require.Equal(t, byte(0x60), result[0]) + + // Verify the amount bytes (positions 1-8) + amountBytes := result[1:9] + amountValue := binary.BigEndian.Uint64(amountBytes) + expectedValue, _ := strconv.ParseUint(tt.input.Value, 10, 64) + require.Equal(t, expectedValue, amountValue) + + // Verify the MPT issuance ID bytes (positions 9-32) + mptIDBytes := result[9:33] + expectedMPTID, _ := hex.DecodeString(string(tt.input.MPTIssuanceID)) + require.Equal(t, expectedMPTID, mptIDBytes) + }) + } +} + +func TestAmountFromJsonWithMPT(t *testing.T) { + amount := &Amount{} + + // Test MPT amount through FromJson + mptAmount := types.MPTCurrencyAmount{ + Value: "100", + MPTIssuanceID: "00002403C84A0A28E0190E208E982C352BBD5006600555CF", + } + + result, err := amount.FromJson(mptAmount) + require.NoError(t, err) + require.NotNil(t, result) + + // Verify the result matches our expected format + expectedHex := "60000000000000006400002403C84A0A28E0190E208E982C352BBD5006600555CF" + resultHex := strings.ToUpper(hex.EncodeToString(result)) + require.Equal(t, expectedHex, resultHex) + + // Verify the length is correct (1 + 8 + 24 = 33 bytes) + require.Equal(t, 33, len(result)) + + // Verify the leading byte is 0x60 + require.Equal(t, byte(0x60), result[0]) + + // Verify the amount bytes (positions 1-8) + amountBytes := result[1:9] + amountValue := binary.BigEndian.Uint64(amountBytes) + require.Equal(t, uint64(100), amountValue) + + // Verify the MPT issuance ID bytes (positions 9-32) + mptIDBytes := result[9:33] + expectedMPTID, _ := hex.DecodeString(string(mptAmount.MPTIssuanceID)) + require.Equal(t, expectedMPTID, mptIDBytes) +} + +func TestMPTSerializationMatchesJavaScript(t *testing.T) { + amount := &Amount{} + + // Test case from JavaScript test: negative MPT value + // JavaScript test shows: 20000000000000006400002403C84A0A28E0190E208E982C352BBD5006600555CF + // Where 20 = leading byte (negative), 0000000000000064 = amount (-100), rest = MPT ID + + // Note: Our current implementation doesn't support negative values (as per XRPL spec), + // but we can verify the structure matches what JavaScript would produce for positive values + + mptAmount := types.MPTCurrencyAmount{ + Value: "100", + MPTIssuanceID: "00002403C84A0A28E0190E208E982C352BBD5006600555CF", + } + + result, err := amount.FromJson(mptAmount) + require.NoError(t, err) + require.NotNil(t, result) + + // The JavaScript test shows this hex for positive 100: + // 60000000000000006400002403C84A0A28E0190E208E982C352BBD5006600555CF + // Where 60 = leading byte (positive), 0000000000000064 = amount (100), rest = MPT ID + + expectedHex := "60000000000000006400002403C84A0A28E0190E208E982C352BBD5006600555CF" + resultHex := strings.ToUpper(hex.EncodeToString(result)) + require.Equal(t, expectedHex, resultHex) + + // Verify the structure matches JavaScript implementation: + // - Leading byte: 0x60 (bits 6 and 5 set for MPT, bit 4 clear for positive) + // - Amount: 8 bytes in big-endian format + // - MPT ID: 24 bytes + + require.Equal(t, byte(0x60), result[0]) // Leading byte should be 0x60 + + // Amount should be 100 (0x64) in big-endian format + amountBytes := result[1:9] + amountValue := binary.BigEndian.Uint64(amountBytes) + require.Equal(t, uint64(100), amountValue) + + // MPT ID should match the input + mptIDBytes := result[9:33] + expectedMPTID, _ := hex.DecodeString(string(mptAmount.MPTIssuanceID)) + require.Equal(t, expectedMPTID, mptIDBytes) +} + +func TestDeserializeMPT(t *testing.T) { + tests := []struct { + name string + inputHex string + expected map[string]any + expectErr bool + }{ + { + name: "Valid MPT amount with positive value 100", + inputHex: "60000000000000006400002403C84A0A28E0190E208E982C352BBD5006600555CF", + expected: map[string]any{ + "value": "100", + "mp_issuance_id": "00002403C84A0A28E0190E208E982C352BBD5006600555CF", + }, + expectErr: false, + }, + { + name: "Valid MPT amount with zero value", + inputHex: "60000000000000000000002403C84A0A28E0190E208E982C352BBD5006600555CF", + expected: map[string]any{ + "value": "0", + "mp_issuance_id": "00002403C84A0A28E0190E208E982C352BBD5006600555CF", + }, + expectErr: false, + }, + { + name: "Valid MPT amount with large value", + inputHex: "607FFFFFFFFFFFFFFF00002403C84A0A28E0190E208E982C352BBD5006600555CF", + expected: map[string]any{ + "value": "9223372036854775807", + "mp_issuance_id": "00002403C84A0A28E0190E208E982C352BBD5006600555CF", + }, + expectErr: false, + }, + { + name: "Invalid MPT data length", + inputHex: "60000000000000006400002403C84A0A28E0190E208E982C352BBD5006600555CF00", + expected: nil, + expectErr: true, + }, + { + name: "Invalid MPT leading byte (not MPT)", + inputHex: "80000000000000006400002403C84A0A28E0190E208E982C352BBD5006600555CF", + expected: nil, + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Decode hex string to bytes + inputBytes, err := hex.DecodeString(tt.inputHex) + require.NoError(t, err) + + // Test the deserializeMPT function directly + + result, err := deserializeMPT(inputBytes) + + if tt.expectErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.NotNil(t, result) + + // Check that all expected fields are present + for key, expectedValue := range tt.expected { + actualValue, exists := result[key] + require.True(t, exists, "Field %s should exist", key) + require.Equal(t, expectedValue, actualValue, "Field %s should match", key) + } + + // Check that no unexpected fields are present + require.Equal(t, len(tt.expected), len(result), "Result should have exactly the expected number of fields") + }) + } +} + +func TestIsMPT(t *testing.T) { + tests := []struct { + name string + input byte + expected bool + }{ + { + name: "MPT amount - leading byte 0x60", + input: 0x60, + expected: true, + }, + { + name: "MPT amount with positive sign - leading byte 0x70", + input: 0x70, + expected: true, + }, + { + name: "Native XRP amount - leading byte 0x40", + input: 0x40, + expected: false, + }, + { + name: "IOU amount - leading byte 0x80", + input: 0x80, + expected: false, + }, + { + name: "Zero byte", + input: 0x00, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.expected, isMPT(tt.input)) + }) + } +} + +func TestMPTFullCycle(t *testing.T) { + amount := &Amount{} + + // Test data + mptAmount := types.MPTCurrencyAmount{ + Value: "100", + MPTIssuanceID: "00002403C84A0A28E0190E208E982C352BBD5006600555CF", + } + + // Step 1: Serialize MPT to bytes + serializedBytes, err := amount.FromJson(mptAmount) + require.NoError(t, err) + require.NotNil(t, serializedBytes) + + // Step 2: Verify the serialized format + expectedHex := "60000000000000006400002403C84A0A28E0190E208E982C352BBD5006600555CF" + actualHex := strings.ToUpper(hex.EncodeToString(serializedBytes)) + require.Equal(t, expectedHex, actualHex) + + // Step 3: Deserialize back to JSON + // Test the deserializeMPT function directly + + result, err := deserializeMPT(serializedBytes) + require.NoError(t, err) + require.NotNil(t, result) + + // Step 4: Verify the deserialized result matches the original + require.Equal(t, "100", result["value"]) + require.Equal(t, "00002403C84A0A28E0190E208E982C352BBD5006600555CF", result["mp_issuance_id"]) + + // Step 5: Test with negative value + mptAmountNegative := types.MPTCurrencyAmount{ + Value: "50", + MPTIssuanceID: "00002403C84A0A28E0190E208E982C352BBD5006600555CF", + } + + // Note: MPT amounts are always positive in the current implementation + // This test verifies that the sign handling works correctly + serializedBytesNegative, err := amount.FromJson(mptAmountNegative) + require.NoError(t, err) + require.NotNil(t, serializedBytesNegative) + + resultNegative, err := deserializeMPT(serializedBytesNegative) + require.NoError(t, err) + require.NotNil(t, resultNegative) + require.Equal(t, "50", resultNegative["value"]) +} diff --git a/binary-codec/types/hash192.go b/binary-codec/types/hash192.go new file mode 100644 index 000000000..49455e8f2 --- /dev/null +++ b/binary-codec/types/hash192.go @@ -0,0 +1,57 @@ +package types + +import ( + "encoding/hex" + "strings" + + "github.com/CreatureDev/xrpl-go/binary-codec/serdes" + "github.com/CreatureDev/xrpl-go/model/transactions/types" +) + +var _ hashI = (*Hash192)(nil) + +// Hash192 struct represents a 192-bit hash. +type Hash192 struct { +} + +// NewHash192 is a constructor for creating a new 192-bit hash. +func NewHash192() *Hash192 { + return &Hash192{} +} + +// getLength method for hash returns the hash's length. +func (h *Hash192) getLength() int { + return 24 +} + +// FromJson method for hash converts a hexadecimal string from JSON to a byte array. +// It returns an error if the conversion fails or the length of the decoded byte array is not as expected. +func (h *Hash192) FromJson(json any) ([]byte, error) { + var s string + switch json := json.(type) { + case string: + s = json + case types.Hash192: + s = string(json) + default: + return nil, ErrInvalidHashType + } + v, err := hex.DecodeString(s) + if err != nil { + return nil, err + } + if h.getLength() != len(v) { + return nil, &ErrInvalidHashLength{Expected: h.getLength()} + } + return v, nil +} + +// ToJson method for hash reads a certain number of bytes from a BinaryParser and converts it into a hexadecimal string. +// It returns an error if the read operation fails. +func (h *Hash192) ToJson(p *serdes.BinaryParser, opts ...int) (any, error) { + b, err := p.ReadBytes(h.getLength()) + if err != nil { + return nil, err + } + return strings.ToUpper(hex.EncodeToString(b)), nil +} diff --git a/binary-codec/types/hash192_test.go b/binary-codec/types/hash192_test.go new file mode 100644 index 000000000..5fc1217b0 --- /dev/null +++ b/binary-codec/types/hash192_test.go @@ -0,0 +1,65 @@ +package types + +import ( + "testing" + + "github.com/CreatureDev/xrpl-go/binary-codec/serdes" + "github.com/CreatureDev/xrpl-go/model/transactions/types" + "github.com/stretchr/testify/assert" +) + +func TestHash192_FromJson(t *testing.T) { + h := NewHash192() + + // Test with valid hex string + validHex := "1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF" + result, err := h.FromJson(validHex) + assert.NoError(t, err) + assert.Equal(t, 24, len(result)) + + // Test with Hash192 type + hash192 := types.Hash192(validHex) + result, err = h.FromJson(hash192) + assert.NoError(t, err) + assert.Equal(t, 24, len(result)) + + // Test with invalid hex string + invalidHex := "invalid" + _, err = h.FromJson(invalidHex) + assert.Error(t, err) + + // Test with wrong length hex string + wrongLengthHex := "1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF" + _, err = h.FromJson(wrongLengthHex) + assert.Error(t, err) + + // Test with invalid type + _, err = h.FromJson(123) + assert.Error(t, err) +} + +func TestHash192_ToJson(t *testing.T) { + h := NewHash192() + + // Create a binary parser with test data + testData := []byte{0x12, 0x34, 0x56, 0x78, 0x90, 0xAB, 0xCD, 0xEF, + 0x12, 0x34, 0x56, 0x78, 0x90, 0xAB, 0xCD, 0xEF, + 0x12, 0x34, 0x56, 0x78, 0x90, 0xAB, 0xCD, 0xEF} + + parser := serdes.NewBinaryParser(testData) + + result, err := h.ToJson(parser) + assert.NoError(t, err) + assert.Equal(t, "1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF", result) +} + +func TestHash192_GetLength(t *testing.T) { + h := NewHash192() + assert.Equal(t, 24, h.getLength()) +} + +func TestNewHash192(t *testing.T) { + h := NewHash192() + assert.NotNil(t, h) + assert.IsType(t, &Hash192{}, h) +} diff --git a/binary-codec/types/serialized_type.go b/binary-codec/types/serialized_type.go index bdc6393b4..4ac09d474 100644 --- a/binary-codec/types/serialized_type.go +++ b/binary-codec/types/serialized_type.go @@ -33,6 +33,8 @@ func GetSerializedType(t string) SerializedType { return NewHash160() case "Hash256": return NewHash256() + case "Hash192": + return NewHash192() case "AccountID": return &AccountID{} case "Amount": diff --git a/client/faucet.go b/client/faucet.go index 5ba123847..1905be296 100644 --- a/client/faucet.go +++ b/client/faucet.go @@ -20,74 +20,76 @@ type faucetImpl struct { client Client } -func (f *faucetImpl) FundAccount(req *faucet.FundAccountRequest) (*faucet.FundAccountResponse, XRPLResponse, error) { - if req.UserAgent == "" { - req.UserAgent = "xrpl.go" - } - url := f.client.Faucet() - httpClient := http.Client{Timeout: time.Duration(1) * time.Second} - body, _ := json.Marshal(req) +func (f *faucetImpl) doRequest(ctx context.Context, client *http.Client, url string, body []byte) (*http.Response, error) { httpReq, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body)) - httpReq.Header.Add("Content-Type", "application/json") - if err != nil { - return nil, nil, fmt.Errorf("building request: %w", err) + return nil, fmt.Errorf("building request: %w", err) } + httpReq.Header.Add("Content-Type", "application/json") - // add timeout context to prevent hanging - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) - defer cancel() httpReq = httpReq.WithContext(ctx) - - var response *http.Response - - response, err = httpClient.Do(httpReq) + response, err := client.Do(httpReq) if err != nil || response == nil { - return nil, nil, fmt.Errorf("sending request: %w", err) + return nil, fmt.Errorf("sending request: %w", err) } - // allow client to reuse persistant connection - defer response.Body.Close() + return response, nil +} - // Check for service unavailable response and retry if so - if response.StatusCode == 503 { +func (f *faucetImpl) doWithRetry(ctx context.Context, client *http.Client, url string, body []byte, maxRetries int) (*http.Response, error) { + backoff := time.Second + for i := 0; i < maxRetries; i++ { + resp, err := f.doRequest(ctx, client, url, body) + if err != nil { + return nil, fmt.Errorf("request: %w", err) + } - maxRetries := 3 - backoffDuration := 1 * time.Second + if resp.StatusCode != http.StatusServiceUnavailable { + return resp, nil + } - for i := 0; i < maxRetries; i++ { - time.Sleep(backoffDuration) + // Close body to avoid leaking resources + resp.Body.Close() - // Make request again after waiting - response, err = httpClient.Do(httpReq) - if err != nil { - return nil, nil, fmt.Errorf("retrying request: %w", err) - } + if i < maxRetries-1 { + time.Sleep(backoff) + backoff *= 2 + } + } + return nil, fmt.Errorf("server overloaded, rate limit exceeded after %d retries", maxRetries) +} - if response.StatusCode != 503 { - break - } +func (f *faucetImpl) FundAccount(req *faucet.FundAccountRequest) (*faucet.FundAccountResponse, XRPLResponse, error) { + if req.UserAgent == "" { + req.UserAgent = "xrpl.go" + } + url := f.client.Faucet() - // Increase backoff duration for the next retry - backoffDuration *= 2 - } + httpClient := &http.Client{} + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() - if response.StatusCode == 503 { - // Return service unavailable error here after retry 3 times - return nil, nil, fmt.Errorf("Server is overloaded, rate limit exceeded") - } + body, err := json.Marshal(req) + if err != nil { + return nil, nil, fmt.Errorf("marshal request: %w", err) + } + response, err := f.doWithRetry(ctx, httpClient, url, body, 3) + if err != nil { + return nil, nil, err } + defer response.Body.Close() b, err := io.ReadAll(response.Body) - if err != nil || b == nil { + if err != nil { return nil, nil, fmt.Errorf("reading response: %w", err) } + var ret faucet.FundAccountResponse err = json.Unmarshal(b, &ret) if err != nil { return nil, nil, fmt.Errorf("fund unmarshal: %w", err) } - return &ret, nil, nil + return &ret, nil, nil } diff --git a/client/faucet_test.go b/client/faucet_test.go index 9267447f9..ad5475451 100644 --- a/client/faucet_test.go +++ b/client/faucet_test.go @@ -2,8 +2,10 @@ package client_test import ( "fmt" + "net/http" "os" "testing" + "time" addresscodec "github.com/CreatureDev/xrpl-go/address-codec" "github.com/CreatureDev/xrpl-go/client" @@ -24,7 +26,9 @@ func TestFundAccount(t *testing.T) { } func init() { - conf, err := client.NewJsonRpcConfig("https://s.altnet.rippletest.net:51234/") + conf, err := client.NewJsonRpcConfig("https://s.altnet.rippletest.net:51234/", client.WithHttpClient(&http.Client{ + Timeout: 5 * time.Second, + })) if err != nil { fmt.Println(err.Error()) os.Exit(-1) diff --git a/model/client/ledger/ledger_data_test.go b/model/client/ledger/ledger_data_test.go index 5c99d2f96..42e72b6f9 100644 --- a/model/client/ledger/ledger_data_test.go +++ b/model/client/ledger/ledger_data_test.go @@ -41,6 +41,7 @@ func TestLedgerDataResponse(t *testing.T) { PreviousTxnLgrSeq: 6487716, Sequence: 1, OwnerCount: types.SetUInt(0), + Index: "00001A2969BE1FC85F1D7A55282FA2E6D95C71D2E4B9C0FDD3D9994F3C00FF8F", }, Index: "00001A2969BE1FC85F1D7A55282FA2E6D95C71D2E4B9C0FDD3D9994F3C00FF8F", }, @@ -62,6 +63,7 @@ func TestLedgerDataResponse(t *testing.T) { Issuer: "rnuF96W4SZoCJmbHYBFoJZpR8eCaxNvekK", Value: "1", }, + Index: "000037C6659BB98F8D09F2F4CFEB27DE8EFEAFE54DD9E1C13AECDF5794B0C0F5", }, Index: "000037C6659BB98F8D09F2F4CFEB27DE8EFEAFE54DD9E1C13AECDF5794B0C0F5", }, diff --git a/model/ledger/ledger_object.go b/model/ledger/ledger_object.go index 837de5257..bc42678a5 100644 --- a/model/ledger/ledger_object.go +++ b/model/ledger/ledger_object.go @@ -16,6 +16,8 @@ const ( EscrowEntry LedgerEntryType = "Escrow" FeeSettingsEntry LedgerEntryType = "FeeSettings" LedgerHashesEntry LedgerEntryType = "LedgerHashes" + MPTokenEntry LedgerEntryType = "MPToken" + MPTokenIssuanceEntry LedgerEntryType = "MPTokenIssuance" NegativeUNLEntry LedgerEntryType = "NegativeUNL" NFTokenOfferEntry LedgerEntryType = "NFTokenOffer" NFTokenPageEntry LedgerEntryType = "NFTokenPage" @@ -48,6 +50,10 @@ func GetLedgerEntryTypeOfString(t string) LedgerEntryType { return FeeSettingsEntry case LedgerHashesEntry: return LedgerHashesEntry + case MPTokenEntry: + return MPTokenEntry + case MPTokenIssuanceEntry: + return MPTokenIssuanceEntry case NegativeUNLEntry: return NegativeUNLEntry case NFTokenOfferEntry: @@ -87,6 +93,10 @@ func EmptyLedgerObject(t string) (LedgerObject, error) { return &FeeSettings{}, nil case LedgerHashesEntry: return &LedgerHashes{}, nil + case MPTokenEntry: + return &MPToken{}, nil + case MPTokenIssuanceEntry: + return &MPTokenIssuance{}, nil case NegativeUNLEntry: return &NegativeUNL{}, nil case NFTokenOfferEntry: @@ -136,6 +146,10 @@ func UnmarshalLedgerObject(data []byte) (LedgerObject, error) { o = &FeeSettings{} case LedgerHashesEntry: o = &LedgerHashes{} + case MPTokenEntry: + o = &MPToken{} + case MPTokenIssuanceEntry: + o = &MPTokenIssuance{} case NegativeUNLEntry: o = &NegativeUNL{} case NFTokenOfferEntry: diff --git a/model/ledger/mpttoken.go b/model/ledger/mpttoken.go new file mode 100644 index 000000000..a23f676a1 --- /dev/null +++ b/model/ledger/mpttoken.go @@ -0,0 +1,19 @@ +package ledger + +import "github.com/CreatureDev/xrpl-go/model/transactions/types" + +type MPToken struct { + MPTAmount types.MPTCurrencyAmount `json:",omitempty"` + Flags *types.Flag `json:",omitempty"` + MPTokenIssuanceID string `json:"mpt_issuance_id,omitempty"` + LedgerEntryType LedgerEntryType `json:",omitempty"` + LockedAmount string `json:",omitempty"` + OwnerNode string `json:",omitempty"` + PreviousTxnID types.Hash256 `json:",omitempty"` + PreviousTxnLgrSeq uint `json:",omitempty"` + Index types.Hash256 `json:"index,omitempty"` +} + +func (*MPToken) EntryType() LedgerEntryType { + return MPTokenEntry +} diff --git a/model/ledger/mpttoken_issuance.go b/model/ledger/mpttoken_issuance.go new file mode 100644 index 000000000..af172f592 --- /dev/null +++ b/model/ledger/mpttoken_issuance.go @@ -0,0 +1,98 @@ +package ledger + +import ( + "encoding/hex" + "encoding/json" + "fmt" + + "github.com/CreatureDev/xrpl-go/model/transactions/types" +) + +const ( + MPTokenMetadataMaxSize = 1024 +) + +type MPTokenIssuance struct { + LedgerEntryType LedgerEntryType `json:",omitempty"` + Flags *types.Flag `json:",omitempty"` + Issuer types.Address `json:",omitempty"` + AssetScale uint8 `json:",omitempty"` + MaximumAmount string `json:",omitempty"` + OutstandingAmount string `json:",omitempty"` + TransferFee uint16 `json:",omitempty"` + MPTokenMetadata string `json:",omitempty"` + OwnerNode string `json:",omitempty"` + LockedAmount string `json:",omitempty"` + PreviousTxnID types.Hash256 `json:",omitempty"` + PreviousTxnLgrSeq uint32 `json:",omitempty"` + Index types.Hash256 `json:"index,omitempty"` +} + +func (*MPTokenIssuance) EntryType() LedgerEntryType { + return MPTokenIssuanceEntry +} + +type MPTokenMetadataUrl struct { + Url string `json:"url,omitempty"` + Type string `json:"type,omitempty"` + Title string `json:"title,omitempty"` +} + +type MPTokenMetadata struct { + Ticker string `json:"ticker,omitempty"` + Name string `json:"name,omitempty"` + Desc string `json:"desc,omitempty"` + Icon string `json:"icon,omitempty"` + AssetClass string `json:"asset_class,omitempty"` + AssetSubclass string `json:"asset_subclass,omitempty"` + IssuerName string `json:"issuer_name,omitempty"` + Urls []MPTokenMetadataUrl `json:"urls,omitempty"` + AdditionalInfo json.RawMessage `json:"additional_info,omitempty"` +} + +func NewMPTokenMetadataFromBlob(blob string) (*MPTokenMetadata, error) { + b, err := hex.DecodeString(blob) + if err != nil { + return nil, fmt.Errorf("decode from blob in hex: %w", err) + } + m := MPTokenMetadata{} + + err = json.Unmarshal(b, &m) + if err != nil { + // https://github.com/XRPLF/XRPL-Standards/tree/master/XLS-0089d-multi-purpose-token-metadata-schema + return nil, fmt.Errorf("metadata is not in XLS-0089d schema: %w", err) + } + return &m, nil +} + +func (m MPTokenMetadata) GetBlob() (string, error) { + json, err := json.Marshal(m) + if err != nil { + return "", fmt.Errorf("marshal to json for blob: %w", err) + } + + if len(json) > MPTokenMetadataMaxSize { + return "", fmt.Errorf("blob is too large: %d", len(json)) + } + + h := hex.EncodeToString(json) + return h, nil +} + +func (m MPTokenMetadata) Validate() error { + switch m.AssetClass { + case "rwa", "memes", "wrapped", "gaming", "defi", "other": + // ok + default: + return fmt.Errorf("invalid asset class: %s", m.AssetClass) + } + + switch m.AssetSubclass { + case "stablecoin", "commodity", "real_estate", "private_credit", "equity", "treasury", "other": + // ok + default: + return fmt.Errorf("invalid asset subclass: %s", m.AssetSubclass) + } + + return nil +} diff --git a/model/ledger/mpttoken_issuance_test.go b/model/ledger/mpttoken_issuance_test.go new file mode 100644 index 000000000..c7ecc71b8 --- /dev/null +++ b/model/ledger/mpttoken_issuance_test.go @@ -0,0 +1,144 @@ +package ledger + +import ( + "encoding/json" + "testing" + + "github.com/CreatureDev/xrpl-go/model/transactions/types" + "github.com/CreatureDev/xrpl-go/test" +) + +func TestMPTokenIssuance(t *testing.T) { + var s LedgerObject = &MPTokenIssuance{ + LedgerEntryType: MPTokenIssuanceEntry, + Flags: types.SetFlag(types.TfMPTCanTrade | types.TfMPTCanTransfer | types.TfMPTCanEscrow), + Issuer: "rBqb89MRQJnMPq8wTwEbtz4kvxrEDfcYvt", + AssetScale: 6, + MaximumAmount: "1000000", + OutstandingAmount: "500000", + TransferFee: 1000, + MPTokenMetadata: "7B227469636B6572223A2254455354222C226E616D65223A225465737420546F6B656E222C2261737365745F636C617373223A2264656669222C2261737365745F737562636C617373223A22737461626C65636F696E227D", + OwnerNode: "0000000000000000", + LockedAmount: "10000", + PreviousTxnID: "F0AB71E777B2DA54B86231E19B82554EF1F8211F92ECA473121C655BFC5329BF", + PreviousTxnLgrSeq: 14524914, + Index: "F0AB71E777B2DA54B86231E19B82554EF1F8211F92ECA473121C655BFC5329BF", + } + + j := `{ + "LedgerEntryType": "MPTokenIssuance", + "Flags": 56, + "Issuer": "rBqb89MRQJnMPq8wTwEbtz4kvxrEDfcYvt", + "AssetScale": 6, + "MaximumAmount": "1000000", + "OutstandingAmount": "500000", + "TransferFee": 1000, + "MPTokenMetadata": "7B227469636B6572223A2254455354222C226E616D65223A225465737420546F6B656E222C2261737365745F636C617373223A2264656669222C2261737365745F737562636C617373223A22737461626C65636F696E227D", + "OwnerNode": "0000000000000000", + "LockedAmount": "10000", + "PreviousTxnID": "F0AB71E777B2DA54B86231E19B82554EF1F8211F92ECA473121C655BFC5329BF", + "PreviousTxnLgrSeq": 14524914, + "index": "F0AB71E777B2DA54B86231E19B82554EF1F8211F92ECA473121C655BFC5329BF" +}` + + if err := test.SerializeAndDeserialize(t, s, j); err != nil { + t.Error(err) + } +} + +func TestMPTokenIssuance_EntryType(t *testing.T) { + mpti := &MPTokenIssuance{} + if mpti.EntryType() != MPTokenIssuanceEntry { + t.Errorf("Expected EntryType to be %s, got %s", MPTokenIssuanceEntry, mpti.EntryType()) + } +} + +func TestMPTokenMetadata(t *testing.T) { + metadata := MPTokenMetadata{ + Ticker: "TEST", + Name: "Test Token", + Desc: "A test token for testing purposes", + Icon: "https://example.com/icon.png", + AssetClass: "defi", + AssetSubclass: "stablecoin", + IssuerName: "Test Issuer", + Urls: []MPTokenMetadataUrl{ + { + Url: "https://example.com", + Type: "website", + Title: "Official Website", + }, + }, + AdditionalInfo: json.RawMessage(`{"custom_field": "custom_value"}`), + } + + // Test GetBlob + blob, err := metadata.GetBlob() + if err != nil { + t.Errorf("GetBlob failed: %v", err) + } + + // Test NewMPTokenMetadataFromBlob + recoveredMetadata, err := NewMPTokenMetadataFromBlob(blob) + if err != nil { + t.Errorf("NewMPTokenMetadataFromBlob failed: %v", err) + } + + if recoveredMetadata.Ticker != metadata.Ticker { + t.Errorf("Expected Ticker %s, got %s", metadata.Ticker, recoveredMetadata.Ticker) + } + + if recoveredMetadata.Name != metadata.Name { + t.Errorf("Expected Name %s, got %s", metadata.Name, recoveredMetadata.Name) + } + + if recoveredMetadata.AssetClass != metadata.AssetClass { + t.Errorf("Expected AssetClass %s, got %s", metadata.AssetClass, recoveredMetadata.AssetClass) + } + + if recoveredMetadata.AssetSubclass != metadata.AssetSubclass { + t.Errorf("Expected AssetSubclass %s, got %s", metadata.AssetSubclass, recoveredMetadata.AssetSubclass) + } +} + +func TestMPTokenMetadata_Validate(t *testing.T) { + tests := []struct { + name string + assetClass string + assetSubclass string + shouldPass bool + }{ + {"valid defi stablecoin", "defi", "stablecoin", true}, + {"valid rwa real_estate", "rwa", "real_estate", true}, + {"valid gaming equity", "gaming", "equity", true}, + {"valid memes other", "memes", "other", true}, + {"valid wrapped treasury", "wrapped", "treasury", true}, + {"invalid asset class", "invalid", "stablecoin", false}, + {"invalid asset subclass", "defi", "invalid", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + metadata := MPTokenMetadata{ + AssetClass: tt.assetClass, + AssetSubclass: tt.assetSubclass, + } + + err := metadata.Validate() + if tt.shouldPass && err == nil { + // Test should pass + return + } + if !tt.shouldPass && err != nil { + // Test should fail + return + } + + if tt.shouldPass { + t.Errorf("Expected validation to pass, but got error: %v", err) + } else { + t.Errorf("Expected validation to fail, but got no error") + } + }) + } +} diff --git a/model/ledger/mpttoken_test.go b/model/ledger/mpttoken_test.go new file mode 100644 index 000000000..a490fe216 --- /dev/null +++ b/model/ledger/mpttoken_test.go @@ -0,0 +1,51 @@ +package ledger + +import ( + "testing" + + "github.com/CreatureDev/xrpl-go/model/transactions/types" + "github.com/CreatureDev/xrpl-go/test" +) + +func TestMPToken(t *testing.T) { + var s LedgerObject = &MPToken{ + MPTAmount: types.MPTCurrencyAmount{ + MPTIssuanceID: "MPT1234567890ABCDEF", + Value: "1000", + }, + Flags: types.SetFlag(types.TfMPTCanTrade | types.TfMPTCanTransfer), + MPTokenIssuanceID: "MPT1234567890ABCDEF", + LedgerEntryType: MPTokenEntry, + LockedAmount: "100", + OwnerNode: "0000000000000000", + PreviousTxnID: "F0AB71E777B2DA54B86231E19B82554EF1F8211F92ECA473121C655BFC5329BF", + PreviousTxnLgrSeq: 14524914, + Index: "F0AB71E777B2DA54B86231E19B82554EF1F8211F92ECA473121C655BFC5329BF", + } + + j := `{ + "MPTAmount": { + "mp_issuance_id": "MPT1234567890ABCDEF", + "value": "1000" + }, + "Flags": 48, + "mpt_issuance_id": "MPT1234567890ABCDEF", + "LedgerEntryType": "MPToken", + "LockedAmount": "100", + "OwnerNode": "0000000000000000", + "PreviousTxnID": "F0AB71E777B2DA54B86231E19B82554EF1F8211F92ECA473121C655BFC5329BF", + "PreviousTxnLgrSeq": 14524914, + "index": "F0AB71E777B2DA54B86231E19B82554EF1F8211F92ECA473121C655BFC5329BF" +}` + + if err := test.SerializeAndDeserialize(t, s, j); err != nil { + t.Error(err) + } +} + +func TestMPToken_EntryType(t *testing.T) { + mpt := &MPToken{} + if mpt.EntryType() != MPTokenEntry { + t.Errorf("Expected EntryType to be %s, got %s", MPTokenEntry, mpt.EntryType()) + } +} diff --git a/model/transactions/mptoken_authorize.go b/model/transactions/mptoken_authorize.go new file mode 100644 index 000000000..f3d0913d0 --- /dev/null +++ b/model/transactions/mptoken_authorize.go @@ -0,0 +1,16 @@ +package transactions + +import ( + "github.com/CreatureDev/xrpl-go/model/transactions/types" +) + +type MPTokenAuthorize struct { + BaseTx + MPTokenIssuanceID types.Hash192 `json:",omitempty"` + Holder types.Address `json:",omitempty"` + Flags *types.Flag `json:",omitempty"` +} + +func (*MPTokenAuthorize) TxType() TxType { + return MPTokenAuthorizeTx +} diff --git a/model/transactions/mptoken_authorize_test.go b/model/transactions/mptoken_authorize_test.go new file mode 100644 index 000000000..4f605248e --- /dev/null +++ b/model/transactions/mptoken_authorize_test.go @@ -0,0 +1,47 @@ +package transactions + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/CreatureDev/xrpl-go/model/transactions/types" + "github.com/CreatureDev/xrpl-go/test" +) + +func TestMPTokenAuthorizeTx(t *testing.T) { + s := MPTokenAuthorize{ + BaseTx: BaseTx{ + Account: "rBqb89MRQJnMPq8wTwEbtz4kvxrEDfcYvt", + TransactionType: MPTokenAuthorizeTx, + Fee: types.XRPCurrencyAmount(12), + Sequence: 8, + LastLedgerSequence: 7108682, + }, + MPTokenIssuanceID: "MPT1234567890ABCDEF", + Holder: "rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH", + Flags: types.SetFlag(types.TfMPTUnauthorize), + } + + j := `{ + "Account": "rBqb89MRQJnMPq8wTwEbtz4kvxrEDfcYvt", + "TransactionType": "MPTokenAuthorize", + "Fee": "12", + "Sequence": 8, + "LastLedgerSequence": 7108682, + "MPTokenIssuanceID": "MPT1234567890ABCDEF", + "Holder": "rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH", + "Flags": 1 +}` + if err := test.SerializeAndDeserialize(t, s, j); err != nil { + t.Error(err) + } + + tx, err := UnmarshalTx(json.RawMessage(j)) + if err != nil { + t.Errorf("UnmarshalTx error: %s", err.Error()) + } + if !reflect.DeepEqual(tx, &s) { + t.Error("UnmarshalTx result differs from expected") + } +} diff --git a/model/transactions/mptoken_issuance_create.go b/model/transactions/mptoken_issuance_create.go new file mode 100644 index 000000000..6679469b7 --- /dev/null +++ b/model/transactions/mptoken_issuance_create.go @@ -0,0 +1,18 @@ +package transactions + +import ( + "github.com/CreatureDev/xrpl-go/model/transactions/types" +) + +type MPTokenIssuanceCreate struct { + BaseTx + AssetScale uint8 `json:",omitempty"` + MaximumAmount string `json:",omitempty"` + TransferFee uint16 `json:",omitempty"` + MPTokenMetadata string `json:",omitempty"` + Flags *types.Flag `json:",omitempty"` +} + +func (*MPTokenIssuanceCreate) TxType() TxType { + return MPTokenIssuanceCreateTx +} diff --git a/model/transactions/mptoken_issuance_create_test.go b/model/transactions/mptoken_issuance_create_test.go new file mode 100644 index 000000000..4116a5feb --- /dev/null +++ b/model/transactions/mptoken_issuance_create_test.go @@ -0,0 +1,51 @@ +package transactions + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/CreatureDev/xrpl-go/model/transactions/types" + "github.com/CreatureDev/xrpl-go/test" +) + +func TestMPTokenIssuanceCreateTx(t *testing.T) { + s := MPTokenIssuanceCreate{ + BaseTx: BaseTx{ + Account: "rBqb89MRQJnMPq8wTwEbtz4kvxrEDfcYvt", + TransactionType: MPTokenIssuanceCreateTx, + Fee: types.XRPCurrencyAmount(12), + Sequence: 8, + LastLedgerSequence: 7108682, + }, + AssetScale: 6, + MaximumAmount: "1000000", + TransferFee: 1000, + MPTokenMetadata: "7B227469636B6572223A2254455354222C226E616D65223A225465737420546F6B656E222C2261737365745F636C617373223A2264656669222C2261737365745F737562636C617373223A22737461626C65636F696E227D", + Flags: types.SetFlag(types.TfMPTCanTrade | types.TfMPTCanTransfer | types.TfMPTCanEscrow), + } + + j := `{ + "Account": "rBqb89MRQJnMPq8wTwEbtz4kvxrEDfcYvt", + "TransactionType": "MPTokenIssuanceCreate", + "Fee": "12", + "Sequence": 8, + "LastLedgerSequence": 7108682, + "AssetScale": 6, + "MaximumAmount": "1000000", + "TransferFee": 1000, + "MPTokenMetadata": "7B227469636B6572223A2254455354222C226E616D65223A225465737420546F6B656E222C2261737365745F636C617373223A2264656669222C2261737365745F737562636C617373223A22737461626C65636F696E227D", + "Flags": 56 +}` + if err := test.SerializeAndDeserialize(t, s, j); err != nil { + t.Error(err) + } + + tx, err := UnmarshalTx(json.RawMessage(j)) + if err != nil { + t.Errorf("UnmarshalTx error: %s", err.Error()) + } + if !reflect.DeepEqual(tx, &s) { + t.Error("UnmarshalTx result differs from expected") + } +} diff --git a/model/transactions/mptoken_issuance_destroy.go b/model/transactions/mptoken_issuance_destroy.go new file mode 100644 index 000000000..3e2fd1bda --- /dev/null +++ b/model/transactions/mptoken_issuance_destroy.go @@ -0,0 +1,12 @@ +package transactions + +import "github.com/CreatureDev/xrpl-go/model/transactions/types" + +type MPTokenIssuanceDestroy struct { + BaseTx + MPTokenIssuanceID types.Hash192 `json:",omitempty"` +} + +func (*MPTokenIssuanceDestroy) TxType() TxType { + return MPTokenIssuanceDestroyTx +} diff --git a/model/transactions/mptoken_issuance_destroy_test.go b/model/transactions/mptoken_issuance_destroy_test.go new file mode 100644 index 000000000..62c362d91 --- /dev/null +++ b/model/transactions/mptoken_issuance_destroy_test.go @@ -0,0 +1,43 @@ +package transactions + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/CreatureDev/xrpl-go/model/transactions/types" + "github.com/CreatureDev/xrpl-go/test" +) + +func TestMPTokenIssuanceDestroyTx(t *testing.T) { + s := MPTokenIssuanceDestroy{ + BaseTx: BaseTx{ + Account: "rBqb89MRQJnMPq8wTwEbtz4kvxrEDfcYvt", + TransactionType: MPTokenIssuanceDestroyTx, + Fee: types.XRPCurrencyAmount(12), + Sequence: 8, + LastLedgerSequence: 7108682, + }, + MPTokenIssuanceID: "MPT1234567890ABCDEF", + } + + j := `{ + "Account": "rBqb89MRQJnMPq8wTwEbtz4kvxrEDfcYvt", + "TransactionType": "MPTokenIssuanceDestroy", + "Fee": "12", + "Sequence": 8, + "LastLedgerSequence": 7108682, + "MPTokenIssuanceID": "MPT1234567890ABCDEF" +}` + if err := test.SerializeAndDeserialize(t, s, j); err != nil { + t.Error(err) + } + + tx, err := UnmarshalTx(json.RawMessage(j)) + if err != nil { + t.Errorf("UnmarshalTx error: %s", err.Error()) + } + if !reflect.DeepEqual(tx, &s) { + t.Error("UnmarshalTx result differs from expected") + } +} diff --git a/model/transactions/mptoken_issuance_set.go b/model/transactions/mptoken_issuance_set.go new file mode 100644 index 000000000..0be960e69 --- /dev/null +++ b/model/transactions/mptoken_issuance_set.go @@ -0,0 +1,16 @@ +package transactions + +import ( + "github.com/CreatureDev/xrpl-go/model/transactions/types" +) + +type MPTokenIssuanceSet struct { + BaseTx + MPTokenIssuanceID types.Hash192 `json:",omitempty"` + Holder types.Address `json:",omitempty"` + Flags *types.Flag `json:",omitempty"` +} + +func (*MPTokenIssuanceSet) TxType() TxType { + return MPTokenIssuanceSetTx +} diff --git a/model/transactions/mptoken_issuance_set_test.go b/model/transactions/mptoken_issuance_set_test.go new file mode 100644 index 000000000..87a058450 --- /dev/null +++ b/model/transactions/mptoken_issuance_set_test.go @@ -0,0 +1,47 @@ +package transactions + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/CreatureDev/xrpl-go/model/transactions/types" + "github.com/CreatureDev/xrpl-go/test" +) + +func TestMPTokenIssuanceSetTx(t *testing.T) { + s := MPTokenIssuanceSet{ + BaseTx: BaseTx{ + Account: "rBqb89MRQJnMPq8wTwEbtz4kvxrEDfcYvt", + TransactionType: MPTokenIssuanceSetTx, + Fee: types.XRPCurrencyAmount(12), + Sequence: 8, + LastLedgerSequence: 7108682, + }, + MPTokenIssuanceID: "MPT1234567890ABCDEF", + Holder: "rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH", + Flags: types.SetFlag(types.TfMPTLock), + } + + j := `{ + "Account": "rBqb89MRQJnMPq8wTwEbtz4kvxrEDfcYvt", + "TransactionType": "MPTokenIssuanceSet", + "Fee": "12", + "Sequence": 8, + "LastLedgerSequence": 7108682, + "MPTokenIssuanceID": "MPT1234567890ABCDEF", + "Holder": "rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH", + "Flags": 1 +}` + if err := test.SerializeAndDeserialize(t, s, j); err != nil { + t.Error(err) + } + + tx, err := UnmarshalTx(json.RawMessage(j)) + if err != nil { + t.Errorf("UnmarshalTx error: %s", err.Error()) + } + if !reflect.DeepEqual(tx, &s) { + t.Error("UnmarshalTx result differs from expected") + } +} diff --git a/model/transactions/tx.go b/model/transactions/tx.go index 31f9f0819..74e70c34f 100644 --- a/model/transactions/tx.go +++ b/model/transactions/tx.go @@ -166,6 +166,18 @@ func BaseTxForTransaction(tx Tx) *BaseTx { case TicketCreateTx: t := tx.(*TicketCreate) return &t.BaseTx + case MPTokenIssuanceCreateTx: + m := tx.(*MPTokenIssuanceCreate) + return &m.BaseTx + case MPTokenIssuanceDestroyTx: + m := tx.(*MPTokenIssuanceDestroy) + return &m.BaseTx + case MPTokenIssuanceSetTx: + m := tx.(*MPTokenIssuanceSet) + return &m.BaseTx + case MPTokenAuthorizeTx: + m := tx.(*MPTokenAuthorize) + return &m.BaseTx default: return nil } @@ -249,6 +261,14 @@ func UnmarshalTx(data json.RawMessage) (Tx, error) { tx = &TrustSet{} case TicketCreateTx: tx = &TicketCreate{} + case MPTokenIssuanceCreateTx: + tx = &MPTokenIssuanceCreate{} + case MPTokenIssuanceDestroyTx: + tx = &MPTokenIssuanceDestroy{} + case MPTokenIssuanceSetTx: + tx = &MPTokenIssuanceSet{} + case MPTokenAuthorizeTx: + tx = &MPTokenAuthorize{} default: return nil, fmt.Errorf("unsupported transaction type %s", txType.TransactionType) } diff --git a/model/transactions/tx_type.go b/model/transactions/tx_type.go index c4e32f186..dbe3e0693 100644 --- a/model/transactions/tx_type.go +++ b/model/transactions/tx_type.go @@ -3,37 +3,41 @@ package transactions type TxType string const ( - AccountSetTx TxType = "AccountSet" - AccountDeleteTx TxType = "AccountDelete" - AMMBidTx TxType = "AMMBid" - AMMCreateTx TxType = "AMMCreate" - AMMDepositTx TxType = "AMMDeposit" - AMMVoteTx TxType = "AMMVote" - AMMWithdrawTx TxType = "AMMWithdraw" - CheckCancelTx TxType = "CheckCancel" - CheckCashTx TxType = "CheckCash" - CheckCreateTx TxType = "CheckCreate" - DepositPreauthTx TxType = "DepositPreauth" - EscrowCancelTx TxType = "EscrowCancel" - EscrowCreateTx TxType = "EscrowCreate" - EscrowFinishTx TxType = "EscrowFinish" - NFTokenAcceptOfferTx TxType = "NFTokenAcceptOffer" - NFTokenBurnTx TxType = "NFTokenBurn" - NFTokenCancelOfferTx TxType = "NFTokenCancelOffer" - NFTokenCreateOfferTx TxType = "NFTokenCreateOffer" - NFTokenMintTx TxType = "NFTokenMint" - OfferCreateTx TxType = "OfferCreate" - OfferCancelTx TxType = "OfferCancel" - PaymentTx TxType = "Payment" - PaymentChannelClaimTx TxType = "PaymentChannelClaim" - PaymentChannelCreateTx TxType = "PaymentChannelCreate" - PaymentChannelFundTx TxType = "PaymentChannelFund" - SetRegularKeyTx TxType = "SetRegularKey" - SignerListSetTx TxType = "SignerListSet" - TrustSetTx TxType = "TrustSet" - TicketCreateTx TxType = "TicketCreate" - HashedTx TxType = "HASH" // TX stored as a string, rather than complete tx obj - BinaryTx TxType = "BINARY" // TX stored as a string, json tagged as 'tx_blob' + AccountSetTx TxType = "AccountSet" + AccountDeleteTx TxType = "AccountDelete" + AMMBidTx TxType = "AMMBid" + AMMCreateTx TxType = "AMMCreate" + AMMDepositTx TxType = "AMMDeposit" + AMMVoteTx TxType = "AMMVote" + AMMWithdrawTx TxType = "AMMWithdraw" + CheckCancelTx TxType = "CheckCancel" + CheckCashTx TxType = "CheckCash" + CheckCreateTx TxType = "CheckCreate" + DepositPreauthTx TxType = "DepositPreauth" + EscrowCancelTx TxType = "EscrowCancel" + EscrowCreateTx TxType = "EscrowCreate" + EscrowFinishTx TxType = "EscrowFinish" + MPTokenAuthorizeTx TxType = "MPTokenAuthorize" + MPTokenIssuanceCreateTx TxType = "MPTokenIssuanceCreate" + MPTokenIssuanceDestroyTx TxType = "MPTokenIssuanceDestroy" + MPTokenIssuanceSetTx TxType = "MPTokenIssuanceSet" + NFTokenAcceptOfferTx TxType = "NFTokenAcceptOffer" + NFTokenBurnTx TxType = "NFTokenBurn" + NFTokenCancelOfferTx TxType = "NFTokenCancelOffer" + NFTokenCreateOfferTx TxType = "NFTokenCreateOffer" + NFTokenMintTx TxType = "NFTokenMint" + OfferCreateTx TxType = "OfferCreate" + OfferCancelTx TxType = "OfferCancel" + PaymentTx TxType = "Payment" + PaymentChannelClaimTx TxType = "PaymentChannelClaim" + PaymentChannelCreateTx TxType = "PaymentChannelCreate" + PaymentChannelFundTx TxType = "PaymentChannelFund" + SetRegularKeyTx TxType = "SetRegularKey" + SignerListSetTx TxType = "SignerListSet" + TrustSetTx TxType = "TrustSet" + TicketCreateTx TxType = "TicketCreate" + HashedTx TxType = "HASH" // TX stored as a string, rather than complete tx obj + BinaryTx TxType = "BINARY" // TX stored as a string, json tagged as 'tx_blob' ) func GetTxTypeOfString(t string) TxType { @@ -97,6 +101,14 @@ func GetTxTypeOfString(t string) TxType { return TrustSetTx case TicketCreateTx: return TicketCreateTx + case MPTokenAuthorizeTx: + return MPTokenAuthorizeTx + case MPTokenIssuanceCreateTx: + return MPTokenIssuanceCreateTx + case MPTokenIssuanceDestroyTx: + return MPTokenIssuanceDestroyTx + case MPTokenIssuanceSetTx: + return MPTokenIssuanceSetTx case HashedTx: return HashedTx case BinaryTx: diff --git a/model/transactions/types/currency_amount.go b/model/transactions/types/currency_amount.go index d18384127..2c5d6ed0a 100644 --- a/model/transactions/types/currency_amount.go +++ b/model/transactions/types/currency_amount.go @@ -1,6 +1,8 @@ package types import ( + "encoding/binary" + "encoding/hex" "encoding/json" "fmt" "strconv" @@ -11,6 +13,7 @@ type CurrencyKind int const ( XRP CurrencyKind = iota ISSUED + MPT ) type CurrencyAmount interface { @@ -22,20 +25,33 @@ func UnmarshalCurrencyAmount(data []byte) (CurrencyAmount, error) { if len(data) == 0 { return nil, nil } - switch data[0] { - case '{': - var i IssuedCurrencyAmount - if err := json.Unmarshal(data, &i); err != nil { - return nil, err - } - return i, nil - default: - var x XRPCurrencyAmount - if err := json.Unmarshal(data, &x); err != nil { - return nil, err + + // Try to parse as JSON object first to determine the type + var temp map[string]interface{} + if err := json.Unmarshal(data, &temp); err == nil { + // It's a JSON object, determine which type based on fields + if _, hasMPTIssuanceID := temp["mp_issuance_id"]; hasMPTIssuanceID { + var mpt MPTCurrencyAmount + if err := json.Unmarshal(data, &mpt); err != nil { + return nil, err + } + return mpt, nil + } else { + // Object without mp_issuance_id, treat as IssuedCurrencyAmount + var issued IssuedCurrencyAmount + if err := json.Unmarshal(data, &issued); err != nil { + return nil, err + } + return issued, nil } - return x, nil } + + // If not a JSON object, try to parse as XRPCurrencyAmount + var x XRPCurrencyAmount + if err := json.Unmarshal(data, &x); err != nil { + return nil, err + } + return x, nil } type IssuedCurrencyAmount struct { @@ -110,3 +126,45 @@ func (a *XRPCurrencyAmount) UnmarshalText(data []byte) error { *a = XRPCurrencyAmount(v) return nil } + +type MPTCurrencyAmount struct { + MPTIssuanceID Hash192 `json:"mp_issuance_id,omitempty"` + Value string `json:"value,omitempty"` +} + +func (MPTCurrencyAmount) Kind() CurrencyKind { + return MPT +} + +func (a MPTCurrencyAmount) Validate() error { + if err := a.MPTIssuanceID.Validate(); err != nil { + return fmt.Errorf("mp_issuance_id: %w", err) + } + return nil +} + +func (a MPTCurrencyAmount) IssuerAccountID() ([]byte, error) { + if err := a.Validate(); err != nil { + return nil, err + } + + issuanceID, err := hex.DecodeString(string(a.MPTIssuanceID)) + if err != nil { + return nil, err + } + + return issuanceID[4:], nil +} + +func (a MPTCurrencyAmount) SequenceNumber() (uint32, error) { + if err := a.Validate(); err != nil { + return 0, err + } + + issuanceID, err := hex.DecodeString(string(a.MPTIssuanceID)) + if err != nil { + return 0, err + } + + return binary.BigEndian.Uint32(issuanceID[:4]), nil +} diff --git a/model/transactions/types/currency_amount_test.go b/model/transactions/types/currency_amount_test.go new file mode 100644 index 000000000..b9c1f1775 --- /dev/null +++ b/model/transactions/types/currency_amount_test.go @@ -0,0 +1,266 @@ +package types + +import ( + "encoding/hex" + "encoding/json" + "testing" +) + +func TestUnmarshalCurrencyAmount(t *testing.T) { + tests := []struct { + name string + input string + want CurrencyAmount + wantErr bool + }{ + { + name: "XRPCurrencyAmount as string", + input: `"1000000"`, + want: XRPCurrencyAmount(1000000), + wantErr: false, + }, + { + name: "XRPCurrencyAmount zero", + input: `"0"`, + want: XRPCurrencyAmount(0), + wantErr: false, + }, + { + name: "IssuedCurrencyAmount with all fields", + input: `{"issuer":"rajgkBmMxmz161r8bWYH7CQAFZP5bA9oSG","currency":"USD","value":"100.50"}`, + want: IssuedCurrencyAmount{Issuer: "rajgkBmMxmz161r8bWYH7CQAFZP5bA9oSG", Currency: "USD", Value: "100.50"}, + wantErr: false, + }, + { + name: "IssuedCurrencyAmount without issuer", + input: `{"currency":"EUR","value":"250.00"}`, + want: IssuedCurrencyAmount{Currency: "EUR", Value: "250.00"}, + wantErr: false, + }, + { + name: "IssuedCurrencyAmount with empty issuer", + input: `{"issuer":"","currency":"GBP","value":"75.25"}`, + want: IssuedCurrencyAmount{Issuer: "", Currency: "GBP", Value: "75.25"}, + wantErr: false, + }, + { + name: "MPTCurrencyAmount with all fields", + input: `{"mp_issuance_id":"000004C463C52827307480341125DA0577DEFC38405B0E3E","value":"500.00"}`, + want: MPTCurrencyAmount{MPTIssuanceID: "000004C463C52827307480341125DA0577DEFC38405B0E3E", Value: "500.00"}, + wantErr: false, + }, + { + name: "Empty input", + input: ``, + want: nil, + wantErr: false, + }, + { + name: "Invalid JSON object", + input: `{"invalid":}`, + want: nil, + wantErr: true, + }, + { + name: "Invalid XRP amount string", + input: `"not_a_number"`, + want: nil, + wantErr: true, + }, + { + name: "Invalid XRP amount number", + input: `-1000`, + want: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := UnmarshalCurrencyAmount([]byte(tt.input)) + + if (err != nil) != tt.wantErr { + t.Errorf("UnmarshalCurrencyAmount() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if tt.wantErr { + return + } + + if tt.input == "" && got == nil { + // Empty input should return nil + return + } + + if got == nil { + t.Errorf("UnmarshalCurrencyAmount() returned nil, expected %v", tt.want) + return + } + + // Check the kind + if got.Kind() != tt.want.Kind() { + t.Errorf("UnmarshalCurrencyAmount() kind = %v, want %v", got.Kind(), tt.want.Kind()) + } + + // Check specific type and values + switch want := tt.want.(type) { + case XRPCurrencyAmount: + if gotXRPCurrencyAmount, ok := got.(XRPCurrencyAmount); !ok { + t.Errorf("UnmarshalCurrencyAmount() returned wrong type, expected XRPCurrencyAmount") + } else if gotXRPCurrencyAmount != want { + t.Errorf("UnmarshalCurrencyAmount() = %v, want %v", gotXRPCurrencyAmount, want) + } + case IssuedCurrencyAmount: + if gotIssuedCurrencyAmount, ok := got.(IssuedCurrencyAmount); !ok { + t.Errorf("UnmarshalCurrencyAmount() returned wrong type, expected IssuedCurrencyAmount") + } else if gotIssuedCurrencyAmount.Issuer != want.Issuer || + gotIssuedCurrencyAmount.Currency != want.Currency || + gotIssuedCurrencyAmount.Value != want.Value { + t.Errorf("UnmarshalCurrencyAmount() = %v, want %v", gotIssuedCurrencyAmount, want) + } + case MPTCurrencyAmount: + if gotMPTCurrencyAmount, ok := got.(MPTCurrencyAmount); !ok { + t.Errorf("UnmarshalCurrencyAmount() returned wrong type, expected MPTCurrencyAmount") + } else if gotMPTCurrencyAmount.MPTIssuanceID != want.MPTIssuanceID || + gotMPTCurrencyAmount.Value != want.Value { + t.Errorf("UnmarshalCurrencyAmount() = %v, want %v", gotMPTCurrencyAmount, want) + } + } + }) + } +} + +func TestUnmarshalCurrencyAmount_JSONRoundTrip(t *testing.T) { + // Test that we can marshal and unmarshal each type correctly + testCases := []struct { + name string + data CurrencyAmount + }{ + { + name: "XRPCurrencyAmount", + data: XRPCurrencyAmount(1000000), + }, + { + name: "IssuedCurrencyAmount", + data: IssuedCurrencyAmount{ + Issuer: "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + Currency: "USD", + Value: "100.50", + }, + }, + { + name: "MPTCurrencyAmount", + data: MPTCurrencyAmount{ + MPTIssuanceID: "000004C463C52827307480341125DA0577DEFC38405B0E3E", + Value: "500.00", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Marshal to JSON + jsonData, err := json.Marshal(tc.data) + if err != nil { + t.Fatalf("Failed to marshal %s: %v", tc.name, err) + } + + // Unmarshal back + unmarshaled, err := UnmarshalCurrencyAmount(jsonData) + if err != nil { + t.Fatalf("Failed to unmarshal %s: %v", tc.name, err) + } + + // Check that we got the same type back + if unmarshaled.Kind() != tc.data.Kind() { + t.Errorf("Kind mismatch: got %v, want %v", unmarshaled.Kind(), tc.data.Kind()) + } + + // Check that the data is equivalent + if !jsonEqual(tc.data, unmarshaled) { + t.Errorf("Data mismatch: got %v, want %v", unmarshaled, tc.data) + } + }) + } +} + +func TestMPTCurrencyAmount_Methods(t *testing.T) { + // Test with the specific values provided by the user + mpt := MPTCurrencyAmount{ + MPTIssuanceID: "005342F3D0045E31BC8E02B4A85A5FE56B33AA25C3745405", + Value: "100.00", + } + + t.Run("Validate", func(t *testing.T) { + err := mpt.Validate() + if err != nil { + t.Errorf("Validate() failed: %v", err) + } + }) + + t.Run("Kind", func(t *testing.T) { + kind := mpt.Kind() + if kind != MPT { + t.Errorf("Kind() = %v, want %v", kind, MPT) + } + }) + + t.Run("SequenceNumber", func(t *testing.T) { + sequence, err := mpt.SequenceNumber() + if err != nil { + t.Errorf("SequenceNumber() failed: %v", err) + } + + expectedSequence := uint32(5456627) + if sequence != expectedSequence { + t.Errorf("SequenceNumber() = %v, want %v", sequence, expectedSequence) + } + }) + + t.Run("IssuerAccountID", func(t *testing.T) { + issuerID, err := mpt.IssuerAccountID() + if err != nil { + t.Errorf("IssuerAccountID() failed: %v", err) + } + + // The issuer account ID should be the last 20 bytes (40 hex chars) of the MPTIssuanceID + // Removing the first 4 bytes (8 hex chars) which represent the sequence number + expectedIssuerHex := "D0045E31BC8E02B4A85A5FE56B33AA25C3745405" + expectedIssuerBytes, _ := hex.DecodeString(expectedIssuerHex) + + if len(issuerID) != len(expectedIssuerBytes) { + t.Errorf("IssuerAccountID() length = %v, want %v", len(issuerID), len(expectedIssuerBytes)) + } + + for i, b := range issuerID { + if b != expectedIssuerBytes[i] { + t.Errorf("IssuerAccountID()[%d] = %02x, want %02x", i, b, expectedIssuerBytes[i]) + } + } + }) + + t.Run("JSON marshaling", func(t *testing.T) { + jsonData, err := json.Marshal(mpt) + if err != nil { + t.Errorf("JSON marshaling failed: %v", err) + } + + expectedJSON := `{"mp_issuance_id":"005342F3D0045E31BC8E02B4A85A5FE56B33AA25C3745405","value":"100.00"}` + if string(jsonData) != expectedJSON { + t.Errorf("JSON marshaling = %s, want %s", string(jsonData), expectedJSON) + } + }) +} + +// jsonEqual compares two CurrencyAmount values by marshaling them to JSON and comparing the strings +func jsonEqual(a, b CurrencyAmount) bool { + jsonA, errA := json.Marshal(a) + jsonB, errB := json.Marshal(b) + + if errA != nil || errB != nil { + return false + } + + return string(jsonA) == string(jsonB) +} diff --git a/model/transactions/types/flags.go b/model/transactions/types/flags.go index 9a0e42a2c..3b25c933d 100644 --- a/model/transactions/types/flags.go +++ b/model/transactions/types/flags.go @@ -40,6 +40,52 @@ const ( FasfNoFreeze = 6 FasfRequireAuth = 2 FasfRequireDest = 1 + + // MPToken Authorize Flags + + // TfMPTUnauthorize + // If set and transaction is submitted by a holder, it indicates that the holder no + // longer wants to hold the MPToken, which will be deleted as a result. If the the holder's + // MPToken has non-zero balance while trying to set this flag, the transaction will fail. On + // the other hand, if set and transaction is submitted by an issuer, it would mean that the + // issuer wants to unauthorize the holder (only applicable for allow-listing), + // which would unset the lsfMPTAuthorized flag on the MPToken. + TfMPTUnauthorize = 0x00000001 + + // MPToken IssuanceCreate Flags + + // TfMPTCanLock + // If set, indicates that the MPT can be locked both individually and globally. + // If not set, the MPT cannot be locked in any way. + TfMPTCanLock = 0x00000002 + // TfMPTRequireAuth + // If set, indicates that individual holders must be authorized. + // This enables issuers to limit who can hold their assets. + TfMPTRequireAuth = 0x00000004 + // TfMPTCanEscrow + // If set, indicates that individual holders can place their balances into an escrow. + TfMPTCanEscrow = 0x00000008 + // TfMPTCanTrade + // If set, indicates that individual holders can trade their balances + // using the XRP Ledger DEX or AMM. + TfMPTCanTrade = 0x00000010 + // TfMPTCanTransfer + // If set, indicates that tokens may be transferred to other accounts + // that are not the issuer. + TfMPTCanTransfer = 0x00000020 + // TfMPTCanClawback + // If set, indicates that the issuer may use the Clawback transaction + // to clawback value from individual holders. + TfMPTCanClawback = 0x00000040 + + // MPToken IssuanceSet Flags + + // TfMPTLock + // If set, indicates that issuer locks the MPT + TfMPTLock = 0x00000001 + // TfMPTUnlock + // If set, indicates that issuer unlocks the MPT + TfMPTUnlock = 0x00000002 ) // FlagsI is an interface for types that can be converted to a uint. diff --git a/model/transactions/types/hash192.go b/model/transactions/types/hash192.go new file mode 100644 index 000000000..0aba7fc6b --- /dev/null +++ b/model/transactions/types/hash192.go @@ -0,0 +1,15 @@ +package types + +import "fmt" + +type Hash192 string + +func (h Hash192) Validate() error { + if h == "" { + return fmt.Errorf("hash192 value not set") + } + if len(h) != 48 { + return fmt.Errorf("hash192 length was not expected 48 characters") + } + return nil +} diff --git a/test/util.go b/test/util.go index 17b525af2..a30732b2d 100644 --- a/test/util.go +++ b/test/util.go @@ -33,6 +33,7 @@ func SerializeAndDeserialize(t *testing.T, s interface{}, d string) error { if err != nil { return err } + require.Equal(t, s, decode.Elem().Interface(), "json decoding does not match expected struct") return nil }