From a71665cca44b76a758152c5c93d7db9e7059f451 Mon Sep 17 00:00:00 2001 From: AlexanderBuzz Date: Thu, 12 Jan 2023 16:24:36 +0100 Subject: [PATCH 01/12] - Initial commit, should be quite complete --- .../tx-serialization/js/README.md | 27 + .../tx-serialization/js/definitions.json | 1663 +++++++++++++++++ .../tx-serialization/js/helpers.js | 21 + .../tx-serialization/js/index.js | 670 +++++++ .../tx-serialization/js/package.json | 13 + .../tx-serialization/js/test-cases/README.md | 52 + .../js/test-cases/meld-example.png | Bin 0 -> 9584 bytes .../js/test-cases/tx1-binary.txt | 1 + .../tx-serialization/js/test-cases/tx1.json | 18 + .../js/test-cases/tx2-binary.txt | 1 + .../tx-serialization/js/test-cases/tx2.json | 18 + .../js/test-cases/tx3-binary.txt | 1 + .../tx-serialization/js/test-cases/tx3.json | 57 + 13 files changed, 2542 insertions(+) create mode 100644 content/_code-samples/tx-serialization/js/README.md create mode 100644 content/_code-samples/tx-serialization/js/definitions.json create mode 100644 content/_code-samples/tx-serialization/js/helpers.js create mode 100644 content/_code-samples/tx-serialization/js/index.js create mode 100644 content/_code-samples/tx-serialization/js/package.json create mode 100644 content/_code-samples/tx-serialization/js/test-cases/README.md create mode 100644 content/_code-samples/tx-serialization/js/test-cases/meld-example.png create mode 100644 content/_code-samples/tx-serialization/js/test-cases/tx1-binary.txt create mode 100644 content/_code-samples/tx-serialization/js/test-cases/tx1.json create mode 100644 content/_code-samples/tx-serialization/js/test-cases/tx2-binary.txt create mode 100644 content/_code-samples/tx-serialization/js/test-cases/tx2.json create mode 100644 content/_code-samples/tx-serialization/js/test-cases/tx3-binary.txt create mode 100644 content/_code-samples/tx-serialization/js/test-cases/tx3.json diff --git a/content/_code-samples/tx-serialization/js/README.md b/content/_code-samples/tx-serialization/js/README.md new file mode 100644 index 0000000000..cd2fbd25b3 --- /dev/null +++ b/content/_code-samples/tx-serialization/js/README.md @@ -0,0 +1,27 @@ +# JavaScript transaction serialisation examples + +Convert transactions and other XRPL data from JSON to their canonical binary format for signing or cryptographic verification. (This reference implementation is equivalent to the ones included in most client libraries.). + +For a detailed explanation, see [Serialization](https://xrpl.org/serialization.html). + +On first run, you have to install the necessary node.js dependencies: + + npm install + +## Command-line usage: + +### Simple example, use tx1.json default: + + node index.js + +### Verbose output, use --verbose or -v: + + node index.js -v + +### Pick JSON fixture file: + + node index.js -f test-cases/tx3.json + +### Feed JSON as CLI argument: + + node index.js -j "{\"TransactionType\":\"Payment\"}" \ No newline at end of file diff --git a/content/_code-samples/tx-serialization/js/definitions.json b/content/_code-samples/tx-serialization/js/definitions.json new file mode 100644 index 0000000000..cf07fbb033 --- /dev/null +++ b/content/_code-samples/tx-serialization/js/definitions.json @@ -0,0 +1,1663 @@ +{ + "TYPES": { + "Validation": 10003, + "Done": -1, + "Hash128": 4, + "Blob": 7, + "AccountID": 8, + "Amount": 6, + "Hash256": 5, + "UInt8": 16, + "Vector256": 19, + "STObject": 14, + "Unknown": -2, + "Transaction": 10001, + "Hash160": 17, + "PathSet": 18, + "LedgerEntry": 10002, + "UInt16": 1, + "NotPresent": 0, + "UInt64": 3, + "UInt32": 2, + "STArray": 15 + }, + "LEDGER_ENTRY_TYPES": { + "Any": -3, + "Child": -2, + "Invalid": -1, + "AccountRoot": 97, + "DirectoryNode": 100, + "RippleState": 114, + "Ticket": 84, + "SignerList": 83, + "Offer": 111, + "LedgerHashes": 104, + "Amendments": 102, + "FeeSettings": 115, + "Escrow": 117, + "PayChannel": 120, + "DepositPreauth": 112, + "Check": 67, + "Nickname": 110, + "Contract": 99, + "GeneratorMap": 103 + }, + "FIELDS": [ + [ + "Generic", + { + "nth": 0, + "isVLEncoded": false, + "isSerialized": false, + "isSigningField": false, + "type": "Unknown" + } + ], + [ + "Invalid", + { + "nth": -1, + "isVLEncoded": false, + "isSerialized": false, + "isSigningField": false, + "type": "Unknown" + } + ], + [ + "LedgerEntryType", + { + "nth": 1, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt16" + } + ], + [ + "TransactionType", + { + "nth": 2, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt16" + } + ], + [ + "SignerWeight", + { + "nth": 3, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt16" + } + ], + [ + "Flags", + { + "nth": 2, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "SourceTag", + { + "nth": 3, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "Sequence", + { + "nth": 4, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "PreviousTxnLgrSeq", + { + "nth": 5, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "LedgerSequence", + { + "nth": 6, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "CloseTime", + { + "nth": 7, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "ParentCloseTime", + { + "nth": 8, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "SigningTime", + { + "nth": 9, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "Expiration", + { + "nth": 10, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "TransferRate", + { + "nth": 11, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "WalletSize", + { + "nth": 12, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "OwnerCount", + { + "nth": 13, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "DestinationTag", + { + "nth": 14, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "HighQualityIn", + { + "nth": 16, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "HighQualityOut", + { + "nth": 17, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "LowQualityIn", + { + "nth": 18, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "LowQualityOut", + { + "nth": 19, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "QualityIn", + { + "nth": 20, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "QualityOut", + { + "nth": 21, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "StampEscrow", + { + "nth": 22, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "BondAmount", + { + "nth": 23, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "LoadFee", + { + "nth": 24, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "OfferSequence", + { + "nth": 25, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "FirstLedgerSequence", + { + "nth": 26, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "LastLedgerSequence", + { + "nth": 27, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "TransactionIndex", + { + "nth": 28, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "OperationLimit", + { + "nth": 29, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "ReferenceFeeUnits", + { + "nth": 30, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "ReserveBase", + { + "nth": 31, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "ReserveIncrement", + { + "nth": 32, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "SetFlag", + { + "nth": 33, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "ClearFlag", + { + "nth": 34, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "SignerQuorum", + { + "nth": 35, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "CancelAfter", + { + "nth": 36, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "FinishAfter", + { + "nth": 37, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "IndexNext", + { + "nth": 1, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt64" + } + ], + [ + "IndexPrevious", + { + "nth": 2, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt64" + } + ], + [ + "BookNode", + { + "nth": 3, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt64" + } + ], + [ + "OwnerNode", + { + "nth": 4, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt64" + } + ], + [ + "BaseFee", + { + "nth": 5, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt64" + } + ], + [ + "ExchangeRate", + { + "nth": 6, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt64" + } + ], + [ + "LowNode", + { + "nth": 7, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt64" + } + ], + [ + "HighNode", + { + "nth": 8, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt64" + } + ], + [ + "EmailHash", + { + "nth": 1, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Hash128" + } + ], + [ + "LedgerHash", + { + "nth": 1, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Hash256" + } + ], + [ + "ParentHash", + { + "nth": 2, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Hash256" + } + ], + [ + "TransactionHash", + { + "nth": 3, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Hash256" + } + ], + [ + "AccountHash", + { + "nth": 4, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Hash256" + } + ], + [ + "PreviousTxnID", + { + "nth": 5, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Hash256" + } + ], + [ + "LedgerIndex", + { + "nth": 6, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Hash256" + } + ], + [ + "WalletLocator", + { + "nth": 7, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Hash256" + } + ], + [ + "RootIndex", + { + "nth": 8, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Hash256" + } + ], + [ + "AccountTxnID", + { + "nth": 9, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Hash256" + } + ], + [ + "BookDirectory", + { + "nth": 16, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Hash256" + } + ], + [ + "InvoiceID", + { + "nth": 17, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Hash256" + } + ], + [ + "Nickname", + { + "nth": 18, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Hash256" + } + ], + [ + "Amendment", + { + "nth": 19, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Hash256" + } + ], + [ + "TicketID", + { + "nth": 20, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Hash256" + } + ], + [ + "Digest", + { + "nth": 21, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Hash256" + } + ], + [ + "hash", + { + "nth": 257, + "isVLEncoded": false, + "isSerialized": false, + "isSigningField": false, + "type": "Hash256" + } + ], + [ + "index", + { + "nth": 258, + "isVLEncoded": false, + "isSerialized": false, + "isSigningField": false, + "type": "Hash256" + } + ], + [ + "Amount", + { + "nth": 1, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "Balance", + { + "nth": 2, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "LimitAmount", + { + "nth": 3, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "TakerPays", + { + "nth": 4, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "TakerGets", + { + "nth": 5, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "LowLimit", + { + "nth": 6, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "HighLimit", + { + "nth": 7, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "Fee", + { + "nth": 8, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "SendMax", + { + "nth": 9, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "DeliverMin", + { + "nth": 10, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "MinimumOffer", + { + "nth": 16, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "RippleEscrow", + { + "nth": 17, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "DeliveredAmount", + { + "nth": 18, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "taker_gets_funded", + { + "nth": 258, + "isVLEncoded": false, + "isSerialized": false, + "isSigningField": false, + "type": "Amount" + } + ], + [ + "taker_pays_funded", + { + "nth": 259, + "isVLEncoded": false, + "isSerialized": false, + "isSigningField": false, + "type": "Amount" + } + ], + [ + "PublicKey", + { + "nth": 1, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "Blob" + } + ], + [ + "MessageKey", + { + "nth": 2, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "Blob" + } + ], + [ + "SigningPubKey", + { + "nth": 3, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "Blob" + } + ], + [ + "TxnSignature", + { + "nth": 4, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": false, + "type": "Blob" + } + ], + [ + "Generator", + { + "nth": 5, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "Blob" + } + ], + [ + "Signature", + { + "nth": 6, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": false, + "type": "Blob" + } + ], + [ + "Domain", + { + "nth": 7, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "Blob" + } + ], + [ + "FundCode", + { + "nth": 8, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "Blob" + } + ], + [ + "RemoveCode", + { + "nth": 9, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "Blob" + } + ], + [ + "ExpireCode", + { + "nth": 10, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "Blob" + } + ], + [ + "CreateCode", + { + "nth": 11, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "Blob" + } + ], + [ + "MemoType", + { + "nth": 12, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "Blob" + } + ], + [ + "MemoData", + { + "nth": 13, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "Blob" + } + ], + [ + "MemoFormat", + { + "nth": 14, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "Blob" + } + ], + [ + "Fulfillment", + { + "nth": 16, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "Blob" + } + ], + [ + "Condition", + { + "nth": 17, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "Blob" + } + ], + [ + "MasterSignature", + { + "nth": 18, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": false, + "type": "Blob" + } + ], + [ + "Account", + { + "nth": 1, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "AccountID" + } + ], + [ + "Owner", + { + "nth": 2, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "AccountID" + } + ], + [ + "Destination", + { + "nth": 3, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "AccountID" + } + ], + [ + "Issuer", + { + "nth": 4, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "AccountID" + } + ], + [ + "Authorize", + { + "nth": 5, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "AccountID" + } + ], + [ + "Unauthorize", + { + "nth": 6, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "AccountID" + } + ], + [ + "Target", + { + "nth": 7, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "AccountID" + } + ], + [ + "RegularKey", + { + "nth": 8, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "AccountID" + } + ], + [ + "ObjectEndMarker", + { + "nth": 1, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], + [ + "TransactionMetaData", + { + "nth": 2, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], + [ + "CreatedNode", + { + "nth": 3, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], + [ + "DeletedNode", + { + "nth": 4, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], + [ + "ModifiedNode", + { + "nth": 5, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], + [ + "PreviousFields", + { + "nth": 6, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], + [ + "FinalFields", + { + "nth": 7, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], + [ + "NewFields", + { + "nth": 8, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], + [ + "TemplateEntry", + { + "nth": 9, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], + [ + "Memo", + { + "nth": 10, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], + [ + "SignerEntry", + { + "nth": 11, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], + [ + "Signer", + { + "nth": 16, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], + [ + "Majority", + { + "nth": 18, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], + [ + "ArrayEndMarker", + { + "nth": 1, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STArray" + } + ], + [ + "Signers", + { + "nth": 3, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": false, + "type": "STArray" + } + ], + [ + "SignerEntries", + { + "nth": 4, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STArray" + } + ], + [ + "Template", + { + "nth": 5, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STArray" + } + ], + [ + "Necessary", + { + "nth": 6, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STArray" + } + ], + [ + "Sufficient", + { + "nth": 7, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STArray" + } + ], + [ + "AffectedNodes", + { + "nth": 8, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STArray" + } + ], + [ + "Memos", + { + "nth": 9, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STArray" + } + ], + [ + "Majorities", + { + "nth": 16, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STArray" + } + ], + [ + "CloseResolution", + { + "nth": 1, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt8" + } + ], + [ + "Method", + { + "nth": 2, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt8" + } + ], + [ + "TransactionResult", + { + "nth": 3, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt8" + } + ], + [ + "TakerPaysCurrency", + { + "nth": 1, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Hash160" + } + ], + [ + "TakerPaysIssuer", + { + "nth": 2, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Hash160" + } + ], + [ + "TakerGetsCurrency", + { + "nth": 3, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Hash160" + } + ], + [ + "TakerGetsIssuer", + { + "nth": 4, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Hash160" + } + ], + [ + "Paths", + { + "nth": 1, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "PathSet" + } + ], + [ + "Indexes", + { + "nth": 1, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "Vector256" + } + ], + [ + "Hashes", + { + "nth": 2, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "Vector256" + } + ], + [ + "Amendments", + { + "nth": 3, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "Vector256" + } + ], + [ + "Transaction", + { + "nth": 1, + "isVLEncoded": false, + "isSerialized": false, + "isSigningField": false, + "type": "Transaction" + } + ], + [ + "LedgerEntry", + { + "nth": 1, + "isVLEncoded": false, + "isSerialized": false, + "isSigningField": false, + "type": "LedgerEntry" + } + ], + [ + "Validation", + { + "nth": 1, + "isVLEncoded": false, + "isSerialized": false, + "isSigningField": false, + "type": "Validation" + } + ], + [ + "SignerListID", + { + "nth": 38, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "SettleDelay", + { + "nth": 39, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "Channel", + { + "nth": 22, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Hash256" + } + ], + [ + "ConsensusHash", + { + "nth": 23, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Hash256" + } + ], + [ + "CheckID", + { + "nth": 24, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Hash256" + } + ], + [ + "TickSize", + { + "nth": 16, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt8" + } + ], + [ + "DestinationNode", + { + "nth": 9, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt64" + } + ] + ], + "TRANSACTION_RESULTS": { + "telNO_DST_PARTIAL": -393, + "temBAD_SRC_ACCOUNT": -281, + "tefPAST_SEQ": -189, + "terNO_ACCOUNT": -96, + "temREDUNDANT": -275, + "tefCREATED": -194, + "temDST_IS_SRC": -279, + "terRETRY": -99, + "temINVALID_FLAG": -276, + "temBAD_SEND_XRP_LIMIT": -288, + "terNO_LINE": -94, + "tefBAD_AUTH": -196, + "temBAD_EXPIRATION": -295, + "temBAD_SEND_XRP_NO_DIRECT": -286, + "temBAD_SEND_XRP_PATHS": -284, + "tefBAD_LEDGER": -195, + "tefNO_AUTH_REQUIRED": -190, + "terOWNERS": -93, + "terLAST": -91, + "terNO_RIPPLE": -90, + "temBAD_FEE": -294, + "terPRE_SEQ": -92, + "tefMASTER_DISABLED": -187, + "temBAD_CURRENCY": -296, + "tefDST_TAG_NEEDED": -193, + "temBAD_SIGNATURE": -282, + "tefFAILURE": -199, + "telBAD_PATH_COUNT": -397, + "temBAD_TRANSFER_RATE": -280, + "tefWRONG_PRIOR": -188, + "telBAD_DOMAIN": -398, + "temBAD_AMOUNT": -298, + "temBAD_AUTH_MASTER": -297, + "temBAD_LIMIT": -292, + "temBAD_ISSUER": -293, + "telBAD_PUBLIC_KEY": -396, + "tefBAD_ADD_AUTH": -197, + "temBAD_OFFER": -291, + "temBAD_SEND_XRP_PARTIAL": -285, + "temDST_NEEDED": -278, + "tefALREADY": -198, + "temUNCERTAIN": -272, + "telLOCAL_ERROR": -399, + "temREDUNDANT_SEND_MAX": -274, + "tefINTERNAL": -191, + "temBAD_PATH_LOOP": -289, + "tefEXCEPTION": -192, + "temRIPPLE_EMPTY": -273, + "telINSUF_FEE_P": -394, + "temBAD_SEQUENCE": -283, + "tefMAX_LEDGER": -186, + "terFUNDS_SPENT": -98, + "temBAD_SEND_XRP_MAX": -287, + "telFAILED_PROCESSING": -395, + "terINSUF_FEE_B": -97, + "tesSUCCESS": 0, + "temBAD_PATH": -290, + "temMALFORMED": -299, + "temUNKNOWN": -271, + "temINVALID": -277, + "terNO_AUTH": -95, + "temBAD_TICK_SIZE": -270, + + "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 + }, + "TRANSACTION_TYPES": { + "Invalid": -1, + "Payment": 0, + "EscrowCreate": 1, + "EscrowFinish": 2, + "AccountSet": 3, + "EscrowCancel": 4, + "SetRegularKey": 5, + "NickNameSet": 6, + "OfferCreate": 7, + "OfferCancel": 8, + "Contract": 9, + "TicketCreate": 10, + "TicketCancel": 11, + "SignerListSet": 12, + "PaymentChannelCreate": 13, + "PaymentChannelFund": 14, + "PaymentChannelClaim": 15, + "CheckCreate": 16, + "CheckCash": 17, + "CheckCancel": 18, + "DepositPreauth": 19, + "TrustSet": 20, + "EnableAmendment": 100, + "SetFee": 101 + } +} diff --git a/content/_code-samples/tx-serialization/js/helpers.js b/content/_code-samples/tx-serialization/js/helpers.js new file mode 100644 index 0000000000..18b56d9947 --- /dev/null +++ b/content/_code-samples/tx-serialization/js/helpers.js @@ -0,0 +1,21 @@ +'use strict'; + +module.exports = { + + isAmountObject : function (arg) { + const keys = Object.keys(arg).sort() + return ( + keys.length === 3 && + keys[0] === 'currency' && + keys[1] === 'issuer' && + keys[2] === 'value' + ) + }, + + sortFuncCanonical : function (a, b) { + a = this.fieldSortKey(a) + b = this.fieldSortKey(b) + return a.typeCode - b.typeCode || a.fieldCode - b.fieldCode + } + +} \ No newline at end of file diff --git a/content/_code-samples/tx-serialization/js/index.js b/content/_code-samples/tx-serialization/js/index.js new file mode 100644 index 0000000000..1f2d7e76cb --- /dev/null +++ b/content/_code-samples/tx-serialization/js/index.js @@ -0,0 +1,670 @@ +'use strict' + +// Organize imports +const assert = require("assert") +const bigInt = require("big-integer") +const { Buffer } = require('buffer') +const Decimal = require('decimal.js') +const fs = require("fs") +const parseArgs = require('minimist') +const { codec } = require("ripple-address-codec") + +const { isAmountObject, sortFuncCanonical } = require('./helpers') + +const mask = bigInt(0x00000000ffffffff) + +class TxSerializer { + + constructor() { + this.definitions = this._loadDefinitions() + } + + /** + * Loads JSON from the definitions file and converts it to a preferred format. + * + * (The definitions file should be drop-in compatible with the one from the + * ripple-binary-codec JavaScript package.) + * + * @param filename + * @returns {{TYPES, LEDGER_ENTRY_TYPES, FIELDS: {}, TRANSACTION_RESULTS, TRANSACTION_TYPES}} + * @private + */ + _loadDefinitions(filename = "definitions.json") { + const rawJson = fs.readFileSync(filename, 'utf8') + const definitions = JSON.parse(rawJson) + + return { + "TYPES" : definitions["TYPES"], + "FIELDS" : definitions["FIELDS"].reduce(function(accum, tuple) { + accum[tuple[0]] = tuple[1] + + return accum + }, {}), + "LEDGER_ENTRY_TYPES": definitions["LEDGER_ENTRY_TYPES"], + "TRANSACTION_RESULTS": definitions["TRANSACTION_RESULTS"], + "TRANSACTION_TYPES": definitions["TRANSACTION_TYPES"], + } + } + + /** + * Returns a base58 encoded address, f. ex. AccountId + * + * @param address + * @returns {Buffer} + * @private + */ + _decodeAddress(address) { + const decoded = codec.decodeChecked(address) + if (decoded[0] === 0 && decoded.length === 21) { + return decoded.slice(1) + } + + throw new Error("Not an AccountID!") + } + + /** + * Return a tuple sort key for a given field name + * + * @param fieldName + * @returns {{one: *, two: (*|(function(Array, number=): *))}} + */ + fieldSortKey(fieldName) { + const fieldTypeName = this.definitions["FIELDS"][fieldName]["type"] + const typeCode = this.definitions["TYPES"][fieldTypeName] + const fieldCode = this.definitions["FIELDS"][fieldName].nth + + return {typeCode, fieldCode} + } + + /** + * Returns the unique field ID for a given field name. + * This field ID consists of the type code and field code, in 1 to 3 bytes + * depending on whether those values are "common" (<16) or "uncommon" (>=16) + * + * @param fieldName + * @returns {string} + */ + fieldId(fieldName) { + const fieldTypeName = this.definitions["FIELDS"][fieldName]["type"] + const fieldCode = this.definitions["FIELDS"][fieldName].nth + const typeCode = this.definitions["TYPES"][fieldTypeName] + + // Codes must be nonzero and fit in 1 byte + assert.ok(0 < typeCode <= 255) + assert.ok(0 < fieldCode <= 255) + + if (typeCode < 16 && fieldCode < 16) { + // High 4 bits is the type_code + // Low 4 bits is the field code + const combinedCode = (typeCode << 4) | fieldCode + + return this.uint8ToBytes(combinedCode) + } else if (typeCode >= 16 && fieldCode < 16) { + // First 4 bits are zeroes + // Next 4 bits is field code + // Next byte is type code + const byte1 = this.uint8ToBytes(fieldCode) + const byte2 = this.uint8ToBytes(typeCode) + + return "" + byte1 + byte2 + } else if (typeCode < 16 && fieldCode >= 16) { + // Both are >= 16 + // First 4 bits is type code + // Next 4 bits are zeroes + // Next byte is field code + const byte1 = this.uint8ToBytes(typeCode << 4) + const byte2 = this.uint8ToBytes(fieldCode) + + return "" + byte1 + byte2 + } else { + // both are >= 16 + // first byte is all zeroes + // second byte is type + // third byte is field code + const byte1 = this.uint8ToBytes(0) + const byte2 = this.uint8ToBytes(typeCode) + const byte3 = this.uint8ToBytes(fieldCode) + + return "" + byte1 + byte2 + byte3 //TODO: bytes is python function + } + } + + /** + * Helper function for length-prefixed fields including Blob types + * and some AccountID types. + * + * Encodes arbitrary binary data with a length prefix. The length of the prefix + * is 1-3 bytes depending on the length of the contents: + * + * Content length <= 192 bytes: prefix is 1 byte + * 192 bytes < Content length <= 12480 bytes: prefix is 2 bytes + * 12480 bytes < Content length <= 918744 bytes: prefix is 3 bytes + * + * @param content + * @returns {string} + */ + variableLengthEncode(content) { + // Each byte in a hex string has a length of 2 chars + let length = content.length / 2 + + if (length <= 192) { + //const lengthByte = new Uint8Array([length]) + const lengthByte = Buffer.from([length]).toString("hex") + + return "" + lengthByte + content + } else if(length <= 12480) { + length -= 193 + const byte1 = Buffer.from([(length >> 8) + 193]).toString("hex") + const byte2 = Buffer.from([length & 0xff]).toString("hex") + + return "" + byte1 + byte2 + content + } else if (length <= 918744) { + length -= 12481 + const byte1 = Buffer.from([241 + (length >> 16)]).toString("hex") + const byte2 = Buffer.from([(length >> 8) & 0xff]).toString("hex") + const byte3 = Buffer.from([length & 0xff]).toString("hex") + + return "" + byte1 + byte2 + byte3 + content + } + + throw new Error('VariableLength field must be <= 918744 bytes long') + } + + /** + * Serialize an AccountID field type. These are length-prefixed. + * + * Some fields contain nested non-length-prefixed AccountIDs directly; those + * call decode_address() instead of this function. + * + * @param address + * @returns {string} + */ + accountIdToBytes(address) { + return this.variableLengthEncode(this._decodeAddress(address).toString("hex")) + } + + /** + * Serializes an "Amount" type, which can be either XRP or an issued currency: + * - XRP: 64 bits; 0, followed by 1 ("is positive"), followed by 62 bit UInt amount + * - Issued Currency: 64 bits of amount, followed by 160 bit currency code and + * 160 bit issuer AccountID. + * + * @param value + * @returns {string} + */ + amountToBytes(value) { + let amount = Buffer.alloc(8) + + if (typeof value === 'string') { + const number = bigInt(value) + + const intBuf = [Buffer.alloc(4), Buffer.alloc(4)] + intBuf[0].writeUInt32BE(Number(number.shiftRight(32)), 0) + intBuf[1].writeUInt32BE(Number(number.and(mask)), 0) + + amount = Buffer.concat(intBuf) + + amount[0] |= 0x40 + + return amount.toString("hex") + } else if (typeof value === 'object') { + if(!isAmountObject(value)) { + throw new Error("Amount must have currency, value, issuer only") + } + + const number = new Decimal(value["value"]) + + if (number.isZero()) { + amount[0] |= 0x80; + } else { + const integerNumberString = number + .times("1e".concat(-(number.e - 15))) + .abs() + .toString(); + const num = bigInt(integerNumberString) + let intBuf = [Buffer.alloc(4), Buffer.alloc(4)] + intBuf[0].writeUInt32BE(Number(num.shiftRight(32)), 0) + intBuf[1].writeUInt32BE(Number(num.and(mask)), 0) + amount = Buffer.concat(intBuf) + amount[0] |= 0x80 + if (number.gt(new Decimal(0))) { + amount[0] |= 0x40 + } + + const exponent = number.e - 15 + const exponentByte = 97 + exponent + amount[0] |= exponentByte >>> 2 + amount[1] |= (exponentByte & 0x03) << 6 + } + + logger("Issued amount: " + amount.toString("hex")) + + const currencyCode = this.currencyCodeToBytes(value["currency"]) + + return amount.toString("hex") + + currencyCode.toString("hex") + + this._decodeAddress(value["issuer"]).toString("hex") + } + } + + /** + * Serialize an array of objects from decoded JSON. + * Each member object must have a type wrapper and an inner object. + * For example: + * [ + * { + * // wrapper object + * "Memo": { + * // inner object + * "MemoType": "687474703a2f2f6578616d706c652e636f6d2f6d656d6f2f67656e65726963", + * "MemoData": "72656e74" + * } + * } + * ] + * + * @param array + * @returns {string} + */ + arrayToBytes(array) { + let membersAsBytes = [] + + for (let member of array) { + const wrapperKey = Object.keys(member)[0] + const innerObject = member[wrapperKey] + membersAsBytes.push(this.fieldToBytes(wrapperKey, innerObject)) + } + + membersAsBytes.push(this.fieldId("ArrayEndMarker")) + + return membersAsBytes.join('') + } + + /** + * Serializes a string of hex as binary data with a length prefix. + * + * @param fieldValue + * @returns {string} + */ + blobToBytes(fieldValue) { + return this.variableLengthEncode(fieldValue) + } + + /** + * + * @param codeString + * @param isXrpOk + * @returns {string} + */ + currencyCodeToBytes(codeString, isXrpOk = false) { + const ISO_REGEX = /^[A-Z0-9a-z?!@#$%^&*(){}[\]|]{3}$/ + const HEX_REGEX = /^[A-F0-9]{40}$/ + + if(ISO_REGEX.test(codeString)) { + if(codeString === "XRP") { + if (isXrpOk) { + // Rare, but when the currency code "XRP" is serialized, it's + // a special-case all zeroes. + logger("Currency code(XRP): " + "00".repeat(20)) + return "00".repeat(20) + } + + throw new Error("issued currency can't be XRP") + } + const codeAscii = Buffer.from(codeString, 'ascii') + logger("Currency code ASCII: " + codeAscii.toString("hex")) + // standard currency codes: https://xrpl.org/currency-formats.html#standard-currency-codes + // 8 bits type code (0x00) + // 88 bits reserved (0's) + // 24 bits ASCII + // 16 bits version (0x00) + // 24 bits reserved (0's) + const prefix = Buffer.alloc(12) + const postfix = Buffer.alloc(5) + + return Buffer.concat([prefix, codeAscii, postfix]).toString("hex") + } else if (HEX_REGEX.test(codeString)) { + // raw hex code + return Buffer.from(codeString).toString("hex") + } + + throw new Error("invalid currency code") + } + + /** + * Serializes a hexadecimal string as binary and confirms that it's 128 bits + * + * @param contents + * @returns {string} + */ + hash128ToBytes(contents) { + const buffer = this.hashToBytes(contents) + if(buffer.length !== 16) { + // 16 bytes = 128 bits + throw new Error("Hash128 is not 128 bits long") + } + + return buffer.toString("hex") + } + + /** + * Serializes a hexadecimal string as binary and confirms that it's 160 bits + * + * @param contents + * @returns {string} + */ + hash160ToBytes(contents) { + const buffer = this.hashToBytes(contents) + if(buffer.length !== 20) { + // 20 bytes = 160 bits + throw new Error("Hash160 is not 160 bits long") + } + + return buffer.toString("hex") + } + + /** + * Serializes a hexadecimal string as binary and confirms that it's 128 bits + * + * @param contents + * @returns {string} + */ + hash256ToBytes(contents) { + const buffer = this.hashToBytes(contents) + if(buffer.length !== 32) { + // 32 bytes = 256 bits + throw new Error("Hash256 is not 256 bits long") + } + + return buffer.toString("hex") + } + + /** + * Helper function; serializes a hash value from a hexadecimal string + * of any length. + * + * @param contents + * @returns {string} + */ + hashToBytes(contents) { + return Buffer.from(contents).toString("hex") + } + + /** + * Serialize an object from decoded JSON. + * Each object must have a type wrapper and an inner object. For example: + * + * { + * // type wrapper + * "SignerEntry": { + * // inner object + * "Account": "rUpy3eEg8rqjqfUoLeBnZkscbKbFsKXC3v", + * "SignerWeight": 1 + * } + * } + * + * Puts the child fields (e.g. Account, SignerWeight) in canonical order + * and appends an object end marker. + * + * @param object + * @returns {string} + */ + objectToBytes(object) { + const childOrder = Object.keys(object).sort(sortFuncCanonical.bind(this)) + + let fieldsAsBytes = []; + + for (const fieldName of childOrder) { + if (this.definitions["FIELDS"][fieldName]["isSerialized"]) { + const fieldValue = object[fieldName] + const fieldBytes = this.fieldToBytes(fieldName, fieldValue) + logger(fieldName + ": " + fieldBytes) + fieldsAsBytes.push(fieldBytes) + } + } + + fieldsAsBytes.push(this.fieldId("ObjectEndMarker")) + + return fieldsAsBytes.join('') + } + + /** + * Serialize a PathSet, which is an array of arrays, + * where each inner array represents one possible payment path. + * A path consists of "path step" objects in sequence, each with one or + * more of "account", "currency", and "issuer" fields, plus (ignored) "type" + * and "type_hex" fields which indicate which fields are present. + * (We re-create the type field for serialization based on which of the core + * 3 fields are present.) + * + * @param pathset + * @returns {string} + */ + pathSetToBytes(pathset) { + if (pathset.length === 0) { + throw new Error("PathSet type must not be empty") + } + + let pathsAsHexBytes = "" + + for (let [key, path] of Object.entries(pathset)) { + const pathBytes = this.pathToBytes(path) + logger("Path " + path + ": " + pathBytes) + pathsAsHexBytes += pathBytes + + if (parseInt(key) + 1 === pathset.length) { + // Last path; add an end byte + pathsAsHexBytes += "00" + } else { + // Add a path separator byte + pathsAsHexBytes += "ff" + } + } + + return pathsAsHexBytes + } + + /** + * Helper function for representing one member of a pathset as a bytes object + * + * @param path + * @returns {string} + */ + pathToBytes(path) { + + if (path.length === 0) { + throw new Error("Path must not be empty") + } + + let pathContents = [] + + for (let step of path) { + let stepData = "" + let typeByte = 0 + + if (step.hasOwnProperty("account")) { + typeByte |= 0x01 + stepData += this._decodeAddress(step["account"]).toString("hex") + } + + if (step.hasOwnProperty("currency")) { + typeByte |= 0x10 + stepData += this.currencyCodeToBytes(step["currency"], true) + } + + if (step.hasOwnProperty("issuer")) { + typeByte |= 0x20 + stepData += this._decodeAddress(step["issuer"]).toString("hex") + } + + stepData = this.uint8ToBytes(typeByte) + stepData + pathContents.push(stepData) + } + + return pathContents.join('') + } + + /** + * TransactionType field is a special case that is written in JSON + * as a string name but in binary as a UInt16. + * + * @param txType + * @returns {string} + */ + txTypeToBytes(txType) { + const typeUint = this.definitions["TRANSACTION_TYPES"][txType] + + return this.uint16ToBytes(typeUint) + } + + uint8ToBytes(value) { + return Buffer.from([value]).toString("hex") + } + + uint16ToBytes(value) { + let buffer = Buffer.alloc(2) + buffer.writeUInt16BE(value, 0) + + return buffer.toString("hex") + } + + uint32ToBytes(value) { + let buffer = Buffer.alloc(4) + buffer.writeUInt32BE(value, 0) + + return buffer.toString("hex") + } + + // Core serialization logic ----------------------------------------------------- + + /** + * Returns a bytes object containing the serialized version of a field + * including its field ID prefix. + * + * @param fieldName + * @param fieldValue + * @returns {string} + */ + fieldToBytes(fieldName, fieldValue) { + const fieldType = this.definitions["FIELDS"][fieldName]["type"] + logger("Serializing field " + fieldName + " of type " + fieldType) + + const idPrefix = this.fieldId(fieldName) + logger("IdPrefix is: " + idPrefix) + + // Special case: convert from string to UInt16 + if (fieldName === "TransactionType") { + return idPrefix + this.txTypeToBytes(fieldValue) + } + + const dispatch = { + "AccountID": this.accountIdToBytes.bind(this), + "Amount": this.amountToBytes.bind(this), + "Blob": this.blobToBytes.bind(this), + "Hash128": this.hash128ToBytes.bind(this), + "Hash160": this.hash160ToBytes.bind(this), + "Hash256": this.hash256ToBytes.bind(this), + "PathSet": this.pathSetToBytes.bind(this), + "STArray": this.arrayToBytes.bind(this), + "STObject": this.objectToBytes.bind(this), + "UInt8" : this.uint8ToBytes.bind(this), + "UInt16": this.uint16ToBytes.bind(this), + "UInt32": this.uint32ToBytes.bind(this), + } + + const fieldBinary = dispatch[fieldType](fieldValue) + + return idPrefix.toString("hex") + fieldBinary + } + + /** + * Takes a transaction as decoded JSON and returns a bytes object representing + * the transaction in binary format. + * + * The input format should omit transaction metadata and the transaction + * should be formatted with the transaction instructions at the top level. + * ("hash" can be included, but will be ignored) + * + * If for_signing=True, then only signing fields are serialized, so you can use + * the output to sign the transaction. + * + * SigningPubKey and TxnSignature are optional, but the transaction can't + * be submitted without them. + * + * For example: + * + * { + * "TransactionType" : "Payment", + * "Account" : "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + * "Destination" : "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", + * "Amount" : { + * "currency" : "USD", + * "value" : "1", + * "issuer" : "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" + * }, + * "Fee": "12", + * "Flags": 2147483648, + * "Sequence": 2 + * } + * + * @param tx + * @param forSigning + * @returns {string} + */ + serializeTx(tx, forSigning = false) + { + const fieldOrder = Object.keys(tx).sort(sortFuncCanonical.bind(this)) + + let fieldsAsBytes = [] + + for (const fieldName of fieldOrder) { + if (this.definitions["FIELDS"][fieldName]["isSerialized"]) { + if (forSigning && !this.definitions["FIELDS"][fieldName]["isSigningField"]) { + // Skip non-signing fields in forSigning mode. + continue + } + + const fieldValue = tx[fieldName] + const fieldBytes = this.fieldToBytes(fieldName, fieldValue) + logger(fieldName + ": " + fieldBytes) + fieldsAsBytes.push(fieldBytes) + } + } + + return fieldsAsBytes.join('') + } +} + +// Startup stuff begin + +const args = parseArgs(process.argv.slice(2), { + alias: { + 'f': 'filename', + 'j': 'json', + 'v': 'verbose', + }, + default: { + 'f': 'test-cases/tx1.json', + 'v': false + } +}) + +const logger = function(verbose, value) { + if(verbose) { + console.log(value) + } +}.bind(null, args.verbose) + +// Startup stuff end + +let rawJson +if (args.json) { + rawJson = args.json +} else { + rawJson = fs.readFileSync(args.filename, 'utf8') +} + +const json = JSON.parse(rawJson) +const serializer = new TxSerializer(json) +const serializedTx = serializer.serializeTx(json) + +console.log(serializedTx.toUpperCase()) \ No newline at end of file diff --git a/content/_code-samples/tx-serialization/js/package.json b/content/_code-samples/tx-serialization/js/package.json new file mode 100644 index 0000000000..c5a17af93d --- /dev/null +++ b/content/_code-samples/tx-serialization/js/package.json @@ -0,0 +1,13 @@ +{ + "name": "transaction-serialisation-examples", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.7", + "xrpl": "^2.0.0" + }, + "main": "index.js", + "scripts": { + "start": "node index.js" + } +} diff --git a/content/_code-samples/tx-serialization/js/test-cases/README.md b/content/_code-samples/tx-serialization/js/test-cases/README.md new file mode 100644 index 0000000000..0cfa290ed1 --- /dev/null +++ b/content/_code-samples/tx-serialization/js/test-cases/README.md @@ -0,0 +1,52 @@ +# Transaction Serialization Test Cases + +This folder contains several transactions in their JSON and binary forms, which +you can use to verify the behavior of transaction serialization code. + +For example (starting from the `tx-serialization/` dir above this one): + +```bash +$ python3 serialize.py -f test-cases/tx2.json | \ + diff - test-cases/tx2-binary.txt +``` + +The expected result is no output because the output of `serialize.py` matches +the contents of `test-cases/tx2-binary.txt` exactly. + +For an example of how the output is different if you change the `Fee` parameter of sample transaction 1, we can pipe a modified version of the file into the serializer: + +```bash +$ cat test-cases/tx1.json | \ + sed -e 's/"Fee": "10"/"Fee": "100"/' | \ + python3 serialize.py --stdin | \ + diff - test-cases/tx1-binary.txt --color +``` + +The output shows that the two versions of the transaction binary are different (but because they're all on one line, it's not super clear _where_ within the line the difference is): + +```text +1c1 +< 120007220008000024001ABED82A2380BF2C2019001ABED764D55920AC93914000000000000000 +00000000000055534400000000000A20B3C85F482532A9578DBB3950B85CA06594D165400000037E +11D600684000000000000064732103EE83BB432547885C219634A1BC407A9DB0474145D69737D09C +CDC63E1DEE7FE3744630440220143759437C04F7B61F012563AFE90D8DAFC46E86035E1D965A9CED +282C97D4CE02204CFD241E86F17E011298FC1A39B63386C74306A5DE047E213B0F29EFA4571C2C81 +14DD76483FACDEE26E60D8A586BB58D09F27045C46 +--- +> 120007220008000024001ABED82A2380BF2C2019001ABED764D55920AC93914000000000000000 +00000000000055534400000000000A20B3C85F482532A9578DBB3950B85CA06594D165400000037E +11D60068400000000000000A732103EE83BB432547885C219634A1BC407A9DB0474145D69737D09C +CDC63E1DEE7FE3744630440220143759437C04F7B61F012563AFE90D8DAFC46E86035E1D965A9CED +282C97D4CE02204CFD241E86F17E011298FC1A39B63386C74306A5DE047E213B0F29EFA4571C2C81 +14DD76483FACDEE26E60D8A586BB58D09F27045C46 +``` + +(If you're curious, the difference appears in the third line of each blob in this example. The modified version of the transaction serializes the `Fee` amount ending in `64` (hex for 100) while the original version ended in `0A` (hex for 10).) + +For a friendlier display, you could pipe the output of the serializer to a file and use a visual tool like [Meld](http://meldmerge.org/) that shows intra-line differences: + +```bash +$ cat test-cases/tx1.json | sed -e 's/"Fee": "10"/"Fee": "100"/' | python3 serialize.py --stdin > /tmp/tx1-modified.txt && meld /tmp/tx1-modified.txt test-cases/tx1-binary.txt +``` + +![Meld screenshot showing the `0A` / `64` difference](meld-example.png) diff --git a/content/_code-samples/tx-serialization/js/test-cases/meld-example.png b/content/_code-samples/tx-serialization/js/test-cases/meld-example.png new file mode 100644 index 0000000000000000000000000000000000000000..3c7e4f505901b8ed8acfd84e1b93a02f818c647f GIT binary patch literal 9584 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 literal 0 HcmV?d00001 diff --git a/content/_code-samples/tx-serialization/js/test-cases/tx1-binary.txt b/content/_code-samples/tx-serialization/js/test-cases/tx1-binary.txt new file mode 100644 index 0000000000..4b66a67703 --- /dev/null +++ b/content/_code-samples/tx-serialization/js/test-cases/tx1-binary.txt @@ -0,0 +1 @@ +120007220008000024001ABED82A2380BF2C2019001ABED764D55920AC9391400000000000000000000000000055534400000000000A20B3C85F482532A9578DBB3950B85CA06594D165400000037E11D60068400000000000000A732103EE83BB432547885C219634A1BC407A9DB0474145D69737D09CCDC63E1DEE7FE3744630440220143759437C04F7B61F012563AFE90D8DAFC46E86035E1D965A9CED282C97D4CE02204CFD241E86F17E011298FC1A39B63386C74306A5DE047E213B0F29EFA4571C2C8114DD76483FACDEE26E60D8A586BB58D09F27045C46 diff --git a/content/_code-samples/tx-serialization/js/test-cases/tx1.json b/content/_code-samples/tx-serialization/js/test-cases/tx1.json new file mode 100644 index 0000000000..987dd58aa7 --- /dev/null +++ b/content/_code-samples/tx-serialization/js/test-cases/tx1.json @@ -0,0 +1,18 @@ +{ + "Account": "rMBzp8CgpE441cp5PVyA9rpVV7oT8hP3ys", + "Expiration": 595640108, + "Fee": "10", + "Flags": 524288, + "OfferSequence": 1752791, + "Sequence": 1752792, + "SigningPubKey": "03EE83BB432547885C219634A1BC407A9DB0474145D69737D09CCDC63E1DEE7FE3", + "TakerGets": "15000000000", + "TakerPays": { + "currency": "USD", + "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B", + "value": "7072.8" + }, + "TransactionType": "OfferCreate", + "TxnSignature": "30440220143759437C04F7B61F012563AFE90D8DAFC46E86035E1D965A9CED282C97D4CE02204CFD241E86F17E011298FC1A39B63386C74306A5DE047E213B0F29EFA4571C2C", + "hash": "73734B611DDA23D3F5F62E20A173B78AB8406AC5015094DA53F53D39B9EDB06C" +} diff --git a/content/_code-samples/tx-serialization/js/test-cases/tx2-binary.txt b/content/_code-samples/tx-serialization/js/test-cases/tx2-binary.txt new file mode 100644 index 0000000000..d2dbb5f3ac --- /dev/null +++ b/content/_code-samples/tx-serialization/js/test-cases/tx2-binary.txt @@ -0,0 +1 @@ +1200022280000000240000000120190000000B68400000000000277573210268D79CD579D077750740FA18A2370B7C2018B2714ECE70BA65C38D223E79BC9C74473045022100F06FB54049D6D50142E5CF2E2AC21946AF305A13E2A2D4BA881B36484DD01A540220311557EC8BEF536D729605A4CB4D4DC51B1E37C06C93434DD5B7651E1E2E28BF811452C7F01AD13B3CA9C1D133FA8F3482D2EF08FA7D82145A380FBD236B6A1CD14B939AD21101E5B6B6FFA2F9EA7D0F04C4D46544659A2D58525043686174E1F1 diff --git a/content/_code-samples/tx-serialization/js/test-cases/tx2.json b/content/_code-samples/tx-serialization/js/test-cases/tx2.json new file mode 100644 index 0000000000..605ebdaea7 --- /dev/null +++ b/content/_code-samples/tx-serialization/js/test-cases/tx2.json @@ -0,0 +1,18 @@ +{ + "TransactionType": "EscrowFinish", + "Flags": 2147483648, + "Sequence": 1, + "OfferSequence": 11, + "Fee": "10101", + "SigningPubKey": "0268D79CD579D077750740FA18A2370B7C2018B2714ECE70BA65C38D223E79BC9C", + "TxnSignature": "3045022100F06FB54049D6D50142E5CF2E2AC21946AF305A13E2A2D4BA881B36484DD01A540220311557EC8BEF536D729605A4CB4D4DC51B1E37C06C93434DD5B7651E1E2E28BF", + "Account": "r3Y6vCE8XqfZmYBRngy22uFYkmz3y9eCRA", + "Owner": "r9NpyVfLfUG8hatuCCHKzosyDtKnBdsEN3", + "Memos": [ + { + "Memo": { + "MemoData": "04C4D46544659A2D58525043686174" + } + } + ] +} diff --git a/content/_code-samples/tx-serialization/js/test-cases/tx3-binary.txt b/content/_code-samples/tx-serialization/js/test-cases/tx3-binary.txt new file mode 100644 index 0000000000..d1bc34834c --- /dev/null +++ b/content/_code-samples/tx-serialization/js/test-cases/tx3-binary.txt @@ -0,0 +1 @@ +1200002200000000240000034A201B009717BE61400000000098968068400000000000000C69D4564B964A845AC0000000000000000000000000555344000000000069D33B18D53385F8A3185516C2EDA5DEDB8AC5C673210379F17CFA0FFD7518181594BE69FE9A10471D6DE1F4055C6D2746AFD6CF89889E74473045022100D55ED1953F860ADC1BC5CD993ABB927F48156ACA31C64737865F4F4FF6D015A80220630704D2BD09C8E99F26090C25F11B28F5D96A1350454402C2CED92B39FFDBAF811469D33B18D53385F8A3185516C2EDA5DEDB8AC5C6831469D33B18D53385F8A3185516C2EDA5DEDB8AC5C6F9EA7C06636C69656E747D077274312E312E31E1F1011201F3B1997562FD742B54D4EBDEA1D6AEA3D4906B8F100000000000000000000000000000000000000000FF014B4E9C06F24296074F7BC48F92A97916C6DC5EA901DD39C650A96EDA48334E70CC4A85B8B2E8502CD310000000000000000000000000000000000000000000 diff --git a/content/_code-samples/tx-serialization/js/test-cases/tx3.json b/content/_code-samples/tx-serialization/js/test-cases/tx3.json new file mode 100644 index 0000000000..71078f7da9 --- /dev/null +++ b/content/_code-samples/tx-serialization/js/test-cases/tx3.json @@ -0,0 +1,57 @@ +{ + "Account": "rweYz56rfmQ98cAdRaeTxQS9wVMGnrdsFp", + "Amount": "10000000", + "Destination": "rweYz56rfmQ98cAdRaeTxQS9wVMGnrdsFp", + "Fee": "12", + "Flags": 0, + "LastLedgerSequence": 9902014, + "Memos": [ + { + "Memo": { + "MemoData": "7274312E312E31", + "MemoType": "636C69656E74" + } + } + ], + "Paths": [ + [ + { + "account": "rPDXxSZcuVL3ZWoyU82bcde3zwvmShkRyF", + "type": 1, + "type_hex": "0000000000000001" + }, + { + "currency": "XRP", + "type": 16, + "type_hex": "0000000000000010" + } + ], + [ + { + "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "type": 1, + "type_hex": "0000000000000001" + }, + { + "account": "rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q", + "type": 1, + "type_hex": "0000000000000001" + }, + { + "currency": "XRP", + "type": 16, + "type_hex": "0000000000000010" + } + ] + ], + "SendMax": { + "currency": "USD", + "issuer": "rweYz56rfmQ98cAdRaeTxQS9wVMGnrdsFp", + "value": "0.6275558355" + }, + "Sequence": 842, + "SigningPubKey": "0379F17CFA0FFD7518181594BE69FE9A10471D6DE1F4055C6D2746AFD6CF89889E", + "TransactionType": "Payment", + "TxnSignature": "3045022100D55ED1953F860ADC1BC5CD993ABB927F48156ACA31C64737865F4F4FF6D015A80220630704D2BD09C8E99F26090C25F11B28F5D96A1350454402C2CED92B39FFDBAF", + "hash": "B521424226FC100A2A802FE20476A5F8426FD3F720176DC5CCCE0D75738CC208" +} From de56a1f7ade8cf66cf30f17c9629a2f6416f1b34 Mon Sep 17 00:00:00 2001 From: AlexanderBuzz Date: Fri, 20 Jan 2023 08:00:24 +0100 Subject: [PATCH 02/12] - Changed test-cases README.md examples from python-cli to node-cli - added input handling from --stdin - wrapped some logic in callback function in main file as new --stind option works asynchronously --- .../tx-serialization/js/index.js | 31 ++++++++++++++----- .../tx-serialization/js/test-cases/README.md | 10 +++--- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/content/_code-samples/tx-serialization/js/index.js b/content/_code-samples/tx-serialization/js/index.js index 1f2d7e76cb..f8d1a338cd 100644 --- a/content/_code-samples/tx-serialization/js/index.js +++ b/content/_code-samples/tx-serialization/js/index.js @@ -636,10 +636,19 @@ class TxSerializer { // Startup stuff begin +function main(rawJson) { + const json = JSON.parse(rawJson) + const serializer = new TxSerializer(json) + const serializedTx = serializer.serializeTx(json) + + console.log(serializedTx.toUpperCase()) +} + const args = parseArgs(process.argv.slice(2), { alias: { 'f': 'filename', 'j': 'json', + 's': 'stdin', 'v': 'verbose', }, default: { @@ -659,12 +668,20 @@ const logger = function(verbose, value) { let rawJson if (args.json) { rawJson = args.json + main(rawJson) +} else if (args.stdin) { + const stdin = process.openStdin(); + + let data = "" + + stdin.on('data', function(chunk) { + data += chunk + }); + + stdin.on('end', function() { + main(data) + }); } else { rawJson = fs.readFileSync(args.filename, 'utf8') -} - -const json = JSON.parse(rawJson) -const serializer = new TxSerializer(json) -const serializedTx = serializer.serializeTx(json) - -console.log(serializedTx.toUpperCase()) \ No newline at end of file + main(rawJson) +} \ No newline at end of file diff --git a/content/_code-samples/tx-serialization/js/test-cases/README.md b/content/_code-samples/tx-serialization/js/test-cases/README.md index 0cfa290ed1..14cbad4ef7 100644 --- a/content/_code-samples/tx-serialization/js/test-cases/README.md +++ b/content/_code-samples/tx-serialization/js/test-cases/README.md @@ -3,14 +3,14 @@ This folder contains several transactions in their JSON and binary forms, which you can use to verify the behavior of transaction serialization code. -For example (starting from the `tx-serialization/` dir above this one): +For example (starting from the `tx-serialization/js/` dir above this one): ```bash -$ python3 serialize.py -f test-cases/tx2.json | \ +$ node index.js -f test-cases/tx2.json | \ diff - test-cases/tx2-binary.txt ``` -The expected result is no output because the output of `serialize.py` matches +The expected result is no output because the output of `node index.js` matches the contents of `test-cases/tx2-binary.txt` exactly. For an example of how the output is different if you change the `Fee` parameter of sample transaction 1, we can pipe a modified version of the file into the serializer: @@ -18,7 +18,7 @@ For an example of how the output is different if you change the `Fee` parameter ```bash $ cat test-cases/tx1.json | \ sed -e 's/"Fee": "10"/"Fee": "100"/' | \ - python3 serialize.py --stdin | \ + node index.js --json | \ diff - test-cases/tx1-binary.txt --color ``` @@ -46,7 +46,7 @@ CDC63E1DEE7FE3744630440220143759437C04F7B61F012563AFE90D8DAFC46E86035E1D965A9CED For a friendlier display, you could pipe the output of the serializer to a file and use a visual tool like [Meld](http://meldmerge.org/) that shows intra-line differences: ```bash -$ cat test-cases/tx1.json | sed -e 's/"Fee": "10"/"Fee": "100"/' | python3 serialize.py --stdin > /tmp/tx1-modified.txt && meld /tmp/tx1-modified.txt test-cases/tx1-binary.txt +$ cat test-cases/tx1.json | sed -e 's/"Fee": "10"/"Fee": "100"/' | node index.js --stdin --stdin > /tmp/tx1-modified.txt && meld /tmp/tx1-modified.txt test-cases/tx1-binary.txt ``` ![Meld screenshot showing the `0A` / `64` difference](meld-example.png) From fa2231365027698124f002ce86a6fa2a8b2b33d6 Mon Sep 17 00:00:00 2001 From: AlexanderBuzz <102560752+AlexanderBuzz@users.noreply.github.com> Date: Wed, 25 Jan 2023 08:12:48 +0100 Subject: [PATCH 03/12] Update content/_code-samples/tx-serialization/js/test-cases/README.md Co-authored-by: Jackson Mills --- content/_code-samples/tx-serialization/js/test-cases/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/_code-samples/tx-serialization/js/test-cases/README.md b/content/_code-samples/tx-serialization/js/test-cases/README.md index 14cbad4ef7..09408d2608 100644 --- a/content/_code-samples/tx-serialization/js/test-cases/README.md +++ b/content/_code-samples/tx-serialization/js/test-cases/README.md @@ -1,7 +1,7 @@ # Transaction Serialization Test Cases This folder contains several transactions in their JSON and binary forms, which -you can use to verify the behavior of transaction serialization code. +you can use to verify the behavior of the transaction serialization code. For example (starting from the `tx-serialization/js/` dir above this one): From b7e7e24554f88103a02fd1c18f89fa62a899e060 Mon Sep 17 00:00:00 2001 From: AlexanderBuzz <102560752+AlexanderBuzz@users.noreply.github.com> Date: Mon, 30 Jan 2023 12:02:54 +0100 Subject: [PATCH 04/12] Update content/_code-samples/tx-serialization/js/index.js Co-authored-by: Jackson Mills --- content/_code-samples/tx-serialization/js/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/_code-samples/tx-serialization/js/index.js b/content/_code-samples/tx-serialization/js/index.js index f8d1a338cd..9adcf53e49 100644 --- a/content/_code-samples/tx-serialization/js/index.js +++ b/content/_code-samples/tx-serialization/js/index.js @@ -549,7 +549,7 @@ class TxSerializer { logger("Serializing field " + fieldName + " of type " + fieldType) const idPrefix = this.fieldId(fieldName) - logger("IdPrefix is: " + idPrefix) + logger("ID Prefix is: " + idPrefix) // Special case: convert from string to UInt16 if (fieldName === "TransactionType") { From 642b06feb461e3c0d1bc7a9d413fd83590ae66e2 Mon Sep 17 00:00:00 2001 From: AlexanderBuzz Date: Mon, 30 Jan 2023 12:37:51 +0100 Subject: [PATCH 05/12] - Removed duplicate output of Id Prefix when logging in verbose mode --- content/_code-samples/tx-serialization/js/index.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/content/_code-samples/tx-serialization/js/index.js b/content/_code-samples/tx-serialization/js/index.js index 9adcf53e49..7717a2993d 100644 --- a/content/_code-samples/tx-serialization/js/index.js +++ b/content/_code-samples/tx-serialization/js/index.js @@ -553,7 +553,10 @@ class TxSerializer { // Special case: convert from string to UInt16 if (fieldName === "TransactionType") { - return idPrefix + this.txTypeToBytes(fieldValue) + const fieldBytes = this.txTypeToBytes(fieldValue) + logger(fieldName + ' : ' + fieldBytes) + + return idPrefix + fieldBytes } const dispatch = { @@ -571,9 +574,11 @@ class TxSerializer { "UInt32": this.uint32ToBytes.bind(this), } - const fieldBinary = dispatch[fieldType](fieldValue) + const fieldBytes = dispatch[fieldType](fieldValue) - return idPrefix.toString("hex") + fieldBinary + logger(fieldName + ' : ' + fieldBytes) + + return idPrefix.toString("hex") + fieldBytes } /** @@ -625,7 +630,6 @@ class TxSerializer { const fieldValue = tx[fieldName] const fieldBytes = this.fieldToBytes(fieldName, fieldValue) - logger(fieldName + ": " + fieldBytes) fieldsAsBytes.push(fieldBytes) } } From 5e31e5540839860b6db0918dfa1d9b9ab0d40cd8 Mon Sep 17 00:00:00 2001 From: AlexanderBuzz Date: Mon, 30 Jan 2023 12:38:50 +0100 Subject: [PATCH 06/12] - Fixed typo --- content/_code-samples/tx-serialization/js/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/_code-samples/tx-serialization/js/index.js b/content/_code-samples/tx-serialization/js/index.js index 7717a2993d..27dcfc4210 100644 --- a/content/_code-samples/tx-serialization/js/index.js +++ b/content/_code-samples/tx-serialization/js/index.js @@ -576,7 +576,7 @@ class TxSerializer { const fieldBytes = dispatch[fieldType](fieldValue) - logger(fieldName + ' : ' + fieldBytes) + logger(fieldName + ': ' + fieldBytes) return idPrefix.toString("hex") + fieldBytes } From 3b84534b6ab11a765c9158ffda94d126d5eb3776 Mon Sep 17 00:00:00 2001 From: AlexanderBuzz Date: Mon, 30 Jan 2023 13:06:27 +0100 Subject: [PATCH 07/12] - Changed link in xrpl.org/serialization.html from ripple-binary-codec to the code from this PR --- content/references/protocol-reference/serialization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/references/protocol-reference/serialization.md b/content/references/protocol-reference/serialization.md index ac401204f6..8841d00050 100644 --- a/content/references/protocol-reference/serialization.md +++ b/content/references/protocol-reference/serialization.md @@ -59,7 +59,7 @@ Both signed and unsigned transactions can be represented in both JSON and binary The serialization processes described here are implemented in multiple places and programming languages: - In C++ [in the `rippled` code base](https://github.com/ripple/rippled/blob/develop/src/ripple/protocol/impl/STObject.cpp). -- In JavaScript in the [`ripple-binary-codec`](https://github.com/XRPLF/xrpl.js/tree/main/packages/ripple-binary-codec) package. +- In JavaScript in [this repository's code samples section]({{target.github_forkurl}}/blob/{{target.github_branch}}/content/_code-samples/tx-serialization/). - In Python 3 in [this repository's code samples section]({{target.github_forkurl}}/blob/{{target.github_branch}}/content/_code-samples/tx-serialization/). Additionally, many [client libraries](client-libraries.html) provide serialization support under permissive open-source licenses, so you can import, use, or adapt the code for your needs. From bce91a17f2192a524696ef3efee5d7193bf83ae0 Mon Sep 17 00:00:00 2001 From: AlexanderBuzz Date: Mon, 30 Jan 2023 14:20:19 +0100 Subject: [PATCH 08/12] - Restructured code, main logic is now in tx-serializer.js --- .../tx-serialization/js/index.js | 650 +---------------- .../tx-serialization/js/tx-serializer.js | 677 ++++++++++++++++++ 2 files changed, 681 insertions(+), 646 deletions(-) create mode 100644 content/_code-samples/tx-serialization/js/tx-serializer.js diff --git a/content/_code-samples/tx-serialization/js/index.js b/content/_code-samples/tx-serialization/js/index.js index 27dcfc4210..3e56d3542b 100644 --- a/content/_code-samples/tx-serialization/js/index.js +++ b/content/_code-samples/tx-serialization/js/index.js @@ -1,648 +1,14 @@ 'use strict' // Organize imports -const assert = require("assert") -const bigInt = require("big-integer") -const { Buffer } = require('buffer') -const Decimal = require('decimal.js') const fs = require("fs") const parseArgs = require('minimist') -const { codec } = require("ripple-address-codec") +const TxSerializer = require('./tx-serializer') // Main serialization logic can be found in this file -const { isAmountObject, sortFuncCanonical } = require('./helpers') -const mask = bigInt(0x00000000ffffffff) - -class TxSerializer { - - constructor() { - this.definitions = this._loadDefinitions() - } - - /** - * Loads JSON from the definitions file and converts it to a preferred format. - * - * (The definitions file should be drop-in compatible with the one from the - * ripple-binary-codec JavaScript package.) - * - * @param filename - * @returns {{TYPES, LEDGER_ENTRY_TYPES, FIELDS: {}, TRANSACTION_RESULTS, TRANSACTION_TYPES}} - * @private - */ - _loadDefinitions(filename = "definitions.json") { - const rawJson = fs.readFileSync(filename, 'utf8') - const definitions = JSON.parse(rawJson) - - return { - "TYPES" : definitions["TYPES"], - "FIELDS" : definitions["FIELDS"].reduce(function(accum, tuple) { - accum[tuple[0]] = tuple[1] - - return accum - }, {}), - "LEDGER_ENTRY_TYPES": definitions["LEDGER_ENTRY_TYPES"], - "TRANSACTION_RESULTS": definitions["TRANSACTION_RESULTS"], - "TRANSACTION_TYPES": definitions["TRANSACTION_TYPES"], - } - } - - /** - * Returns a base58 encoded address, f. ex. AccountId - * - * @param address - * @returns {Buffer} - * @private - */ - _decodeAddress(address) { - const decoded = codec.decodeChecked(address) - if (decoded[0] === 0 && decoded.length === 21) { - return decoded.slice(1) - } - - throw new Error("Not an AccountID!") - } - - /** - * Return a tuple sort key for a given field name - * - * @param fieldName - * @returns {{one: *, two: (*|(function(Array, number=): *))}} - */ - fieldSortKey(fieldName) { - const fieldTypeName = this.definitions["FIELDS"][fieldName]["type"] - const typeCode = this.definitions["TYPES"][fieldTypeName] - const fieldCode = this.definitions["FIELDS"][fieldName].nth - - return {typeCode, fieldCode} - } - - /** - * Returns the unique field ID for a given field name. - * This field ID consists of the type code and field code, in 1 to 3 bytes - * depending on whether those values are "common" (<16) or "uncommon" (>=16) - * - * @param fieldName - * @returns {string} - */ - fieldId(fieldName) { - const fieldTypeName = this.definitions["FIELDS"][fieldName]["type"] - const fieldCode = this.definitions["FIELDS"][fieldName].nth - const typeCode = this.definitions["TYPES"][fieldTypeName] - - // Codes must be nonzero and fit in 1 byte - assert.ok(0 < typeCode <= 255) - assert.ok(0 < fieldCode <= 255) - - if (typeCode < 16 && fieldCode < 16) { - // High 4 bits is the type_code - // Low 4 bits is the field code - const combinedCode = (typeCode << 4) | fieldCode - - return this.uint8ToBytes(combinedCode) - } else if (typeCode >= 16 && fieldCode < 16) { - // First 4 bits are zeroes - // Next 4 bits is field code - // Next byte is type code - const byte1 = this.uint8ToBytes(fieldCode) - const byte2 = this.uint8ToBytes(typeCode) - - return "" + byte1 + byte2 - } else if (typeCode < 16 && fieldCode >= 16) { - // Both are >= 16 - // First 4 bits is type code - // Next 4 bits are zeroes - // Next byte is field code - const byte1 = this.uint8ToBytes(typeCode << 4) - const byte2 = this.uint8ToBytes(fieldCode) - - return "" + byte1 + byte2 - } else { - // both are >= 16 - // first byte is all zeroes - // second byte is type - // third byte is field code - const byte1 = this.uint8ToBytes(0) - const byte2 = this.uint8ToBytes(typeCode) - const byte3 = this.uint8ToBytes(fieldCode) - - return "" + byte1 + byte2 + byte3 //TODO: bytes is python function - } - } - - /** - * Helper function for length-prefixed fields including Blob types - * and some AccountID types. - * - * Encodes arbitrary binary data with a length prefix. The length of the prefix - * is 1-3 bytes depending on the length of the contents: - * - * Content length <= 192 bytes: prefix is 1 byte - * 192 bytes < Content length <= 12480 bytes: prefix is 2 bytes - * 12480 bytes < Content length <= 918744 bytes: prefix is 3 bytes - * - * @param content - * @returns {string} - */ - variableLengthEncode(content) { - // Each byte in a hex string has a length of 2 chars - let length = content.length / 2 - - if (length <= 192) { - //const lengthByte = new Uint8Array([length]) - const lengthByte = Buffer.from([length]).toString("hex") - - return "" + lengthByte + content - } else if(length <= 12480) { - length -= 193 - const byte1 = Buffer.from([(length >> 8) + 193]).toString("hex") - const byte2 = Buffer.from([length & 0xff]).toString("hex") - - return "" + byte1 + byte2 + content - } else if (length <= 918744) { - length -= 12481 - const byte1 = Buffer.from([241 + (length >> 16)]).toString("hex") - const byte2 = Buffer.from([(length >> 8) & 0xff]).toString("hex") - const byte3 = Buffer.from([length & 0xff]).toString("hex") - - return "" + byte1 + byte2 + byte3 + content - } - - throw new Error('VariableLength field must be <= 918744 bytes long') - } - - /** - * Serialize an AccountID field type. These are length-prefixed. - * - * Some fields contain nested non-length-prefixed AccountIDs directly; those - * call decode_address() instead of this function. - * - * @param address - * @returns {string} - */ - accountIdToBytes(address) { - return this.variableLengthEncode(this._decodeAddress(address).toString("hex")) - } - - /** - * Serializes an "Amount" type, which can be either XRP or an issued currency: - * - XRP: 64 bits; 0, followed by 1 ("is positive"), followed by 62 bit UInt amount - * - Issued Currency: 64 bits of amount, followed by 160 bit currency code and - * 160 bit issuer AccountID. - * - * @param value - * @returns {string} - */ - amountToBytes(value) { - let amount = Buffer.alloc(8) - - if (typeof value === 'string') { - const number = bigInt(value) - - const intBuf = [Buffer.alloc(4), Buffer.alloc(4)] - intBuf[0].writeUInt32BE(Number(number.shiftRight(32)), 0) - intBuf[1].writeUInt32BE(Number(number.and(mask)), 0) - - amount = Buffer.concat(intBuf) - - amount[0] |= 0x40 - - return amount.toString("hex") - } else if (typeof value === 'object') { - if(!isAmountObject(value)) { - throw new Error("Amount must have currency, value, issuer only") - } - - const number = new Decimal(value["value"]) - - if (number.isZero()) { - amount[0] |= 0x80; - } else { - const integerNumberString = number - .times("1e".concat(-(number.e - 15))) - .abs() - .toString(); - const num = bigInt(integerNumberString) - let intBuf = [Buffer.alloc(4), Buffer.alloc(4)] - intBuf[0].writeUInt32BE(Number(num.shiftRight(32)), 0) - intBuf[1].writeUInt32BE(Number(num.and(mask)), 0) - amount = Buffer.concat(intBuf) - amount[0] |= 0x80 - if (number.gt(new Decimal(0))) { - amount[0] |= 0x40 - } - - const exponent = number.e - 15 - const exponentByte = 97 + exponent - amount[0] |= exponentByte >>> 2 - amount[1] |= (exponentByte & 0x03) << 6 - } - - logger("Issued amount: " + amount.toString("hex")) - - const currencyCode = this.currencyCodeToBytes(value["currency"]) - - return amount.toString("hex") - + currencyCode.toString("hex") - + this._decodeAddress(value["issuer"]).toString("hex") - } - } - - /** - * Serialize an array of objects from decoded JSON. - * Each member object must have a type wrapper and an inner object. - * For example: - * [ - * { - * // wrapper object - * "Memo": { - * // inner object - * "MemoType": "687474703a2f2f6578616d706c652e636f6d2f6d656d6f2f67656e65726963", - * "MemoData": "72656e74" - * } - * } - * ] - * - * @param array - * @returns {string} - */ - arrayToBytes(array) { - let membersAsBytes = [] - - for (let member of array) { - const wrapperKey = Object.keys(member)[0] - const innerObject = member[wrapperKey] - membersAsBytes.push(this.fieldToBytes(wrapperKey, innerObject)) - } - - membersAsBytes.push(this.fieldId("ArrayEndMarker")) - - return membersAsBytes.join('') - } - - /** - * Serializes a string of hex as binary data with a length prefix. - * - * @param fieldValue - * @returns {string} - */ - blobToBytes(fieldValue) { - return this.variableLengthEncode(fieldValue) - } - - /** - * - * @param codeString - * @param isXrpOk - * @returns {string} - */ - currencyCodeToBytes(codeString, isXrpOk = false) { - const ISO_REGEX = /^[A-Z0-9a-z?!@#$%^&*(){}[\]|]{3}$/ - const HEX_REGEX = /^[A-F0-9]{40}$/ - - if(ISO_REGEX.test(codeString)) { - if(codeString === "XRP") { - if (isXrpOk) { - // Rare, but when the currency code "XRP" is serialized, it's - // a special-case all zeroes. - logger("Currency code(XRP): " + "00".repeat(20)) - return "00".repeat(20) - } - - throw new Error("issued currency can't be XRP") - } - const codeAscii = Buffer.from(codeString, 'ascii') - logger("Currency code ASCII: " + codeAscii.toString("hex")) - // standard currency codes: https://xrpl.org/currency-formats.html#standard-currency-codes - // 8 bits type code (0x00) - // 88 bits reserved (0's) - // 24 bits ASCII - // 16 bits version (0x00) - // 24 bits reserved (0's) - const prefix = Buffer.alloc(12) - const postfix = Buffer.alloc(5) - - return Buffer.concat([prefix, codeAscii, postfix]).toString("hex") - } else if (HEX_REGEX.test(codeString)) { - // raw hex code - return Buffer.from(codeString).toString("hex") - } - - throw new Error("invalid currency code") - } - - /** - * Serializes a hexadecimal string as binary and confirms that it's 128 bits - * - * @param contents - * @returns {string} - */ - hash128ToBytes(contents) { - const buffer = this.hashToBytes(contents) - if(buffer.length !== 16) { - // 16 bytes = 128 bits - throw new Error("Hash128 is not 128 bits long") - } - - return buffer.toString("hex") - } - - /** - * Serializes a hexadecimal string as binary and confirms that it's 160 bits - * - * @param contents - * @returns {string} - */ - hash160ToBytes(contents) { - const buffer = this.hashToBytes(contents) - if(buffer.length !== 20) { - // 20 bytes = 160 bits - throw new Error("Hash160 is not 160 bits long") - } - - return buffer.toString("hex") - } - - /** - * Serializes a hexadecimal string as binary and confirms that it's 128 bits - * - * @param contents - * @returns {string} - */ - hash256ToBytes(contents) { - const buffer = this.hashToBytes(contents) - if(buffer.length !== 32) { - // 32 bytes = 256 bits - throw new Error("Hash256 is not 256 bits long") - } - - return buffer.toString("hex") - } - - /** - * Helper function; serializes a hash value from a hexadecimal string - * of any length. - * - * @param contents - * @returns {string} - */ - hashToBytes(contents) { - return Buffer.from(contents).toString("hex") - } - - /** - * Serialize an object from decoded JSON. - * Each object must have a type wrapper and an inner object. For example: - * - * { - * // type wrapper - * "SignerEntry": { - * // inner object - * "Account": "rUpy3eEg8rqjqfUoLeBnZkscbKbFsKXC3v", - * "SignerWeight": 1 - * } - * } - * - * Puts the child fields (e.g. Account, SignerWeight) in canonical order - * and appends an object end marker. - * - * @param object - * @returns {string} - */ - objectToBytes(object) { - const childOrder = Object.keys(object).sort(sortFuncCanonical.bind(this)) - - let fieldsAsBytes = []; - - for (const fieldName of childOrder) { - if (this.definitions["FIELDS"][fieldName]["isSerialized"]) { - const fieldValue = object[fieldName] - const fieldBytes = this.fieldToBytes(fieldName, fieldValue) - logger(fieldName + ": " + fieldBytes) - fieldsAsBytes.push(fieldBytes) - } - } - - fieldsAsBytes.push(this.fieldId("ObjectEndMarker")) - - return fieldsAsBytes.join('') - } - - /** - * Serialize a PathSet, which is an array of arrays, - * where each inner array represents one possible payment path. - * A path consists of "path step" objects in sequence, each with one or - * more of "account", "currency", and "issuer" fields, plus (ignored) "type" - * and "type_hex" fields which indicate which fields are present. - * (We re-create the type field for serialization based on which of the core - * 3 fields are present.) - * - * @param pathset - * @returns {string} - */ - pathSetToBytes(pathset) { - if (pathset.length === 0) { - throw new Error("PathSet type must not be empty") - } - - let pathsAsHexBytes = "" - - for (let [key, path] of Object.entries(pathset)) { - const pathBytes = this.pathToBytes(path) - logger("Path " + path + ": " + pathBytes) - pathsAsHexBytes += pathBytes - - if (parseInt(key) + 1 === pathset.length) { - // Last path; add an end byte - pathsAsHexBytes += "00" - } else { - // Add a path separator byte - pathsAsHexBytes += "ff" - } - } - - return pathsAsHexBytes - } - - /** - * Helper function for representing one member of a pathset as a bytes object - * - * @param path - * @returns {string} - */ - pathToBytes(path) { - - if (path.length === 0) { - throw new Error("Path must not be empty") - } - - let pathContents = [] - - for (let step of path) { - let stepData = "" - let typeByte = 0 - - if (step.hasOwnProperty("account")) { - typeByte |= 0x01 - stepData += this._decodeAddress(step["account"]).toString("hex") - } - - if (step.hasOwnProperty("currency")) { - typeByte |= 0x10 - stepData += this.currencyCodeToBytes(step["currency"], true) - } - - if (step.hasOwnProperty("issuer")) { - typeByte |= 0x20 - stepData += this._decodeAddress(step["issuer"]).toString("hex") - } - - stepData = this.uint8ToBytes(typeByte) + stepData - pathContents.push(stepData) - } - - return pathContents.join('') - } - - /** - * TransactionType field is a special case that is written in JSON - * as a string name but in binary as a UInt16. - * - * @param txType - * @returns {string} - */ - txTypeToBytes(txType) { - const typeUint = this.definitions["TRANSACTION_TYPES"][txType] - - return this.uint16ToBytes(typeUint) - } - - uint8ToBytes(value) { - return Buffer.from([value]).toString("hex") - } - - uint16ToBytes(value) { - let buffer = Buffer.alloc(2) - buffer.writeUInt16BE(value, 0) - - return buffer.toString("hex") - } - - uint32ToBytes(value) { - let buffer = Buffer.alloc(4) - buffer.writeUInt32BE(value, 0) - - return buffer.toString("hex") - } - - // Core serialization logic ----------------------------------------------------- - - /** - * Returns a bytes object containing the serialized version of a field - * including its field ID prefix. - * - * @param fieldName - * @param fieldValue - * @returns {string} - */ - fieldToBytes(fieldName, fieldValue) { - const fieldType = this.definitions["FIELDS"][fieldName]["type"] - logger("Serializing field " + fieldName + " of type " + fieldType) - - const idPrefix = this.fieldId(fieldName) - logger("ID Prefix is: " + idPrefix) - - // Special case: convert from string to UInt16 - if (fieldName === "TransactionType") { - const fieldBytes = this.txTypeToBytes(fieldValue) - logger(fieldName + ' : ' + fieldBytes) - - return idPrefix + fieldBytes - } - - const dispatch = { - "AccountID": this.accountIdToBytes.bind(this), - "Amount": this.amountToBytes.bind(this), - "Blob": this.blobToBytes.bind(this), - "Hash128": this.hash128ToBytes.bind(this), - "Hash160": this.hash160ToBytes.bind(this), - "Hash256": this.hash256ToBytes.bind(this), - "PathSet": this.pathSetToBytes.bind(this), - "STArray": this.arrayToBytes.bind(this), - "STObject": this.objectToBytes.bind(this), - "UInt8" : this.uint8ToBytes.bind(this), - "UInt16": this.uint16ToBytes.bind(this), - "UInt32": this.uint32ToBytes.bind(this), - } - - const fieldBytes = dispatch[fieldType](fieldValue) - - logger(fieldName + ': ' + fieldBytes) - - return idPrefix.toString("hex") + fieldBytes - } - - /** - * Takes a transaction as decoded JSON and returns a bytes object representing - * the transaction in binary format. - * - * The input format should omit transaction metadata and the transaction - * should be formatted with the transaction instructions at the top level. - * ("hash" can be included, but will be ignored) - * - * If for_signing=True, then only signing fields are serialized, so you can use - * the output to sign the transaction. - * - * SigningPubKey and TxnSignature are optional, but the transaction can't - * be submitted without them. - * - * For example: - * - * { - * "TransactionType" : "Payment", - * "Account" : "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", - * "Destination" : "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", - * "Amount" : { - * "currency" : "USD", - * "value" : "1", - * "issuer" : "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" - * }, - * "Fee": "12", - * "Flags": 2147483648, - * "Sequence": 2 - * } - * - * @param tx - * @param forSigning - * @returns {string} - */ - serializeTx(tx, forSigning = false) - { - const fieldOrder = Object.keys(tx).sort(sortFuncCanonical.bind(this)) - - let fieldsAsBytes = [] - - for (const fieldName of fieldOrder) { - if (this.definitions["FIELDS"][fieldName]["isSerialized"]) { - if (forSigning && !this.definitions["FIELDS"][fieldName]["isSigningField"]) { - // Skip non-signing fields in forSigning mode. - continue - } - - const fieldValue = tx[fieldName] - const fieldBytes = this.fieldToBytes(fieldName, fieldValue) - fieldsAsBytes.push(fieldBytes) - } - } - - return fieldsAsBytes.join('') - } -} - -// Startup stuff begin - -function main(rawJson) { +function main(rawJson, verbose) { const json = JSON.parse(rawJson) - const serializer = new TxSerializer(json) + const serializer = new TxSerializer(verbose) const serializedTx = serializer.serializeTx(json) console.log(serializedTx.toUpperCase()) @@ -661,14 +27,6 @@ const args = parseArgs(process.argv.slice(2), { } }) -const logger = function(verbose, value) { - if(verbose) { - console.log(value) - } -}.bind(null, args.verbose) - -// Startup stuff end - let rawJson if (args.json) { rawJson = args.json @@ -687,5 +45,5 @@ if (args.json) { }); } else { rawJson = fs.readFileSync(args.filename, 'utf8') - main(rawJson) + main(rawJson, args.verbose) } \ No newline at end of file diff --git a/content/_code-samples/tx-serialization/js/tx-serializer.js b/content/_code-samples/tx-serialization/js/tx-serializer.js new file mode 100644 index 0000000000..a07b7556ae --- /dev/null +++ b/content/_code-samples/tx-serialization/js/tx-serializer.js @@ -0,0 +1,677 @@ +'use strict' + +// Organize imports +const assert = require("assert") +const bigInt = require("big-integer") +const { Buffer } = require('buffer') +const Decimal = require('decimal.js') +const fs = require("fs") +const { codec } = require("ripple-address-codec") + +const mask = bigInt(0x00000000ffffffff) + +/** + * Helper function that checks wether an amount object has a proper signature + * + * @param arg + * @returns {boolean} + */ +const isAmountObject = function (arg) { + const keys = Object.keys(arg).sort() + return ( + keys.length === 3 && + keys[0] === 'currency' && + keys[1] === 'issuer' && + keys[2] === 'value' + ) +} + +/** + * Helper function for sorting fields in a tx object + * + * @param a + * @param b + * @returns {number} + */ +const sortFuncCanonical = function (a, b) { + a = this.fieldSortKey(a) + b = this.fieldSortKey(b) + return a.typeCode - b.typeCode || a.fieldCode - b.fieldCode +} + +/** + * Main Class + */ +class TxSerializer { + + constructor(verbose = false) { + this.verbose = verbose + this.definitions = this._loadDefinitions() + } + + /** + * Loads JSON from the definitions file and converts it to a preferred format. + * + * (The definitions file should be drop-in compatible with the one from the + * ripple-binary-codec JavaScript package.) + * + * @param filename + * @returns {{TYPES, LEDGER_ENTRY_TYPES, FIELDS: {}, TRANSACTION_RESULTS, TRANSACTION_TYPES}} + * @private + */ + _loadDefinitions(filename = "definitions.json") { + const rawJson = fs.readFileSync(filename, 'utf8') + const definitions = JSON.parse(rawJson) + + return { + "TYPES" : definitions["TYPES"], + "FIELDS" : definitions["FIELDS"].reduce(function(accum, tuple) { + accum[tuple[0]] = tuple[1] + + return accum + }, {}), + "LEDGER_ENTRY_TYPES": definitions["LEDGER_ENTRY_TYPES"], + "TRANSACTION_RESULTS": definitions["TRANSACTION_RESULTS"], + "TRANSACTION_TYPES": definitions["TRANSACTION_TYPES"], + } + } + + _logger(message) { + if (this.verbose) { + console.log(message) + } + } + + /** + * Returns a base58 encoded address, f. ex. AccountId + * + * @param address + * @returns {Buffer} + * @private + */ + _decodeAddress(address) { + const decoded = codec.decodeChecked(address) + if (decoded[0] === 0 && decoded.length === 21) { + return decoded.slice(1) + } + + throw new Error("Not an AccountID!") + } + + /** + * Return a tuple sort key for a given field name + * + * @param fieldName + * @returns {{one: *, two: (*|(function(Array, number=): *))}} + */ + fieldSortKey(fieldName) { + const fieldTypeName = this.definitions["FIELDS"][fieldName]["type"] + const typeCode = this.definitions["TYPES"][fieldTypeName] + const fieldCode = this.definitions["FIELDS"][fieldName].nth + + return {typeCode, fieldCode} + } + + /** + * Returns the unique field ID for a given field name. + * This field ID consists of the type code and field code, in 1 to 3 bytes + * depending on whether those values are "common" (<16) or "uncommon" (>=16) + * + * @param fieldName + * @returns {string} + */ + fieldId(fieldName) { + const fieldTypeName = this.definitions["FIELDS"][fieldName]["type"] + const fieldCode = this.definitions["FIELDS"][fieldName].nth + const typeCode = this.definitions["TYPES"][fieldTypeName] + + // Codes must be nonzero and fit in 1 byte + assert.ok(0 < typeCode <= 255) + assert.ok(0 < fieldCode <= 255) + + if (typeCode < 16 && fieldCode < 16) { + // High 4 bits is the type_code + // Low 4 bits is the field code + const combinedCode = (typeCode << 4) | fieldCode + + return this.uint8ToBytes(combinedCode) + } else if (typeCode >= 16 && fieldCode < 16) { + // First 4 bits are zeroes + // Next 4 bits is field code + // Next byte is type code + const byte1 = this.uint8ToBytes(fieldCode) + const byte2 = this.uint8ToBytes(typeCode) + + return "" + byte1 + byte2 + } else if (typeCode < 16 && fieldCode >= 16) { + // Both are >= 16 + // First 4 bits is type code + // Next 4 bits are zeroes + // Next byte is field code + const byte1 = this.uint8ToBytes(typeCode << 4) + const byte2 = this.uint8ToBytes(fieldCode) + + return "" + byte1 + byte2 + } else { + // both are >= 16 + // first byte is all zeroes + // second byte is type + // third byte is field code + const byte1 = this.uint8ToBytes(0) + const byte2 = this.uint8ToBytes(typeCode) + const byte3 = this.uint8ToBytes(fieldCode) + + return "" + byte1 + byte2 + byte3 //TODO: bytes is python function + } + } + + /** + * Helper function for length-prefixed fields including Blob types + * and some AccountID types. + * + * Encodes arbitrary binary data with a length prefix. The length of the prefix + * is 1-3 bytes depending on the length of the contents: + * + * Content length <= 192 bytes: prefix is 1 byte + * 192 bytes < Content length <= 12480 bytes: prefix is 2 bytes + * 12480 bytes < Content length <= 918744 bytes: prefix is 3 bytes + * + * @param content + * @returns {string} + */ + variableLengthEncode(content) { + // Each byte in a hex string has a length of 2 chars + let length = content.length / 2 + + if (length <= 192) { + //const lengthByte = new Uint8Array([length]) + const lengthByte = Buffer.from([length]).toString("hex") + + return "" + lengthByte + content + } else if(length <= 12480) { + length -= 193 + const byte1 = Buffer.from([(length >> 8) + 193]).toString("hex") + const byte2 = Buffer.from([length & 0xff]).toString("hex") + + return "" + byte1 + byte2 + content + } else if (length <= 918744) { + length -= 12481 + const byte1 = Buffer.from([241 + (length >> 16)]).toString("hex") + const byte2 = Buffer.from([(length >> 8) & 0xff]).toString("hex") + const byte3 = Buffer.from([length & 0xff]).toString("hex") + + return "" + byte1 + byte2 + byte3 + content + } + + throw new Error('VariableLength field must be <= 918744 bytes long') + } + + /** + * Serialize an AccountID field type. These are length-prefixed. + * + * Some fields contain nested non-length-prefixed AccountIDs directly; those + * call decode_address() instead of this function. + * + * @param address + * @returns {string} + */ + accountIdToBytes(address) { + return this.variableLengthEncode(this._decodeAddress(address).toString("hex")) + } + + /** + * Serializes an "Amount" type, which can be either XRP or an issued currency: + * - XRP: 64 bits; 0, followed by 1 ("is positive"), followed by 62 bit UInt amount + * - Issued Currency: 64 bits of amount, followed by 160 bit currency code and + * 160 bit issuer AccountID. + * + * @param value + * @returns {string} + */ + amountToBytes(value) { + let amount = Buffer.alloc(8) + + if (typeof value === 'string') { + const number = bigInt(value) + + const intBuf = [Buffer.alloc(4), Buffer.alloc(4)] + intBuf[0].writeUInt32BE(Number(number.shiftRight(32)), 0) + intBuf[1].writeUInt32BE(Number(number.and(mask)), 0) + + amount = Buffer.concat(intBuf) + + amount[0] |= 0x40 + + return amount.toString("hex") + } else if (typeof value === 'object') { + if(!isAmountObject(value)) { + throw new Error("Amount must have currency, value, issuer only") + } + + const number = new Decimal(value["value"]) + + if (number.isZero()) { + amount[0] |= 0x80; + } else { + const integerNumberString = number + .times("1e".concat(-(number.e - 15))) + .abs() + .toString(); + const num = bigInt(integerNumberString) + let intBuf = [Buffer.alloc(4), Buffer.alloc(4)] + intBuf[0].writeUInt32BE(Number(num.shiftRight(32)), 0) + intBuf[1].writeUInt32BE(Number(num.and(mask)), 0) + amount = Buffer.concat(intBuf) + amount[0] |= 0x80 + if (number.gt(new Decimal(0))) { + amount[0] |= 0x40 + } + + const exponent = number.e - 15 + const exponentByte = 97 + exponent + amount[0] |= exponentByte >>> 2 + amount[1] |= (exponentByte & 0x03) << 6 + } + + this._logger("Issued amount: " + amount.toString("hex")) + + const currencyCode = this.currencyCodeToBytes(value["currency"]) + + return amount.toString("hex") + + currencyCode.toString("hex") + + this._decodeAddress(value["issuer"]).toString("hex") + } + } + + /** + * Serialize an array of objects from decoded JSON. + * Each member object must have a type wrapper and an inner object. + * For example: + * [ + * { + * // wrapper object + * "Memo": { + * // inner object + * "MemoType": "687474703a2f2f6578616d706c652e636f6d2f6d656d6f2f67656e65726963", + * "MemoData": "72656e74" + * } + * } + * ] + * + * @param array + * @returns {string} + */ + arrayToBytes(array) { + let membersAsBytes = [] + + for (let member of array) { + const wrapperKey = Object.keys(member)[0] + const innerObject = member[wrapperKey] + membersAsBytes.push(this.fieldToBytes(wrapperKey, innerObject)) + } + + membersAsBytes.push(this.fieldId("ArrayEndMarker")) + + return membersAsBytes.join('') + } + + /** + * Serializes a string of hex as binary data with a length prefix. + * + * @param fieldValue + * @returns {string} + */ + blobToBytes(fieldValue) { + return this.variableLengthEncode(fieldValue) + } + + /** + * + * @param codeString + * @param isXrpOk + * @returns {string} + */ + currencyCodeToBytes(codeString, isXrpOk = false) { + const ISO_REGEX = /^[A-Z0-9a-z?!@#$%^&*(){}[\]|]{3}$/ + const HEX_REGEX = /^[A-F0-9]{40}$/ + + if(ISO_REGEX.test(codeString)) { + if(codeString === "XRP") { + if (isXrpOk) { + // Rare, but when the currency code "XRP" is serialized, it's + // a special-case all zeroes. + this._logger("Currency code(XRP): " + "00".repeat(20)) + return "00".repeat(20) + } + + throw new Error("issued currency can't be XRP") + } + const codeAscii = Buffer.from(codeString, 'ascii') + this._logger("Currency code ASCII: " + codeAscii.toString("hex")) + // standard currency codes: https://xrpl.org/currency-formats.html#standard-currency-codes + // 8 bits type code (0x00) + // 88 bits reserved (0's) + // 24 bits ASCII + // 16 bits version (0x00) + // 24 bits reserved (0's) + const prefix = Buffer.alloc(12) + const postfix = Buffer.alloc(5) + + return Buffer.concat([prefix, codeAscii, postfix]).toString("hex") + } else if (HEX_REGEX.test(codeString)) { + // raw hex code + return Buffer.from(codeString).toString("hex") + } + + throw new Error("invalid currency code") + } + + /** + * Serializes a hexadecimal string as binary and confirms that it's 128 bits + * + * @param contents + * @returns {string} + */ + hash128ToBytes(contents) { + const buffer = this.hashToBytes(contents) + if(buffer.length !== 16) { + // 16 bytes = 128 bits + throw new Error("Hash128 is not 128 bits long") + } + + return buffer.toString("hex") + } + + /** + * Serializes a hexadecimal string as binary and confirms that it's 160 bits + * + * @param contents + * @returns {string} + */ + hash160ToBytes(contents) { + const buffer = this.hashToBytes(contents) + if(buffer.length !== 20) { + // 20 bytes = 160 bits + throw new Error("Hash160 is not 160 bits long") + } + + return buffer.toString("hex") + } + + /** + * Serializes a hexadecimal string as binary and confirms that it's 128 bits + * + * @param contents + * @returns {string} + */ + hash256ToBytes(contents) { + const buffer = this.hashToBytes(contents) + if(buffer.length !== 32) { + // 32 bytes = 256 bits + throw new Error("Hash256 is not 256 bits long") + } + + return buffer.toString("hex") + } + + /** + * Helper function; serializes a hash value from a hexadecimal string + * of any length. + * + * @param contents + * @returns {string} + */ + hashToBytes(contents) { + return Buffer.from(contents).toString("hex") + } + + /** + * Serialize an object from decoded JSON. + * Each object must have a type wrapper and an inner object. For example: + * + * { + * // type wrapper + * "SignerEntry": { + * // inner object + * "Account": "rUpy3eEg8rqjqfUoLeBnZkscbKbFsKXC3v", + * "SignerWeight": 1 + * } + * } + * + * Puts the child fields (e.g. Account, SignerWeight) in canonical order + * and appends an object end marker. + * + * @param object + * @returns {string} + */ + objectToBytes(object) { + const childOrder = Object.keys(object).sort(sortFuncCanonical.bind(this)) + + let fieldsAsBytes = []; + + for (const fieldName of childOrder) { + if (this.definitions["FIELDS"][fieldName]["isSerialized"]) { + const fieldValue = object[fieldName] + const fieldBytes = this.fieldToBytes(fieldName, fieldValue) + this._logger(fieldName + ": " + fieldBytes) + fieldsAsBytes.push(fieldBytes) + } + } + + fieldsAsBytes.push(this.fieldId("ObjectEndMarker")) + + return fieldsAsBytes.join('') + } + + /** + * Serialize a PathSet, which is an array of arrays, + * where each inner array represents one possible payment path. + * A path consists of "path step" objects in sequence, each with one or + * more of "account", "currency", and "issuer" fields, plus (ignored) "type" + * and "type_hex" fields which indicate which fields are present. + * (We re-create the type field for serialization based on which of the core + * 3 fields are present.) + * + * @param pathset + * @returns {string} + */ + pathSetToBytes(pathset) { + if (pathset.length === 0) { + throw new Error("PathSet type must not be empty") + } + + let pathsAsHexBytes = "" + + for (let [key, path] of Object.entries(pathset)) { + const pathBytes = this.pathToBytes(path) + this._logger("Path " + path + ": " + pathBytes) + pathsAsHexBytes += pathBytes + + if (parseInt(key) + 1 === pathset.length) { + // Last path; add an end byte + pathsAsHexBytes += "00" + } else { + // Add a path separator byte + pathsAsHexBytes += "ff" + } + } + + return pathsAsHexBytes + } + + /** + * Helper function for representing one member of a pathset as a bytes object + * + * @param path + * @returns {string} + */ + pathToBytes(path) { + + if (path.length === 0) { + throw new Error("Path must not be empty") + } + + let pathContents = [] + + for (let step of path) { + let stepData = "" + let typeByte = 0 + + if (step.hasOwnProperty("account")) { + typeByte |= 0x01 + stepData += this._decodeAddress(step["account"]).toString("hex") + } + + if (step.hasOwnProperty("currency")) { + typeByte |= 0x10 + stepData += this.currencyCodeToBytes(step["currency"], true) + } + + if (step.hasOwnProperty("issuer")) { + typeByte |= 0x20 + stepData += this._decodeAddress(step["issuer"]).toString("hex") + } + + stepData = this.uint8ToBytes(typeByte) + stepData + pathContents.push(stepData) + } + + return pathContents.join('') + } + + /** + * TransactionType field is a special case that is written in JSON + * as a string name but in binary as a UInt16. + * + * @param txType + * @returns {string} + */ + txTypeToBytes(txType) { + const typeUint = this.definitions["TRANSACTION_TYPES"][txType] + + return this.uint16ToBytes(typeUint) + } + + uint8ToBytes(value) { + return Buffer.from([value]).toString("hex") + } + + uint16ToBytes(value) { + let buffer = Buffer.alloc(2) + buffer.writeUInt16BE(value, 0) + + return buffer.toString("hex") + } + + uint32ToBytes(value) { + let buffer = Buffer.alloc(4) + buffer.writeUInt32BE(value, 0) + + return buffer.toString("hex") + } + + // Core serialization logic ----------------------------------------------------- + + /** + * Returns a bytes object containing the serialized version of a field + * including its field ID prefix. + * + * @param fieldName + * @param fieldValue + * @returns {string} + */ + fieldToBytes(fieldName, fieldValue) { + const fieldType = this.definitions["FIELDS"][fieldName]["type"] + this._logger("Serializing field " + fieldName + " of type " + fieldType) + + const idPrefix = this.fieldId(fieldName) + this._logger("ID Prefix is: " + idPrefix) + + // Special case: convert from string to UInt16 + if (fieldName === "TransactionType") { + const fieldBytes = this.txTypeToBytes(fieldValue) + this._logger(fieldName + ' : ' + fieldBytes) + + return idPrefix + fieldBytes + } + + const dispatch = { + "AccountID": this.accountIdToBytes.bind(this), + "Amount": this.amountToBytes.bind(this), + "Blob": this.blobToBytes.bind(this), + "Hash128": this.hash128ToBytes.bind(this), + "Hash160": this.hash160ToBytes.bind(this), + "Hash256": this.hash256ToBytes.bind(this), + "PathSet": this.pathSetToBytes.bind(this), + "STArray": this.arrayToBytes.bind(this), + "STObject": this.objectToBytes.bind(this), + "UInt8" : this.uint8ToBytes.bind(this), + "UInt16": this.uint16ToBytes.bind(this), + "UInt32": this.uint32ToBytes.bind(this), + } + + const fieldBytes = dispatch[fieldType](fieldValue) + + this._logger(fieldName + ': ' + fieldBytes) + + return idPrefix.toString("hex") + fieldBytes + } + + /** + * Takes a transaction as decoded JSON and returns a bytes object representing + * the transaction in binary format. + * + * The input format should omit transaction metadata and the transaction + * should be formatted with the transaction instructions at the top level. + * ("hash" can be included, but will be ignored) + * + * If for_signing=True, then only signing fields are serialized, so you can use + * the output to sign the transaction. + * + * SigningPubKey and TxnSignature are optional, but the transaction can't + * be submitted without them. + * + * For example: + * + * { + * "TransactionType" : "Payment", + * "Account" : "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + * "Destination" : "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", + * "Amount" : { + * "currency" : "USD", + * "value" : "1", + * "issuer" : "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" + * }, + * "Fee": "12", + * "Flags": 2147483648, + * "Sequence": 2 + * } + * + * @param tx + * @param forSigning + * @returns {string} + */ + serializeTx(tx, forSigning = false) + { + const fieldOrder = Object.keys(tx).sort(sortFuncCanonical.bind(this)) + + let fieldsAsBytes = [] + + for (const fieldName of fieldOrder) { + if (this.definitions["FIELDS"][fieldName]["isSerialized"]) { + if (forSigning && !this.definitions["FIELDS"][fieldName]["isSigningField"]) { + // Skip non-signing fields in forSigning mode. + continue + } + + const fieldValue = tx[fieldName] + const fieldBytes = this.fieldToBytes(fieldName, fieldValue) + fieldsAsBytes.push(fieldBytes) + } + } + + return fieldsAsBytes.join('') + } +} + +module.exports = TxSerializer \ No newline at end of file From 59306cdf6365fcf0c23b0d35611a09c898534f65 Mon Sep 17 00:00:00 2001 From: AlexanderBuzz Date: Mon, 30 Jan 2023 14:22:31 +0100 Subject: [PATCH 09/12] - Removed now obsolete file --- .../tx-serialization/js/helpers.js | 21 ------------------- 1 file changed, 21 deletions(-) delete mode 100644 content/_code-samples/tx-serialization/js/helpers.js diff --git a/content/_code-samples/tx-serialization/js/helpers.js b/content/_code-samples/tx-serialization/js/helpers.js deleted file mode 100644 index 18b56d9947..0000000000 --- a/content/_code-samples/tx-serialization/js/helpers.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -module.exports = { - - isAmountObject : function (arg) { - const keys = Object.keys(arg).sort() - return ( - keys.length === 3 && - keys[0] === 'currency' && - keys[1] === 'issuer' && - keys[2] === 'value' - ) - }, - - sortFuncCanonical : function (a, b) { - a = this.fieldSortKey(a) - b = this.fieldSortKey(b) - return a.typeCode - b.typeCode || a.fieldCode - b.fieldCode - } - -} \ No newline at end of file From 47f27d94fb141d1faa56106eeadc54e518ca20ea Mon Sep 17 00:00:00 2001 From: AlexanderBuzz Date: Tue, 31 Jan 2023 19:07:44 +0100 Subject: [PATCH 10/12] - Made output more user-friendly --- content/_code-samples/tx-serialization/js/index.js | 11 +++++++++-- .../tx-serialization/js/tx-serializer.js | 12 ++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/content/_code-samples/tx-serialization/js/index.js b/content/_code-samples/tx-serialization/js/index.js index 3e56d3542b..bdf3d656b7 100644 --- a/content/_code-samples/tx-serialization/js/index.js +++ b/content/_code-samples/tx-serialization/js/index.js @@ -8,9 +8,16 @@ const TxSerializer = require('./tx-serializer') // Main serialization logic can function main(rawJson, verbose) { const json = JSON.parse(rawJson) - const serializer = new TxSerializer(verbose) - const serializedTx = serializer.serializeTx(json) + console.log('\x1b[33m%s\x1b[0m', '\nXRPL Transaction Serialization Example') + console.log('\x1b[33m%s\x1b[0m', '--------------------------------------') + console.log('\x1b[37m%s\x1b[0m', '\nSerializing the following transaction:') + console.log(json) + if (verbose) console.log('') + const serializer = new TxSerializer(verbose) + + const serializedTx = serializer.serializeTx(json) + console.log('\x1b[37m%s\x1b[0m', '\nSerialized Transaction:') console.log(serializedTx.toUpperCase()) } diff --git a/content/_code-samples/tx-serialization/js/tx-serializer.js b/content/_code-samples/tx-serialization/js/tx-serializer.js index a07b7556ae..8b2672fdc8 100644 --- a/content/_code-samples/tx-serialization/js/tx-serializer.js +++ b/content/_code-samples/tx-serialization/js/tx-serializer.js @@ -273,7 +273,7 @@ class TxSerializer { amount[1] |= (exponentByte & 0x03) << 6 } - this._logger("Issued amount: " + amount.toString("hex")) + this._logger("Issued amount: " + amount.toString("hex").toUpperCase()) const currencyCode = this.currencyCodeToBytes(value["currency"]) @@ -453,7 +453,7 @@ class TxSerializer { if (this.definitions["FIELDS"][fieldName]["isSerialized"]) { const fieldValue = object[fieldName] const fieldBytes = this.fieldToBytes(fieldName, fieldValue) - this._logger(fieldName + ": " + fieldBytes) + this._logger(fieldName + ": " + fieldBytes.toUpperCase()) fieldsAsBytes.push(fieldBytes) } } @@ -484,7 +484,7 @@ class TxSerializer { for (let [key, path] of Object.entries(pathset)) { const pathBytes = this.pathToBytes(path) - this._logger("Path " + path + ": " + pathBytes) + this._logger("Path " + path + ": " + pathBytes.toUpperCase()) pathsAsHexBytes += pathBytes if (parseInt(key) + 1 === pathset.length) { @@ -585,12 +585,12 @@ class TxSerializer { this._logger("Serializing field " + fieldName + " of type " + fieldType) const idPrefix = this.fieldId(fieldName) - this._logger("ID Prefix is: " + idPrefix) + this._logger("ID Prefix is: " + idPrefix.toUpperCase()) // Special case: convert from string to UInt16 if (fieldName === "TransactionType") { const fieldBytes = this.txTypeToBytes(fieldValue) - this._logger(fieldName + ' : ' + fieldBytes) + this._logger(fieldName + ' : ' + fieldBytes.toUpperCase()) return idPrefix + fieldBytes } @@ -612,7 +612,7 @@ class TxSerializer { const fieldBytes = dispatch[fieldType](fieldValue) - this._logger(fieldName + ': ' + fieldBytes) + this._logger(fieldName + ': ' + fieldBytes.toUpperCase()) return idPrefix.toString("hex") + fieldBytes } From 6af6309b8247989c31c75a8c98de35694a01dc97 Mon Sep 17 00:00:00 2001 From: AlexanderBuzz Date: Tue, 31 Jan 2023 20:41:25 +0100 Subject: [PATCH 11/12] - Added Hash128 edge case --- content/_code-samples/tx-serialization/js/index.js | 7 +++---- .../tx-serialization/js/tx-serializer.js | 14 +++++++++++++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/content/_code-samples/tx-serialization/js/index.js b/content/_code-samples/tx-serialization/js/index.js index bdf3d656b7..67fef50a15 100644 --- a/content/_code-samples/tx-serialization/js/index.js +++ b/content/_code-samples/tx-serialization/js/index.js @@ -3,8 +3,7 @@ // Organize imports const fs = require("fs") const parseArgs = require('minimist') -const TxSerializer = require('./tx-serializer') // Main serialization logic can be found in this file - +const TxSerializer = require('./tx-serializer') function main(rawJson, verbose) { const json = JSON.parse(rawJson) @@ -37,7 +36,7 @@ const args = parseArgs(process.argv.slice(2), { let rawJson if (args.json) { rawJson = args.json - main(rawJson) + main(rawJson, args.verbose) } else if (args.stdin) { const stdin = process.openStdin(); @@ -48,7 +47,7 @@ if (args.json) { }); stdin.on('end', function() { - main(data) + main(data, args.verbose) }); } else { rawJson = fs.readFileSync(args.filename, 'utf8') diff --git a/content/_code-samples/tx-serialization/js/tx-serializer.js b/content/_code-samples/tx-serialization/js/tx-serializer.js index 8b2672fdc8..5cba755d63 100644 --- a/content/_code-samples/tx-serialization/js/tx-serializer.js +++ b/content/_code-samples/tx-serialization/js/tx-serializer.js @@ -373,6 +373,11 @@ class TxSerializer { * @returns {string} */ hash128ToBytes(contents) { + if (/^0+$/.exec(contents)) { + // Edge case, an all-zero bytes input returns an empty string + return "" + } + const buffer = this.hashToBytes(contents) if(buffer.length !== 16) { // 16 bytes = 128 bits @@ -585,11 +590,11 @@ class TxSerializer { this._logger("Serializing field " + fieldName + " of type " + fieldType) const idPrefix = this.fieldId(fieldName) - this._logger("ID Prefix is: " + idPrefix.toUpperCase()) // Special case: convert from string to UInt16 if (fieldName === "TransactionType") { const fieldBytes = this.txTypeToBytes(fieldValue) + this._logger("ID Prefix is: " + idPrefix.toUpperCase()) this._logger(fieldName + ' : ' + fieldBytes.toUpperCase()) return idPrefix + fieldBytes @@ -612,6 +617,13 @@ class TxSerializer { const fieldBytes = dispatch[fieldType](fieldValue) + if (fieldBytes.length === 0) { + this._logger('Unset field: ' + fieldName) + + return '' + } + + this._logger("ID Prefix is: " + idPrefix.toUpperCase()) this._logger(fieldName + ': ' + fieldBytes.toUpperCase()) return idPrefix.toString("hex") + fieldBytes From f9900061690411f15b1c885d8b2628a685d4014a Mon Sep 17 00:00:00 2001 From: AlexanderBuzz Date: Wed, 1 Feb 2023 15:39:48 +0100 Subject: [PATCH 12/12] - Added --raw CLI option --- .../tx-serialization/js/README.md | 4 ++++ .../tx-serialization/js/index.js | 20 +++++++++++++------ .../tx-serialization/js/test-cases/README.md | 4 ++-- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/content/_code-samples/tx-serialization/js/README.md b/content/_code-samples/tx-serialization/js/README.md index cd2fbd25b3..1a1bc47518 100644 --- a/content/_code-samples/tx-serialization/js/README.md +++ b/content/_code-samples/tx-serialization/js/README.md @@ -18,6 +18,10 @@ On first run, you have to install the necessary node.js dependencies: node index.js -v +### Raw output without formatting, use --raw or -r: + + node index.js -r + ### Pick JSON fixture file: node index.js -f test-cases/tx3.json diff --git a/content/_code-samples/tx-serialization/js/index.js b/content/_code-samples/tx-serialization/js/index.js index 67fef50a15..859fa34be1 100644 --- a/content/_code-samples/tx-serialization/js/index.js +++ b/content/_code-samples/tx-serialization/js/index.js @@ -7,16 +7,16 @@ const TxSerializer = require('./tx-serializer') function main(rawJson, verbose) { const json = JSON.parse(rawJson) - console.log('\x1b[33m%s\x1b[0m', '\nXRPL Transaction Serialization Example') - console.log('\x1b[33m%s\x1b[0m', '--------------------------------------') - console.log('\x1b[37m%s\x1b[0m', '\nSerializing the following transaction:') - console.log(json) - if (verbose) console.log('') + _pretty('\nXRPL Transaction Serialization Example', '\x1b[33m%s\x1b[0m') + _pretty('--------------------------------------', '\x1b[33m%s\x1b[0m') + _pretty('\nSerializing the following transaction:', '\x1b[37m%s\x1b[0m') + _pretty(json) + if (verbose) _pretty('') const serializer = new TxSerializer(verbose) const serializedTx = serializer.serializeTx(json) - console.log('\x1b[37m%s\x1b[0m', '\nSerialized Transaction:') + _pretty('\nSerialized Transaction:', '\x1b[37m%s\x1b[0m') console.log(serializedTx.toUpperCase()) } @@ -24,15 +24,23 @@ const args = parseArgs(process.argv.slice(2), { alias: { 'f': 'filename', 'j': 'json', + 'r': 'raw', 's': 'stdin', 'v': 'verbose', }, default: { 'f': 'test-cases/tx1.json', + 'r': false, 'v': false } }) +function _pretty(message, color) { + if (!args.raw) { + console.log(color, message) + } +} + let rawJson if (args.json) { rawJson = args.json diff --git a/content/_code-samples/tx-serialization/js/test-cases/README.md b/content/_code-samples/tx-serialization/js/test-cases/README.md index 09408d2608..fac686e78c 100644 --- a/content/_code-samples/tx-serialization/js/test-cases/README.md +++ b/content/_code-samples/tx-serialization/js/test-cases/README.md @@ -18,7 +18,7 @@ For an example of how the output is different if you change the `Fee` parameter ```bash $ cat test-cases/tx1.json | \ sed -e 's/"Fee": "10"/"Fee": "100"/' | \ - node index.js --json | \ + node index.js --raw --stdin | \ diff - test-cases/tx1-binary.txt --color ``` @@ -46,7 +46,7 @@ CDC63E1DEE7FE3744630440220143759437C04F7B61F012563AFE90D8DAFC46E86035E1D965A9CED For a friendlier display, you could pipe the output of the serializer to a file and use a visual tool like [Meld](http://meldmerge.org/) that shows intra-line differences: ```bash -$ cat test-cases/tx1.json | sed -e 's/"Fee": "10"/"Fee": "100"/' | node index.js --stdin --stdin > /tmp/tx1-modified.txt && meld /tmp/tx1-modified.txt test-cases/tx1-binary.txt +$ cat test-cases/tx1.json | sed -e 's/"Fee": "10"/"Fee": "100"/' | node index.js --stdin > /tmp/tx1-modified.txt && meld /tmp/tx1-modified.txt test-cases/tx1-binary.txt ``` ![Meld screenshot showing the `0A` / `64` difference](meld-example.png)