8 Commits 54ff658207 ... 2f6e52be73

Author SHA1 Message Date
  The MMGen Project 2f6e52be73 mmgen-swaptx{create,do}: add price protection via --trade-limit option 9 months ago
  The MMGen Project 7300c1ec84 update wiki documentation 9 months ago
  The MMGen Project 5135b8dbdd tx.new_swap: deduct estimated fee from quote for one-output TXs 9 months ago
  The MMGen Project 81e11f3405 tx.new_swap: add initializer, `swap_proto_mod` attribute 9 months ago
  The MMGen Project 809856c07d fixes and cleanups 9 months ago
  The MMGen Project 90d500302f swap.proto.thorchain: move `params` class to `.` 9 months ago
  The MMGen Project 4e0a6755f8 new `ExpInt` class 9 months ago
  The MMGen Project 424253b1e7 support Bitcoin Cash Node v28.0.1 9 months ago
48 changed files with 396 additions and 155 deletions
  1. 4 4
      doc/wiki/Recovering-Your-Keys-Without-the-MMGen-Wallet-Software.md
  2. 1 1
      doc/wiki/commands/command-help-addrgen.md
  3. 1 1
      doc/wiki/commands/command-help-addrimport.md
  4. 1 1
      doc/wiki/commands/command-help-autosign.md
  5. 1 1
      doc/wiki/commands/command-help-keygen.md
  6. 1 1
      doc/wiki/commands/command-help-msg.md
  7. 1 1
      doc/wiki/commands/command-help-passchg.md
  8. 1 1
      doc/wiki/commands/command-help-passgen.md
  9. 1 1
      doc/wiki/commands/command-help-regtest.md
  10. 1 1
      doc/wiki/commands/command-help-seedjoin.md
  11. 1 1
      doc/wiki/commands/command-help-seedsplit.md
  12. 1 1
      doc/wiki/commands/command-help-subwalletgen.md
  13. 22 1
      doc/wiki/commands/command-help-swaptxcreate.md
  14. 22 1
      doc/wiki/commands/command-help-swaptxdo.md
  15. 1 1
      doc/wiki/commands/command-help-tool(detail).md
  16. 1 1
      doc/wiki/commands/command-help-tool(usage).md
  17. 1 1
      doc/wiki/commands/command-help-tool.md
  18. 1 1
      doc/wiki/commands/command-help-txbump.md
  19. 1 1
      doc/wiki/commands/command-help-txcreate.md
  20. 1 1
      doc/wiki/commands/command-help-txdo.md
  21. 1 1
      doc/wiki/commands/command-help-txsend.md
  22. 1 1
      doc/wiki/commands/command-help-txsign.md
  23. 1 1
      doc/wiki/commands/command-help-walletchk.md
  24. 1 1
      doc/wiki/commands/command-help-walletconv.md
  25. 1 1
      doc/wiki/commands/command-help-walletgen.md
  26. 1 1
      doc/wiki/commands/command-help-xmrwallet.md
  27. 2 1
      mmgen/amt.py
  28. 1 1
      mmgen/data/release_date
  29. 1 1
      mmgen/data/version
  30. 19 0
      mmgen/help/swaptxcreate.py
  31. 2 0
      mmgen/main_txcreate.py
  32. 2 0
      mmgen/main_txdo.py
  33. 1 1
      mmgen/proto/btc/daemon.py
  34. 7 0
      mmgen/proto/btc/tx/base.py
  35. 13 4
      mmgen/proto/btc/tx/new_swap.py
  36. 21 3
      mmgen/swap/proto/thorchain/__init__.py
  37. 33 16
      mmgen/swap/proto/thorchain/memo.py
  38. 40 9
      mmgen/swap/proto/thorchain/midgard.py
  39. 0 28
      mmgen/swap/proto/thorchain/params.py
  40. 4 0
      mmgen/tx/bump.py
  41. 4 7
      mmgen/tx/new.py
  42. 28 6
      mmgen/tx/new_swap.py
  43. 32 0
      mmgen/util2.py
  44. 1 1
      nix/bitcoin-cash-node.nix
  45. 39 22
      test/cmdtest_d/ct_swap.py
  46. 1 1
      test/include/unit_test.py
  47. 38 0
      test/modtest_d/ut_misc.py
  48. 36 25
      test/modtest_d/ut_tx.py

+ 4 - 4
doc/wiki/Recovering-Your-Keys-Without-the-MMGen-Wallet-Software.md

@@ -19,11 +19,11 @@ I recover my coins?”
 
 
 Let’s take this scenario to its logical extreme and assume you’ve lost all
 Let’s take this scenario to its logical extreme and assume you’ve lost all
 backup copies of the software, the MMGen Wallet project page has disappeared
 backup copies of the software, the MMGen Wallet project page has disappeared
-from all of [Github][04], [Gitlab][05], [Gitflic][06] and [mmgen.org][07] (or
+from all of [Github][04], [Gitlab][05], [Codeberg][06] and [mmgen.org][07] (or
 been hacked), and no other verifiable repositories or copies are available on
 been hacked), and no other verifiable repositories or copies are available on
 the Internet.  The following tutorial will show you how to recover the private
 the Internet.  The following tutorial will show you how to recover the private
-keys for your coin addresses in the event this very unlikely combination of
-circumstances ever occurs.
+keys for your coin addresses in the event this extremely unlikely combination
+of circumstances ever occurs.
 
 
 In addition to private keys, this tutorial can also be used to recover passwords
 In addition to private keys, this tutorial can also be used to recover passwords
 generated with the `mmgen-passgen` command.
 generated with the `mmgen-passgen` command.
@@ -506,5 +506,5 @@ False
 [03]: https://github.com/spesmilo/electrum/blob/1.9.5/lib/mnemonic.py
 [03]: https://github.com/spesmilo/electrum/blob/1.9.5/lib/mnemonic.py
 [04]: https://github.com/mmgen/mmgen-wallet
 [04]: https://github.com/mmgen/mmgen-wallet
 [05]: https://gitlab.com/mmgen/mmgen-wallet
 [05]: https://gitlab.com/mmgen/mmgen-wallet
-[06]: https://gitflic.ru/project/mmgen/mmgen-wallet
+[06]: https://codeberg.org/mmgen/mmgen-wallet
 [07]: https://mmgen.org/project/mmgen/mmgen-wallet
 [07]: https://mmgen.org/project/mmgen/mmgen-wallet

+ 1 - 1
doc/wiki/commands/command-help-addrgen.md

@@ -120,5 +120,5 @@
     MMGenWallet        .mmdat    wallet,w
     MMGenWallet        .mmdat    wallet,w
     PlainHexSeedFile   .hex      hex,rawhex,plainhex
     PlainHexSeedFile   .hex      hex,rawhex,plainhex
 
 
-  MMGEN v15.1.dev17              February 2025                MMGEN-ADDRGEN(1)
+  MMGEN v15.1.dev18              March 2025                   MMGEN-ADDRGEN(1)
 ```
 ```

+ 1 - 1
doc/wiki/commands/command-help-addrimport.md

@@ -31,5 +31,5 @@
 
 
   It’s recommended to use ‘--rpc-backend=aio’ with ‘--rescan’.
   It’s recommended to use ‘--rpc-backend=aio’ with ‘--rescan’.
 
 
-  MMGEN v15.1.dev17              February 2025             MMGEN-ADDRIMPORT(1)
+  MMGEN v15.1.dev18              March 2025                MMGEN-ADDRIMPORT(1)
 ```
 ```

+ 1 - 1
doc/wiki/commands/command-help-autosign.md

@@ -127,5 +127,5 @@
   Always remember to power off the signing machine when your signing session
   Always remember to power off the signing machine when your signing session
   is over.
   is over.
 
 
-  MMGEN v15.1.dev17              February 2025               MMGEN-AUTOSIGN(1)
+  MMGEN v15.1.dev18              March 2025                  MMGEN-AUTOSIGN(1)
 ```
 ```

+ 1 - 1
doc/wiki/commands/command-help-keygen.md

@@ -125,5 +125,5 @@
     MMGenWallet        .mmdat    wallet,w
     MMGenWallet        .mmdat    wallet,w
     PlainHexSeedFile   .hex      hex,rawhex,plainhex
     PlainHexSeedFile   .hex      hex,rawhex,plainhex
 
 
-  MMGEN v15.1.dev17              February 2025                 MMGEN-KEYGEN(1)
+  MMGEN v15.1.dev18              March 2025                    MMGEN-KEYGEN(1)
 ```
 ```

+ 1 - 1
doc/wiki/commands/command-help-msg.md

@@ -106,5 +106,5 @@
   Verify and display the exported JSON signature data:
   Verify and display the exported JSON signature data:
   $ mmgen-msg verify signatures.json
   $ mmgen-msg verify signatures.json
 
 
-  MMGEN v15.1.dev17              February 2025                    MMGEN-MSG(1)
+  MMGEN v15.1.dev18              March 2025                       MMGEN-MSG(1)
 ```
 ```

+ 1 - 1
doc/wiki/commands/command-help-passchg.md

@@ -53,5 +53,5 @@
     MMGenWallet        .mmdat    wallet,w
     MMGenWallet        .mmdat    wallet,w
     PlainHexSeedFile   .hex      hex,rawhex,plainhex
     PlainHexSeedFile   .hex      hex,rawhex,plainhex
 
 
-  MMGEN v15.1.dev17              February 2025                MMGEN-PASSCHG(1)
+  MMGEN v15.1.dev18              March 2025                   MMGEN-PASSCHG(1)
 ```
 ```

