memo.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. #!/usr/bin/env python3
  2. #
  3. # MMGen Wallet, a terminal-based cryptocurrency wallet
  4. # Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
  5. # Licensed under the GNU General Public License, Version 3:
  6. # https://www.gnu.org/licenses
  7. # Public project repositories:
  8. # https://github.com/mmgen/mmgen-wallet
  9. # https://gitlab.com/mmgen/mmgen-wallet
  10. """
  11. swap.proto.thorchain.memo: THORChain swap protocol memo class
  12. """
  13. from ....util import die
  14. from . import name as proto_name
  15. class Memo:
  16. # The trade limit, i.e., set 100000000 to get a minimum of 1 full asset, else a refund
  17. # Optional. 1e8 or scientific notation
  18. trade_limit = 0
  19. # Swap interval in blocks. Optional. If 0, do not stream
  20. stream_interval = 1
  21. # Swap quantity. The interval value determines the frequency of swaps in blocks
  22. # Optional. If 0, network will determine the number of swaps
  23. stream_quantity = 0
  24. max_len = 250
  25. function = 'SWAP'
  26. asset_abbrevs = {
  27. 'BTC.BTC': 'b',
  28. 'LTC.LTC': 'l',
  29. 'BCH.BCH': 'c',
  30. 'ETH.ETH': 'e',
  31. 'DOGE.DOGE': 'd',
  32. 'THOR.RUNE': 'r',
  33. }
  34. function_abbrevs = {
  35. 'SWAP': '=',
  36. }
  37. @classmethod
  38. def is_partial_memo(cls, s):
  39. import re
  40. ops = {
  41. 'swap': ('SWAP', 's', '='),
  42. 'add': ('ADD', 'a', r'\+'),
  43. 'withdraw': ('WITHDRAW', 'wd', '-'),
  44. 'loan': (r'LOAN(\+|-)', r'\$(\+|-)'), # open/repay
  45. 'pool': (r'POOL(\+|-)',),
  46. 'trade': (r'TRADE(\+|-)',),
  47. 'secure': (r'SECURE(\+|-)',),
  48. 'misc': ('BOND', 'UNBOND', 'LEAVE', 'MIGRATE', 'NOOP', 'DONATE', 'RESERVE'),
  49. }
  50. pat = r'^(' + '|'.join('|'.join(pats) for pats in ops.values()) + r'):\S\S+'
  51. return bool(re.search(pat, str(s)))
  52. @classmethod
  53. def parse(cls, s):
  54. """
  55. All fields are validated, excluding address (cannot validate, since network is unknown)
  56. """
  57. from collections import namedtuple
  58. from ....util import is_int
  59. def get_item(desc):
  60. try:
  61. return fields.pop(0)
  62. except IndexError:
  63. die('SwapMemoParseError', f'malformed {proto_name} memo (missing {desc} field)')
  64. def get_id(data, item, desc):
  65. if item in data:
  66. return item
  67. rev_data = {v:k for k,v in data.items()}
  68. if item in rev_data:
  69. return rev_data[item]
  70. die('SwapMemoParseError', f'{item!r}: unrecognized {proto_name} {desc} abbreviation')
  71. fields = str(s).split(':')
  72. if len(fields) < 4:
  73. die('SwapMemoParseError', 'memo must contain at least 4 comma-separated fields')
  74. function = get_id(cls.function_abbrevs, get_item('function'), 'function')
  75. chain, asset = get_id(cls.asset_abbrevs, get_item('asset'), 'asset').split('.')
  76. address = get_item('address')
  77. desc = 'trade_limit/stream_interval/stream_quantity'
  78. lsq = get_item(desc)
  79. try:
  80. limit, interval, quantity = lsq.split('/')
  81. except ValueError:
  82. die('SwapMemoParseError', f'malformed memo (failed to parse {desc} field) [{lsq}]')
  83. for n in (limit, interval, quantity):
  84. if not is_int(n):
  85. die('SwapMemoParseError', f'malformed memo (non-integer in {desc} field [{lsq}])')
  86. if fields:
  87. die('SwapMemoParseError', 'malformed memo (unrecognized extra data)')
  88. ret = namedtuple(
  89. 'parsed_memo',
  90. ['proto', 'function', 'chain', 'asset', 'address', 'trade_limit', 'stream_interval', 'stream_quantity'])
  91. return ret(proto_name, function, chain, asset, address, int(limit), int(interval), int(quantity))
  92. def __init__(self, proto, addr, chain=None):
  93. self.proto = proto
  94. self.chain = chain or proto.coin
  95. from ....addr import CoinAddr
  96. assert isinstance(addr, CoinAddr)
  97. self.addr = addr.views[addr.view_pref]
  98. assert not ':' in self.addr # colon is record separator, so address mustn’t contain one
  99. def __str__(self):
  100. suf = '/'.join(str(n) for n in (self.trade_limit, self.stream_interval, self.stream_quantity))
  101. asset = f'{self.chain}.{self.proto.coin}'
  102. ret = ':'.join([
  103. self.function_abbrevs[self.function],
  104. self.asset_abbrevs[asset],
  105. self.addr,
  106. suf])
  107. assert len(ret) <= self.max_len, f'{proto_name} memo exceeds maximum length of {self.max_len}'
  108. return ret