RUNE low-level transaction support
Testing:
$ test/modtest.py rune
This commit is contained in:
parent
70810aff69
commit
55e59cee12
14 changed files with 795 additions and 2 deletions
|
|
@ -1 +1,2 @@
|
|||
pycryptodome
|
||||
pure-protobuf
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
May 2025
|
||||
June 2025
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
15.1.dev42
|
||||
15.1.dev43
|
||||
|
|
|
|||
173
mmgen/proto/cosmos/tx/protobuf.py
Executable file
173
mmgen/proto/cosmos/tx/protobuf.py
Executable 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)
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
276
mmgen/proto/rune/tx/protobuf.py
Executable 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])
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
228
test/modtest_d/rune.py
Executable 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
|
||||
14
test/ref/thorchain/mainnet-tx-msgdeposit1.binpb
Normal file
14
test/ref/thorchain/mainnet-tx-msgdeposit1.binpb
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
Þ
|
||||
–
|
||||
/types.MsgDeposit€
|
||||
#
|
||||
|
||||
THORRUNERUNE29923861844C=: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í
|
||||
BIN
test/ref/thorchain/mainnet-tx-msgdeposit2.binpb
Normal file
BIN
test/ref/thorchain/mainnet-tx-msgdeposit2.binpb
Normal file
Binary file not shown.
15
test/ref/thorchain/mainnet-tx-msgdeposit3.binpb
Normal file
15
test/ref/thorchain/mainnet-tx-msgdeposit3.binpb
Normal 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
|
||||
11
test/ref/thorchain/mainnet-tx-msgsend1.binpb
Normal file
11
test/ref/thorchain/mainnet-tx-msgsend1.binpb
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue