Initial commit; receive all eth code from chainlib package

This commit is contained in:
nolash
2021-06-28 07:48:36 +02:00
commit f8afc172c6
59 changed files with 4535 additions and 0 deletions

5
tests/Makefile Normal file
View File

@@ -0,0 +1,5 @@
SOLC = /usr/bin/solc
all:
$(SOLC) --bin TestContract.sol --evm-version byzantium | awk 'NR>3' > TestContract.bin
truncate -s -1 TestContract.bin

1
tests/TestContract.bin Normal file
View File

@@ -0,0 +1 @@
608060405234801561001057600080fd5b50610260806100206000396000f3fe608060405234801561001057600080fd5b5060043610610048576000357c01000000000000000000000000000000000000000000000000000000009004806333aa24121461004d575b600080fd5b61006760048036038101906100629190610122565b61007d565b604051610074919061018b565b60405180910390f35b6000827fa8ed44a382304c8db2c9059e4de342080401fb8e9a71986396d595c869fa3792836040516100af91906101a6565b60405180910390a27f33c4ea6ccc21f1a14e9de7326edcc52c2b8a302ac875ae6b8fee2560eadd75ad836040516100e691906101c1565b60405180910390a16001905092915050565b600081359050610107816101fc565b92915050565b60008135905061011c81610213565b92915050565b6000806040838503121561013557600080fd5b60006101438582860161010d565b9250506020610154858286016100f8565b9150509250929050565b610167816101dc565b82525050565b610176816101e8565b82525050565b610185816101f2565b82525050565b60006020820190506101a0600083018461015e565b92915050565b60006020820190506101bb600083018461016d565b92915050565b60006020820190506101d6600083018461017c565b92915050565b60008115159050919050565b6000819050919050565b6000819050919050565b610205816101e8565b811461021057600080fd5b50565b61021c816101f2565b811461022757600080fd5b5056fea26469706673582212205c428504ca5a53a9250d76b287bec8ee4b13d5c7cb386a0f67cbf77ecb96a9ea64736f6c63430008040033

13
tests/TestContract.sol Normal file
View File

@@ -0,0 +1,13 @@
pragma solidity ^0.8.0;
contract TestEventContract {
event TestEventOne(uint256 indexed _foo, bytes32 _bar);
event TestEventTwo(uint256 _foo);
function foo(uint256 _foo, bytes32 _bar) public returns (bool) {
emit TestEventOne(_foo, _bar);
emit TestEventTwo(_foo);
return true;
}
}

66
tests/contract.py Normal file
View File

@@ -0,0 +1,66 @@
# standard imports
import os
# external iports
from chainlib.eth.tx import (
TxFactory,
TxFormat,
receipt,
)
from chainlib.eth.contract import (
ABIContractEncoder,
#ABIContractDecoder,
ABIContractType,
)
from hexathon import add_0x
script_dir = os.path.realpath(os.path.dirname(__file__))
data_dir = script_dir
class TestContract(TxFactory):
__abi = None
__bytecode = None
@staticmethod
def gas(code=None):
return 1000000
@staticmethod
def abi():
if TestContract.__abi == None:
f = open(os.path.join(data_dir, 'TestContract.json'), 'r')
TestContract.__abi = json.load(f)
f.close()
return TestContract.__abi
@staticmethod
def bytecode():
if TestContract.__bytecode == None:
f = open(os.path.join(data_dir, 'TestContract.bin'))
TestContract.__bytecode = f.read()
f.close()
return TestContract.__bytecode
def constructor(self, sender_address, tx_format=TxFormat.JSONRPC, id_generator=None):
code = TestContract.bytecode()
tx = self.template(sender_address, None, use_nonce=True)
tx = self.set_code(tx, code)
return self.finalize(tx, tx_format, id_generator=id_generator)
def foo(self, contract_address, sender_address, x, y, tx_format=TxFormat.JSONRPC, id_generator=None):
enc = ABIContractEncoder()
enc.method('foo')
enc.typ(ABIContractType.UINT256)
enc.typ(ABIContractType.BYTES32)
enc.uint256(x)
enc.bytes32(y)
data = add_0x(enc.get())
tx = self.template(sender_address, contract_address, use_nonce=True)
tx = self.set_code(tx, data)
tx = self.finalize(tx, tx_format, id_generator=id_generator)
return tx

