RUNE low-level transaction support

Testing:

    $ test/modtest.py rune
This commit is contained in:
The MMGen Project 2025-06-10 20:34:07 +03:00
commit 55e59cee12
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
14 changed files with 795 additions and 2 deletions

View file

@ -1 +1,2 @@
pycryptodome
pure-protobuf

View file

@ -1 +1 @@
May 2025
June 2025

View file

@ -1 +1 @@
15.1.dev42
15.1.dev43

173
mmgen/proto/cosmos/tx/protobuf.py Executable file
View file

@ -0,0 +1,173 @@
#!/usr/bin/env python3
#
# MMGen Wallet, a terminal-based cryptocurrency wallet
# Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
# Licensed under the GNU General Public License, Version 3:
# https://www.gnu.org/licenses
# Public project repositories:
# https://github.com/mmgen/mmgen-wallet
# https://gitlab.com/mmgen/mmgen-wallet
"""
proto.cosmos.tx.protobuf: transaction serialization for Cosmos protocol
"""
# cosmjs-types/src/cosmos/bank/v1beta1/tx.ts
# cosmjs-types/src/cosmos/tx/v1beta1/tx.ts
# cosmjs-types/src/cosmos/tx/signing/v1beta1/signing.ts
from hashlib import sha256
from dataclasses import dataclass
from pure_protobuf.annotations import Field
from pure_protobuf.message import BaseMessage
from typing import Annotated, List, Optional
from enum import IntEnum
class SignMode(IntEnum):
SIGN_MODE_UNSPECIFIED = 0
SIGN_MODE_DIRECT = 1
SIGN_MODE_TEXTUAL = 2
SIGN_MODE_DIRECT_AUX = 3
SIGN_MODE_LEGACY_AMINO_JSON = 127
SIGN_MODE_EIP_191 = 191
UNRECOGNIZED = -1
@dataclass
class PublicKeyData(BaseMessage):
data: Annotated[bytes, Field(1)]
@dataclass
class PublicKey(BaseMessage):
id: Annotated[str, Field(1)] # '/cosmos.crypto.secp256k1.PubKey'
key: Annotated[PublicKeyData, Field(2)]
@dataclass
class ModeInfo(BaseMessage):
@dataclass
class Single(BaseMessage):
mode: Annotated[SignMode, Field(1)]
@dataclass
class Multi(BaseMessage):
bitarray: Annotated[bytes, Field(1)]
modeInfos: Annotated[List[bytes], Field(2)]
single: Annotated[Optional[Single], Field(1)] = None
multi: Annotated[Optional[Multi], Field(2)] = None
@dataclass
class SignerInfo(BaseMessage):
publicKey: Annotated[PublicKey, Field(1)]
# mode_info describes the signing mode of the signer and is a nested
# structure to support nested multisig pubkeys
modeInfo: Annotated[ModeInfo, Field(2)]
# sequence is the sequence of the account, which describes the number of committed
# transactions signed by a given address. It is used to prevent replay attacks.
sequence: Annotated[int, Field(3)]
@dataclass
class Coin(BaseMessage):
denom: Annotated[str, Field(1)]
amount: Annotated[str, Field(2)]
@dataclass
class Fee(BaseMessage):
amount: Annotated[Optional[List[Coin]], Field(1)] = None # = field(default_factory=list)
gasLimit: Annotated[Optional[int], Field(2)] = None
payer: Annotated[Optional[str], Field(3)] = None
granter: Annotated[Optional[str], Field(4)] = None
@dataclass
class AuthInfo(BaseMessage):
signerInfos: Annotated[List[SignerInfo], Field(1)]
fee: Annotated[Optional[Fee], Field(2)] = None
tip: Annotated[Optional[int], Field(3)] = None
# TxRaw is a variant of Tx that pins the signer's exact binary representation of body
# and auth_info. This is used for signing, broadcasting and verification. The binary
# `serialize(tx: TxRaw)` is stored in Tendermint and the hash `sha256(serialize(tx:
# TxRaw))` becomes the "txhash", commonly used as the transaction ID.
@dataclass
class RawTx(BaseMessage): # TxRaw (cosmjs)
bodyBytes: Annotated[bytes, Field(1)]
authInfoBytes: Annotated[bytes, Field(2)]
signatures: Annotated[List[bytes], Field(3)]
# SignDoc is the type used for generating sign bytes for SIGN_MODE_DIRECT
@dataclass
class SignDoc(BaseMessage):
bodyBytes: Annotated[bytes, Field(1)]
authInfoBytes: Annotated[bytes, Field(2)]
chainId: Annotated[str, Field(3)]
accountNumber: Annotated[int, Field(4)]
@dataclass
class TxMsg(BaseMessage):
msgs_cls = None
id: Annotated[str, Field(1)]
bodyBytes: Annotated[bytes, Field(2)]
def __new__(cls, *, id, bodyBytes):
msg_cls = getattr(cls.msgs_cls, id.removeprefix('/types.'))
me = BaseMessage.__new__(msg_cls)
me.id = id
me.body = getattr(msg_cls, 'Body').loads(bodyBytes)
return me
@dataclass
class Tx(BaseMessage):
body: Annotated[bytes, Field(1)]
authInfo: Annotated[bytes, Field(2)]
signatures: Annotated[List[bytes], Field(3)]
@property
def raw(self):
return RawTx(
bodyBytes = bytes(self.body),
authInfoBytes = bytes(self.authInfo),
signatures = self.signatures)
@property
def txid(self):
return sha256(bytes(self.raw)).hexdigest()
# raises exception on failure:
def verify_sig(self, proto, account_number, backend='ecdsa'):
sign_doc = SignDoc(
bodyBytes = bytes(self.body),
authInfoBytes = bytes(self.authInfo),
chainId = proto.chain_id,
accountNumber = account_number)
sig = self.signatures[0]
pubkey = self.authInfo.signerInfos[0].publicKey.key.data
msghash = sha256(bytes(sign_doc)).digest()
if backend == 'ecdsa':
# ecdsa.keys.VerifyingKey.verify_digest():
# raises BadSignatureError if the signature is invalid or malformed
import ecdsa
ec_pubkey = ecdsa.VerifyingKey.from_string(pubkey, curve=ecdsa.curves.SECP256k1)
ec_pubkey.verify_digest(sig, msghash)
elif backend == 'py_ecc':
from py_ecc.secp256k1.secp256k1 import ecdsa_raw_recover
y_is_odd = pubkey[0] - 2
x, y = ecdsa_raw_recover(
msghash,
(28 - y_is_odd, int.from_bytes(sig[:32]), int.from_bytes(sig[32:])))
assert y & 1 == y_is_odd, f'verify_sig(): parity mismatch for TX {self.txid}'
assert int.from_bytes(pubkey[1:]) == x, f'verify_sig(): invalid signature for TX {self.txid}'
else:
raise ValueError(f'verify_sig(): {backend}: unrecognized backend')
# cosmjs/packages/crypto/src/secp256k1signature.ts
# cosmjs/packages/amino/src/signature.ts
# Signature must be 64 bytes long. Cosmos SDK uses a 2x32 byte fixed length
# encoding for the secp256k1 signature integers r and s.
def make_sig(*, sign_doc, sec_bytes):
from py_ecc.secp256k1.secp256k1 import ecdsa_raw_sign
msghash = sha256(bytes(sign_doc)).digest()
_, r, s = ecdsa_raw_sign(msghash, sec_bytes)
# pmsg('v:', _) # DEBUG
return r.to_bytes(length=32) + s.to_bytes(length=32)

