mirror of
https://github.com/XRPLF/xrpl-dev-portal.git
synced 2025-11-22 20:55:50 +00:00
Serialization: implement issued currency amounts, pathsets
This commit is contained in:
@@ -9,6 +9,7 @@ import logging
|
||||
import re
|
||||
|
||||
from address import decode_address
|
||||
from xrpl_num import IssuedAmount
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.addHandler(logging.StreamHandler())
|
||||
@@ -81,6 +82,11 @@ def bytes_from_uint(i, bits):
|
||||
return i.to_bytes(bits // 8, byteorder="big", signed=False)
|
||||
|
||||
def amount_to_bytes(a):
|
||||
"""
|
||||
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
|
||||
"""
|
||||
if type(a) == str:
|
||||
# is XRP
|
||||
xrp_amt = int(a)
|
||||
@@ -95,32 +101,42 @@ def amount_to_bytes(a):
|
||||
if sorted(a.keys()) != ["currency", "issuer", "value"]:
|
||||
raise ValueError("amount must have currency, value, issuer only (actually had: %s)" %
|
||||
sorted(a.keys()))
|
||||
#TODO: canonicalize mantissa/exponent/etc. of issued currency amount
|
||||
# https://developers.ripple.com/currency-formats.html#issued-currency-math
|
||||
# temporarily returning all zeroes
|
||||
issued_amt = bytes(8)
|
||||
|
||||
issued_amt = IssuedAmount(a["value"]).to_bytes()
|
||||
logger.debug("Issued amount: %s"%issued_amt.hex())
|
||||
currency_code = currency_code_to_bytes(a["currency"])
|
||||
return issued_amt + currency_code + decode_address(a["issuer"])
|
||||
else:
|
||||
raise ValueError("amount must be XRP string or {currency, value, issuer}")
|
||||
|
||||
def issued_amount_as_bytes(strnum):
|
||||
num = Decimal(strnum)
|
||||
|
||||
|
||||
def tx_type_to_bytes(txtype):
|
||||
type_uint = DEFINITIONS["TRANSACTION_TYPES"][txtype]
|
||||
return type_uint.to_bytes(2, byteorder="big", signed=False)
|
||||
|
||||
def currency_code_to_bytes(code_string):
|
||||
def currency_code_to_bytes(code_string, xrp_ok=False):
|
||||
if re.match(r"^[A-Za-z0-9?!@#$%^&*<>(){}\[\]|]{3}$", code_string):
|
||||
# ISO 4217-like code
|
||||
if code_string == "XRP":
|
||||
if xrp_ok:
|
||||
# Rare, but when the currency code "XRP" is serialized, it's
|
||||
# a special-case all zeroes.
|
||||
logger.debug("Currency code(XRP): "+("0"*40))
|
||||
return bytes(20)
|
||||
raise ValueError("issued currency can't be XRP")
|
||||
|
||||
code_ascii = code_string.encode("ASCII")
|
||||
logger.debug("Currency code ASCII: %s"%code_ascii.hex())
|
||||
# standard currency codes: https://developers.ripple.com/currency-formats.html#standard-currency-codes
|
||||
# 8 bits type code (0x00)
|
||||
# 96 bits reserved (0's)
|
||||
# 88 bits reserved (0's)
|
||||
# 24 bits ASCII
|
||||
# 8 bits version (0x00)
|
||||
# 16 bits version (0x00)
|
||||
# 24 bits reserved (0's)
|
||||
return b''.join( ( bytes(13), code_ascii, bytes(4) ) )
|
||||
return b''.join( ( bytes(12), code_ascii, bytes(5) ) )
|
||||
elif re.match(r"^[0-9a-fA-F]{40}$", code_string):
|
||||
# raw hex code
|
||||
return bytes.fromhex(code_string) # requires Python 3.5+
|
||||
@@ -197,6 +213,58 @@ def object_to_bytes(obj):
|
||||
fields_as_bytes.append(field_id("ObjectEndMarker"))
|
||||
return b''.join(fields_as_bytes)
|
||||
|
||||
def pathset_to_bytes(pathset):
|
||||
"""
|
||||
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.)
|
||||
"""
|
||||
|
||||
if not len(pathset):
|
||||
raise ValueError("PathSet type must not be empty")
|
||||
|
||||
paths_as_bytes = []
|
||||
for n in range(len(pathset)):
|
||||
path = path_as_bytes(pathset[n])
|
||||
logger.debug("Path %d: %s"%(n, path.hex()))
|
||||
paths_as_bytes.append(path)
|
||||
if n + 1 == len(pathset): # last path; add an end byte
|
||||
paths_as_bytes.append(bytes.fromhex("00"))
|
||||
else: # add a path separator byte
|
||||
paths_as_bytes.append(bytes.fromhex("ff"))
|
||||
|
||||
return b''.join(paths_as_bytes)
|
||||
|
||||
def path_as_bytes(path):
|
||||
"""
|
||||
Helper function for representing one member of a pathset as a bytes object
|
||||
"""
|
||||
|
||||
if not len(path):
|
||||
raise ValueError("Path must not be empty")
|
||||
|
||||
path_contents = []
|
||||
for step in path:
|
||||
step_data = []
|
||||
type_byte = 0
|
||||
if "account" in step.keys():
|
||||
type_byte |= 0x01
|
||||
step_data.append(decode_address(step["account"]))
|
||||
if "currency" in step.keys():
|
||||
type_byte |= 0x10
|
||||
step_data.append(currency_code_to_bytes(step["currency"], xrp_ok=True))
|
||||
if "issuer" in step.keys():
|
||||
type_byte |= 0x20
|
||||
step_data.append(decode_address(step["issuer"]))
|
||||
step_data = [bytes_from_uint(type_byte, 8)] + step_data
|
||||
path_contents += step_data
|
||||
|
||||
return b''.join(path_contents)
|
||||
|
||||
|
||||
def field_to_bytes(field_name, field_val):
|
||||
"""
|
||||
@@ -221,7 +289,7 @@ def field_to_bytes(field_name, field_val):
|
||||
"Hash128": hash_to_bytes,
|
||||
"Hash160": hash_to_bytes,
|
||||
"Hash256": hash_to_bytes,
|
||||
# TODO: PathSet
|
||||
"PathSet": pathset_to_bytes,
|
||||
"STArray": array_to_bytes,
|
||||
"STObject": object_to_bytes,
|
||||
"UInt8" : lambda x:bytes_from_uint(x, 8),
|
||||
@@ -270,7 +338,7 @@ if __name__ == "__main__":
|
||||
# "Sequence": 2
|
||||
# }
|
||||
|
||||
with open("test-cases/tx2-nometa.json") as f:
|
||||
with open("test-cases/tx3-nometa.json") as f:
|
||||
example_tx = json.load(f)
|
||||
|
||||
serialize_tx(example_tx)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
1200002200000000240000034A201B009717BE61400000000098968068400000000000000C69D4564B964A845AC0000000000000000000000000555344000000000069D33B18D53385F8A3185516C2EDA5DEDB8AC5C673210379F17CFA0FFD7518181594BE69FE9A10471D6DE1F4055C6D2746AFD6CF89889E74473045022100D55ED1953F860ADC1BC5CD993ABB927F48156ACA31C64737865F4F4FF6D015A80220630704D2BD09C8E99F26090C25F11B28F5D96A1350454402C2CED92B39FFDBAF811469D33B18D53385F8A3185516C2EDA5DEDB8AC5C6831469D33B18D53385F8A3185516C2EDA5DEDB8AC5C6F9EA7C06636C69656E747D077274312E312E31E1F1011201F3B1997562FD742B54D4EBDEA1D6AEA3D4906B8F100000000000000000000000000000000000000000FF014B4E9C06F24296074F7BC48F92A97916C6DC5EA901DD39C650A96EDA48334E70CC4A85B8B2E8502CD310000000000000000000000000000000000000000000
|
||||
@@ -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"
|
||||
}
|
||||
65
content/_code-samples/tx-serialization/xrpl_num.py
Normal file
65
content/_code-samples/tx-serialization/xrpl_num.py
Normal file
@@ -0,0 +1,65 @@
|
||||
# Serializes issued currency amounts from string number representations,
|
||||
# matching the precision of the XRP Ledger.
|
||||
|
||||
from decimal import getcontext, Decimal
|
||||
|
||||
class IssuedAmount:
|
||||
MIN_MANTISSA = 10**15
|
||||
MAX_MANTISSA = 10**16 - 1
|
||||
MIN_EXP = -96
|
||||
MAX_EXP = 80
|
||||
def __init__(self, strnum):
|
||||
self.context = getcontext()
|
||||
self.context.prec = 15
|
||||
self.context.Emin = self.MIN_EXP
|
||||
self.context.Emax = self.MAX_EXP
|
||||
|
||||
self.dec = Decimal(strnum)
|
||||
|
||||
def to_bytes(self):
|
||||
if self.dec.is_zero():
|
||||
return self.canonical_zero_serial()
|
||||
|
||||
# Convert components to integers ---------------------------------------
|
||||
sign, digits, exp = self.dec.as_tuple()
|
||||
mantissa = int("".join([str(d) for d in digits]))
|
||||
|
||||
# Canonicalize to expected range ---------------------------------------
|
||||
while mantissa < self.MIN_MANTISSA and exp > self.MIN_EXP:
|
||||
mantissa *= 10
|
||||
exp -= 1
|
||||
|
||||
while mantissa > self.MAX_MANTISSA:
|
||||
if exp >= self.MAX_EXP:
|
||||
raise ValueError("amount overflow")
|
||||
mantissa //= 10
|
||||
exp += 1
|
||||
|
||||
if exp < self.MIN_EXP or mantissa < self.MIN_MANTISSA:
|
||||
# Round to zero
|
||||
return self.canonical_zero_serial()
|
||||
|
||||
if exp > self.MAX_EXP or mantissa > self.MAX_MANTISSA:
|
||||
raise ValueError("amount overflow")
|
||||
|
||||
# Convert to bytes -----------------------------------------------------
|
||||
serial = 0x8000000000000000 # "Not XRP" bit set
|
||||
if sign == 0:
|
||||
serial |= 0x4000000000000000 # "Is positive" bit set
|
||||
serial |= ((exp+97) << 54) # next 8 bits are exponent
|
||||
serial |= mantissa # last 54 bits are mantissa
|
||||
|
||||
return serial.to_bytes(8, byteorder="big", signed=False)
|
||||
|
||||
|
||||
def canonical_zero_serial(self):
|
||||
"""
|
||||
Returns canonical format for zero:
|
||||
- "Not XRP" bit = 1
|
||||
- "Sign bit" = 1 (for positive !!)
|
||||
- exponent ???
|
||||
"""
|
||||
# Mantissa is all zeroes. Must be positive zero.
|
||||
#bitval = 0b1100000001000000000000000000000000000000000000000000000000000000
|
||||
#return bitval.to_bytes(8, byteorder="big", signed=False)
|
||||
return (0x8000000000000000).to_bytes(8, byteorder="big", signed=False)
|
||||
Reference in New Issue
Block a user