mirror of
https://github.com/XRPLF/xrpl-dev-portal.git
synced 2025-11-19 11:15:49 +00:00
- Restructured code, main logic is now in tx-serializer.js
This commit is contained in:
@@ -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)
|
||||
}
|
||||
677
content/_code-samples/tx-serialization/js/tx-serializer.js
Normal file
677
content/_code-samples/tx-serialization/js/tx-serializer.js
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user