Merge pull request #1673 from AlexanderBuzz/DGE-94_javascript-code-samples_tx_ser

Added tx-serialization JavaScript examples analog to python version
This commit is contained in:
jonathanlei
2023-02-03 11:49:33 -08:00
committed by GitHub
14 changed files with 2608 additions and 1 deletions

View File

@@ -0,0 +1,31 @@
# 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
### Raw output without formatting, use --raw or -r:
node index.js -r
### Pick JSON fixture file:
node index.js -f test-cases/tx3.json
### Feed JSON as CLI argument:
node index.js -j "{\"TransactionType\":\"Payment\"}"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,63 @@
'use strict'
// Organize imports
const fs = require("fs")
const parseArgs = require('minimist')
const TxSerializer = require('./tx-serializer')
function main(rawJson, verbose) {
const json = JSON.parse(rawJson)
_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)
_pretty('\nSerialized Transaction:', '\x1b[37m%s\x1b[0m')
console.log(serializedTx.toUpperCase())
}
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
main(rawJson, args.verbose)
} else if (args.stdin) {
const stdin = process.openStdin();
let data = ""
stdin.on('data', function(chunk) {
data += chunk
});
stdin.on('end', function() {
main(data, args.verbose)
});
} else {
rawJson = fs.readFileSync(args.filename, 'utf8')
main(rawJson, args.verbose)
}

View File

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

View File

@@ -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 the transaction serialization code.
For example (starting from the `tx-serialization/js/` dir above this one):
```bash
$ node index.js -f test-cases/tx2.json | \
diff - test-cases/tx2-binary.txt
```
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:
```bash
$ cat test-cases/tx1.json | \
sed -e 's/"Fee": "10"/"Fee": "100"/' | \
node index.js --raw --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"/' | 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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -0,0 +1 @@
120007220008000024001ABED82A2380BF2C2019001ABED764D55920AC9391400000000000000000000000000055534400000000000A20B3C85F482532A9578DBB3950B85CA06594D165400000037E11D60068400000000000000A732103EE83BB432547885C219634A1BC407A9DB0474145D69737D09CCDC63E1DEE7FE3744630440220143759437C04F7B61F012563AFE90D8DAFC46E86035E1D965A9CED282C97D4CE02204CFD241E86F17E011298FC1A39B63386C74306A5DE047E213B0F29EFA4571C2C8114DD76483FACDEE26E60D8A586BB58D09F27045C46

View File

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

View File

@@ -0,0 +1 @@
1200022280000000240000000120190000000B68400000000000277573210268D79CD579D077750740FA18A2370B7C2018B2714ECE70BA65C38D223E79BC9C74473045022100F06FB54049D6D50142E5CF2E2AC21946AF305A13E2A2D4BA881B36484DD01A540220311557EC8BEF536D729605A4CB4D4DC51B1E37C06C93434DD5B7651E1E2E28BF811452C7F01AD13B3CA9C1D133FA8F3482D2EF08FA7D82145A380FBD236B6A1CD14B939AD21101E5B6B6FFA2F9EA7D0F04C4D46544659A2D58525043686174E1F1

View File

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

View File

@@ -0,0 +1 @@
1200002200000000240000034A201B009717BE61400000000098968068400000000000000C69D4564B964A845AC0000000000000000000000000555344000000000069D33B18D53385F8A3185516C2EDA5DEDB8AC5C673210379F17CFA0FFD7518181594BE69FE9A10471D6DE1F4055C6D2746AFD6CF89889E74473045022100D55ED1953F860ADC1BC5CD993ABB927F48156ACA31C64737865F4F4FF6D015A80220630704D2BD09C8E99F26090C25F11B28F5D96A1350454402C2CED92B39FFDBAF811469D33B18D53385F8A3185516C2EDA5DEDB8AC5C6831469D33B18D53385F8A3185516C2EDA5DEDB8AC5C6F9EA7C06636C69656E747D077274312E312E31E1F1011201F3B1997562FD742B54D4EBDEA1D6AEA3D4906B8F100000000000000000000000000000000000000000FF014B4E9C06F24296074F7BC48F92A97916C6DC5EA901DD39C650A96EDA48334E70CC4A85B8B2E8502CD310000000000000000000000000000000000000000000

View File

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

View File

@@ -0,0 +1,689 @@
'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").toUpperCase())
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) {
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
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.toUpperCase())
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.toUpperCase())
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)
// 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
}
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)
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
}
/**
* 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