mirror of
https://github.com/XRPLF/xrpl-dev-portal.git
synced 2025-11-22 12:45:50 +00:00
Serialization: implement issued currency amounts, pathsets
This commit is contained in:
@@ -9,6 +9,7 @@ import logging
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from address import decode_address
|
from address import decode_address
|
||||||
|
from xrpl_num import IssuedAmount
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
logger.addHandler(logging.StreamHandler())
|
logger.addHandler(logging.StreamHandler())
|
||||||
@@ -81,6 +82,11 @@ def bytes_from_uint(i, bits):
|
|||||||
return i.to_bytes(bits // 8, byteorder="big", signed=False)
|
return i.to_bytes(bits // 8, byteorder="big", signed=False)
|
||||||
|
|
||||||
def amount_to_bytes(a):
|
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:
|
if type(a) == str:
|
||||||
# is XRP
|
# is XRP
|
||||||
xrp_amt = int(a)
|
xrp_amt = int(a)
|
||||||
@@ -95,32 +101,42 @@ def amount_to_bytes(a):
|
|||||||
if sorted(a.keys()) != ["currency", "issuer", "value"]:
|
if sorted(a.keys()) != ["currency", "issuer", "value"]:
|
||||||
raise ValueError("amount must have currency, value, issuer only (actually had: %s)" %
|
raise ValueError("amount must have currency, value, issuer only (actually had: %s)" %
|
||||||
sorted(a.keys()))
|
sorted(a.keys()))
|
||||||
#TODO: canonicalize mantissa/exponent/etc. of issued currency amount
|
|
||||||
# https://developers.ripple.com/currency-formats.html#issued-currency-math
|
issued_amt = IssuedAmount(a["value"]).to_bytes()
|
||||||
# temporarily returning all zeroes
|
logger.debug("Issued amount: %s"%issued_amt.hex())
|
||||||
issued_amt = bytes(8)
|
|
||||||
currency_code = currency_code_to_bytes(a["currency"])
|
currency_code = currency_code_to_bytes(a["currency"])
|
||||||
return issued_amt + currency_code + decode_address(a["issuer"])
|
return issued_amt + currency_code + decode_address(a["issuer"])
|
||||||
else:
|
else:
|
||||||
raise ValueError("amount must be XRP string or {currency, value, issuer}")
|
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):
|
def tx_type_to_bytes(txtype):
|
||||||
type_uint = DEFINITIONS["TRANSACTION_TYPES"][txtype]
|
type_uint = DEFINITIONS["TRANSACTION_TYPES"][txtype]
|
||||||
return type_uint.to_bytes(2, byteorder="big", signed=False)
|
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):
|
if re.match(r"^[A-Za-z0-9?!@#$%^&*<>(){}\[\]|]{3}$", code_string):
|
||||||
# ISO 4217-like code
|
# ISO 4217-like code
|
||||||
if code_string == "XRP":
|
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")
|
raise ValueError("issued currency can't be XRP")
|
||||||
|
|
||||||
code_ascii = code_string.encode("ASCII")
|
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
|
# standard currency codes: https://developers.ripple.com/currency-formats.html#standard-currency-codes
|
||||||
# 8 bits type code (0x00)
|
# 8 bits type code (0x00)
|
||||||
# 96 bits reserved (0's)
|
# 88 bits reserved (0's)
|
||||||
# 24 bits ASCII
|
# 24 bits ASCII
|
||||||
# 8 bits version (0x00)
|
# 16 bits version (0x00)
|
||||||
# 24 bits reserved (0's)
|
# 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):
|
elif re.match(r"^[0-9a-fA-F]{40}$", code_string):
|
||||||
# raw hex code
|
# raw hex code
|
||||||
return bytes.fromhex(code_string) # requires Python 3.5+
|
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"))
|
fields_as_bytes.append(field_id("ObjectEndMarker"))
|
||||||
return b''.join(fields_as_bytes)
|
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):
|
def field_to_bytes(field_name, field_val):
|
||||||
"""
|
"""
|
||||||
@@ -221,7 +289,7 @@ def field_to_bytes(field_name, field_val):
|
|||||||
"Hash128": hash_to_bytes,
|
"Hash128": hash_to_bytes,
|
||||||
"Hash160": hash_to_bytes,
|
"Hash160": hash_to_bytes,
|
||||||
"Hash256": hash_to_bytes,
|
"Hash256": hash_to_bytes,
|
||||||
# TODO: PathSet
|
"PathSet": pathset_to_bytes,
|
||||||
"STArray": array_to_bytes,
|
"STArray": array_to_bytes,
|
||||||
"STObject": object_to_bytes,
|
"STObject": object_to_bytes,
|
||||||
"UInt8" : lambda x:bytes_from_uint(x, 8),
|
"UInt8" : lambda x:bytes_from_uint(x, 8),
|
||||||
@@ -270,7 +338,7 @@ if __name__ == "__main__":
|
|||||||
# "Sequence": 2
|
# "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)
|
example_tx = json.load(f)
|
||||||
|
|
||||||
serialize_tx(example_tx)
|
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