29
tests/test_abi.py Normal file
View File

@@ -0,0 +1,29 @@
from chainlib.eth.contract import (
ABIContractEncoder,
ABIContractType,
)
def test_abi_param():
e = ABIContractEncoder()
e.uint256(42)
e.bytes32('0x666f6f')
e.address('0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef')
e.method('foo')
e.typ(ABIContractType.UINT256)
e.typ(ABIContractType.BYTES32)
e.typ(ABIContractType.ADDRESS)
assert e.types[0] == ABIContractType.UINT256
assert e.types[1] == ABIContractType.BYTES32
assert e.types[2] == ABIContractType.ADDRESS
assert e.contents[0] == '000000000000000000000000000000000000000000000000000000000000002a'
assert e.contents[1] == '0000000000000000000000000000000000000000000000000000000000666f6f'
assert e.contents[2] == '000000000000000000000000deadbeefdeadbeefdeadbeefdeadbeefdeadbeef'
assert e.get() == 'a08f54bb000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000666f6f000000000000000000000000deadbeefdeadbeefdeadbeefdeadbeefdeadbeef'
if __name__ == '__main__':
test_abi_param()

35
tests/test_address.py Normal file
View File

@@ -0,0 +1,35 @@
import unittest
from chainlib.eth.address import (
is_address,
is_checksum_address,
to_checksum,
)
from tests.base import TestBase
class TestChain(TestBase):
def test_chain_spec(self):
checksum_address = '0xEb3907eCad74a0013c259D5874AE7f22DcBcC95C'
plain_address = checksum_address.lower()
self.assertEqual(checksum_address, to_checksum(checksum_address))
self.assertTrue(is_address(plain_address))
self.assertFalse(is_checksum_address(plain_address))
self.assertTrue(is_checksum_address(checksum_address))
self.assertFalse(is_address(plain_address + "00"))
self.assertFalse(is_address(plain_address[:len(plain_address)-2]))
with self.assertRaises(ValueError):
to_checksum(plain_address + "00")
with self.assertRaises(ValueError):
to_checksum(plain_address[:len(plain_address)-2])
if __name__ == '__main__':
unittest.main()

120
tests/test_bloom.py Normal file
View File