+ 1 - 1
doc/wiki/commands/command-help-passgen.md

@@ -101,5 +101,5 @@
     MMGenWallet        .mmdat    wallet,w
     MMGenWallet        .mmdat    wallet,w
     PlainHexSeedFile   .hex      hex,rawhex,plainhex
     PlainHexSeedFile   .hex      hex,rawhex,plainhex
 
 
-  MMGEN v15.1.dev17              February 2025                MMGEN-PASSGEN(1)
+  MMGEN v15.1.dev18              March 2025                   MMGEN-PASSGEN(1)
 ```
 ```

+ 1 - 1
doc/wiki/commands/command-help-regtest.md

@@ -25,5 +25,5 @@
     wallet_cli      - execute a wallet RPC call with supplied arguments (wallet
     wallet_cli      - execute a wallet RPC call with supplied arguments (wallet
                       is first argument)
                       is first argument)
 
 
-  MMGEN v15.1.dev17              February 2025                MMGEN-REGTEST(1)
+  MMGEN v15.1.dev18              March 2025                   MMGEN-REGTEST(1)
 ```
 ```

+ 1 - 1
doc/wiki/commands/command-help-seedjoin.md

@@ -62,5 +62,5 @@
     MMGenWallet        .mmdat    wallet,w
     MMGenWallet        .mmdat    wallet,w
     PlainHexSeedFile   .hex      hex,rawhex,plainhex
     PlainHexSeedFile   .hex      hex,rawhex,plainhex
 
 
-  MMGEN v15.1.dev17              February 2025               MMGEN-SEEDJOIN(1)
+  MMGEN v15.1.dev18              March 2025                  MMGEN-SEEDJOIN(1)
 ```
 ```

+ 1 - 1
doc/wiki/commands/command-help-seedsplit.md

@@ -144,5 +144,5 @@
     MMGenWallet        .mmdat    wallet,w
     MMGenWallet        .mmdat    wallet,w
     PlainHexSeedFile   .hex      hex,rawhex,plainhex
     PlainHexSeedFile   .hex      hex,rawhex,plainhex
 
 
-  MMGEN v15.1.dev17              February 2025              MMGEN-SEEDSPLIT(1)
+  MMGEN v15.1.dev18              March 2025                 MMGEN-SEEDSPLIT(1)
 ```
 ```

+ 1 - 1
doc/wiki/commands/command-help-subwalletgen.md

@@ -97,5 +97,5 @@
     MMGenWallet        .mmdat    wallet,w
     MMGenWallet        .mmdat    wallet,w
     PlainHexSeedFile   .hex      hex,rawhex,plainhex
     PlainHexSeedFile   .hex      hex,rawhex,plainhex
 
 
-  MMGEN v15.1.dev17              February 2025            MMGEN-SUBWALLETGEN(1)
+  MMGEN v15.1.dev18              March 2025               MMGEN-SUBWALLETGEN(1)
 ```
 ```

+ 22 - 1
doc/wiki/commands/command-help-swaptxcreate.md

@@ -23,6 +23,8 @@
   -I, --inputs       i  Specify transaction inputs (comma-separated list of
   -I, --inputs       i  Specify transaction inputs (comma-separated list of
                         MMGen IDs or coin addresses).  Note that ALL unspent
                         MMGen IDs or coin addresses).  Note that ALL unspent
                         outputs associated with each address will be included.
                         outputs associated with each address will be included.
+  -l, --trade-limit L   Minimum swap amount, as either percentage or absolute
+                        coin amount (see TRADE LIMIT below)
   -L, --autochg-ignore-labels Ignore labels when autoselecting change addresses
   -L, --autochg-ignore-labels Ignore labels when autoselecting change addresses
   -m, --minconf      n  Minimum number of confirmations required to spend
   -m, --minconf      n  Minimum number of confirmations required to spend
                         outputs (default: 1)
                         outputs (default: 1)
@@ -102,6 +104,25 @@
   send value.
   send value.
 
 
 
 
+                                  TRADE LIMIT
+
+  A target value for the swap may be set, known as the “trade limit”.  If
+  this target cannot be met, the network will refund the user’s coins, minus
+  transaction fees (note that the refund goes to the address associated with the
+  transaction’s first input, leading to coin reuse).  Since under certain
+  circumstances large amounts of slippage can occur, resulting in significant
+  losses, setting a trade limit is highly recommended.
+
+  The target may be given as either an absolute coin amount or percentage value.
+  In the latter case, it’s interpreted as the percentage below the “expected
+  amount out” returned by the swap quote server.  Zero or negative percentage
+  values are also accepted, but are likely to result in your coins being
+  refunded.
+
+  The trade limit is rounded to four digits of precision in order to reduce
+  transaction size.
+
+
   ADDRESS TYPES:
   ADDRESS TYPES:
 
 
     Code Type           Description
     Code Type           Description
@@ -175,5 +196,5 @@
 
 
       $ mmgen-tool --coin=bch --bch-rpc-host=gemini twview minconf=0
       $ mmgen-tool --coin=bch --bch-rpc-host=gemini twview minconf=0
 
 
-  MMGEN v15.1.dev17              February 2025            MMGEN-SWAPTXCREATE(1)
+  MMGEN v15.1.dev18              March 2025               MMGEN-SWAPTXCREATE(1)
 ```
 ```

+ 22 - 1
doc/wiki/commands/command-help-swaptxdo.md

@@ -31,6 +31,8 @@
   -k, --keys-from-file f Provide additional keys for non-MMGen addresses
   -k, --keys-from-file f Provide additional keys for non-MMGen addresses
   -K, --keygen-backend n Use backend 'n' for public key generation.  Options
   -K, --keygen-backend n Use backend 'n' for public key generation.  Options
                          for BTC: 1:libsecp256k1 [default] 2:python-ecdsa
                          for BTC: 1:libsecp256k1 [default] 2:python-ecdsa
+  -l, --trade-limit L    Minimum swap amount, as either percentage or absolute
+                         coin amount (see TRADE LIMIT below)
   -L, --autochg-ignore-labels Ignore labels when autoselecting change addresses
   -L, --autochg-ignore-labels Ignore labels when autoselecting change addresses
   -m, --minconf n        Minimum number of confirmations required to spend
   -m, --minconf n        Minimum number of confirmations required to spend
                          outputs (default: 1)
                          outputs (default: 1)
@@ -123,6 +125,25 @@
   send value.
   send value.
 
 
 
 
+                                  TRADE LIMIT
+
+  A target value for the swap may be set, known as the “trade limit”.  If
+  this target cannot be met, the network will refund the user’s coins, minus
+  transaction fees (note that the refund goes to the address associated with the
+  transaction’s first input, leading to coin reuse).  Since under certain
+  circumstances large amounts of slippage can occur, resulting in significant
+  losses, setting a trade limit is highly recommended.
+
+  The target may be given as either an absolute coin amount or percentage value.
+  In the latter case, it’s interpreted as the percentage below the “expected
+  amount out” returned by the swap quote server.  Zero or negative percentage
+  values are also accepted, but are likely to result in your coins being
+  refunded.
+
+  The trade limit is rounded to four digits of precision in order to reduce
+  transaction size.
+
+
   ADDRESS TYPES:
   ADDRESS TYPES:
 
 
     Code Type           Description
     Code Type           Description
@@ -239,5 +260,5 @@
 
 
       $ mmgen-tool --coin=bch --bch-rpc-host=gemini twview minconf=0
       $ mmgen-tool --coin=bch --bch-rpc-host=gemini twview minconf=0
 
 
-  MMGEN v15.1.dev17              February 2025               MMGEN-SWAPTXDO(1)
+  MMGEN v15.1.dev18              March 2025                  MMGEN-SWAPTXDO(1)
 ```
 ```

+ 1 - 1
doc/wiki/commands/command-help-tool(detail).md

@@ -1240,5 +1240,5 @@ Optional KEYWORD ARGS (type and default value shown in square brackets):
 ```
 ```
 
 
 ```text
 ```text
-MMGEN v15.1.dev17              February 2025              MMGEN-TOOL(DETAIL)(1)
+MMGEN v15.1.dev18              March 2025                 MMGEN-TOOL(DETAIL)(1)
 ```
 ```

+ 1 - 1
doc/wiki/commands/command-help-tool(usage).md

@@ -187,5 +187,5 @@ EXAMPLES:
   Same as above, but supply input via STDIN:
   Same as above, but supply input via STDIN:
   $ echo "deadbeefcafe" | mmgen-tool hexreverse -
   $ echo "deadbeefcafe" | mmgen-tool hexreverse -
 
 
-  MMGEN v15.1.dev17              February 2025            MMGEN-TOOL(USAGE)(1)
+  MMGEN v15.1.dev18              March 2025               MMGEN-TOOL(USAGE)(1)
 ```
 ```

+ 1 - 1
doc/wiki/commands/command-help-tool.md

@@ -198,5 +198,5 @@
 
 
   Type ‘mmgen-tool help <command>’ for help on a particular command
   Type ‘mmgen-tool help <command>’ for help on a particular command
 
 
-  MMGEN v15.1.dev17              February 2025                   MMGEN-TOOL(1)
+  MMGEN v15.1.dev18              March 2025                      MMGEN-TOOL(1)
 ```
 ```

+ 1 - 1
doc/wiki/commands/command-help-txbump.md

@@ -116,5 +116,5 @@
     MMGenWallet        .mmdat    wallet,w
     MMGenWallet        .mmdat    wallet,w
     PlainHexSeedFile   .hex      hex,rawhex,plainhex
     PlainHexSeedFile   .hex      hex,rawhex,plainhex
 
 