View file

@ -22,6 +22,7 @@ from ..btc.params import mainnet as btc_mainnet
class mainnet(CoinProtocol.Secp256k1):
mod_clsname = 'THORChain'
network_names = _nw('mainnet', 'stagenet', 'testnet')
chain_id = 'thorchain-1'
mmtypes = ('X',)
preferred_mmtypes = ('X',)
dfl_mmtype = 'X'

View file

@ -25,6 +25,16 @@ def process_response(json_response, errmsg):
die('RPCFailure', errmsg)
return data['result']
# HTTP POST, JSON-RPC response:
class ThornodeRemoteRPCClient(HTTPClient):
timeout = 30
def __init__(self, cfg, proto, *, network_proto=None, host=None):
for k, v in proto.rpc_remote_rpc_params.items():
setattr(self, k, v)
super().__init__(cfg, network_proto=network_proto, host=host)
# HTTP GET, params in query string, JSON-RPC response:
class ThornodeRemoteRESTClient(HTTPClient):
@ -45,6 +55,7 @@ class THORChainRemoteRPCClient(RemoteRPCClient):
super().__init__(cfg, proto)
self.caps = ('lbl_id',)
self.rest_api = ThornodeRemoteRESTClient(cfg, proto)
self.rpc_api = ThornodeRemoteRPCClient(cfg, proto)
def get_balance(self, addr, *, block=None):
res = process_response(
@ -53,3 +64,24 @@ class THORChainRemoteRPCClient(RemoteRPCClient):
rune_res = [d for d in res if d['denom'] == 'rune']
assert len(rune_res) == 1, f'{rune_res}: result length is not one!'
return self.proto.coin_amt(int(rune_res[0]['amount']), from_unit='satoshi')
def get_account_info(self, addr, *, block=None):
return process_response(
self.rest_api.get(path=f'/auth/accounts/{addr}'),
errmsg = f'address ‘{addr}’ not found in blockchain')['value']
def get_tx_info(self, txhash):
return process_response(
self.rpc_api.post(
path = '/tx',
data = {'hash': '0x' + txhash}),
errmsg = f'get info for transaction {txhash} failed')
def tx_op(self, txbytes, op=None):
assert isinstance(txbytes, bytes)
assert op in ('check_tx', 'broadcast_tx_sync', 'broadcast_tx_async')
return process_response(
self.rpc_api.post(
path = '/' + op,
data = {'tx': '0x' + txbytes.hex()}),
errmsg = f'transaction operation ‘{op}’ failed')

276
mmgen/proto/rune/tx/protobuf.py Executable file
View file

@ -0,0 +1,276 @@
#!/usr/bin/env python3
#
# MMGen Wallet, a terminal-based cryptocurrency wallet
# Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
# Licensed under the GNU General Public License, Version 3:
# https://www.gnu.org/licenses
# Public project repositories:
# https://github.com/mmgen/mmgen-wallet
# https://gitlab.com/mmgen/mmgen-wallet
"""
proto.rune.tx.protobuf: transaction serialization for THORChain protocol
"""
# https://dev.thorchain.org/technical-faq.html
from decimal import Decimal
from collections import namedtuple
from dataclasses import dataclass #, field
from typing import Annotated, List, Optional
from pure_protobuf.annotations import Field
from pure_protobuf.message import BaseMessage
from ...cosmos.tx.protobuf import (
ModeInfo,
SignDoc,
SignerInfo,
SignMode,
PublicKey,
PublicKeyData,
Coin,
Fee,
AuthInfo,
Tx,
TxMsg,
make_sig)
send_tx_parms = namedtuple(
'rune_send_tx_parms', [
'from_addr',
'to_addr',
'amt',
'gas_limit',
'account_number',
'sequence',
'pubkey',
'wifkey',
'signature'],
defaults = (None, None, None))
deposit_tx_parms = namedtuple(
'rune_deposit_tx_parms', [
'chain',
'symbol',
'ticker',
'from_addr',
'amt',
'gas_limit',
'account_number',
'sequence',
'decimals',
'memo',
'synth', # begin default fields
'trade',
'secured',
'pubkey',
'wifkey',
'signature'],
defaults = (None, None, None, None, None, None))
# subset of deposit_tx_parms:
swap_tx_parms = namedtuple(
'rune_swap_tx_parms', [
'from_addr',
'amt',
'gas_limit',
'account_number',
'sequence',
'memo',
'pubkey', # begin default fields
'wifkey',
'signature'],
defaults = (None, None, None))
@dataclass
class Asset(BaseMessage):
chain: Annotated[str, Field(1)]
symbol: Annotated[str, Field(2)]
ticker: Annotated[str, Field(3)]
synth: Annotated[Optional[bool], Field(4)] = None
trade: Annotated[Optional[bool], Field(5)] = None
secured: Annotated[Optional[bool], Field(6)] = None
@dataclass
class CoinWithAsset(BaseMessage):
asset: Annotated[Asset, Field(1)]
amount: Annotated[str, Field(2)]
decimals: Annotated[Optional[int], Field(3)] = None
class Messages:
@dataclass
class MsgSend(BaseMessage):
@dataclass
class Body(BaseMessage):
fromAddress: Annotated[bytes, Field(1)]
toAddress: Annotated[bytes, Field(2)]
amount: Annotated[List[Coin], Field(3)]
id: Annotated[str, Field(1)] # '/types.MsgSend'
body: Annotated[Body, Field(2)]
@dataclass
class MsgDeposit(BaseMessage):
# To initiate a $RUNE -> $ASSET swap a MsgDeposit must be broadcasted to the THORChain blockchain.
# The MsgDeposit does not have a destination address, and has the following properties.
# MsgDeposit{
# Coins: coins,
# Memo: memo,
# Signer: signer,
# }
@dataclass
class Body(BaseMessage):
coins: Annotated[List[CoinWithAsset], Field(1)]
memo: Annotated[str, Field(2)]
signer: Annotated[bytes, Field(3)]
id: Annotated[str, Field(1)] # '/types.MsgDeposit'
body: Annotated[Body, Field(2)]
@dataclass
class RuneTxMsg(TxMsg):
msgs_cls = Messages
id: Annotated[str, Field(1)]
bodyBytes: Annotated[bytes, Field(2)]
@dataclass
class RuneTxBody(BaseMessage):
messages: Annotated[List[RuneTxMsg], Field(1)]
memo: Annotated[Optional[str], Field(2)] = None
timeoutHeight: Annotated[Optional[int], Field(3)] = None
extensionOptions: Annotated[Optional[bytes], Field(4)] = None
nonCriticalExtensionOptions: Annotated[Optional[bytes], Field(5)] = None
@dataclass
class RuneTx(Tx):
body: Annotated[RuneTxBody, Field(1)]
authInfo: Annotated[AuthInfo, Field(2)]
signatures: Annotated[List[bytes], Field(3)]
def amt_to_base_unit(amt, *, decimals):
return int(Decimal(amt) * (Decimal('10') ** decimals))
def base_unit_to_amt(n, *, decimals):
return n * Decimal('10') ** -decimals
def tx_info(tx, proto):
b = tx.body.messages[0].body
s = tx.authInfo.signerInfos[0]
msg_type = tx.body.messages[0].id.removeprefix('/types.')
if msg_type == 'MsgSend':
from_addr = proto.encode_addr_bech32x(b.fromAddress)
to_addr = proto.encode_addr_bech32x(b.toAddress)
asset = b.amount[0].denom.upper()
memo = tx.body.memo
amt = base_unit_to_amt(int(b.amount[0].amount), decimals=8)
elif msg_type == 'MsgDeposit':
from_addr = proto.encode_addr_bech32x(b.signer)
to_addr = 'None'
asset = b.coins[0].asset.symbol
memo = b.memo
amt = base_unit_to_amt(int(b.coins[0].amount), decimals=b.coins[0].decimals or 8)
yield f'TxID: {tx.txid}'
yield f'Type: {msg_type}'
yield f'From: {from_addr}'
yield f'To: {to_addr}'
yield f'Asset: {asset}'
yield f'Amount: {amt}'
yield f'Sequence: {int(s.sequence)}'
yield f'Gas limit: {tx.authInfo.fee.gasLimit}'
yield f'Memo: {memo}'
yield f'Pubkey: {s.publicKey.key.data.hex()}'
def build_swap_tx(cfg, proto, parms, *, skip_body_memo=False):
p = parms
assert type(p) == swap_tx_parms, f'{p}: invalid ‘parms’ (not swap_tx_parms instance)'
return build_tx(
cfg,
proto,
deposit_tx_parms(
chain = 'THOR',
symbol = 'RUNE',
ticker = 'RUNE',
from_addr = p.from_addr,
amt = p.amt,
gas_limit = p.gas_limit,
account_number = p.account_number,
sequence = p.sequence,
decimals = 8,
memo = p.memo,
pubkey = p.pubkey,
wifkey = p.wifkey,
signature = p.signature),
skip_body_memo = skip_body_memo)
def build_tx(cfg, proto, parms, *, null_fee=False, skip_body_memo=False):
p = parms
assert type(p) in (send_tx_parms, deposit_tx_parms), f'{p}: invalid ‘parms’ (not *_tx_parms instance)'
msg_type = 'MsgSend' if type(p) == send_tx_parms else 'MsgDeposit'
if p.wifkey:
assert p.pubkey is None and p.signature is None
from ....key import PrivKey
from ....keygen import KeyGenerator
privkey = PrivKey(proto=proto, wif=p.wifkey)
pubkey = KeyGenerator(cfg, proto, 'std').to_pubkey(privkey)
else:
assert p.pubkey and p.signature
pubkey = p.pubkey
cls = getattr(Messages, msg_type)
if msg_type == 'MsgSend':
message = cls(
id = '/types.MsgSend',
body = cls.Body(
fromAddress = proto.decode_addr(p.from_addr).bytes,
toAddress = proto.decode_addr(p.to_addr).bytes,
amount = [Coin(denom='rune', amount=str(amt_to_base_unit(p.amt, decimals=8)))]))
fee_amt = None
elif msg_type == 'MsgDeposit':
coin_data = CoinWithAsset(
asset = Asset(
chain = p.chain,
symbol = p.symbol,
ticker = p.ticker,
synth = p.synth,
trade = p.trade,
secured = p.secured),
amount = str(amt_to_base_unit(p.amt, decimals=p.decimals or 8)),
decimals = p.decimals)
message = cls(
id = '/types.MsgDeposit',
body = cls.Body(
coins = [coin_data],
memo = p.memo,
signer = proto.decode_addr(p.from_addr).bytes))
fee_amt = None if null_fee else [Coin(denom='rune', amount='0')]
signer_info = SignerInfo(
publicKey = PublicKey(
id = '/cosmos.crypto.secp256k1.PubKey',
key = PublicKeyData(data=pubkey)),
modeInfo = ModeInfo(
single = ModeInfo.Single(mode=SignMode.SIGN_MODE_DIRECT),
multi = None),
sequence = p.sequence)
body = RuneTxBody(messages=[message], memo=None if skip_body_memo else getattr(p, 'memo', None))
auth_info = AuthInfo(signerInfos=[signer_info], fee=Fee(gasLimit=p.gas_limit, amount=fee_amt))
signature = make_sig(
sign_doc = SignDoc(
bodyBytes = bytes(body),
authInfoBytes = bytes(auth_info),
chainId = proto.chain_id,
accountNumber = p.account_number),
sec_bytes = privkey) if p.wifkey else p.signature
return RuneTx(body=body, authInfo=auth_info, signatures=[signature])

View file

@ -84,6 +84,8 @@ packages =
mmgen.proto.btc.rpc
mmgen.proto.btc.tx
mmgen.proto.btc.tw
mmgen.proto.cosmos
mmgen.proto.cosmos.tx
mmgen.proto.etc
mmgen.proto.eth
mmgen.proto.eth.pyethereum
@ -96,6 +98,7 @@ packages =
mmgen.proto.rune
mmgen.proto.rune.rpc
mmgen.proto.rune.tw
mmgen.proto.rune.tx
mmgen.proto.secp256k1
mmgen.proto.xchain
mmgen.proto.xmr

View file

@ -30,6 +30,45 @@ class ThornodeRPCServer(ThornodeServer):
{'denom': 'rune', 'amount': 987654321321},
{'denom': 'barcoin', 'amount': 123123123123},
]}
elif m := re.search(r'/auth/accounts/(\S+)', req_str):
data = {
'result': {
'value': {
'address': m[1],
'pub_key': 'PubKeySecp256k1{0000}',
'account_number': '1234',
'sequence': '333444'
}}}
elif m := re.search(r'/tx$', req_str):
assert method == 'POST'
txid = environ['wsgi.input'].read(71).decode().removeprefix('hash=0x').upper()
data = {
'result': {
'hash': txid,
'height': '21298600',
'index': 2,
'tx_result': {
'gas_used': '173222',
'events': [],
'codespace': ''
},
'tx': 'MHgwMGZvb2Jhcg=='
}
}
elif m := re.search(r'/check_tx$', req_str):
assert method == 'POST'
data = {
'result': {
'code': 0,
'data': '',
'log': '',
'info': '',
'gas_wanted': '-1',
'gas_used': '53774',
'events': [],
'codespace': ''
}
}
else:
raise ValueError(f'{req_str}’: malformed query path')