@@ -0,0 +1,120 @@
# standard imports
import os
import unittest
import logging
# local imports
from chainlib.eth.unittest.ethtester import EthTesterCase
from chainlib.eth.nonce import RPCNonceOracle
from chainlib.eth.gas import OverrideGasOracle
from chainlib.eth.tx import receipt
from chainlib.eth.block import block_by_number
from chainlib.eth.log import LogBloom
from hexathon import (
strip_0x,
add_0x,
)
# test imports
from tests.contract import TestContract
script_dir = os.path.realpath(os.path.dirname(__file__))
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
#{'blockHash': '0xe657e31045be85cfff8c28af6b4fd6417cace7150c4ebbeb736e638313d8e66d', 'block_hash': '0xe657e31045be85cfff8c28af6b4fd6417cace7150c4ebbeb736e638313d8e66d', 'blockNumber': '0xc1ee5a', 'block_number': '0xc1ee5a', 'contractAddress': None, 'contract_address': None, 'cumulativeGasUsed': '0xbc659', 'cumulative_gas_used': '0xbc659', 'from': '0xf6025e63cee5e436a5f1486e040aeead7e97b745', 'gasUsed': '0x1dddb', 'gas_used': '0x1dddb', 'logs': [
#{'address': '0x4e58ab12d2051ea2068e78e4fcee7ddee6785848', 'blockHash': '0xe657e31045be85cfff8c28af6b4fd6417cace7150c4ebbeb736e638313d8e66d', 'blockNumber': '0xc1ee5a', 'data': '0x', 'logIndex': '0xd', 'removed': False, 'topics': ['0x92e98423f8adac6e64d0608e519fd1cefb861498385c6dee70d58fc926ddc68c', '0x0000000000000000000000000000000000000000000000000000000005f6aa5a', '0x00000000000000000000000000000000000000000000000000000000000000d6', '0x000000000000000000000000f6025e63cee5e436a5f1486e040aeead7e97b745'], 'transactionHash': '0xd0f039591953d277d55f628694248cb442590fab95ac53fcfb69e9dbba7db97a', 'transactionIndex': '0xe'},
#{'address': '0x4e58ab12d2051ea2068e78e4fcee7ddee6785848', 'blockHash': '0xe657e31045be85cfff8c28af6b4fd6417cace7150c4ebbeb736e638313d8e66d', 'blockNumber': '0xc1ee5a', 'data': '0x0000000000000000000000000000000000000000000000000000000060d7119f', 'logIndex': '0xe', 'removed': False, 'topics': ['0x0559884fd3a460db3073b7fc896cc77986f16e378210ded43186175bf646fc5f', '0x0000000000000000000000000000000000000000000000000000000005f6aa5a', '0x00000000000000000000000000000000000000000000000000000000000000d6'], 'transactionHash': '0xd0f039591953d277d55f628694248cb442590fab95ac53fcfb69e9dbba7db97a', 'transactionIndex': '0xe'},
#{'address': '0x4e58ab12d2051ea2068e78e4fcee7ddee6785848', 'blockHash': '0xe657e31045be85cfff8c28af6b4fd6417cace7150c4ebbeb736e638313d8e66d', 'blockNumber': '0xc1ee5a', 'data': '0x', 'logIndex': '0xf', 'removed': False, 'topics': ['0xfe25c73e3b9089fac37d55c4c7efcba6f04af04cebd2fc4d6d7dbb07e1e5234f', '0x0000000000000000000000000000000000000000000000813b65aa80e5770000'], 'transactionHash': '0xd0f039591953d277d55f628694248cb442590fab95ac53fcfb69e9dbba7db97a', 'transactionIndex': '0xe'}]
#, 'logsBloom': '0x0000000000000000000000000000000000000080000000000000000000c000000000000000408000000000000000000000000000000200080000000000000000100000000000000000000000000000000000000000000200000020000000000000000000000000800000400000000000400000000400000000000400100000000000000000000000000000000000000000000480000000000000000000000000000000000000000000000000008000000000080000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000002008000000000000000', 'logs_bloom': '0x0000000000000000000000000000000000000080000000000000000000c000000000000000408000000000000000000000000000000200080000000000000000100000000000000000000000000000000000000000000200000020000000000000000000000000800000400000000000400000000400000000000400100000000000000000000000000000000000000000000480000000000000000000000000000000000000000000000000008000000000080000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000002008000000000000000', 'status': '0x1', 'to': '0x4e58ab12d2051ea2068e78e4fcee7ddee6785848', 'transactionHash': '0xd0f039591953d277d55f628694248cb442590fab95ac53fcfb69e9dbba7db97a', 'transaction_hash': '0xd0f039591953d277d55f628694248cb442590fab95ac53fcfb69e9dbba7db97a', 'transactionIndex': '0xe', 'transaction_index': '0xe', 'type': '0x0'}
class BloomTestCase(EthTesterCase):
def setUp(self):
super(BloomTestCase, self).setUp()
nonce_oracle = RPCNonceOracle(self.accounts[0], conn=self.rpc)
c = TestContract(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
(tx_hash, o) = c.constructor(self.accounts[0])
r = self.rpc.do(o)
o = receipt(tx_hash)
r = self.rpc.do(o)
self.assertEqual(r['status'], 1)
self.address = r['contract_address']
logg.info('deployed contract on {}'.format(self.address))
def test_log_proof(self):
bloom = LogBloom()
address = bytes.fromhex(strip_0x('0x4e58ab12d2051ea2068e78e4fcee7ddee6785848'))
logs = [
['0x92e98423f8adac6e64d0608e519fd1cefb861498385c6dee70d58fc926ddc68c', '0x0000000000000000000000000000000000000000000000000000000005f6aa5a', '0x00000000000000000000000000000000000000000000000000000000000000d6', '0x000000000000000000000000f6025e63cee5e436a5f1486e040aeead7e97b745'],
['0x0559884fd3a460db3073b7fc896cc77986f16e378210ded43186175bf646fc5f', '0x0000000000000000000000000000000000000000000000000000000005f6aa5a', '0x00000000000000000000000000000000000000000000000000000000000000d6'],
['0xfe25c73e3b9089fac37d55c4c7efcba6f04af04cebd2fc4d6d7dbb07e1e5234f', '0x0000000000000000000000000000000000000000000000813b65aa80e5770000'],
]
bloom.add(address)
for topics in logs:
topics_bytes = []
for topic in topics:
topic_bytes = bytes.fromhex(strip_0x(topic))
bloom.add(topic_bytes)
log_proof_hex = '0x0000000000000000000000000000000000000080000000000000000000c000000000000000408000000000000000000000000000000200080000000000000000100000000000000000000000000000000000000000000200000020000000000000000000000000800000400000000000400000000400000000000400100000000000000000000000000000000000000000000480000000000000000000000000000000000000000000000000008000000000080000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000002008000000000000000'
log_proof = bytes.fromhex(strip_0x(log_proof_hex))
log_proof_bitcount = 0
for b in log_proof:
for i in range(8):
if b & (1 << (7 - i)) > 0:
log_proof_bitcount += 1
logg.debug('proof log has {} bits set'.format(log_proof_bitcount))
log_created_bitcount = 0
for b in bloom.content:
for i in range(8):
if b & (1 << (7 - i)) > 0:
log_created_bitcount += 1
logg.debug('created log has {} bits set'.format(log_created_bitcount))
logg.debug('log_proof:\n{}'.format(log_proof_hex))
logg.debug('log_created:\n{}'.format(add_0x(bloom.content.hex())))
for i in range(len(bloom.content)):
chk = bloom.content[i] & log_proof[i]
if chk != bloom.content[i]:
self.fail('mismatch at {}: {} != {}'.format(i, chk, bloom.content[i]))
@unittest.skip('pyevm tester produces bogus log blooms')
def test_log(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], conn=self.rpc)
gas_oracle = OverrideGasOracle(limit=50000, conn=self.rpc)
c = TestContract(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle)
b = b'\xee' * 32
(tx_hash, o) = c.foo(self.address, self.accounts[0], 42, b.hex())
r = self.rpc.do(o)
o = receipt(tx_hash)
rcpt = self.rpc.do(o)
self.assertEqual(rcpt['status'], 1)
bloom = LogBloom()
topic = rcpt['logs'][0]['topics'][0]
topic = bytes.fromhex(strip_0x(topic))
address = bytes.fromhex(strip_0x(self.address))
bloom.add(topic, address)
o = block_by_number(rcpt['block_number'])
r = self.rpc.do(o)
if __name__ == '__main__':
unittest.main()