-  MMGEN v15.1.dev17              February 2025                 MMGEN-TXBUMP(1)
+  MMGEN v15.1.dev18              March 2025                    MMGEN-TXBUMP(1)
 ```
 ```

+ 1 - 1
doc/wiki/commands/command-help-txcreate.md

@@ -123,5 +123,5 @@
 
 
       $ mmgen-txcreate B
       $ mmgen-txcreate B
 
 
-  MMGEN v15.1.dev17              February 2025               MMGEN-TXCREATE(1)
+  MMGEN v15.1.dev18              March 2025                  MMGEN-TXCREATE(1)
 ```
 ```

+ 1 - 1
doc/wiki/commands/command-help-txdo.md

@@ -187,5 +187,5 @@
 
 
       $ mmgen-txdo B
       $ mmgen-txdo B
 
 
-  MMGEN v15.1.dev17              February 2025                   MMGEN-TXDO(1)
+  MMGEN v15.1.dev18              March 2025                      MMGEN-TXDO(1)
 ```
 ```

+ 1 - 1
doc/wiki/commands/command-help-txsend.md

@@ -18,5 +18,5 @@
   -v, --verbose   Be more verbose
   -v, --verbose   Be more verbose
   -y, --yes       Answer 'yes' to prompts, suppress non-essential output
   -y, --yes       Answer 'yes' to prompts, suppress non-essential output
 
 
-  MMGEN v15.1.dev17              February 2025                 MMGEN-TXSEND(1)
+  MMGEN v15.1.dev18              March 2025                    MMGEN-TXSEND(1)
 ```
 ```

+ 1 - 1
doc/wiki/commands/command-help-txsign.md

@@ -82,5 +82,5 @@
     MMGenWallet        .mmdat    wallet,w
     MMGenWallet        .mmdat    wallet,w
     PlainHexSeedFile   .hex      hex,rawhex,plainhex
     PlainHexSeedFile   .hex      hex,rawhex,plainhex
 
 
-  MMGEN v15.1.dev17              February 2025                 MMGEN-TXSIGN(1)
+  MMGEN v15.1.dev18              March 2025                    MMGEN-TXSIGN(1)
 ```
 ```

+ 1 - 1
doc/wiki/commands/command-help-walletchk.md

@@ -51,5 +51,5 @@
     MMGenWallet        .mmdat    wallet,w
     MMGenWallet        .mmdat    wallet,w
     PlainHexSeedFile   .hex      hex,rawhex,plainhex
     PlainHexSeedFile   .hex      hex,rawhex,plainhex
 
 
-  MMGEN v15.1.dev17              February 2025              MMGEN-WALLETCHK(1)
+  MMGEN v15.1.dev18              March 2025                 MMGEN-WALLETCHK(1)
 ```
 ```

+ 1 - 1
doc/wiki/commands/command-help-walletconv.md

@@ -62,5 +62,5 @@
     MMGenWallet        .mmdat    wallet,w
     MMGenWallet        .mmdat    wallet,w
     PlainHexSeedFile   .hex      hex,rawhex,plainhex
     PlainHexSeedFile   .hex      hex,rawhex,plainhex
 
 
-  MMGEN v15.1.dev17              February 2025             MMGEN-WALLETCONV(1)
+  MMGEN v15.1.dev18              March 2025                MMGEN-WALLETCONV(1)
 ```
 ```

+ 1 - 1
doc/wiki/commands/command-help-walletgen.md

@@ -54,5 +54,5 @@
     MMGenWallet        .mmdat    wallet,w
     MMGenWallet        .mmdat    wallet,w
     PlainHexSeedFile   .hex      hex,rawhex,plainhex
     PlainHexSeedFile   .hex      hex,rawhex,plainhex
 
 
-  MMGEN v15.1.dev17              February 2025              MMGEN-WALLETGEN(1)
+  MMGEN v15.1.dev18              March 2025                 MMGEN-WALLETGEN(1)
 ```
 ```

+ 1 - 1
doc/wiki/commands/command-help-xmrwallet.md

@@ -499,5 +499,5 @@
   to delete your old hot wallets, make sure to do so securely using ‘shred’,
   to delete your old hot wallets, make sure to do so securely using ‘shred’,
   ‘wipe’ or some other secure deletion utility.
   ‘wipe’ or some other secure deletion utility.
 
 
-  MMGEN v15.1.dev17              February 2025              MMGEN-XMRWALLET(1)
+  MMGEN v15.1.dev18              March 2025                 MMGEN-XMRWALLET(1)
 ```
 ```

+ 2 - 1
mmgen/amt.py

@@ -48,7 +48,7 @@ class CoinAmt(Decimal, Hilite, InitErrors): # abstract class
 		try:
 		try:
 			if from_unit:
 			if from_unit:
 				assert from_unit in cls.units, f'{from_unit!r}: unrecognized coin unit for {cls.__name__}'
 				assert from_unit in cls.units, f'{from_unit!r}: unrecognized coin unit for {cls.__name__}'
-				assert type(num) is int, 'value is not an integer'
+				assert isinstance(num, int), 'value is not an integer'
 				me = Decimal.__new__(cls, num * getattr(cls, from_unit))
 				me = Decimal.__new__(cls, num * getattr(cls, from_unit))
 			elif from_decimal:
 			elif from_decimal:
 				assert isinstance(num, Decimal), f'number must be of type Decimal, not {type(num).__name__})'
 				assert isinstance(num, Decimal), f'number must be of type Decimal, not {type(num).__name__})'
@@ -157,6 +157,7 @@ class CoinAmt(Decimal, Hilite, InitErrors): # abstract class
 		self.method_not_implemented()
 		self.method_not_implemented()
 
 
 def is_coin_amt(proto, num, from_unit=None, from_decimal=False):
 def is_coin_amt(proto, num, from_unit=None, from_decimal=False):
+	assert proto.coin_amt, 'proto.coin_amt is None!  Did you call init_proto() with ‘need_amt’?'
 	return get_obj(proto.coin_amt, num=num, from_unit=from_unit, from_decimal=from_decimal, silent=True, return_bool=True)
 	return get_obj(proto.coin_amt, num=num, from_unit=from_unit, from_decimal=from_decimal, silent=True, return_bool=True)
 
 
 class BTCAmt(CoinAmt):
 class BTCAmt(CoinAmt):

+ 1 - 1
mmgen/data/release_date

@@ -1 +1 @@
-February 2025
+March 2025

+ 1 - 1
mmgen/data/version

@@ -1 +1 @@
-15.1.dev17
+15.1.dev18

+ 19 - 0
mmgen/help/swaptxcreate.py

@@ -79,4 +79,23 @@ transaction fees, ‘mmnode-feeview’ and ‘mmnode-blocks-info’, in addition
 ‘mmnode-ticker’, which can be used to calculate the current cross-rate between
 ‘mmnode-ticker’, which can be used to calculate the current cross-rate between
 the asset pair of a swap, as well as the total receive value in terms of the
 the asset pair of a swap, as well as the total receive value in terms of the
 send value.
 send value.
+
+
+                                TRADE LIMIT
+
+A target value for the swap may be set, known as the “trade limit”.  If
+this target cannot be met, the network will refund the user’s coins, minus
+transaction fees (note that the refund goes to the address associated with the
+transaction’s first input, leading to coin reuse).  Since under certain
+circumstances large amounts of slippage can occur, resulting in significant
+losses, setting a trade limit is highly recommended.
+
+The target may be given as either an absolute coin amount or percentage value.
+In the latter case, it’s interpreted as the percentage below the “expected
+amount out” returned by the swap quote server.  Zero or negative percentage
+values are also accepted, but are likely to result in your coins being
+refunded.
+
+The trade limit is rounded to four digits of precision in order to reduce
+transaction size.
 """
 """

+ 2 - 0
mmgen/main_txcreate.py

@@ -63,6 +63,8 @@ opts_data = {
 			+                        MMGen IDs or coin addresses).  Note that ALL unspent
 			+                        MMGen IDs or coin addresses).  Note that ALL unspent
 			+                        outputs associated with each address will be included.
 			+                        outputs associated with each address will be included.
 			bt -l, --locktime=    t  Lock time (block height or unix seconds) (default: 0)
 			bt -l, --locktime=    t  Lock time (block height or unix seconds) (default: 0)
+			-s -l, --trade-limit=L   Minimum swap amount, as either percentage or absolute
+			+                        coin amount (see TRADE LIMIT below)
 			b- -L, --autochg-ignore-labels Ignore labels when autoselecting change addresses
 			b- -L, --autochg-ignore-labels Ignore labels when autoselecting change addresses
 			-- -m, --minconf=     n  Minimum number of confirmations required to spend
 			-- -m, --minconf=     n  Minimum number of confirmations required to spend
 			+                        outputs (default: 1)
 			+                        outputs (default: 1)

+ 2 - 0
mmgen/main_txdo.py

