Serialization: clean up sample code, add CLI usage

This commit is contained in:
mDuo13
2018-12-03 18:12:59 -08:00
parent 1904ce1de9
commit 8342ddc16a
8 changed files with 254 additions and 314 deletions

View File

@@ -4,9 +4,11 @@
# Author: rome@ripple.com
# Copyright Ripple 2018
import argparse
import json
import logging
import re
import sys
from address import decode_address
from xrpl_num import IssuedAmount
@@ -15,6 +17,12 @@ logger = logging.getLogger(__name__)
logger.addHandler(logging.StreamHandler())
def load_defs(fname="definitions.json"):
"""
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.)
"""
with open(fname) as definitions_file:
definitions = json.load(definitions_file)
return {
@@ -53,33 +61,71 @@ def field_id(field_name):
# high 4 bits is the type_code
# low 4 bits is the field code
combined_code = (type_code << 4) | field_code
return bytes_from_uint(combined_code, 8)
return uint8_to_bytes(combined_code)
elif type_code >= 16 and field_code < 16:
# first 4 bits are zeroes
# next 4 bits is field code
# next byte is type code
byte1 = bytes_from_uint(field_code, 8)
byte2 = bytes_from_uint(type_code, 8)
byte1 = uint8_to_bytes(field_code)
byte2 = uint8_to_bytes(type_code)
return b''.join( (byte1, byte2) )
elif type_code < 16 and field_code >= 16:
# first 4 bits is type code
# next 4 bits are zeroes
# next byte is field code
byte1 = bytes_from_uint(type_code << 4, 8)
byte2 = bytes_from_uint(field_code, 8)
byte1 = uint8_to_bytes(type_code << 4)
byte2 = uint8_to_bytes(field_code)
return b''.join( (byte1, byte2) )
else: # both are >= 16
# first byte is all zeroes
# second byte is type
# third byte is field code
byte2 = bytes_from_uint(type_code, 8)
byte3 = bytes_from_uint(field_code, 8)
byte2 = uint8_to_bytes(type_code)
byte3 = uint8_to_bytes(field_code)
return b''.join( (bytes(1), byte2, byte3) )
def bytes_from_uint(i, bits):
if bits % 8:
raise ValueError("bytes_from_uint: bits must be divisible by 8")
return i.to_bytes(bits // 8, byteorder="big", signed=False)
def vl_encode(vl_contents):
"""
Helper function for variable-length 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
"""
vl_len = len(vl_contents)
if vl_len <= 192:
len_byte = vl_len.to_bytes(1, byteorder="big", signed=False)
return b''.join( (len_byte, vl_contents) )
elif vl_len <= 12480:
vl_len -= 193
byte1 = ((vl_len >> 8) + 193).to_bytes(1, byteorder="big", signed=False)
byte2 = (vl_len & 0xff).to_bytes(1, byteorder="big", signed=False)
return b''.join( (byte1, byte2, vl_contents) )
elif vl_len <= 918744:
vl_len -= 12481
byte1 = (241 + (vl_len >> 16)).to_bytes(1, byteorder="big", signed=False)
byte2 = ((vl_len >> 8) & 0xff).to_bytes(1, byteorder="big", signed=False)
byte3 = (vl_len & 0xff).to_bytes(1, byteorder="big", signed=False)
return b''.join( (byte1, byte2, byte3, vl_contents) )
raise ValueError("VariableLength field must be <= 918744 bytes long")
# Individual field type serialization routines ---------------------------------
def accountid_to_bytes(address):
"""
Serialize an AccountID field type. These are variable-length encoded.
Some fields contain nested non-VL-encoded AccountIDs directly; those call
decode_address() instead of this function.
"""
return vl_encode(decode_address(address))
def amount_to_bytes(a):
"""
@@ -109,13 +155,38 @@ def amount_to_bytes(a):
else:
raise ValueError("amount must be XRP string or {currency, value, issuer}")
def issued_amount_as_bytes(strnum):
num = Decimal(strnum)
def array_to_bytes(array):
"""
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"
}
}
]
"""
members_as_bytes = []
for el in array:
wrapper_key = list(el.keys())[0]
inner_obj = el[wrapper_key]
members_as_bytes.append(field_to_bytes(field_name=wrapper_key, field_val=el))
members_as_bytes.append(field_id("ArrayEndMarker"))
return b''.join(members_as_bytes)
def tx_type_to_bytes(txtype):
type_uint = DEFINITIONS["TRANSACTION_TYPES"][txtype]
return type_uint.to_bytes(2, byteorder="big", signed=False)
def blob_to_bytes(field_val):
"""
Serializes a string of hex as binary data with a variable-length encoded
length prefix. (The prefix is 1-3 bytes depending on the length of the
contents.)
"""
vl_contents = bytes.fromhex(field_val)
return vl_encode(vl_contents)
def currency_code_to_bytes(code_string, xrp_ok=False):
if re.match(r"^[A-Za-z0-9?!@#$%^&*<>(){}\[\]|]{3}$", code_string):
@@ -143,54 +214,43 @@ def currency_code_to_bytes(code_string, xrp_ok=False):
else:
raise ValueError("invalid currency code")
def vl_encode(vl_contents):
vl_len = len(vl_contents)
if vl_len <= 192:
len_byte = vl_len.to_bytes(1, byteorder="big", signed=False)
return b''.join( (len_byte, vl_contents) )
elif vl_len <= 12480:
vl_len -= 193
byte1 = ((vl_len >> 8) + 193).to_bytes(1, byteorder="big", signed=False)
byte2 = (vl_len & 0xff).to_bytes(1, byteorder="big", signed=False)
return b''.join( (byte1, byte2, vl_contents) )
elif vl_len <= 918744:
vl_len -= 12481
byte1 = (241 + (vl_len >> 16)).to_bytes(1, byteorder="big", signed=False)
byte2 = ((vl_len >> 8) & 0xff).to_bytes(1, byteorder="big", signed=False)
byte3 = (vl_len & 0xff).to_bytes(1, byteorder="big", signed=False)
return b''.join( (byte1, byte2, byte3, vl_contents) )
def hash128_to_bytes(contents):
"""
Serializes a hexadecimal string as binary and confirms that it's 128 bits
"""
b = hash_to_bytes(contents)
if len(b) != 16: # 16 bytes = 128 bits
raise ValueError("Hash128 is not 128 bits long")
return b
raise ValueError("VariableLength field must be <= 918744 bytes long")
def hash160_to_bytes(contents):
b = hash_to_bytes(contents)
if len(b) != 20: # 20 bytes = 160 bits
raise ValueError("Hash160 is not 160 bits long")
return b
def vl_to_bytes(field_val):
vl_contents = bytes.fromhex(field_val)
return vl_encode(vl_contents)
def hash256_to_bytes(contents):
b = hash_to_bytes(contents)
if len(b) != 32: # 32 bytes = 256 bits
raise ValueError("Hash256 is not 256 bits long")
return b
def hash_to_bytes(contents):
"""
Helper function; serializes a hash value from a hexadecimal string
of any length.
"""
return bytes.fromhex(field_val)
def accountid_to_bytes(address):
return vl_encode(decode_address(address))
def array_to_bytes(array):
"""
Serialize an array of objects.
Each member object must have a type wrapper and an inner object.
"""
members_as_bytes = []
for el in array:
wrapper_key = list(el.keys())[0]
inner_obj = el[wrapper_key]
members_as_bytes.append(field_to_bytes(field_name=wrapper_key, field_val=el))
members_as_bytes.append(field_id("ArrayEndMarker"))
return b''.join(members_as_bytes)
def object_to_bytes(obj):
"""
Serialize an object, assuming a type wrapper, for example:
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
}
@@ -260,11 +320,31 @@ def path_as_bytes(path):
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
step_data = [uint8_to_bytes(type_byte)] + step_data
path_contents += step_data
return b''.join(path_contents)
def tx_type_to_bytes(txtype):
"""
TransactionType field is a special case that is written in JSON
as a string name but in binary as a UInt16.
"""
type_uint = DEFINITIONS["TRANSACTION_TYPES"][txtype]
return uint16_to_bytes(type_uint)
def uint8_to_bytes(i):
return i.to_bytes(1, byteorder="big", signed=False)
def uint16_to_bytes(i):
return i.to_bytes(2, byteorder="big", signed=False)
def uint32_to_bytes(i):
return i.to_bytes(4, byteorder="big", signed=False)
# Core serialization logic -----------------------------------------------------
def field_to_bytes(field_name, field_val):
"""
@@ -285,22 +365,51 @@ def field_to_bytes(field_name, field_val):
# TypeName: function(field): bytes object
"AccountID": accountid_to_bytes,
"Amount": amount_to_bytes,
"Blob": vl_to_bytes,
"Hash128": hash_to_bytes,
"Hash160": hash_to_bytes,
"Hash256": hash_to_bytes,
"Blob": blob_to_bytes,
"Hash128": hash128_to_bytes,
"Hash160": hash160_to_bytes,
"Hash256": hash256_to_bytes,
"PathSet": pathset_to_bytes,
"STArray": array_to_bytes,
"STObject": object_to_bytes,
"UInt8" : lambda x:bytes_from_uint(x, 8),
"UInt16": lambda x:bytes_from_uint(x, 16),
"UInt32": lambda x:bytes_from_uint(x, 32),
"UInt64": lambda x:bytes_from_uint(x, 64),
"UInt8" : uint8_to_bytes,
"UInt16": uint16_to_bytes,
"UInt32": uint32_to_bytes,
}
field_binary = dispatch[field_type](field_val)
return b''.join( (id_prefix, field_binary) )
def serialize_tx(tx):
def serialize_tx(tx, for_signing=False):
"""
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
}
"""
field_order = sorted(tx.keys(), key=field_sort_key)
logger.debug("Canonical field order: %s" % field_order)
@@ -313,41 +422,39 @@ def serialize_tx(tx):
fields_as_bytes.append(field_bytes)
all_serial = b''.join(fields_as_bytes)
logger.info(all_serial.hex().upper())
logger.debug(all_serial.hex().upper())
return all_serial
# Startup stuff ----------------------------------------------------------------
logger.setLevel(logging.WARNING)
DEFINITIONS = load_defs()
################################################################################
# Commandline utility ----------------------------------------------------------
# parses JSON from a file or commandline argument and prints the serialized
# form of the transaction as hex
if __name__ == "__main__":
logger.setLevel(logging.DEBUG)
DEFINITIONS = load_defs()
p = argparse.ArgumentParser()
txsource = p.add_mutually_exclusive_group()
txsource.add_argument("-f", "--filename", default="test-cases/tx1.json",
help="Read input transaction from a JSON file. (Uses test-cases/tx1.json by default)")
txsource.add_argument("-j", "--json",
help="Read input transaction JSON from the command line")
txsource.add_argument("--stdin", action="store_true", default=False,
help="Read input transaction JSON from standard input (stdin)")
p.add_argument("-v", "--verbose", action="store_true", default=False,
help="Display debug messages (such as individual field serializations)")
args = p.parse_args()
# example_tx = {
# "TransactionType" : "Payment",
# "Account" : "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
# "Destination" : "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
# "Amount" : {
# "currency" : "USD",
# "value" : "1",
# "issuer" : "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"
# },
# "Fee": "12",
# "Flags": 2147483648,
# "Sequence": 2
# }
if args.verbose:
logger.setLevel(logging.DEBUG)
with open("test-cases/tx3-nometa.json") as f:
example_tx = json.load(f)
# Determine source of JSON transaction:
if args.json:
example_tx = json.loads(args.json)
elif args.stdin:
example_tx = json.load(sys.stdin)
else:
with open(args.filename) as f:
example_tx = json.load(f)
serialize_tx(example_tx)
# example rippled signature:
# ./rippled sign masterpassphrase (the above JSON)
# where "masterpassphrase" is the key behind rHb9...
# snoPBrXtMeMyMHUVTgbuqAfg1SUTb in base58
# "tx_blob" : "1200002280000000240000000261D4838D7EA4C6800000000000000000000000000055534400000000004B4E9C06F24296074F7BC48F92A97916C6DC5EA968400000000000000C73210330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD0207446304402201FE0A74FC1BDB509C8F42B861EF747C43B92917706BB623F0A0D621891933AF402205206FBA8B0BF6733DB5B03AD76B5A76A2D46DF9093916A3BEC78897E58A3DF148114B5F762798A53D543A014CAF8B297CFF8F2F937E883143E9D4A2B8AA0780F682D136F7A56D6724EF53754",
# "SigningPubKey" : "0330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD020",
# "TxnSignature" : "304402201FE0A74FC1BDB509C8F42B861EF747C43B92917706BB623F0A0D621891933AF402205206FBA8B0BF6733DB5B03AD76B5A76A2D46DF9093916A3BEC78897E58A3DF14",
# "hash" : "8BA1509E4FB80CCF76CD9DE924B8B71597637C775BA2DC515F90C333DA534BF3"
print(serialize_tx(example_tx).hex().upper())

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 transaction serialization code.
For example (starting from the `tx-serialization/` dir above this one):
```bash
$ python3 serialize.py -f test-cases/tx2.json | \
diff - test-cases/tx2-binary.txt
```
The expected result is no output because the output of `serialize.py` matches
the contents of `test-cases/tx2-binary.txt` exactly.
For an example of how the output is different if you change the `Fee` parameter of sample transaction 1, we can pipe a modified version of the file into the serializer:
```bash
$ cat test-cases/tx1.json | \
sed -e 's/"Fee": "10"/"Fee": "100"/' | \
python3 serialize.py --stdin | \
diff - test-cases/tx1-binary.txt --color
```
The output shows that the two versions of the transaction binary are different (but because they're all on one line, it's not super clear _where_ within the line the difference is):
```text
1c1
< 120007220008000024001ABED82A2380BF2C2019001ABED764D55920AC93914000000000000000
00000000000055534400000000000A20B3C85F482532A9578DBB3950B85CA06594D165400000037E
11D600684000000000000064732103EE83BB432547885C219634A1BC407A9DB0474145D69737D09C
CDC63E1DEE7FE3744630440220143759437C04F7B61F012563AFE90D8DAFC46E86035E1D965A9CED
282C97D4CE02204CFD241E86F17E011298FC1A39B63386C74306A5DE047E213B0F29EFA4571C2C81
14DD76483FACDEE26E60D8A586BB58D09F27045C46
---
> 120007220008000024001ABED82A2380BF2C2019001ABED764D55920AC93914000000000000000
00000000000055534400000000000A20B3C85F482532A9578DBB3950B85CA06594D165400000037E
11D60068400000000000000A732103EE83BB432547885C219634A1BC407A9DB0474145D69737D09C
CDC63E1DEE7FE3744630440220143759437C04F7B61F012563AFE90D8DAFC46E86035E1D965A9CED
282C97D4CE02204CFD241E86F17E011298FC1A39B63386C74306A5DE047E213B0F29EFA4571C2C81
14DD76483FACDEE26E60D8A586BB58D09F27045C46
```
(If you're curious, the difference appears in the third line of each blob in this example. The modified version of the transaction serializes the `Fee` amount ending in `64` (hex for 100) while the original version ended in `0A` (hex for 10).)
For a friendlier display, you could pipe the output of the serializer to a file and use a visual tool like [Meld](http://meldmerge.org/) that shows intra-line differences:
```bash
$ cat test-cases/tx1.json | sed -e 's/"Fee": "10"/"Fee": "100"/' | python3 serialize.py --stdin > /tmp/tx1-modified.txt && meld /tmp/tx1-modified.txt test-cases/tx1-binary.txt
```
![Meld screenshot showing the `0A` / `64` difference](meld-example.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -1,125 +0,0 @@
{
"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",
"metaData": {
"AffectedNodes": [
{
"ModifiedNode": {
"FinalFields": {
"Flags": 0,
"IndexNext": "0000000000000000",
"IndexPrevious": "0000000000000000",
"Owner": "rMBzp8CgpE441cp5PVyA9rpVV7oT8hP3ys",
"RootIndex": "10AF5737F535F47CA9E8B6F82C4B7F4D998B1B7C44185C6078A22C751FD9FB7D"
},
"LedgerEntryType": "DirectoryNode",
"LedgerIndex": "10AF5737F535F47CA9E8B6F82C4B7F4D998B1B7C44185C6078A22C751FD9FB7D"
}
},
{
"DeletedNode": {
"FinalFields": {
"Account": "rMBzp8CgpE441cp5PVyA9rpVV7oT8hP3ys",
"BookDirectory": "DFA3B6DDAB58C7E8E5D944E736DA4B7046C30E4F460FD9DE4E10BFD011CB2800",
"BookNode": "0000000000000000",
"Expiration": 595640096,
"Flags": 131072,
"OwnerNode": "0000000000000000",
"PreviousTxnID": "50C2CBD3BEF831D80C2950D3001E67F1D257665569A9D77B1F0E0B8B4D178CEB",
"PreviousTxnLgrSeq": 43010795,
"Sequence": 1752791,
"TakerGets": "15000000000",
"TakerPays": {
"currency": "USD",
"issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B",
"value": "7071.75"
}
},
"LedgerEntryType": "Offer",
"LedgerIndex": "233E9A034C083E895EF7B4F6A643291FEF1608D55C8C5783F71E9C5F82D3E7FB"
}
},
{
"ModifiedNode": {
"FinalFields": {
"Account": "rMBzp8CgpE441cp5PVyA9rpVV7oT8hP3ys",
"Balance": "37180946255",
"Flags": 0,
"OwnerCount": 6,
"Sequence": 1752793
},
"LedgerEntryType": "AccountRoot",
"LedgerIndex": "9EB65374048F2AED1995A6725D4234545432083B0C5728627E06443A8E1F4C98",
"PreviousFields": {
"Balance": "37180946265",
"Sequence": 1752792
},
"PreviousTxnID": "50C2CBD3BEF831D80C2950D3001E67F1D257665569A9D77B1F0E0B8B4D178CEB",
"PreviousTxnLgrSeq": 43010795
}
},
{
"DeletedNode": {
"FinalFields": {
"ExchangeRate": "4E10BFD011CB2800",
"Flags": 0,
"RootIndex": "DFA3B6DDAB58C7E8E5D944E736DA4B7046C30E4F460FD9DE4E10BFD011CB2800",
"TakerGetsCurrency": "0000000000000000000000000000000000000000",
"TakerGetsIssuer": "0000000000000000000000000000000000000000",
"TakerPaysCurrency": "0000000000000000000000005553440000000000",
"TakerPaysIssuer": "0A20B3C85F482532A9578DBB3950B85CA06594D1"
},
"LedgerEntryType": "DirectoryNode",
"LedgerIndex": "DFA3B6DDAB58C7E8E5D944E736DA4B7046C30E4F460FD9DE4E10BFD011CB2800"
}
},
{
"CreatedNode": {
"LedgerEntryType": "DirectoryNode",
"LedgerIndex": "DFA3B6DDAB58C7E8E5D944E736DA4B7046C30E4F460FD9DE4E10C0730D0B8000",
"NewFields": {
"ExchangeRate": "4E10C0730D0B8000",
"RootIndex": "DFA3B6DDAB58C7E8E5D944E736DA4B7046C30E4F460FD9DE4E10C0730D0B8000",
"TakerPaysCurrency": "0000000000000000000000005553440000000000",
"TakerPaysIssuer": "0A20B3C85F482532A9578DBB3950B85CA06594D1"
}
}
},
{
"CreatedNode": {
"LedgerEntryType": "Offer",
"LedgerIndex": "FD6C2E2D72319FB0C22FC50B0AF993B2AF26717927EAEB1E8857971EE2C3CADD",
"NewFields": {
"Account": "rMBzp8CgpE441cp5PVyA9rpVV7oT8hP3ys",
"BookDirectory": "DFA3B6DDAB58C7E8E5D944E736DA4B7046C30E4F460FD9DE4E10C0730D0B8000",
"Expiration": 595640108,
"Flags": 131072,
"Sequence": 1752792,
"TakerGets": "15000000000",
"TakerPays": {
"currency": "USD",
"issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B",
"value": "7072.8"
}
}
}
}
],
"TransactionIndex": 0,
"TransactionResult": "tesSUCCESS"
}
}

View File

@@ -1,94 +0,0 @@
{
"hash": "C0B450C8601E19CB0BDED71C4B523B2A4AAB77415B08E7923D8DA3F831631702",
"ledger_index": 36259236,
"date": "2018-02-01T09:45:32+00:00",
"tx": {
"TransactionType": "EscrowFinish",
"Flags": 2147483648,
"Sequence": 1,
"OfferSequence": 11,
"Fee": "10101",
"SigningPubKey": "0268D79CD579D077750740FA18A2370B7C2018B2714ECE70BA65C38D223E79BC9C",
"TxnSignature": "3045022100F06FB54049D6D50142E5CF2E2AC21946AF305A13E2A2D4BA881B36484DD01A540220311557EC8BEF536D729605A4CB4D4DC51B1E37C06C93434DD5B7651E1E2E28BF",
"Account": "r3Y6vCE8XqfZmYBRngy22uFYkmz3y9eCRA",
"Owner": "r9NpyVfLfUG8hatuCCHKzosyDtKnBdsEN3",
"Memos": [
{
"Memo": {
"MemoData": "04C4D46544659A2D58525043686174"
}
}
]
},
"meta": {
"TransactionIndex": 35,
"AffectedNodes": [
{
"DeletedNode": {
"LedgerEntryType": "Escrow",
"LedgerIndex": "983EBDF89C1C30CECDB105B593E7DEBE602AF89012EDB4DD76D24ACEF92C89EF",
"FinalFields": {
"Flags": 0,
"PreviousTxnLgrSeq": 35059511,
"FinishAfter": 570758400,
"OwnerNode": "0000000000000000",
"PreviousTxnID": "6F54E04D7B205CBE2FCAEF1C249E62A9759721C7FE1F6992FD800266C8E4814C",
"Amount": "1000000000000000",
"Account": "r9NpyVfLfUG8hatuCCHKzosyDtKnBdsEN3",
"Destination": "r9NpyVfLfUG8hatuCCHKzosyDtKnBdsEN3"
}
}
},
{
"ModifiedNode": {
"LedgerEntryType": "DirectoryNode",
"LedgerIndex": "C05DC35EBDCA5D8697190FF41950EC5AFCBF7F61C1177DC73000CC17C2149886",
"FinalFields": {
"Flags": 0,
"RootIndex": "C05DC35EBDCA5D8697190FF41950EC5AFCBF7F61C1177DC73000CC17C2149886",
"Owner": "r9NpyVfLfUG8hatuCCHKzosyDtKnBdsEN3"
}
}
},
{
"ModifiedNode": {
"LedgerEntryType": "AccountRoot",
"PreviousTxnLgrSeq": 36230933,
"PreviousTxnID": "9366F75CD350ACE0EEFC0A392ECBD5AC2B84C06E5DEC2DE895B76FFC7BD55553",
"LedgerIndex": "C180CA555CE8820D8F1086CDCA756FF8D5813FEB2AE3FF002B9F48F870CA08A0",
"PreviousFields": {
"Sequence": 1,
"Balance": "26000000"
},
"FinalFields": {
"Flags": 0,
"Sequence": 2,
"OwnerCount": 0,
"Balance": "25989899",
"Account": "r3Y6vCE8XqfZmYBRngy22uFYkmz3y9eCRA"
}
}
},
{
"ModifiedNode": {
"LedgerEntryType": "AccountRoot",
"PreviousTxnLgrSeq": 36253527,
"PreviousTxnID": "F2AA5584005847C19F59A9C87E7BF3108F97F2567C5083159EEC40B08ED90F46",
"LedgerIndex": "DCED5AAFE87AA8D00E651DBBFBA2F992927BC3DC5FFD905EF49019ED02824B3A",
"PreviousFields": {
"OwnerCount": 14,
"Balance": "200923000"
},
"FinalFields": {
"Flags": 1048576,
"Sequence": 18,
"OwnerCount": 13,
"Balance": "1000000200923000",
"Account": "r9NpyVfLfUG8hatuCCHKzosyDtKnBdsEN3"
}
}
}
],
"TransactionResult": "tesSUCCESS"
}
}