38
tests/test_event.py Normal file
View File

@@ -0,0 +1,38 @@
# standard imports
import unittest
import logging
# local imports
from chainlib.eth.unittest.ethtester import EthTesterCase
from chainlib.eth.contract import (
ABIContractLogDecoder,
ABIContractType,
)
logging.basicConfig(level=logging.DEBUG)
class TestContractLog(EthTesterCase):
def test_log(self):
dec = ABIContractLogDecoder()
dec.topic('TestEventOne')
dec.typ(ABIContractType.UINT256)
dec.typ(ABIContractType.BYTES32)
s = dec.get_method_signature()
n = 42
topics = [
s,
n.to_bytes(32, byteorder='big'),
]
data = [
(b'\xee' * 32),
]
dec.apply(topics, data)
o = dec.decode()
self.assertEqual(o[0], 42)
self.assertEqual(o[1], data[0].hex())
if __name__ == '__main__':
unittest.main()

26
tests/test_nonce.py Normal file
View File

@@ -0,0 +1,26 @@
# standard imports
import os
import unittest
# local imports
from chainlib.eth.address import to_checksum_address
from chainlib.eth.nonce import OverrideNonceOracle
from hexathon import add_0x
# test imports
from tests.base import TestBase
class TestNonce(TestBase):
def test_nonce(self):
addr_bytes = os.urandom(20)
addr = add_0x(to_checksum_address(addr_bytes.hex()))
n = OverrideNonceOracle(addr, 42)
self.assertEqual(n.get_nonce(), 42)
self.assertEqual(n.next_nonce(), 42)
self.assertEqual(n.next_nonce(), 43)
if __name__ == '__main__':
unittest.main()