@@ -70,6 +70,8 @@ opts_data = {
 			-- -k, --keys-from-file=f Provide additional keys for non-{pnm} addresses
 			-- -k, --keys-from-file=f Provide additional keys for non-{pnm} addresses
 			-- -K, --keygen-backend=n Use backend 'n' for public key generation.  Options
 			-- -K, --keygen-backend=n Use backend 'n' for public key generation.  Options
 			+                         for {coin_id}: {kgs}
 			+                         for {coin_id}: {kgs}
+			-s -l, --trade-limit=L    Minimum swap amount, as either percentage or absolute
+			+                         coin amount (see TRADE LIMIT below)
 			bt -l, --locktime=      t Lock time (block height or unix seconds) (default: 0)
 			bt -l, --locktime=      t Lock time (block height or unix seconds) (default: 0)
 			b- -L, --autochg-ignore-labels Ignore labels when autoselecting change addresses
 			b- -L, --autochg-ignore-labels Ignore labels when autoselecting change addresses
 			-- -m, --minconf=n        Minimum number of confirmations required to spend
 			-- -m, --minconf=n        Minimum number of confirmations required to spend

+ 1 - 1
mmgen/proto/btc/daemon.py

@@ -124,7 +124,7 @@ class bitcoin_core_daemon(CoinDaemon):
 		return e.args[0]
 		return e.args[0]
 
 
 class bitcoin_cash_node_daemon(bitcoin_core_daemon):
 class bitcoin_cash_node_daemon(bitcoin_core_daemon):
-	daemon_data = _dd('Bitcoin Cash Node', 28000000, '28.0.0')
+	daemon_data = _dd('Bitcoin Cash Node', 28000100, '28.0.1')
 	exec_fn = 'bitcoind-bchn'
 	exec_fn = 'bitcoind-bchn'
 	cli_fn = 'bitcoin-cli-bchn'
 	cli_fn = 'bitcoin-cli-bchn'
 	rpc_ports = _nw(8432, 18432, 18543) # use non-standard ports (core+100)
 	rpc_ports = _nw(8432, 18432, 18543) # use non-standard ports (core+100)

+ 7 - 0
mmgen/proto/btc/tx/base.py

@@ -305,6 +305,13 @@ class Base(TxBase):
 			raise ValueError(f'{res}: too many data outputs in transaction (only one allowed)')
 			raise ValueError(f'{res}: too many data outputs in transaction (only one allowed)')
 		return res[0] if len(res) == 1 else None
 		return res[0] if len(res) == 1 else None
 
 
+	@data_output.setter
+	def data_output(self, val):
+		dbool = [bool(o.data) for o in self.outputs]
+		if dbool.count(True) != 1:
+			raise ValueError('more or less than one data output in transaction!')
+		self.outputs[dbool.index(True)] = val
+
 	@property
 	@property
 	def data_outputs(self):
 	def data_outputs(self):
 		return [o for o in self.outputs if o.data]
 		return [o for o in self.outputs if o.data]

+ 13 - 4
mmgen/proto/btc/tx/new_swap.py

@@ -41,10 +41,6 @@ class NewSwap(New, TxNewSwap):
 
 
 	async def process_swap_cmdline_args(self, cmd_args, addrfiles):
 	async def process_swap_cmdline_args(self, cmd_args, addrfiles):
 
 
-		from ....protocol import init_proto
-		import importlib
-		sp = importlib.import_module(f'mmgen.swap.proto.{self.swap_proto}')
-
 		class CmdlineArgs: # listed in command-line order
 		class CmdlineArgs: # listed in command-line order
 			# send_coin      # required: uppercase coin symbol
 			# send_coin      # required: uppercase coin symbol
 			send_amt  = None # optional: Omit to skip change addr and send value of all inputs minus fees to vault
 			send_amt  = None # optional: Omit to skip change addr and send value of all inputs minus fees to vault
@@ -102,6 +98,8 @@ class NewSwap(New, TxNewSwap):
 			if args_in: # done parsing, all args consumed
 			if args_in: # done parsing, all args consumed
 				self.cfg._usage()
 				self.cfg._usage()
 
 
+		from ....protocol import init_proto
+		sp = self.swap_proto_mod
 		args_in = list(cmd_args)
 		args_in = list(cmd_args)
 		args = CmdlineArgs()
 		args = CmdlineArgs()
 		parse()
 		parse()
@@ -130,6 +128,17 @@ class NewSwap(New, TxNewSwap):
 			[f'vault,{args.send_amt}', chg_output.mmid, f'data:{memo}'] if args.send_amt else
 			[f'vault,{args.send_amt}', chg_output.mmid, f'data:{memo}'] if args.send_amt else
 			['vault', f'data:{memo}'])
 			['vault', f'data:{memo}'])
 
 
+	def update_data_output(self, trade_limit):
+		sp = self.swap_proto_mod
+		o = self.data_output._asdict()
+		parsed_memo = sp.data.parse(o['data'].decode())
+		memo = sp.data(
+			self.recv_proto,
+			self.recv_proto.coin_addr(parsed_memo.address),
+			trade_limit = trade_limit)
+		o['data'] = f'data:{memo}'
+		self.data_output = self.Output(self.proto, **o)
+
 	def update_vault_addr(self, addr):
 	def update_vault_addr(self, addr):
 		vault_idx = self.vault_idx
 		vault_idx = self.vault_idx
 		assert vault_idx == 0, f'{vault_idx}: vault index is not zero!'
 		assert vault_idx == 0, f'{vault_idx}: vault index is not zero!'

+ 21 - 3
mmgen/swap/proto/thorchain/__init__.py

@@ -12,14 +12,32 @@
 swap.proto.thorchain: THORChain swap protocol implementation for the MMGen Wallet suite
 swap.proto.thorchain: THORChain swap protocol implementation for the MMGen Wallet suite
 """
 """
 
 
-__all__ = ['params', 'data']
+__all__ = ['data']
 
 
 name = 'THORChain'
 name = 'THORChain'
 
 
-from .params import params
+class params:
+	exp_prec = 4
+	coins = {
+		'send': {
+			'BTC': 'Bitcoin',
+			'LTC': 'Litecoin',
+			'BCH': 'Bitcoin Cash',
+		},
+		'receive': {
+			'BTC': 'Bitcoin',
+			'LTC': 'Litecoin',
+			'BCH': 'Bitcoin Cash',
+		}
+	}
 
 
-from .memo import Memo as data
+from ....util2 import ExpInt
+class ExpInt4(ExpInt):
+	def __new__(cls, spec):
+		return ExpInt.__new__(cls, spec, prec=params.exp_prec)
 
 
 def rpc_client(tx, amt):
 def rpc_client(tx, amt):
 	from .midgard import Midgard
 	from .midgard import Midgard
 	return Midgard(tx, amt)
 	return Midgard(tx, amt)
+
+from .memo import Memo as data

+ 33 - 16
mmgen/swap/proto/thorchain/memo.py

@@ -12,13 +12,15 @@
 swap.proto.thorchain.memo: THORChain swap protocol memo class
 swap.proto.thorchain.memo: THORChain swap protocol memo class
 """
 """
 
 
+from ....util import die
+
 from . import name as proto_name
 from . import name as proto_name
 
 
 class Memo:
 class Memo:
 
 
 	# The trade limit, i.e., set 100000000 to get a minimum of 1 full asset, else a refund
 	# The trade limit, i.e., set 100000000 to get a minimum of 1 full asset, else a refund
 	# Optional. 1e8 or scientific notation
 	# Optional. 1e8 or scientific notation
-	trade_limit = 0
+	trade_limit = None
 
 
 	# Swap interval in blocks. Optional. If 0, do not stream
 	# Swap interval in blocks. Optional. If 0, do not stream
 	stream_interval = 1
 	stream_interval = 1
@@ -65,14 +67,13 @@ class Memo:
 		All fields are validated, excluding address (cannot validate, since network is unknown)
 		All fields are validated, excluding address (cannot validate, since network is unknown)
 		"""
 		"""
 		from collections import namedtuple
 		from collections import namedtuple
-		from ....exception import SwapMemoParseError
 		from ....util import is_int
 		from ....util import is_int
 
 
 		def get_item(desc):
 		def get_item(desc):
 			try:
 			try:
 				return fields.pop(0)
 				return fields.pop(0)
-			except IndexError as e:
-				raise SwapMemoParseError(f'malformed {proto_name} memo (missing {desc} field)') from e
+			except IndexError:
+				die('SwapMemoParseError', f'malformed {proto_name} memo (missing {desc} field)')
 
 
 		def get_id(data, item, desc):
 		def get_id(data, item, desc):
 			if item in data:
 			if item in data:
@@ -80,12 +81,12 @@ class Memo:
 			rev_data = {v:k for k,v in data.items()}
 			rev_data = {v:k for k,v in data.items()}
 			if item in rev_data:
 			if item in rev_data:
 				return rev_data[item]
 				return rev_data[item]
-			raise SwapMemoParseError(f'{item!r}: unrecognized {proto_name} {desc} abbreviation')
+			die('SwapMemoParseError', f'{item!r}: unrecognized {proto_name} {desc} abbreviation')
 
 
 		fields = str(s).split(':')
 		fields = str(s).split(':')
 
 
 		if len(fields) < 4:
 		if len(fields) < 4:
-			raise SwapMemoParseError('memo must contain at least 4 comma-separated fields')
+			die('SwapMemoParseError', 'memo must contain at least 4 comma-separated fields')
 
 
 		function = get_id(cls.function_abbrevs, get_item('function'), 'function')
 		function = get_id(cls.function_abbrevs, get_item('function'), 'function')
 
 
@@ -98,32 +99,48 @@ class Memo:
 
 
 		try:
 		try:
 			limit, interval, quantity = lsq.split('/')
 			limit, interval, quantity = lsq.split('/')
-		except ValueError as e:
-			raise SwapMemoParseError(f'malformed memo (failed to parse {desc} field) [{lsq}]') from e
+		except ValueError:
+			die('SwapMemoParseError', f'malformed memo (failed to parse {desc} field) [{lsq}]')
+
+		from . import ExpInt4
+		try:
+			limit_int = ExpInt4(limit)
+		except Exception as e:
+			die('SwapMemoParseError', str(e))
 
 
-		for n in (limit, interval, quantity):
+		for n in (interval, quantity):
 			if not is_int(n):
 			if not is_int(n):