228
test/modtest_d/rune.py Executable file
View file

@ -0,0 +1,228 @@
#!/usr/bin/env python3
import os
from collections import namedtuple
from decimal import Decimal
from mmgen.cfg import Config
from mmgen.util import pp_fmt, ymsg
from mmgen.proto.btc.common import hash160
from mmgen.proto.cosmos.tx.protobuf import RawTx, SignDoc
from mmgen.proto.rune.tx.protobuf import (
RuneTx,
build_tx,
build_swap_tx,
tx_info,
send_tx_parms,
deposit_tx_parms,
swap_tx_parms)
from ..include.common import vmsg, silence, end_silence
test_cfg = Config({'coin': 'rune', 'test_suite': True})
altcoin_dep = True
_pv = namedtuple('parse_vector', ['fn', 'txid', 'parms', 'null_fee'], defaults=[None])
parse_vectors = [
_pv(
'mainnet-tx-msgsend1.binpb',
'36f91982c1911fe1aa66b44eed60e29175e5b8ae3301feef9158b7617779b00e',
send_tx_parms(
'thor1t60f02r8jvzjrhtnjgfj4ne6rs5wjnejwmj7fh',
'thor166n4w5039meulfa3p6ydg60ve6ueac7tlt0jws',
'12613.15290000',
8000000,
45060,
302033,
pubkey = '02f9cbb8409443ccf043f26d8f91c2550d2578ecc49bb3ad89d4e21a7882bf1e23',
signature = 'd44b2e0c7546c5fae24a2c829757f49cce1bb29553f7e1a2f87c1ac2f1c46e22' # r
'509d765fc605d85e8967639864622ebc7c39a1a93fc20cf0fe5d703c4aa3636d')), # s
_pv(
'mainnet-tx-msgdeposit1.binpb',
'1089bbd54746bbc6a40e264d3ce8085561978739094c9c5aac59c569b4c28ba9',
deposit_tx_parms(
'THOR', 'RUNE', 'RUNE',
'thor1lukwlve7hayy66qrdkp4k7sh0emjqwergy7tl3',
'299.23861844',
600000000,
125632,
348388,
decimals = 8,
memo = '=:LTC~LTC:thor1lukwlve7hayy66qrdkp4k7sh0emjqwergy7tl3:605926421/0/1',
pubkey = '03da157f891abfe7822efb91f59667aa6cc6c3768a7e280caeb9ae243c969eb3e7',
signature = '869399bcc2ccb9c9c286bdf214439ad132221cb8206547ceb012e06efbc3ff3e' # r
'0ecc1ba4106702fb5b60cd7a8b94193ea71af5e6e860d978c49a6a63d97e4ded')), # s
_pv(
'mainnet-tx-msgdeposit2.binpb',
'44f45b91e97558e63a11758ac3c186196b9b46f6331f32eff1256888ea879b62',
deposit_tx_parms(
'ETH', 'USDT-0XDAC17F958D2EE523A2206206994597C13D831EC7', 'USDT',
'thor1xxncvuptvmgcl5ep7rry3xehtw97jsg9uyv6rn',
'500.00000000',
50000000,
88176,
104625,
decimals = None,
synth = False,
trade = True,
memo = '=:AVAX~AVAX:thor1xxncvuptvmgcl5ep7rry3xehtw97jsg9uyv6rn:2113883178',
pubkey = '02a6e97e3f20809511500d8895117d4344badda9e6af4216d41b10a105d1070254',
signature = 'b0673ab89781199d35b94051f26db30996f055abd71804d67fe4bdf33934bdb9' # r
'30d830675d398c6d042328ef681bf1a23e856ed0ab33d948ea149489400db953'), # s
null_fee = True),
_pv(
'mainnet-tx-msgdeposit3.binpb',
'02d2fb2f2e5ac00ad4a31c37ffc43b72963f93598f8b3c8f4d3932c2e950b459',
deposit_tx_parms(
'ETH', 'USDC-0XA0B86991C6218B36C1D19D4A2E9EB0CE3606EB48', 'USDC',
'thor1lukwlve7hayy66qrdkp4k7sh0emjqwergy7tl3',
'2425.75973697',
600000000,
125632,
375988,
decimals = 8,
synth = None,
trade = True,
memo = '=:THOR.RUNE:thor1lukwlve7hayy66qrdkp4k7sh0emjqwergy7tl3:127025748855/0/1',
pubkey = '03da157f891abfe7822efb91f59667aa6cc6c3768a7e280caeb9ae243c969eb3e7',
signature = 'be8324f6a1535b971d63715532e2e42ee3c35c05b81ed9b6bccd9a1765688eca'
'4ca00a66119b07c5168c6c78e22299becc82d5f311ee79ae9183776b0dff3269'))
]
_bv = namedtuple('build_vector', ['txid', 'parms', 'null_fee'], defaults=[None])
build_vectors = [
_bv(
'3939612d0ddc55fd4d1c6ef118d1b2085a6655cb57d55ac9efd658467e039e0c',
send_tx_parms(
'thor1tx3nm6xfynq3re5ehtm6530z0pah9qjeu0r9nd',
'thor1j5u6vlr8kzt76fe7896hsmurkhgn68j0z4qa6w',
'123.456789',
8000000,
12345,
37,
wifkey = 'L5nWojqqMLq7wh3CfhxUNYQ38acABD6sUao9dfb8i5B5wSefCJXe')),
_bv(
'0d41e0ee40cd18a991cd8f0ef0e60e4c5bea898c53d54e00b6dddc0c9ce7edb7',
deposit_tx_parms(
'THOR', 'RUNE', 'RUNE',
'thor18ug6p4zs5dsy0m3u69gf5md5ssdg8hqkk8aya4',
'123.456789',
8000000,
12345,
37,
decimals = 8,
memo = '=:MEMO',
wifkey = 'Ky9bSjPUD35uUaY3JReXiESivnfxV6rLMsW1wTFyvVZwYXpX95vF'))
]
swap_build_vectors = [
_bv(
'0d41e0ee40cd18a991cd8f0ef0e60e4c5bea898c53d54e00b6dddc0c9ce7edb7',
swap_tx_parms(
'thor18ug6p4zs5dsy0m3u69gf5md5ssdg8hqkk8aya4',
'123.456789',
8000000,
12345,
37,
memo = '=:MEMO',
wifkey = 'Ky9bSjPUD35uUaY3JReXiESivnfxV6rLMsW1wTFyvVZwYXpX95vF'))
]
def test_tx(src, cfg, vec):
proto = cfg._proto
parms = vec.parms._replace(amt=Decimal(vec.parms.amt))
if parms.pubkey:
parms = parms._replace(
pubkey = bytes.fromhex(parms.pubkey),
signature = bytes.fromhex(parms.signature))
assert src in ('parse', 'build', 'swapbuild')
if src == 'parse':
tx_in = open(os.path.join('test/ref/thorchain', vec.fn), 'br').read()
tx = RuneTx.loads(tx_in)
if not parms.from_addr:
ymsg(f'Warning: missing test vector data for {vec.fn}')
assert bytes(tx) == tx_in
elif src == 'build':
tx = build_tx(cfg, proto, parms, null_fee=vec.null_fee)
elif src == 'swapbuild':
tx = build_swap_tx(cfg, proto, parms)
vmsg(pp_fmt(tx))
msg_type = 'MsgSend' if tx.body.messages[0].id == '/types.MsgSend' else 'MsgDeposit'
vmsg('\n TX info:\n ' + '\n '.join(tx_info(tx, proto)) + '\n')
tx.verify_sig(proto, parms.account_number)
pubkey = tx.authInfo.signerInfos[0].publicKey.key.data
assert hash160(pubkey) == getattr(
tx.body.messages[0].body,
'fromAddress' if msg_type == 'MsgSend' else 'signer')
assert tx.txid == vec.txid, tx.txid
if src == 'parse' and parms.from_addr:
built_tx = build_tx(cfg, proto, parms, null_fee=vec.null_fee)
addr_from_pubkey = proto.encode_addr_bech32x(hash160(pubkey))
assert addr_from_pubkey == parms.from_addr
assert bytes(built_tx) == tx_in
raw_tx = RawTx(bytes(tx.body), bytes(tx.authInfo), tx.signatures)
assert bytes(raw_tx) == bytes(RawTx.loads(tx_in))
assert bytes(raw_tx) == bytes(tx.raw)
assert tx_in == bytes(tx)
class unit_tests:
def txparse(self, name, ut, desc='transaction parsing and signature verification'):
for vec in parse_vectors:
test_tx('parse', test_cfg, vec)
return True
def txbuild(self, name, ut, desc='transaction building and signing (MsgSend, MsgDeposit)'):
for vec in build_vectors:
test_tx('build', test_cfg, vec)
return True
def swaptxbuild(self, name, ut, desc='transaction building and signing (Swap TX)'):
for vec in swap_build_vectors:
test_tx('swapbuild', test_cfg, vec)
return True
def rpc(self, name, ut, desc='remote RPC operations'):
import sys, asyncio
from mmgen.rpc import rpc_init
from ..cmdtest_d.httpd.thornode.rpc import ThornodeRPCServer
silence()
regtest_cfg = Config({'coin': 'rune', 'regtest': True, 'test_suite': True})
end_silence()
thornode_server = ThornodeRPCServer()
thornode_server.start()
addr = 'thor1lukwlve7hayy66qrdkp4k7sh0emjqwergy7tl3'
txhash = 'abcdef01' * 8
txbytes = open('test/ref/thorchain/mainnet-tx-msgsend1.binpb', 'rb').read()
async def main():
rpc = await rpc_init(regtest_cfg)
res = rpc.get_account_info(addr)
assert res['address'] == addr
assert res['account_number']
assert res['sequence']
res = rpc.get_tx_info(txhash)
assert res['hash'] == txhash.upper()
res = rpc.tx_op(txbytes, op='check_tx')
assert res['code'] == 0
asyncio.run(main())
return True