119
tests/test_sign.py Normal file
View File

@@ -0,0 +1,119 @@
# standard imports
import os
import socket
import unittest
import unittest.mock
import logging
import json
# external imports
from crypto_dev_signer.eth.transaction import EIP155Transaction
from crypto_dev_signer.eth.signer.defaultsigner import ReferenceSigner
from crypto_dev_signer.keystore.dict import DictKeystore
# local imports
import chainlib
from chainlib.eth.connection import EthUnixSignerConnection
from chainlib.eth.sign import sign_transaction
from chainlib.eth.tx import TxFactory
from chainlib.eth.address import to_checksum_address
from chainlib.jsonrpc import (
jsonrpc_response,
jsonrpc_error,
)
from hexathon import (
add_0x,
)
from chainlib.chain import ChainSpec
from tests.base import TestBase
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
keystore = DictKeystore()
alice = keystore.new()
bob = keystore.new()
class Mocket(socket.socket):
req_id = None
error = False
tx = None
signer = None
def connect(self, v):
return self
def send(self, v):
o = json.loads(v)
logg.debug('mocket received {}'.format(v))
Mocket.req_id = o['id']
params = o['params'][0]
if to_checksum_address(params.get('from')) != alice:
logg.error('from does not match alice {}'.format(params))
Mocket.error = True
if to_checksum_address(params.get('to')) != bob:
logg.error('to does not match bob {}'.format(params))
Mocket.error = True
if not Mocket.error:
Mocket.tx = EIP155Transaction(params, params['nonce'], params['chainId'])
logg.debug('mocket {}'.format(Mocket.tx))
return len(v)
def recv(self, c):
if Mocket.req_id != None:
o = None
if Mocket.error:
o = jsonrpc_error(Mocket.req_id)
else:
tx = Mocket.tx
r = Mocket.signer.sign_transaction_to_rlp(tx)
Mocket.tx = None
o = jsonrpc_response(Mocket.req_id, add_0x(r.hex()))
Mocket.req_id = None
return json.dumps(o).encode('utf-8')
return b''
class TestSign(TestBase):
def setUp(self):
super(TestSign, self).__init__()
self.chain_spec = ChainSpec('evm', 'foo', 42)
logg.debug('alice {}'.format(alice))
logg.debug('bob {}'.format(bob))
self.signer = ReferenceSigner(keystore)
Mocket.signer = self.signer
def test_sign_build(self):
with unittest.mock.patch('chainlib.connection.socket.socket', Mocket) as m:
rpc = EthUnixSignerConnection('foo', chain_spec=self.chain_spec)
f = TxFactory(self.chain_spec, signer=rpc)
tx = f.template(alice, bob, use_nonce=True)
tx = f.build(tx)
logg.debug('tx result {}'.format(tx))
def test_sign_rpc(self):
with unittest.mock.patch('chainlib.connection.socket.socket', Mocket) as m:
rpc = EthUnixSignerConnection('foo')
f = TxFactory(self.chain_spec, signer=rpc)
tx = f.template(alice, bob, use_nonce=True)
tx_o = sign_transaction(tx)
rpc.do(tx_o)
if __name__ == '__main__':
unittest.main()

49
tests/test_stat.py Normal file
View File