-				raise SwapMemoParseError(f'malformed memo (non-integer in {desc} field [{lsq}])')
+				die('SwapMemoParseError', f'malformed memo (non-integer in {desc} field [{lsq}])')
 
 
 		if fields:
 		if fields:
-			raise SwapMemoParseError('malformed memo (unrecognized extra data)')
+			die('SwapMemoParseError', 'malformed memo (unrecognized extra data)')
 
 
 		ret = namedtuple(
 		ret = namedtuple(
 			'parsed_memo',
 			'parsed_memo',
 			['proto', 'function', 'chain', 'asset', 'address', 'trade_limit', 'stream_interval', 'stream_quantity'])
 			['proto', 'function', 'chain', 'asset', 'address', 'trade_limit', 'stream_interval', 'stream_quantity'])
 
 
-		return ret(proto_name, function, chain, asset, address, int(limit), int(interval), int(quantity))
+		return ret(proto_name, function, chain, asset, address, limit_int, int(interval), int(quantity))
 
 
-	def __init__(self, proto, addr, chain=None):
+	def __init__(self, proto, addr, chain=None, trade_limit=None):
 		self.proto = proto
 		self.proto = proto
 		self.chain = chain or proto.coin
 		self.chain = chain or proto.coin
-		from ....addr import CoinAddr
-		assert isinstance(addr, CoinAddr)
+		if trade_limit is None:
+			self.trade_limit = self.proto.coin_amt('0')
+		else:
+			assert type(trade_limit) is self.proto.coin_amt, f'{type(trade_limit)} != {self.proto.coin_amt}'
+			self.trade_limit = trade_limit
+		from ....addr import is_coin_addr
+		assert is_coin_addr(proto, addr)
 		self.addr = addr.views[addr.view_pref]
 		self.addr = addr.views[addr.view_pref]
 		assert not ':' in self.addr # colon is record separator, so address mustn’t contain one
 		assert not ':' in self.addr # colon is record separator, so address mustn’t contain one
 
 
 	def __str__(self):
 	def __str__(self):