View file

@ -0,0 +1,14 @@
Þ
–
/types.MsgDeposit€
#

THORRUNERUNE 29923861844C=:LTC~LTC:thor1lukwlve7hayy66qrdkp4k7sh0emjqwergy7tl3:605926421/0/1ÿ,ï³>¿HMhmƒ[z~w ;#C=:LTC~LTC:thor1lukwlve7hayy66qrdkp4k7sh0emjqwergy7tl3:605926421/0/1g
R
F
/cosmos.crypto.secp256k1.PubKey#
!Ú‰¿ç‚.û‘õ–gªlÆÃvŠ~( ®¹®$<–ž³ç
ä¡
rune0€Œ�ž@†“™¼Â̹Ɇ½òCšÑ2"¸ eGΰànûÃÿ>̤gû[`Íz‹”>§õæè`ÙxÄšjcÙ~Mí

Binary file not shown.

View file

@ -0,0 +1,15 @@
•
È
/types.MsgDeposit²
P
>
ETH/USDC-0XA0B86991C6218B36C1D19D4A2E9EB0CE3606EB48USDC( 242575973697H=:THOR.RUNE:thor1lukwlve7hayy66qrdkp4k7sh0emjqwergy7tl3:127025748855/0/1ÿ,ï³>¿HMhmƒ[z~w ;#H=:THOR.RUNE:thor1lukwlve7hayy66qrdkp4k7sh0emjqwergy7tl3:127025748855/0/1g
R
F
/cosmos.crypto.secp256k1.PubKey#
!Ú‰¿ç‚.û‘õ–gªlÆÃvŠ~( ®¹®$<–ž³ç
´ù
rune0€Œ�ž@¾ƒ$ö¡S[—cqU2âä.ãÃ\¸Ù¶¼ÍšehŽÊL 
f›ÅŒlxâ"™¾Ì‚Õóîy®‘ƒwk ÿ2i

View file

@ -0,0 +1,11 @@
W
U
/types.MsgSendC
^ž—¨g“!Ýs’*Ï:(éO2Ö§WQñ.óϧ±ˆÔiìιžãË
rune 1261315290000[
R
F
/cosmos.crypto.secp256k1.PubKey#
!ù˸@”CÌðCòm�‘ÂU %xìÄ›³­‰Ôâx‚¿#
Ñ·€¤è@ÔK. uFÅúâJ,‚—WôœÎ²•S÷á¢ø|ÂñÄn"P�v_ÆØ^‰gc˜db.¼|9¡©? ðþ]p<J£cm