@@ -0,0 +1,49 @@
# standard imports
import unittest
import datetime
# external imports
from chainlib.stat import ChainStat
from chainlib.eth.block import Block
class TestStat(unittest.TestCase):
def test_block(self):
s = ChainStat()
d = datetime.datetime.utcnow() - datetime.timedelta(seconds=30)
block_a = Block({
'timestamp': d.timestamp(),
'hash': None,
'transactions': [],
'number': 41,
})
d = datetime.datetime.utcnow()
block_b = Block({
'timestamp': d.timestamp(),
'hash': None,
'transactions': [],
'number': 42,
})
s.block_apply(block_a)
s.block_apply(block_b)
self.assertEqual(s.block_average(), 30.0)
d = datetime.datetime.utcnow() + datetime.timedelta(seconds=10)
block_c = Block({
'timestamp': d.timestamp(),
'hash': None,
'transactions': [],
'number': 43,
})
s.block_apply(block_c)
self.assertEqual(s.block_average(), 20.0)
if __name__ == '__main__':
unittest.main()

79
tests/test_tx.py Normal file
View File

@@ -0,0 +1,79 @@
# standard imports
import os
import unittest
import logging
# local imports
from chainlib.eth.unittest.ethtester import EthTesterCase
from chainlib.eth.nonce import RPCNonceOracle
from chainlib.eth.gas import (
RPCGasOracle,
Gas,
)
from chainlib.eth.tx import (
unpack,
pack,
raw,
transaction,
TxFormat,
TxFactory,
Tx,
)
from chainlib.eth.contract import (
ABIContractEncoder,
ABIContractType,
)
from chainlib.eth.address import to_checksum_address
from hexathon import (
strip_0x,
add_0x,
)
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
class TxTestCase(EthTesterCase):
def test_tx_reciprocal(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
gas_oracle = RPCGasOracle(self.rpc)
c = Gas(signer=self.signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle, chain_spec=self.chain_spec)
(tx_hash_hex, o) = c.create(self.accounts[0], self.accounts[1], 1024, tx_format=TxFormat.RLP_SIGNED)
tx = unpack(bytes.fromhex(strip_0x(o)), self.chain_spec)
self.assertEqual(tx['from'], self.accounts[0])
self.assertEqual(tx['to'], self.accounts[1])
def test_tx_pack(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
gas_oracle = RPCGasOracle(self.rpc)
mock_contract = to_checksum_address(add_0x(os.urandom(20).hex()))
f = TxFactory(self.chain_spec, signer=self.rpc)
enc = ABIContractEncoder()
enc.method('fooMethod')
enc.typ(ABIContractType.UINT256)
enc.uint256(13)
data = enc.get()
tx = f.template(self.accounts[0], mock_contract, use_nonce=True)
tx = f.set_code(tx, data)
(tx_hash, tx_signed_raw_hex) = f.finalize(tx, TxFormat.RLP_SIGNED)
logg.debug('tx result {}'.format(tx))
o = raw(tx_signed_raw_hex)
r = self.rpc.do(o)
o = transaction(tx_hash)
tx_rpc_src = self.rpc.do(o)
logg.debug('rpc src {}'.format(tx_rpc_src))
tx_signed_raw_bytes = bytes.fromhex(strip_0x(tx_signed_raw_hex))
tx_src = unpack(tx_signed_raw_bytes, self.chain_spec)
txo = Tx(tx_src)
tx_signed_raw_bytes_recovered = pack(txo, self.chain_spec)
logg.debug('o {}'.format(tx_signed_raw_bytes.hex()))
logg.debug('r {}'.format(tx_signed_raw_bytes_recovered.hex()))
self.assertEqual(tx_signed_raw_bytes, tx_signed_raw_bytes_recovered)
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1 @@
{"address":"eb3907ecad74a0013c259d5874ae7f22dcbcc95c","crypto":{"cipher":"aes-128-ctr","ciphertext":"b0f70a8af4071faff2267374e2423cbc7a71012096fd2215866d8de7445cc215","cipherparams":{"iv":"9ac89383a7793226446dcb7e1b45cdf3"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"299f7b5df1d08a0a7b7f9c9eb44fe4798683b78da3513fcf9603fd913ab3336f"},"mac":"6f4ed36c11345a9a48353cd2f93f1f92958c96df15f3112a192bc994250e8d03"},"id":"61a9dd88-24a9-495c-9a51-152bd1bfaa5b","version":3}