-		suf = '/'.join(str(n) for n in (self.trade_limit, self.stream_interval, self.stream_quantity))
+		from . import ExpInt4
+		try:
+			tl_enc = ExpInt4(self.trade_limit.to_unit('satoshi')).enc
+		except Exception as e:
+			die('SwapMemoParseError', str(e))
+		suf = '/'.join(str(n) for n in (tl_enc, self.stream_interval, self.stream_quantity))
 		asset = f'{self.chain}.{self.proto.coin}'
 		asset = f'{self.chain}.{self.proto.coin}'
 		ret = ':'.join([
 		ret = ':'.join([
 			self.function_abbrevs[self.function],
 			self.function_abbrevs[self.function],

+ 40 - 9
mmgen/swap/proto/thorchain/midgard.py

@@ -58,20 +58,50 @@ class Midgard:
 			c = self.in_amt.to_unit('satoshi'))
 			c = self.in_amt.to_unit('satoshi'))
 		self.result = self.rpc.get(self.get_str)
 		self.result = self.rpc.get(self.get_str)
 		self.data = json.loads(self.result.content)
 		self.data = json.loads(self.result.content)
+		if not 'expiry' in self.data:
+			from ....util import pp_fmt, die
+			die(2, pp_fmt(self.data))
 
 
-	def format_quote(self):
-		from ....util import make_timestr, pp_fmt, die
+	def format_quote(self, trade_limit, usr_trade_limit, *, deduct_est_fee=False):
+		from ....util import make_timestr, ymsg
 		from ....util2 import format_elapsed_hr
 		from ....util2 import format_elapsed_hr
-		from ....color import blue, cyan, pink, orange
+		from ....color import blue, green, cyan, pink, orange, redbg, yelbg, grnbg
 		from . import name
 		from . import name
 
 
 		d = self.data
 		d = self.data
-		if not 'expiry' in d:
-			die(2, pp_fmt(d))
 		tx = self.tx
 		tx = self.tx
 		in_coin = tx.send_proto.coin
 		in_coin = tx.send_proto.coin
 		out_coin = tx.recv_proto.coin
 		out_coin = tx.recv_proto.coin
+		in_amt = self.in_amt
 		out_amt = tx.recv_proto.coin_amt(int(d['expected_amount_out']), from_unit='satoshi')
 		out_amt = tx.recv_proto.coin_amt(int(d['expected_amount_out']), from_unit='satoshi')
+
+		if trade_limit:
+			from . import ExpInt4
+			e = ExpInt4(trade_limit.to_unit('satoshi'))
+			tl_rounded = tx.recv_proto.coin_amt(e.trunc, from_unit='satoshi')
+			ratio = usr_trade_limit if type(usr_trade_limit) is float else float(tl_rounded / out_amt)
+			direction = 'ABOVE' if ratio > 1 else 'below'
+			mcolor, lblcolor = (
+				(redbg, redbg) if (ratio < 0.93 or ratio > 0.999) else
+				(yelbg, yelbg) if ratio < 0.97 else
+				(green, grnbg))
+			trade_limit_disp = f"""
+  {lblcolor('Trade limit:')}                   {tl_rounded.hl()} {out_coin} """ + mcolor(
+				f'({abs(1 - ratio) * 100:0.2f}% {direction} expected amount)')
+			tx_size_adj = len(e.enc) - 1
+		else:
+			trade_limit_disp = ''
+			tx_size_adj = 0
+
+		_amount_in_label = 'Amount in:'
+		if deduct_est_fee:
+			if d['gas_rate_units'] == 'satsperbyte':
+				in_amt -= tx.feespec2abs(d['recommended_gas_rate'] + 's', tx.estimate_size() + tx_size_adj)
+				out_amt *= (in_amt / self.in_amt)
+				_amount_in_label = 'Amount in (estimated):'
+			else:
+				ymsg('Warning: unknown gas unit ‘{}’, cannot estimate fee'.format(d['gas_rate_units']))
+
 		min_in_amt = tx.send_proto.coin_amt(int(d['recommended_min_amount_in']), from_unit='satoshi')
 		min_in_amt = tx.send_proto.coin_amt(int(d['recommended_min_amount_in']), from_unit='satoshi')
 		gas_unit = {
 		gas_unit = {
 			'satsperbyte': 'sat/byte',
 			'satsperbyte': 'sat/byte',
@@ -82,16 +112,17 @@ class Midgard:
 		fees_pct_disp = str(fees['total_bps'] / 100) + '%'
 		fees_pct_disp = str(fees['total_bps'] / 100) + '%'
 		slip_pct_disp = str(fees['slippage_bps'] / 100) + '%'
 		slip_pct_disp = str(fees['slippage_bps'] / 100) + '%'
 		hdr = f'SWAP QUOTE (source: {self.rpc.host})'
 		hdr = f'SWAP QUOTE (source: {self.rpc.host})'
+
 		return f"""
 		return f"""
 {cyan(hdr)}
 {cyan(hdr)}
   Protocol:                      {blue(name)}
   Protocol:                      {blue(name)}
   Direction:                     {orange(f'{in_coin} => {out_coin}')}
   Direction:                     {orange(f'{in_coin} => {out_coin}')}
   Vault address:                 {cyan(d['inbound_address'])}
   Vault address:                 {cyan(d['inbound_address'])}
   Quote expires:                 {pink(elapsed_disp)} [{make_timestr(d['expiry'])}]
   Quote expires:                 {pink(elapsed_disp)} [{make_timestr(d['expiry'])}]
-  Amount in:                     {self.in_amt.hl()} {in_coin}
-  Expected amount out:           {out_amt.hl()} {out_coin}
-  Rate:                          {(out_amt / self.in_amt).hl()} {out_coin}/{in_coin}
-  Reverse rate:                  {(self.in_amt / out_amt).hl()} {in_coin}/{out_coin}
+  {_amount_in_label:<22}         {in_amt.hl()} {in_coin}
+  Expected amount out:           {out_amt.hl()} {out_coin}{trade_limit_disp}
+  Rate:                          {(out_amt / in_amt).hl()} {out_coin}/{in_coin}
+  Reverse rate:                  {(in_amt / out_amt).hl()} {in_coin}/{out_coin}
   Recommended minimum in amount: {min_in_amt.hl()} {in_coin}
   Recommended minimum in amount: {min_in_amt.hl()} {in_coin}
   Recommended fee:               {pink(d['recommended_gas_rate'])} {pink(gas_unit)}
   Recommended fee:               {pink(d['recommended_gas_rate'])} {pink(gas_unit)}
   Fees:
   Fees:

+ 0 - 28
mmgen/swap/proto/thorchain/params.py

@@ -1,28 +0,0 @@
-#!/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
-
-"""
-swap.proto.thorchain.params: THORChain swap protocol parameters
-"""
-
-class params:
-
-	coins = {
-		'send': {
-			'BTC': 'Bitcoin',
-			'LTC': 'Litecoin',
-			'BCH': 'Bitcoin Cash',
-		},
-		'receive': {
-			'BTC': 'Bitcoin',
-			'LTC': 'Litecoin',
-			'BCH': 'Bitcoin Cash',
-		}
-	}

+ 4 - 0
mmgen/tx/bump.py

@@ -42,6 +42,9 @@ class Bump(Completed, NewSwap):
 					setattr(self, attr, getattr(Base, attr))
 					setattr(self, attr, getattr(Base, attr))
 			self.outputs = self.OutputList(self)
 			self.outputs = self.OutputList(self)
 			self.cfg = kwargs['cfg'] # must use current cfg opts, not those from orig_tx
 			self.cfg = kwargs['cfg'] # must use current cfg opts, not those from orig_tx
+		elif self.is_swap:
+			import importlib
+			self.swap_proto_mod = importlib.import_module(f'mmgen.swap.proto.{self.swap_proto}')
 
 
 		if not self.is_replaceable():
 		if not self.is_replaceable():
 			die(1, f'Transaction {self.txid} is not replaceable')
 			die(1, f'Transaction {self.txid} is not replaceable')
@@ -86,6 +89,7 @@ class Bump(Completed, NewSwap):
 		if self.is_swap:
 		if self.is_swap:
 			self.send_proto = self.proto
 			self.send_proto = self.proto
 			self.recv_proto = self.check_swap_memo().proto
 			self.recv_proto = self.check_swap_memo().proto
+			self.process_swap_options()
 			fee_hint = self.update_vault_output(self.send_amt)
 			fee_hint = self.update_vault_output(self.send_amt)
 		else:
 		else:
 			fee_hint = None
 			fee_hint = None

+ 4 - 7
mmgen/tx/new.py

@@ -82,12 +82,6 @@ class New(Base):
 	chg_autoselected = False
 	chg_autoselected = False
 	_funds_available = namedtuple('funds_available', ['is_positive', 'amt'])
 	_funds_available = namedtuple('funds_available', ['is_positive', 'amt'])
 
 
-	def __init__(self, *args, target=None, **kwargs):
-		if target == 'swaptx':
-			self.is_swap = True
-			self.swap_proto = kwargs['cfg'].swap_proto
-		super().__init__(*args, **kwargs)
-
 	def warn_insufficient_funds(self, amt, coin):
 	def warn_insufficient_funds(self, amt, coin):
 		msg(self.msg_insufficient_funds.format(amt.hl(), coin))
 		msg(self.msg_insufficient_funds.format(amt.hl(), coin))
 
 
@@ -438,6 +432,7 @@ class New(Base):
 			cmd_args, addrfile_args = self.get_addrfiles_from_cmdline(cmd_args)
 			cmd_args, addrfile_args = self.get_addrfiles_from_cmdline(cmd_args)
 			if self.is_swap:
 			if self.is_swap:
 				cmd_args = await self.process_swap_cmdline_args(cmd_args, addrfile_args)
 				cmd_args = await self.process_swap_cmdline_args(cmd_args, addrfile_args)
+				self.process_swap_options()
 				self.proto = self.send_proto # updating self.proto!
 				self.proto = self.send_proto # updating self.proto!
 			from ..rpc import rpc_init
 			from ..rpc import rpc_init
 			self.rpc = await rpc_init(self.cfg, self.proto)
 			self.rpc = await rpc_init(self.cfg, self.proto)
@@ -480,7 +475,9 @@ class New(Base):
 				continue
 				continue
 			fee_hint = None
 			fee_hint = None
 			if self.is_swap:
 			if self.is_swap:
-				fee_hint = self.update_vault_output(self.vault_output.amt or self.sum_inputs())
+				fee_hint = self.update_vault_output(
+					self.vault_output.amt or self.sum_inputs(),
+					deduct_est_fee = self.vault_output == self.chg_output)
 			if funds_left := await self.get_fee(
 			if funds_left := await self.get_fee(
 					self.cfg.fee or fee_hint,
 					self.cfg.fee or fee_hint,
 					outputs_sum,
 					outputs_sum,

+ 28 - 6
mmgen/tx/new_swap.py

@@ -17,21 +17,42 @@ from .new import New
 class NewSwap(New):
 class NewSwap(New):
 	desc = 'swap transaction'
 	desc = 'swap transaction'
 
 
-	async def process_swap_cmdline_args(self, cmd_args, addrfiles):
-		raise NotImplementedError(f'Swap not implemented for protocol {self.proto.__name__}')
-
-	def update_vault_output(self, amt):
+	def __init__(self, *args, **kwargs):
 		import importlib
 		import importlib
-		sp = importlib.import_module(f'mmgen.swap.proto.{self.swap_proto}')
+		self.is_swap = True
+		self.swap_proto = kwargs['cfg'].swap_proto
+		self.swap_proto_mod = importlib.import_module(f'mmgen.swap.proto.{self.swap_proto}')
+		New.__init__(self, *args, **kwargs)
+
+	def process_swap_options(self):
+		if s := self.cfg.trade_limit:
+			self.usr_trade_limit = (
+				1 - float(s[:-1]) / 100 if s.endswith('%') else
+				self.recv_proto.coin_amt(self.cfg.trade_limit))
+		else:
+			self.usr_trade_limit = None
+
+	def update_vault_output(self, amt, *, deduct_est_fee=False):
+		sp = self.swap_proto_mod
 		c = sp.rpc_client(self, amt)
 		c = sp.rpc_client(self, amt)
 
 
 		from ..util import msg
 		from ..util import msg
 		from ..term import get_char
 		from ..term import get_char
+
+		def get_trade_limit():
+			if type(self.usr_trade_limit) is self.recv_proto.coin_amt:
+				return self.usr_trade_limit
+			elif type(self.usr_trade_limit) is float:
+				return (
+					self.recv_proto.coin_amt(int(c.data['expected_amount_out']), from_unit='satoshi')
+					* self.usr_trade_limit)
+
 		while True:
 		while True:
 			self.cfg._util.qmsg(f'Retrieving data from {c.rpc.host}...')
 			self.cfg._util.qmsg(f'Retrieving data from {c.rpc.host}...')
 			c.get_quote()
 			c.get_quote()
+			trade_limit = get_trade_limit()
 			self.cfg._util.qmsg('OK')
 			self.cfg._util.qmsg('OK')
-			msg(c.format_quote())
+			msg(c.format_quote(trade_limit, self.usr_trade_limit, deduct_est_fee=deduct_est_fee))
 			ch = get_char('Press ‘r’ to refresh quote, any other key to continue: ')
 			ch = get_char('Press ‘r’ to refresh quote, any other key to continue: ')
 			msg('')
 			msg('')
 			if ch not in 'Rr':
 			if ch not in 'Rr':
@@ -39,4 +60,5 @@ class NewSwap(New):
 
 
 		self.swap_quote_expiry = c.data['expiry']
 		self.swap_quote_expiry = c.data['expiry']
 		self.update_vault_addr(c.inbound_address)
 		self.update_vault_addr(c.inbound_address)
+		self.update_data_output(trade_limit)
 		return c.rel_fee_hint
 		return c.rel_fee_hint

+ 32 - 0
mmgen/util2.py

@@ -196,3 +196,35 @@ def decode_pretty_hexdump(data):
 	except:
 	except:
 		msg('Data not in hexdump format')
 		msg('Data not in hexdump format')
 		return False
 		return False
+
+class ExpInt(int):
+	'encode or parse an integer in exponential notation with specified precision'
+
+	max_prec = 10
+
+	def __new__(cls, spec, *, prec):
+		assert 0 < prec < cls.max_prec
+		cls.prec = prec
+
+		from .util import is_int
+		if is_int(spec):
+			return int.__new__(cls, spec)
+		else:
+			assert isinstance(spec, str), f'ExpInt: {spec!r}: not a string!'
+			assert len(spec) >= 3, f'ExpInt: {spec!r}: invalid specifier'
+			val, exp = spec.split('e')
+			assert is_int(val) and is_int(exp)
+			return int.__new__(cls, val + '0' * int(exp))
+
+	@property
+	def trunc(self):
+		s = str(self)
+		return int(s[:self.prec] + '0' * (len(s) - self.prec))
+
+	@property
+	def enc(self):
+		s = str(self)
+		s_len = len(s)
+		digits = s[:min(s_len, self.prec)].rstrip('0')
+		ret = '{}e{}'.format(digits, s_len - len(digits))
+		return ret if len(ret) < s_len else s

+ 1 - 1
nix/bitcoin-cash-node.nix

@@ -2,7 +2,7 @@
 
 
 pkgs.stdenv.mkDerivation rec {
 pkgs.stdenv.mkDerivation rec {
     pname = "bitcoin-cash-node";
     pname = "bitcoin-cash-node";
-    version = "v28.0.0";
+    version = "v28.0.1";
     src = fetchGit {
     src = fetchGit {
         url = "https://gitlab.com/bitcoin-cash-node/bitcoin-cash-node";
         url = "https://gitlab.com/bitcoin-cash-node/bitcoin-cash-node";
         ref = "refs/tags/${version}";
         ref = "refs/tags/${version}";

+ 39 - 22
test/cmdtest_d/ct_swap.py

@@ -317,10 +317,10 @@ class CmdTestSwap(CmdTestRegtest, CmdTestAutosignThreaded):
 		return self.addrimport('bob', mmtypes=['C'], proto=self.protos[2])
 		return self.addrimport('bob', mmtypes=['C'], proto=self.protos[2])
 
 
 	def fund_bob_send(self):
 	def fund_bob_send(self):
-		return self._fund_bob(2, 'C', '500')
+		return self._fund_bob(2, 'C', '5')
 
 
 	def bob_bal_send(self):
 	def bob_bal_send(self):
-		return self._bob_bal(2, '500')
+		return self._bob_bal(2, '5')
 
 
 	def setup_recv_coin(self):
 	def setup_recv_coin(self):
 		return self._setup(proto=self.protos[1], remove_datadir=False)
 		return self._setup(proto=self.protos[1], remove_datadir=False)
@@ -332,10 +332,10 @@ class CmdTestSwap(CmdTestRegtest, CmdTestAutosignThreaded):
 		return self._addrimport_bob(1)
 		return self._addrimport_bob(1)
 
 
 	def fund_bob_recv1(self):
 	def fund_bob_recv1(self):
-		return self._fund_bob(1, 'S', '500')
+		return self._fund_bob(1, 'S', '5')
 
 
 	def fund_bob_recv2(self):
 	def fund_bob_recv2(self):
-		return self._fund_bob(1, 'B', '500')
+		return self._fund_bob(1, 'B', '5')
 
 
 	def addrgen_bob_recv_subwallet(self):
 	def addrgen_bob_recv_subwallet(self):
 		return self._addrgen_bob(1, ['C', 'B'], subseed_idx='29L')
 		return self._addrgen_bob(1, ['C', 'B'], subseed_idx='29L')
@@ -343,7 +343,7 @@ class CmdTestSwap(CmdTestRegtest, CmdTestAutosignThreaded):
 	def addrimport_bob_recv_subwallet(self):
 	def addrimport_bob_recv_subwallet(self):
 		return self._subwallet_addrimport('bob', '29L', ['C', 'B'], proto=self.protos[1])
 		return self._subwallet_addrimport('bob', '29L', ['C', 'B'], proto=self.protos[1])
 
 
-	def fund_bob_recv_subwallet(self, proto_idx=1, amt='500'):
+	def fund_bob_recv_subwallet(self, proto_idx=1, amt='5'):
 		coin_arg = f'--coin={self.protos[proto_idx].coin}'
 		coin_arg = f'--coin={self.protos[proto_idx].coin}'
 		t = self.spawn('mmgen-tool', ['--bob', coin_arg, 'listaddresses'])
 		t = self.spawn('mmgen-tool', ['--bob', coin_arg, 'listaddresses'])
 		addr = [s for s in strip_ansi_escapes(t.read()).splitlines() if 'C:1 No' in s][0].split()[3]
 		addr = [s for s in strip_ansi_escapes(t.read()).splitlines() if 'C:1 No' in s][0].split()[3]
@@ -351,7 +351,7 @@ class CmdTestSwap(CmdTestRegtest, CmdTestAutosignThreaded):
 		return t
 		return t
 
 
 	def bob_bal_recv(self):
 	def bob_bal_recv(self):
-		return self._bob_bal(1, '1500')
+		return self._bob_bal(1, '15')
 
 
 	def _swaptxcreate_ui_common(
 	def _swaptxcreate_ui_common(
 			self,
 			self,
@@ -361,7 +361,8 @@ class CmdTestSwap(CmdTestRegtest, CmdTestAutosignThreaded):
 			interactive_fee = None,
 			interactive_fee = None,
 			file_desc       = 'Unsigned transaction',
 			file_desc       = 'Unsigned transaction',
 			reload_quote    = False,
 			reload_quote    = False,
-			sign_and_send   = False):
+			sign_and_send   = False,
+			expect         = None):
 		t.expect('abel:\b', 'q')
 		t.expect('abel:\b', 'q')
 		t.expect('to spend: ', f'{inputs}\n')
 		t.expect('to spend: ', f'{inputs}\n')
 		if reload_quote:
 		if reload_quote:
@@ -374,6 +375,8 @@ class CmdTestSwap(CmdTestRegtest, CmdTestAutosignThreaded):
 			t.expect('to continue: ', 'r')  # reload swap quote
 			t.expect('to continue: ', 'r')  # reload swap quote
 		t.expect('to continue: ', '\n')     # exit swap quote view
 		t.expect('to continue: ', '\n')     # exit swap quote view
 		t.expect('view: ', 'y')             # view TX
 		t.expect('view: ', 'y')             # view TX
+		if expect:
+			t.expect(expect)
 		t.expect('to continue: ', '\n')
 		t.expect('to continue: ', '\n')
 		if sign_and_send:
 		if sign_and_send:
 			t.passphrase(dfl_wcls.desc, rt_pw)
 			t.passphrase(dfl_wcls.desc, rt_pw)
@@ -393,44 +396,58 @@ class CmdTestSwap(CmdTestRegtest, CmdTestAutosignThreaded):
 
 
 	def swaptxcreate1(self, idx=3):
 	def swaptxcreate1(self, idx=3):
 		return self._swaptxcreate_ui_common(
 		return self._swaptxcreate_ui_common(
-			self._swaptxcreate(['BCH', '1.234', f'{self.sid}:C:{idx}', 'LTC', f'{self.sid}:B:3']))
+			self._swaptxcreate(
+				['BCH', '1.234', f'{self.sid}:C:{idx}', 'LTC', f'{self.sid}:B:3'],
+				add_opts = ['--trade-limit=0%']),
+			expect = ':3541e5/1/0')
 
 
 	def swaptxcreate2(self):
 	def swaptxcreate2(self):
-		t = self._swaptxcreate(['BCH', 'LTC'], add_opts=['--no-quiet'])
+		t = self._swaptxcreate(
+			['BCH', 'LTC'],
+			add_opts = ['--no-quiet', '--trade-limit=3.337%'])
 		t.expect('Enter a number> ', '1')
 		t.expect('Enter a number> ', '1')
 		t.expect('OK? (Y/n): ', 'y')
 		t.expect('OK? (Y/n): ', 'y')
-		return self._swaptxcreate_ui_common(t, reload_quote=True)
+		return self._swaptxcreate_ui_common(t, reload_quote=True, expect=':1386e6/1/0')
 
 
 	def swaptxcreate3(self):
 	def swaptxcreate3(self):
 		return self._swaptxcreate_ui_common(
 		return self._swaptxcreate_ui_common(
-			self._swaptxcreate(['BCH', 'LTC', f'{self.sid}:B:3']))
+			self._swaptxcreate(
+				['BCH', 'LTC', f'{self.sid}:B:3'],
+				add_opts = ['--trade-limit=10.1%']),
+			expect = ':1289e6/1/0')
 
 
 	def swaptxcreate4(self):
 	def swaptxcreate4(self):
-		t = self._swaptxcreate(['BCH', '1.234', 'C', 'LTC', 'B'])
+		t = self._swaptxcreate(
+			['BCH', '1.234', 'C', 'LTC', 'B'],
+			add_opts = ['--trade-limit=-1.123%'])
 		t.expect('OK? (Y/n): ', 'y')
 		t.expect('OK? (Y/n): ', 'y')
 		t.expect('Enter a number> ', '1')
 		t.expect('Enter a number> ', '1')
 		t.expect('OK? (Y/n): ', 'y')
 		t.expect('OK? (Y/n): ', 'y')
-		return self._swaptxcreate_ui_common(t)
+		return self._swaptxcreate_ui_common(t, expect=':358e6/1/0')
 
 
 	def swaptxcreate5(self):
 	def swaptxcreate5(self):
-		t = self._swaptxcreate(['BCH', '1.234', f'{self.sid}:C', 'LTC', f'{self.sid}:B'])
+		t = self._swaptxcreate(
+			['BCH', '1.234', f'{self.sid}:C', 'LTC', f'{self.sid}:B'],
+			add_opts = ['--trade-limit=3.6'])
 		t.expect('OK? (Y/n): ', 'y')
 		t.expect('OK? (Y/n): ', 'y')
 		t.expect('OK? (Y/n): ', 'y')
 		t.expect('OK? (Y/n): ', 'y')
-		return self._swaptxcreate_ui_common(t)
+		return self._swaptxcreate_ui_common(t, expect=':36e7/1/0')
 
 
 	def swaptxcreate6(self):
 	def swaptxcreate6(self):
 		addr = make_burn_addr(self.protos[1], mmtype='bech32')
 		addr = make_burn_addr(self.protos[1], mmtype='bech32')
-		t = self._swaptxcreate(['BCH', '1.234', f'{self.sid}:C', 'LTC', addr])
+		t = self._swaptxcreate(
+			['BCH', '1.234', f'{self.sid}:C', 'LTC', addr],
+			add_opts = ['--trade-limit=2.7%'])
 		t.expect('OK? (Y/n): ', 'y')
 		t.expect('OK? (Y/n): ', 'y')
 		t.expect('to confirm: ', 'YES\n')
 		t.expect('to confirm: ', 'YES\n')
-		return self._swaptxcreate_ui_common(t)
+		return self._swaptxcreate_ui_common(t, expect=':3445e5/1/0')
 
 
 	def swaptxcreate7(self):
 	def swaptxcreate7(self):
 		t = self._swaptxcreate(['BCH', '0.56789', 'LTC'])
 		t = self._swaptxcreate(['BCH', '0.56789', 'LTC'])
 		t.expect('OK? (Y/n): ', 'y')
 		t.expect('OK? (Y/n): ', 'y')
 		t.expect('Enter a number> ', '1')
 		t.expect('Enter a number> ', '1')
 		t.expect('OK? (Y/n): ', 'y')
 		t.expect('OK? (Y/n): ', 'y')
-		return self._swaptxcreate_ui_common(t)
+		return self._swaptxcreate_ui_common(t, expect=':0/1/0')
 
 
 	def _swaptxcreate_bad(self, args, *, exit_val=1, expect1=None, expect2=None):
 	def _swaptxcreate_bad(self, args, *, exit_val=1, expect1=None, expect2=None):
 		t = self._swaptxcreate(args, exit_val=exit_val)
 		t = self._swaptxcreate(args, exit_val=exit_val)
@@ -487,7 +504,7 @@ class CmdTestSwap(CmdTestRegtest, CmdTestAutosignThreaded):
 	def swaptxsign1_create(self):
 	def swaptxsign1_create(self):
 		self.get_file_with_ext('rawtx', delete_all=True)
 		self.get_file_with_ext('rawtx', delete_all=True)
 		return self._swaptxcreate_ui_common(
 		return self._swaptxcreate_ui_common(
-			self._swaptxcreate(['LTC', '5.4321', f'{self.sid}:S:2', 'BCH', f'{self.sid}:C:2']))
+			self._swaptxcreate(['LTC', '4.321', f'{self.sid}:S:2', 'BCH', f'{self.sid}:C:2']))
 
 
 	def swaptxsign1(self):
 	def swaptxsign1(self):
 		return self._swaptxsign()
 		return self._swaptxsign()
@@ -605,17 +622,17 @@ class CmdTestSwap(CmdTestRegtest, CmdTestAutosignThreaded):
 		return self._generate_for_proto(2)
 		return self._generate_for_proto(2)
 
 
 	def swap_bal1(self):
 	def swap_bal1(self):
-		return self._bob_bal(1, '1494.56784238')
+		return self._bob_bal(1, '10.67894238')
 
 
 	def swap_bal2(self):
 	def swap_bal2(self):
-		return self._bob_bal(1, '1382.79038152')
+		return self._bob_bal(1, '8.90148152')
 
 
 	def swap_bal3(self):
 	def swap_bal3(self):
 		return self._bob_bal(0, '999.99990407')
 		return self._bob_bal(0, '999.99990407')
 
 
 	def swaptxsign1_do(self):
 	def swaptxsign1_do(self):
 		return self._swaptxcreate_ui_common(
 		return self._swaptxcreate_ui_common(
-			self._swaptxcreate(['LTC', '111.777444', f'{self.sid}:B:2', 'BCH', f'{self.sid}:C:2'], action='txdo'),
+			self._swaptxcreate(['LTC', '1.777444', f'{self.sid}:B:2', 'BCH', f'{self.sid}:C:2'], action='txdo'),
 			sign_and_send = True,
 			sign_and_send = True,
 			file_desc = 'Sent transaction')
 			file_desc = 'Sent transaction')
 
 

+ 1 - 1
test/include/unit_test.py

@@ -144,7 +144,7 @@ class UnitTestHelpers:
 					asyncio.run(ret)
 					asyncio.run(ret)
 			except Exception as e:
 			except Exception as e:
 				exc = type(e).__name__
 				exc = type(e).__name__
-				emsg = e.args[0]
+				emsg = e.args[0] if e.args else '(unspecified error)'
 				cfg._util.vmsg(f' {exc:{exc_w}} [{emsg}]')
 				cfg._util.vmsg(f' {exc:{exc_w}} [{emsg}]')
 				assert exc == exc_chk, m_exc.format(exc, exc_chk)
 				assert exc == exc_chk, m_exc.format(exc, exc_chk)
 				assert re.search(emsg_chk, emsg), m_err.format(emsg, emsg_chk)
 				assert re.search(emsg_chk, emsg), m_err.format(emsg, emsg_chk)

+ 38 - 0
test/modtest_d/ut_misc.py

@@ -154,3 +154,41 @@ class unit_tests:
 		assert '4.3' == ver['4.3'] == '4.3'
 		assert '4.3' == ver['4.3'] == '4.3'
 
 
 		return True
 		return True
+
+	def exp_int(self, name, ut, desc='ExpInt() class'):
+		from mmgen.util2 import ExpInt
+
+		for num, trunc, chk, out in (
+				('0e0',              0,                  0,                  '0'),
+				('0e1',              0,                  0,                  '0'),
+				('3e0',              3,                  3,                  '3'),
+				('3e1',              30,                 30,                 '30'),
+				('3e2',              300,                300,                '300'),
+				('3e3',              3000,               3000,               '3e3'),
+				(3000,               3000,               3000,               '3e3'),
+				(1,                  1,                  1,                  '1'),
+				('123e0',            123,                123,                '123'),
+				(123,                123,                123,                '123'),
+				(12345,              12340,              12345,              '12345'),
+				(123456,             123400,             123456,             '123456'),
+				(1234567,            1234000,            1234000,            '1234e3'),
+				(123456789,          123400000,          123400000,          '1234e5'),
+				(998877665544332211, 998800000000000000, 998800000000000000, '9988e14'),
+			):
+			e = ExpInt(num, prec=4)
+			enc = e.enc
+			assert enc == out, f'{enc} != {out}'
+			assert e.trunc == trunc, f'{e.trunc} != {trunc}'
+			vmsg('')
+			vmsg(f'num        {num}')
+			vmsg(f'enc        {enc}')
+			dec = ExpInt(enc, prec=4)
+			vmsg(f'enc -> dec {dec}')
+			vmsg(f'chk        {chk}')
+			assert dec == chk, f'{dec} != {chk}'
+			dec_enc = dec.enc
+			vmsg(f'dec -> enc {dec_enc}')
+			vmsg(f'out        {out}')
+			assert dec_enc == out, f'{dec_enc} != {out}'
+
+		return True

+ 36 - 25
test/modtest_d/ut_tx.py

@@ -177,26 +177,37 @@ class unit_tests:
 			('ltc', 'bech32'),
 			('ltc', 'bech32'),
 			('bch', 'compressed'),
 			('bch', 'compressed'),
 		):
 		):
-			proto = init_proto(cfg, coin)
+			proto = init_proto(cfg, coin, need_amt=True)
 			addr = make_burn_addr(proto, addrtype)
 			addr = make_burn_addr(proto, addrtype)
 
 
-			vmsg('\nTesting memo initialization:')
-			m = Memo(proto, addr)
-			vmsg(f'str(memo):  {m}')
-			vmsg(f'repr(memo): {m!r}')
-
-			vmsg('\nTesting memo parsing:')
-			p = Memo.parse(m)
-			from pprint import pformat
-			vmsg(pformat(p._asdict()))
-			assert p.proto == 'THORChain'
-			assert p.function == 'SWAP'
-			assert p.chain == coin.upper()
-			assert p.asset == coin.upper()
-			assert p.address == addr.views[addr.view_pref]
-			assert p.trade_limit == 0
-			assert p.stream_interval == 1
-			assert p.stream_quantity == 0 # auto
+			for limit, limit_chk in (
+				('123.4567',   12340000000),
+				('1.234567',   123400000),
+				('0.01234567', 1234000),
+				('0.00012345', 12345),
+				(None, 0),
+			):
+				vmsg('\nTesting memo initialization:')
+				m = Memo(proto, addr, trade_limit=proto.coin_amt(limit) if limit else None)
+				vmsg(f'str(memo):  {m}')
+				vmsg(f'repr(memo): {m!r}')
+				vmsg(f'limit:      {limit}')
+
+				p = Memo.parse(m)
+				limit_dec = proto.coin_amt(p.trade_limit, from_unit='satoshi')
+				vmsg(f'limit_dec:  {limit_dec.hl()}')
+
+				vmsg('\nTesting memo parsing:')
+				from pprint import pformat
+				vmsg(pformat(p._asdict()))
+				assert p.proto == 'THORChain'
+				assert p.function == 'SWAP'
+				assert p.chain == coin.upper()
+				assert p.asset == coin.upper()
+				assert p.address == addr.views[addr.view_pref]
+				assert p.trade_limit == limit_chk
+				assert p.stream_interval == 1
+				assert p.stream_quantity == 0 # auto
 
 
 			vmsg('\nTesting is_partial_memo():')
 			vmsg('\nTesting is_partial_memo():')
 			for vec in (
 			for vec in (
@@ -230,13 +241,13 @@ class unit_tests:
 				return lambda: Memo.parse(s)
 				return lambda: Memo.parse(s)
 
 
 			ut.process_bad_data((
 			ut.process_bad_data((
-				('bad1', 'SwapMemoParseError', 'must contain',    bad('x')),
-				('bad2', 'SwapMemoParseError', 'must contain',    bad('y:z:x')),
-				('bad3', 'SwapMemoParseError', 'function abbrev', bad('z:l:foobar:0/1/0')),
-				('bad4', 'SwapMemoParseError', 'asset abbrev',    bad('=:x:foobar:0/1/0')),
-				('bad5', 'SwapMemoParseError', 'failed to parse', bad('=:l:foobar:n')),
-				('bad6', 'SwapMemoParseError', 'non-integer',     bad('=:l:foobar:x/1/0')),
-				('bad7', 'SwapMemoParseError', 'extra',           bad('=:l:foobar:0/1/0:x')),
+				('bad1', 'SwapMemoParseError', 'must contain',      bad('x')),
+				('bad2', 'SwapMemoParseError', 'must contain',      bad('y:z:x')),
+				('bad3', 'SwapMemoParseError', 'function abbrev',   bad('z:l:foobar:0/1/0')),
+				('bad4', 'SwapMemoParseError', 'asset abbrev',      bad('=:x:foobar:0/1/0')),
+				('bad5', 'SwapMemoParseError', 'failed to parse',   bad('=:l:foobar:n')),
+				('bad6', 'SwapMemoParseError', 'invalid specifier', bad('=:l:foobar:x/1/0')),
+				('bad7', 'SwapMemoParseError', 'extra',             bad('=:l:foobar:0/1/0:x')),
 			), pfx='')
 			), pfx='')
 
 
 		return True
 		return True