Browse Source

Add Decred support (#550)

* Refactor reorg_hashes function

* Add Decred support
patch-2
John L. Jegutanis 7 years ago
committed by Neil
parent
commit
0815ff8e24
  1. 63
      electrumx/lib/coins.py
  2. 61
      electrumx/lib/tx.py
  3. 27
      electrumx/server/block_processor.py
  4. 90
      electrumx/server/daemon.py
  5. 15
      tests/blocks/decred_mainnet_100.json
  6. 21
      tests/blocks/decred_mainnet_120000.json
  7. 25
      tests/lib/test_addresses.py
  8. 60
      tests/test_transactions.py
  9. 79
      tests/transactions/decred_mainnet_9cde4a.json

63
electrumx/lib/coins.py

@ -43,7 +43,7 @@ from electrumx.lib.hash import Base58, hash160, double_sha256, hash_to_hex_str
from electrumx.lib.hash import HASHX_LEN
from electrumx.lib.script import ScriptPubKey, OpCodes
import electrumx.lib.tx as lib_tx
from electrumx.server.block_processor import BlockProcessor
import electrumx.server.block_processor as block_proc
import electrumx.server.daemon as daemon
from electrumx.server.session import ElectrumX, DashElectrumX
@ -69,7 +69,7 @@ class Coin(object):
SESSIONCLS = ElectrumX
DESERIALIZER = lib_tx.Deserializer
DAEMON = daemon.Daemon
BLOCK_PROCESSOR = BlockProcessor
BLOCK_PROCESSOR = block_proc.BlockProcessor
MEMPOOL_HISTOGRAM_REFRESH_SECS = 500
XPUB_VERBYTES = bytes('????', 'utf-8')
XPRV_VERBYTES = bytes('????', 'utf-8')
@ -1676,6 +1676,65 @@ class BitcoinAtom(Coin):
return deserializer.read_header(height, cls.BASIC_HEADER_SIZE)
class Decred(Coin):
NAME = "Decred"
SHORTNAME = "DCR"
NET = "mainnet"
XPUB_VERBYTES = bytes.fromhex("02fda926")
XPRV_VERBYTES = bytes.fromhex("02fda4e8")
P2PKH_VERBYTE = bytes.fromhex("073f")
P2SH_VERBYTES = [bytes.fromhex("071a")]
WIF_BYTE = bytes.fromhex("230e")
GENESIS_HASH = ('298e5cc3d985bfe7f81dc135f360abe0'
'89edd4396b86d2de66b0cef42b21d980')
BASIC_HEADER_SIZE = 180
HEADER_HASH = lib_tx.DeserializerDecred.blake256
DESERIALIZER = lib_tx.DeserializerDecred
DAEMON = daemon.DecredDaemon
BLOCK_PROCESSOR = block_proc.DecredBlockProcessor
ENCODE_CHECK = partial(Base58.encode_check,
hash_fn=lib_tx.DeserializerDecred.blake256d)
DECODE_CHECK = partial(Base58.decode_check,
hash_fn=lib_tx.DeserializerDecred.blake256d)
HEADER_UNPACK = struct.Struct('<i32s32s32sH6sHBBIIQIIII32sI').unpack_from
TX_COUNT = 4629388
TX_COUNT_HEIGHT = 260628
TX_PER_BLOCK = 17
REORG_LIMIT = 1000
RPC_PORT = 9109
@classmethod
def header_hash(cls, header):
'''Given a header return the hash.'''
return cls.HEADER_HASH(header)
@classmethod
def block(cls, raw_block, height):
'''Return a Block namedtuple given a raw block and its height.'''
if height > 0:
return super().block(raw_block, height)
else:
return Block(raw_block, cls.block_header(raw_block, height), [])
@classmethod
def electrum_header(cls, header, height):
labels = ('version', 'prev_block_hash', 'merkle_root', 'stake_root',
'vote_bits', 'final_state', 'voters', 'fresh_stake',
'revocations', 'pool_size', 'bits', 'sbits', 'block_height',
'size', 'timestamp', 'nonce', 'extra_data', 'stake_version')
values = cls.HEADER_UNPACK(header)
h = dict(zip(labels, values))
# Convert some values
assert h['block_height'] == height
h['prev_block_hash'] = hash_to_hex_str(h['prev_block_hash'])
h['merkle_root'] = hash_to_hex_str(h['merkle_root'])
h['stake_root'] = hash_to_hex_str(h['stake_root'])
h['final_state'] = h['final_state'].hex()
h['extra_data'] = h['extra_data'].hex()
return h
class Axe(Dash):
NAME = "Axe"
SHORTNAME = "AXE"

61
electrumx/lib/tx.py

@ -29,6 +29,7 @@
from collections import namedtuple
from struct import pack
from electrumx.lib.hash import sha256, double_sha256, hash_to_hex_str
from electrumx.lib.util import (
@ -428,8 +429,6 @@ class TxInputDcr(namedtuple("TxInput", "prev_hash prev_idx tree sequence")):
@cachedproperty
def is_coinbase(self):
# The previous output of a coin base must have a max value index and a
# zero hash.
return (self.prev_hash == TxInputDcr.ZERO and
self.prev_idx == TxInputDcr.MINUS_1)
@ -440,13 +439,13 @@ class TxInputDcr(namedtuple("TxInput", "prev_hash prev_idx tree sequence")):
class TxOutputDcr(namedtuple("TxOutput", "value version pk_script")):
'''Class representing a transaction output.'''
'''Class representing a Decred transaction output.'''
pass
class TxDcr(namedtuple("Tx", "version inputs outputs locktime expiry "
"witness")):
'''Class representing transaction that has a time field.'''
'''Class representing a Decred transaction.'''
@cachedproperty
def is_coinbase(self):
@ -454,22 +453,38 @@ class TxDcr(namedtuple("Tx", "version inputs outputs locktime expiry "
class DeserializerDecred(Deserializer):
@staticmethod
def blake256(data):
from blake256.blake256 import blake_hash
return blake_hash(data)
@staticmethod
def blake256d(data):
from blake256.blake256 import blake_hash
return blake_hash(blake_hash(data))
def read_tx(self):
return self._read_tx_parts(produce_hash=False)[0]
def read_tx_and_hash(self):
tx, tx_hash, vsize = self._read_tx_parts()
return tx, tx_hash
def read_tx_and_vsize(self):
tx, tx_hash, vsize = self._read_tx_parts(produce_hash=False)
return tx, vsize
def read_tx_block(self):
'''Returns a list of (deserialized_tx, tx_hash) pairs.'''
read_tx = self.read_tx
txs = [read_tx() for _ in range(self._read_varint())]
stxs = [read_tx() for _ in range(self._read_varint())]
read = self.read_tx_and_hash
txs = [read() for _ in range(self._read_varint())]
stxs = [read() for _ in range(self._read_varint())]
return txs + stxs
def _read_inputs(self):
read_input = self._read_input
return [read_input() for i in range(self._read_varint())]
def read_tx_tree(self):
'''Returns a list of deserialized_tx without tx hashes.'''
read_tx = self.read_tx
return [read_tx() for _ in range(self._read_varint())]
def _read_input(self):
return TxInputDcr(
@ -479,10 +494,6 @@ class DeserializerDecred(Deserializer):
self._read_le_uint32(), # sequence
)
def _read_outputs(self):
read_output = self._read_output
return [read_output() for _ in range(self._read_varint())]
def _read_output(self):
return TxOutputDcr(
self._read_le_int64(), # value
@ -502,15 +513,29 @@ class DeserializerDecred(Deserializer):
script = self._read_varbytes()
return value_in, block_height, block_index, script
def read_tx(self):
def _read_tx_parts(self, produce_hash=True):
start = self.cursor
version = self._read_le_int32()
inputs = self._read_inputs()
outputs = self._read_outputs()
locktime = self._read_le_uint32()
expiry = self._read_le_uint32()
no_witness_tx = b'\x01\x00\x01\x00' + self.binary[start+4:self.cursor]
end_prefix = self.cursor
witness = self._read_witness(len(inputs))
# Drop the coinbase-like input from a vote tx as it creates problems
# with UTXOs lookups and mempool management
if inputs[0].is_coinbase and len(inputs) > 1:
inputs = inputs[1:]
if produce_hash:
# TxSerializeNoWitness << 16 == 0x10000
no_witness_header = pack('<I', 0x10000 | (version & 0xffff))
prefix_tx = no_witness_header + self.binary[start+4:end_prefix]
tx_hash = self.blake256(prefix_tx)
else:
tx_hash = None
return TxDcr(
version,
inputs,
@ -518,4 +543,4 @@ class DeserializerDecred(Deserializer):
locktime,
expiry,
witness
), DeserializerDecred.blake256(no_witness_tx)
), tx_hash, self.cursor - start

27
electrumx/server/block_processor.py

@ -263,6 +263,16 @@ class BlockProcessor(electrumx.server.db.DB):
The hashes are returned in order of increasing height. Start
is the height of the first hash, last of the last.
'''
start, count = self.calc_reorg_range(count)
last = start + count - 1
s = '' if count == 1 else 's'
self.logger.info(f'chain was reorganised replacing {count:,d} '
f'block{s} at heights {start:,d}-{last:,d}')
return start, last, self.fs_block_hashes(start, count)
async def calc_reorg_range(self, count):
'''Calculate the reorg range'''
def diff_pos(hashes1, hashes2):
'''Returns the index of the first difference in the hash lists.
@ -291,12 +301,7 @@ class BlockProcessor(electrumx.server.db.DB):
else:
start = (self.height - count) + 1
last = start + count - 1
s = '' if count == 1 else 's'
self.logger.info(f'chain was reorganised replacing {count:,d} '
f'block{s} at heights {start:,d}-{last:,d}')
return start, last, self.fs_block_hashes(start, count)
return start, count
def flush_state(self, batch):
'''Flush chain state to the batch.'''
@ -826,3 +831,13 @@ class BlockProcessor(electrumx.server.db.DB):
self.blocks_event.set()
return True
return False
class DecredBlockProcessor(BlockProcessor):
async def calc_reorg_range(self, count):
start, count = super().calc_reorg_range(count)
if start > 0:
# A reorg in Decred can invalidate the previous block
start -= 1
count += 1
return start, count

90
electrumx/server/daemon.py

@ -17,8 +17,10 @@ from time import strptime
import aiohttp
from electrumx.lib.util import int_to_varint, hex_to_bytes, class_logger
from electrumx.lib.hash import hex_str_to_hash
from electrumx.lib.util import int_to_varint, hex_to_bytes, class_logger, \
unpack_uint16_from
from electrumx.lib.hash import hex_str_to_hash, hash_to_hex_str
from electrumx.lib.tx import DeserializerDecred
from aiorpcx import JSONRPC
@ -365,3 +367,87 @@ class LegacyRPCDaemon(Daemon):
if isinstance(t, int):
return t
return timegm(strptime(t, "%Y-%m-%d %H:%M:%S %Z"))
class DecredDaemon(Daemon):
async def raw_blocks(self, hex_hashes):
'''Return the raw binary blocks with the given hex hashes.'''
params_iterable = ((h, False) for h in hex_hashes)
blocks = await self._send_vector('getblock', params_iterable)
raw_blocks = []
valid_tx_tree = {}
for block in blocks:
# Convert to bytes from hex
raw_block = hex_to_bytes(block)
raw_blocks.append(raw_block)
# Check if previous block is valid
prev = self.prev_hex_hash(raw_block)
votebits = unpack_uint16_from(raw_block[100:102])[0]
valid_tx_tree[prev] = self.is_valid_tx_tree(votebits)
processed_raw_blocks = []
for hash, raw_block in zip(hex_hashes, raw_blocks):
if hash in valid_tx_tree:
is_valid = valid_tx_tree[hash]
else:
# Do something complicated to figure out if this block is valid
header = await self._send_single('getblockheader', (hash, ))
if 'nextblockhash' not in header:
raise DaemonError(f'Could not find next block for {hash}')
next_hash = header['nextblockhash']
next_header = await self._send_single('getblockheader',
(next_hash, ))
is_valid = self.is_valid_tx_tree(next_header['votebits'])
if is_valid:
processed_raw_blocks.append(raw_block)
else:
# If this block is invalid remove the normal transactions
self.logger.info(f'block {hash} is invalidated')
processed_raw_blocks.append(self.strip_tx_tree(raw_block))
return processed_raw_blocks
@staticmethod
def prev_hex_hash(raw_block):
return hash_to_hex_str(raw_block[4:36])
@staticmethod
def is_valid_tx_tree(votebits):
# Check if previous block was invalidated.
return bool(votebits & (1 << 0) != 0)
def strip_tx_tree(self, raw_block):
c = self.coin
assert issubclass(c.DESERIALIZER, DeserializerDecred)
d = c.DESERIALIZER(raw_block, start=c.BASIC_HEADER_SIZE)
d.read_tx_tree() # Skip normal transactions
# Create a fake block without any normal transactions
return raw_block[:c.BASIC_HEADER_SIZE] + b'\x00' + raw_block[d.cursor:]
async def height(self):
height = await super().height()
if height > 0:
# Lie about the daemon height as the current tip can be invalidated
height -= 1
self._height = height
return height
async def mempool_hashes(self):
mempool = await super().mempool_hashes()
# Add current tip transactions to the 'fake' mempool.
real_height = await self._send_single('getblockcount')
tip_hash = await self._send_single('getblockhash', (real_height,))
tip = await self.deserialised_block(tip_hash)
# Add normal transactions except coinbase
mempool += tip['tx'][1:]
# Add stake transactions if applicable
mempool += tip.get('stx', [])
return mempool
def client_session(self):
# FIXME allow self signed certificates
connector = aiohttp.TCPConnector(verify_ssl=False)
return aiohttp.ClientSession(connector=connector)

15
tests/blocks/decred_mainnet_100.json

@ -0,0 +1,15 @@
{
"hash": "0000000000017dd91008ec7c0ea63749b81d9a5188d9efc8d2d8cc0bdcff4d2a",
"size": 382,
"height": 100,
"merkleroot": "5c49629cefa3d5eb640a3236f6e970386e0b0826a5d33d566de36aec534fa93d",
"stakeroot": "0000000000000000000000000000000000000000000000000000000000000000",
"tx": [
"c813acfcad624ccf19e6240358b95cbbb1b728ee94557556dc373cceae1a7e4b"
],
"time": 1454961067,
"nonce": 3396292691,
"bits": "1b01ffff",
"previousblockhash": "000000000000dcecdf2c1ae9bb3e2e3135e7765b1902938ff67e2be489ab8131",
"block": "010000003181ab89e42b7ef68f9302195b76e735312e3ebbe91a2cdfecdc0000000000003da94f53ec6ae36d563dd3a526080b6e3870e9f636320a64ebd5a3ef9c62495c000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000ffff011b00c2eb0b00000000640000007e010000abf1b85653506fca9885f1c26941ecf1010000000000000000000000000000000000000000000000000000000101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff03fa1a981200000000000017a914f5916158e3e2c4551c1796708db8367207ed13bb8700000000000000000000266a2464000000000000000000000000000000000000000000000000000000733ea5b290c04d1fdea1906f0000000000001976a9145b98376242c78de2003e7940d7e44270c39b83eb88ac000000000000000001d8bc28820000000000000000ffffffff0800002f646372642f00"
}

21
tests/blocks/decred_mainnet_120000.json

File diff suppressed because one or more lines are too long

25
tests/lib/test_addresses.py

@ -26,28 +26,31 @@
import pytest
from electrumx.lib.coins import Litecoin, BitcoinCash, Zcash, Emercoin, BitcoinGold
from electrumx.lib.hash import Base58
import electrumx.lib.coins as coins
addresses = [
(BitcoinCash, "13xDKJbjh4acmLpNVr6Lc9hFcXRr9fyt4x",
(coins.BitcoinCash, "13xDKJbjh4acmLpNVr6Lc9hFcXRr9fyt4x",
"206168f5322583ff37f8e55665a4789ae8963532", "b8cb80b26e8932f5b12a7e"),
(BitcoinCash, "3GxRZWkJufR5XA8hnNJgQ2gkASSheoBcmW",
(coins.BitcoinCash, "3GxRZWkJufR5XA8hnNJgQ2gkASSheoBcmW",
"a773db925b09add367dcc253c1f9bbc1d11ec6fd", "062d8515e50cb92b8a3a73"),
(BitcoinGold, "GZjH8pETu5xXd5DTt5VAqS9giooLNoHjnJ",
(coins.BitcoinGold, "GZjH8pETu5xXd5DTt5VAqS9giooLNoHjnJ",
"ae40655d7006806fd668248d10e7822c0b774dab", "3a1af301b378ad92493b17"),
(BitcoinGold, "AXfENBm9FP1PMa8AWnVPZZ4tHEwBiqNZav",
(coins.BitcoinGold, "AXfENBm9FP1PMa8AWnVPZZ4tHEwBiqNZav",
"ae40655d7006806fd668248d10e7822c0b774dab", "cb3db4271432c0ac9f88d5"),
(Emercoin, "ELAeVHQg2mmdTTrTrZSzMgAQyXfC9TSRys",
(coins.Emercoin, "ELAeVHQg2mmdTTrTrZSzMgAQyXfC9TSRys",
"210c4482ad8eacb0d349992973608300677adb15", "d71f2df4ef1b397088d731"),
(Litecoin, "LNBAaWuZmipg29WXfz5dtAm1pjo8FEH8yg",
(coins.Litecoin, "LNBAaWuZmipg29WXfz5dtAm1pjo8FEH8yg",
"206168f5322583ff37f8e55665a4789ae8963532", "b8cb80b26e8932f5b12a7e"),
(Litecoin, "MPAZsQAGrnGWKfQbtFJ2Dfw9V939e7D3E2",
(coins.Litecoin, "MPAZsQAGrnGWKfQbtFJ2Dfw9V939e7D3E2",
"a773db925b09add367dcc253c1f9bbc1d11ec6fd", "062d8515e50cb92b8a3a73"),
(Zcash, "t1LppKe1sfPNDMysGSGuTjxoAsBcvvSYv5j",
(coins.Zcash, "t1LppKe1sfPNDMysGSGuTjxoAsBcvvSYv5j",
"206168f5322583ff37f8e55665a4789ae8963532", "b8cb80b26e8932f5b12a7e"),
(Zcash, "t3Zq2ZrASszCg7oBbio7oXqnfR6dnSWqo76",
(coins.Zcash, "t3Zq2ZrASszCg7oBbio7oXqnfR6dnSWqo76",
"a773db925b09add367dcc253c1f9bbc1d11ec6fd", "062d8515e50cb92b8a3a73"),
(coins.Decred, "DsUZxxoHJSty8DCfwfartwTYbuhmVct7tJu",
"2789d58cfa0957d206f025c2af056fc8a77cebb0", "8cc9b11122272bd7b79a50"),
(coins.Decred, "DcuQKx8BES9wU7C6Q5VmLBjw436r27hayjS",
"f0b4e85100aee1a996f22915eb3c3f764d53779a", "a03c1a27de9ac3b3122e8d"),
]

60
tests/test_transactions.py

@ -0,0 +1,60 @@
# Copyright (c) 2018, John L. Jegutanis
#
# All rights reserved.
#
# See the file "LICENCE" for information about the copyright
# and warranty status of this software.
import json
import os
from binascii import unhexlify
import pytest
from electrumx.lib.coins import Coin
from electrumx.lib.hash import hash_to_hex_str
TRANSACTION_DIR = os.path.join(
os.path.dirname(os.path.realpath(__file__)), 'transactions')
# Find out which db engines to test
# Those that are not installed will be skipped
transactions = []
for name in os.listdir(TRANSACTION_DIR):
try:
name_parts = name.split("_")
coinFound = Coin.lookup_coin_class(name_parts[0], name_parts[1])
with open(os.path.join(TRANSACTION_DIR, name)) as f:
transactions.append((coinFound, json.load(f)))
except Exception as e:
transactions.append(pytest.fail(name))
@pytest.fixture(params=transactions)
def transaction_details(request):
return request.param
def test_transaction(transaction_details):
coin, tx_info = transaction_details
raw_tx = unhexlify(tx_info['hex'])
tx, tx_hash = coin.DESERIALIZER(raw_tx, 0).read_tx_and_hash()
assert tx_info['txid'] == hash_to_hex_str(tx_hash)
vin = tx_info['vin']
for i in range(len(vin)):
assert vin[i]['txid'] == hash_to_hex_str(tx.inputs[i].prev_hash)
assert vin[i]['vout'] == tx.inputs[i].prev_idx
vout = tx_info['vout']
for i in range(len(vout)):
# value pk_script
assert vout[i]['value'] == tx.outputs[i].value
spk = vout[i]['scriptPubKey']
tx_pks = tx.outputs[i].pk_script
assert spk['hex'] == tx_pks.hex()
assert spk['address'] == coin.address_from_script(tx_pks)
assert coin.address_to_hashX(spk['address']) == \
coin.hashX_from_script(tx_pks)

79
tests/transactions/decred_mainnet_9cde4a.json

@ -0,0 +1,79 @@
{
"hex": "01000000022b7a5d0b09429a430ee2d465ff4ec1cec8080f66c59989ef03680dcb26a817460400000000ffffffff4d1bb7488ea38c16542cb80f505cc3d0d1757e3887ba25c8256619ec9ab1c5c00000000000ffffffff09eb3334030000000000001976a914ea9ab497c58159b8b0bf23becd179c076f8fbb9188ac78d0f4020000000000001976a9144820daabe97c3efcd3b34dfa51caf432d68d6a4d88ac541965340000000000001976a91495880e8c485ee28349d9885aa0fe448aa96e237488acb42f94000000000000001976a914a0d1579a51dbb6f26469944711d7e3f2f7a0b26088ac2a01fa020000000000001976a91444386bacdf3d7a353dabb2b334db9506791e83f288ac5c3be50b0000000000001976a9149f66566103e93e3cc29bebfed48450c3869e9a7688acc6fc91000000000000001976a91492ebcb796c918f5db19540726f91ca06a3b7640a88acd2b1c23c0000000000001976a9148806f0ae0b862006e39835d8cf91ac7f9d1c97d588ac6790f5020000000000001976a91418f82896bef63e67e125951bc464362eefdb00bb88ac0000000000000000021ee5573600000000bfd40100010000006a473044022051876acd9f0b716eaf383873db604cd7f39d659db2e909701dcbd655b469121e022036c165ab21c16865bab81696b41b3dcb85eb802867490b034f4cce2ac643b7640121026ea7725f46b9ab3931dd789907e80380eb91771cc681a1711c4468b71589d390ea20fe5300000000bad40100010000006a4730440220401007a7c4ba4b982babb08120b62f260484f61491e557f0a0d7d9c4484acf0e022041288a676750d421c63e09e7c11787a49bfec49e5490202220ddeb3406e2d3da01210371f6b9914081c10adfbac8fde9c9731c29fdb1bb235ba70f28cb0cf5f7268b2e",
"txid": "9cde4a9685fa9e38ccf4da4bda1c1a123b3f2ed2c418f6bc128d7f5fbbee413d",
"vin": [
{
"txid": "4617a826cb0d6803ef8999c5660f08c8cec14eff65d4e20e439a42090b5d7a2b",
"vout": 4
},
{
"txid": "c0c5b19aec196625c825ba87387e75d1d0c35c500fb82c54168ca38e48b71b4d",
"vout": 0
}
],
"vout": [
{
"value": 53752811,
"scriptPubKey": {
"hex": "76a914ea9ab497c58159b8b0bf23becd179c076f8fbb9188ac",
"address": "DsnMNwSYjwgks3c1kWVbaB6npxv8LXnGc8U"
}
},
{
"value": 49598584,
"scriptPubKey": {
"hex": "76a9144820daabe97c3efcd3b34dfa51caf432d68d6a4d88ac",
"address": "DsXYHVzSPfo4jrYAkLJN7yFuAfY3mxA4JGy"
}
},
{
"value": 879040852,
"scriptPubKey": {
"hex": "76a91495880e8c485ee28349d9885aa0fe448aa96e237488ac",
"address": "DsebZAQadwBurUCowGbsbgYTpMETHBibZLf"
}
},
{
"value": 9711540,
"scriptPubKey": {
"hex": "76a914a0d1579a51dbb6f26469944711d7e3f2f7a0b26088ac",
"address": "DsfdEPUTpsCBJjjS8ovgp65uYwws18FQpas"
}
},
{
"value": 49938730,
"scriptPubKey": {
"hex": "76a91444386bacdf3d7a353dabb2b334db9506791e83f288ac",
"address": "DsXBd2eWDHujo7wjYTsUwRw7wHoiYN5Ynhg"
}
},
{
"value": 199572316,
"scriptPubKey": {
"hex": "76a9149f66566103e93e3cc29bebfed48450c3869e9a7688ac",
"address": "DsfVjXTdcisQ6j5c5ZrhLFfJC56ZMCCwkr2"
}
},
{
"value": 9567430,
"scriptPubKey": {
"hex": "76a91492ebcb796c918f5db19540726f91ca06a3b7640a88ac",
"address": "DseMkckQQVxjEu6t2bTD65gW4EY2GiGuhNk"
}
},
{
"value": 1019392466,
"scriptPubKey": {
"hex": "76a9148806f0ae0b862006e39835d8cf91ac7f9d1c97d588ac",
"address": "DsdN9hjHeY8FiDYdEpesfTy215sdN5Ly74v"
}
},
{
"value": 49647719,
"scriptPubKey": {
"hex": "76a91418f82896bef63e67e125951bc464362eefdb00bb88ac",
"address": "DsTEvzPjXppZGYkLXZdZN16FwGaeMEjudpe"
}
}
]
}
Loading…
Cancel
Save