tx.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. #!/usr/bin/env python3
  2. """
  3. test.modtest_d.tx: TX unit tests for the MMGen suite
  4. """
  5. import os
  6. from mmgen.tx import CompletedTX, UnsignedTX
  7. from mmgen.tx.file import MMGenTxFile
  8. from mmgen.cfg import Config
  9. from mmgen.color import cyan
  10. from ..include.common import cfg, qmsg, vmsg, gr_uc, make_burn_addr
  11. async def do_txfile_test(desc, fns, cfg=cfg, check=False):
  12. qmsg(f'\n Testing CompletedTX initializer ({desc})')
  13. for fn in fns:
  14. qmsg(f' parsing: {os.path.basename(fn)}')
  15. fpath = os.path.join('test', 'ref', fn)
  16. tx = await CompletedTX(cfg=cfg, filename=fpath, quiet_open=True)
  17. vmsg('\n' + tx.info.format())
  18. f = MMGenTxFile(tx)
  19. fn_gen = f.make_filename()
  20. if cfg.debug_utf8:
  21. fn_gen = fn_gen.replace('-α', '')
  22. assert fn_gen == os.path.basename(fn), f'{fn_gen} != {fn}'
  23. if check:
  24. import json
  25. from mmgen.tx.file import json_dumps
  26. from mmgen.util import make_chksum_6
  27. text = f.format()
  28. with open(fpath) as fh:
  29. text_chk = fh.read()
  30. data_chk = json.loads(text_chk)
  31. outputs = data_chk['MMGenTransaction']['outputs']
  32. for n, o in enumerate(outputs):
  33. outputs[n] = {k:v for k,v in o.items() if not (type(v) is bool and v is False)}
  34. data_chk['chksum'] = make_chksum_6(json_dumps(data_chk['MMGenTransaction']))
  35. text_chk_fixed = json_dumps(data_chk)
  36. assert text == text_chk_fixed, f'\nformatted text:\n{text}\n !=\noriginal file:\n{text_chk_fixed}'
  37. qmsg(' OK')
  38. return True
  39. class unit_tests:
  40. altcoin_deps = ('txfile_alt', 'txfile_alt_legacy')
  41. async def txfile(self, name, ut, desc='displaying transaction files (BTC)'):
  42. return await do_txfile_test(
  43. 'Bitcoin',
  44. (
  45. 'tx/7A8157[6.65227,34].rawtx',
  46. 'tx/BB3FD2[7.57134314,123].sigtx',
  47. 'tx/0A869F[1.23456,32].regtest.asubtx',
  48. ),
  49. check = True
  50. )
  51. async def txfile_alt(self, name, ut, desc='displaying transaction files (LTC, BCH, ETH)'):
  52. return await do_txfile_test(
  53. 'altcoins',
  54. (
  55. 'tx/C09D73-LTC[981.73747,2000].testnet.rawtx',
  56. 'tx/91060A-BCH[1.23456].regtest.arawtx',
  57. 'tx/D850C6-MM1[43.21,50000].subtx', # token tx
  58. ),
  59. # token resolved by tracking wallet under data_dir:
  60. cfg = Config({'data_dir': 'test/ref/data_dir'}),
  61. check = True
  62. )
  63. async def txfile_legacy(self, name, ut, desc='displaying transaction files (legacy format, BTC)'):
  64. return await do_txfile_test(
  65. 'Bitcoin - legacy file format',
  66. (
  67. '0B8D5A[15.31789,14,tl=1320969600].rawtx',
  68. '542169[5.68152,34].sigtx',
  69. '0C7115[15.86255,14,tl=1320969600].testnet.rawtx',
  70. '25EFA3[2.34].testnet.rawtx',
  71. )
  72. )
  73. async def txfile_alt_legacy(self, name, ut, desc='displaying transaction files (legacy format, LTC, BCH, ETH)'):
  74. return await do_txfile_test(
  75. 'altcoins - legacy file format',
  76. (
  77. '460D4D-BCH[10.19764,tl=1320969600].rawtx',
  78. 'ethereum/5881D2-MM1[1.23456,50000].rawtx',
  79. 'ethereum/6BDB25-MM1[1.23456,50000].testnet.rawtx',
  80. 'ethereum/88FEFD-ETH[23.45495,40000].rawtx',
  81. 'litecoin/A5A1E0-LTC[1454.64322,1453,tl=1320969600].testnet.rawtx',
  82. 'litecoin/AF3CDF-LTC[620.76194,1453,tl=1320969600].rawtx',
  83. )
  84. )
  85. def errors(self, name, ut, desc='reading transaction files (error handling)'):
  86. async def bad1():
  87. await CompletedTX(cfg, filename='foo')
  88. def bad2():
  89. UnsignedTX(cfg, filename='foo')
  90. bad_data = (
  91. ('forbidden positional args', 'TypeError', 'positional arguments', bad1),
  92. ('forbidden positional args', 'TypeError', 'positional arguments', bad2),
  93. )
  94. ut.process_bad_data(bad_data)
  95. return True
  96. def op_return_data(self, name, ut, desc='OpReturnData class'):
  97. from mmgen.proto.btc.tx.op_return_data import OpReturnData
  98. vecs = [
  99. 'data:=:ETH.ETH:0x86d526d6624AbC0178cF7296cD538Ecc080A95F1:0/1/0',
  100. 'hexdata:3d3a4554482e4554483a30783836643532366436363234416243303137'
  101. '38634637323936634435333845636330383041393546313a302f312f30',
  102. 'hexdata:00010203040506',
  103. 'data:a\n',
  104. 'data:a\tb',
  105. 'data:' + gr_uc[:24],
  106. ]
  107. assert OpReturnData(cfg._proto, vecs[0]) == OpReturnData(cfg._proto, vecs[1])
  108. for vec in vecs:
  109. d = OpReturnData(cfg._proto, vec)
  110. assert d == OpReturnData(cfg._proto, repr(d)) # repr() must return a valid initializer
  111. assert isinstance(d, bytes)
  112. assert isinstance(str(d), str)
  113. vmsg('-' * 80)
  114. vmsg(vec)
  115. vmsg(repr(d))
  116. vmsg(d.hl())
  117. vmsg(d.hl(add_label=True))
  118. bad_data = [
  119. 'data:',
  120. 'hexdata:',
  121. 'data:' + ('x' * 81),
  122. 'hexdata:' + ('deadbeef' * 20) + 'ee',
  123. 'hex:0abc',
  124. 'da:xyz',
  125. 'hexdata:xyz',
  126. 'hexdata:abcde',
  127. b'data:abc',
  128. ]
  129. def bad(n):
  130. return lambda: OpReturnData(cfg._proto, bad_data[n])
  131. vmsg('-' * 80)
  132. vmsg('Testing error handling:')
  133. ut.process_bad_data((
  134. ('bad1', 'AssertionError', 'not in range', bad(0)),
  135. ('bad2', 'AssertionError', 'not in range', bad(1)),
  136. ('bad3', 'AssertionError', 'not in range', bad(2)),
  137. ('bad4', 'AssertionError', 'not in range', bad(3)),
  138. ('bad5', 'ValueError', 'must start', bad(4)),
  139. ('bad6', 'ValueError', 'must start', bad(5)),
  140. ('bad7', 'AssertionError', 'not in hex', bad(6)),
  141. ('bad8', 'AssertionError', 'even', bad(7)),
  142. ('bad9', 'AssertionError', 'a string', bad(8)),
  143. ), pfx='')
  144. return True
  145. def memo(self, name, ut, desc='Swap transaction memo'):
  146. from mmgen.protocol import init_proto
  147. from mmgen.amt import UniAmt
  148. from mmgen.swap.proto.thorchain.memo import Memo
  149. for coin, addrtype in (
  150. ('ltc', 'bech32'),
  151. ('bch', 'compressed'),
  152. ('eth', None),
  153. ):
  154. proto = init_proto(cfg, coin, need_amt=True)
  155. addr = make_burn_addr(proto, addrtype)
  156. vmsg(f'\nTesting coin {cyan(coin.upper())}:')
  157. for limit, limit_chk in (
  158. ('123.4567', 12340000000),
  159. ('1.234567', 123400000),
  160. ('0.01234567', 1234000),
  161. ('0.00012345', 12345),
  162. (None, 0),
  163. ):
  164. vmsg('\nTesting memo initialization:')
  165. m = Memo(proto, addr, trade_limit=UniAmt(limit) if limit else None)
  166. vmsg(f'str(memo): {m}')
  167. vmsg(f'repr(memo): {m!r}')
  168. vmsg(f'limit: {limit}')
  169. p = Memo.parse(m)
  170. limit_dec = UniAmt(p.trade_limit, from_unit='satoshi')
  171. vmsg(f'limit_dec: {limit_dec.hl()}')
  172. vmsg('\nTesting memo parsing:')
  173. from pprint import pformat
  174. vmsg(pformat(p._asdict()))
  175. assert p.proto == 'THORChain'
  176. assert p.function == 'SWAP'
  177. assert p.chain == coin.upper()
  178. assert p.asset == coin.upper()
  179. assert p.address == addr.views[addr.view_pref]
  180. assert p.trade_limit == limit_chk
  181. assert p.stream_interval == 1
  182. assert p.stream_quantity == 0 # auto
  183. vmsg('\nTesting is_partial_memo():')
  184. for vec in (
  185. str(m),
  186. 'SWAP:xyz',
  187. '=:xyz',
  188. 's:xyz',
  189. 'a:xz',
  190. '+:xz',
  191. 'WITHDRAW:xz',
  192. 'LOAN+:xz:x:x',
  193. 'TRADE-:xz:x:x',
  194. 'BOND:xz',
  195. ):
  196. vmsg(f' pass: {vec}')
  197. assert Memo.is_partial_memo(vec.encode('ascii')), vec
  198. for vec in (
  199. '=',
  200. 'swap',
  201. 'swap:',
  202. 'swap:abc',
  203. 'SWAP:a',
  204. ):
  205. vmsg(f' fail: {vec}')
  206. assert not Memo.is_partial_memo(vec.encode('ascii')), vec
  207. vmsg('\nTesting error handling:')
  208. def bad(s):
  209. return lambda: Memo.parse(s)
  210. ut.process_bad_data((
  211. ('bad1', 'SwapMemoParseError', 'must contain', bad('x')),
  212. ('bad2', 'SwapMemoParseError', 'must contain', bad('y:z:x')),
  213. ('bad3', 'SwapMemoParseError', 'function abbrev', bad('z:l:foobar:0/1/0')),
  214. ('bad4', 'SwapMemoParseError', 'asset abbrev', bad('=:x:foobar:0/1/0')),
  215. ('bad5', 'SwapMemoParseError', 'failed to parse', bad('=:l:foobar:n')),
  216. ('bad6', 'SwapMemoParseError', 'invalid specifier', bad('=:l:foobar:x/1/0')),
  217. ('bad7', 'SwapMemoParseError', 'extra', bad('=:l:foobar:0/1/0:x')),
  218. ), pfx='')
  219. return True