From 8a279dc71f45869d6cbf4dcf63ead8a88a1c9bf1 Mon Sep 17 00:00:00 2001
From: tecnovert <tecnovert@tecnovert.net>
Date: Sun, 14 Apr 2024 14:45:13 +0200
Subject: [PATCH] Raise version, transmit amount to instead of rate.

---
 basicswap/__init__.py           |   2 +-
 basicswap/basicswap.py          | 131 ++++++++++++++++++--------------
 basicswap/db.py                 |   3 +-
 basicswap/db_upgrades.py        |   6 +-
 basicswap/js_server.py          |  11 ++-
 basicswap/messages.proto        | 121 +++++++++++++----------------
 basicswap/messages_pb2.py       |  61 +++++++--------
 basicswap/ui/page_offers.py     |  11 ++-
 basicswap/ui/util.py            |   4 +-
 doc/release-notes.md            |  10 +++
 tests/basicswap/test_btc_xmr.py |   2 +-
 tests/basicswap/test_other.py   |  96 ++++++++++++++++++++++-
 12 files changed, 288 insertions(+), 170 deletions(-)

diff --git a/basicswap/__init__.py b/basicswap/__init__.py
index e10d01d..f48cb4b 100644
--- a/basicswap/__init__.py
+++ b/basicswap/__init__.py
@@ -1,3 +1,3 @@
 name = "basicswap"
 
-__version__ = "0.12.7"
+__version__ = "0.13.0"
diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py
index c48508a..3b1da83 100644
--- a/basicswap/basicswap.py
+++ b/basicswap/basicswap.py
@@ -147,11 +147,11 @@ from basicswap.db_util import (
     remove_expired_data,
 )
 
-PROTOCOL_VERSION_SECRET_HASH = 3
-MINPROTO_VERSION_SECRET_HASH = 2
+PROTOCOL_VERSION_SECRET_HASH = 4
+MINPROTO_VERSION_SECRET_HASH = 4
 
-PROTOCOL_VERSION_ADAPTOR_SIG = 3
-MINPROTO_VERSION_ADAPTOR_SIG = 3
+PROTOCOL_VERSION_ADAPTOR_SIG = 4
+MINPROTO_VERSION_ADAPTOR_SIG = 4
 
 
 def validOfferStateToReceiveBid(offer_state):
@@ -1455,14 +1455,13 @@ class BasicSwap(BaseApp):
         finally:
             self.closeSession(session)
 
-    def validateOfferAmounts(self, coin_from, coin_to, amount: int, rate: int, min_bid_amount: int) -> None:
+    def validateOfferAmounts(self, coin_from, coin_to, amount: int, amount_to: int, min_bid_amount: int) -> None:
         ci_from = self.ci(coin_from)
         ci_to = self.ci(coin_to)
         ensure(amount >= min_bid_amount, 'amount < min_bid_amount')
         ensure(amount > ci_from.min_amount(), 'From amount below min value for chain')
         ensure(amount < ci_from.max_amount(), 'From amount above max value for chain')
 
-        amount_to = int((amount * rate) // ci_from.COIN())
         ensure(amount_to > ci_to.min_amount(), 'To amount below min value for chain')
         ensure(amount_to < ci_to.max_amount(), 'To amount above max value for chain')
 
@@ -1524,7 +1523,7 @@ class BasicSwap(BaseApp):
             return extra_options['addr_send_to']
         return self.network_addr
 
-    def postOffer(self, coin_from, coin_to, amount: int, rate, min_bid_amount: int, swap_type,
+    def postOffer(self, coin_from, coin_to, amount: int, rate: int, min_bid_amount: int, swap_type,
                   lock_type=TxLockTypes.SEQUENCE_LOCK_TIME, lock_value: int = 48 * 60 * 60, auto_accept_bids: bool = False, addr_send_from: str = None, extra_options={}) -> bytes:
         # Offer to send offer.amount_from of coin_from in exchange for offer.amount_from * offer.rate of coin_to
 
@@ -1540,10 +1539,14 @@ class BasicSwap(BaseApp):
         except Exception:
             raise ValueError('Unknown coin to type')
 
-        valid_for_seconds = extra_options.get('valid_for_seconds', 60 * 60)
+        valid_for_seconds: int = extra_options.get('valid_for_seconds', 60 * 60)
+        amount_to: int = extra_options.get('amount_to', int((amount * rate) // ci_from.COIN()))
+
+        # Recalculate the rate so it will match the bid rate
+        rate = ci_from.make_int(amount_to / amount, r=1)
 
         self.validateSwapType(coin_from_t, coin_to_t, swap_type)
-        self.validateOfferAmounts(coin_from_t, coin_to_t, amount, rate, min_bid_amount)
+        self.validateOfferAmounts(coin_from_t, coin_to_t, amount, amount_to, min_bid_amount)
         self.validateOfferLockValue(swap_type, coin_from_t, coin_to_t, lock_type, lock_value)
         self.validateOfferValidTime(swap_type, coin_from_t, coin_to_t, valid_for_seconds)
 
@@ -1564,7 +1567,7 @@ class BasicSwap(BaseApp):
             msg_buf.coin_from = int(coin_from)
             msg_buf.coin_to = int(coin_to)
             msg_buf.amount_from = int(amount)
-            msg_buf.rate = int(rate)
+            msg_buf.amount_to = int(amount_to)
             msg_buf.min_bid_amount = int(min_bid_amount)
 
             msg_buf.time_valid = valid_for_seconds
@@ -1644,7 +1647,8 @@ class BasicSwap(BaseApp):
                 coin_from=msg_buf.coin_from,
                 coin_to=msg_buf.coin_to,
                 amount_from=msg_buf.amount_from,
-                rate=msg_buf.rate,
+                amount_to=msg_buf.amount_to,
+                rate=rate,
                 min_bid_amount=msg_buf.min_bid_amount,
                 time_valid=msg_buf.time_valid,
                 lock_type=int(msg_buf.lock_type),
@@ -2264,24 +2268,34 @@ class BasicSwap(BaseApp):
         valid_for_seconds = extra_options.get('valid_for_seconds', 60 * 10)
         self.validateBidValidTime(offer.swap_type, offer.coin_from, offer.coin_to, valid_for_seconds)
 
-        bid_rate = extra_options.get('bid_rate', offer.rate)
+        if not isinstance(amount, int):
+            amount = int(amount)
+            self.log.warning('postBid amount should be an integer type.')
+
+        coin_from = Coins(offer.coin_from)
+        coin_to = Coins(offer.coin_to)
+        ci_from = self.ci(coin_from)
+        ci_to = self.ci(coin_to)
+
+        if 'amount_to' in extra_options:
+            amount_to: int = extra_options['amount_to']
+            bid_rate: int = ci_from.make_int(amount_to / amount, r=1)
+        else:
+            bid_rate = extra_options.get('bid_rate', offer.rate)
+            amount_to: int = int((amount * bid_rate) // ci_from.COIN())
+
         self.validateBidAmount(offer, amount, bid_rate)
 
         self.mxDB.acquire()
         try:
+            self.checkCoinsReady(coin_from, coin_to)
+
             msg_buf = BidMessage()
             msg_buf.protocol_version = PROTOCOL_VERSION_SECRET_HASH
             msg_buf.offer_msg_id = offer_id
             msg_buf.time_valid = valid_for_seconds
-            msg_buf.amount = int(amount)  # amount of coin_from
-            msg_buf.rate = bid_rate
-
-            coin_from = Coins(offer.coin_from)
-            coin_to = Coins(offer.coin_to)
-            ci_from = self.ci(coin_from)
-            ci_to = self.ci(coin_to)
-
-            self.checkCoinsReady(coin_from, coin_to)
+            msg_buf.amount = amount  # amount of coin_from
+            msg_buf.amount_to = amount_to
 
             amount_to = int((msg_buf.amount * bid_rate) // ci_from.COIN())
 
@@ -2314,14 +2328,14 @@ class BasicSwap(BaseApp):
                 bid_id=bid_id,
                 offer_id=offer_id,
                 amount=msg_buf.amount,
-                rate=msg_buf.rate,
+                amount_to=msg_buf.amount_to,
+                rate=bid_rate,
                 pkhash_buyer=msg_buf.pkhash_buyer,
                 proof_address=msg_buf.proof_address,
                 proof_utxos=msg_buf.proof_utxos,
 
                 created_at=now,
                 contract_count=contract_count,
-                amount_to=amount_to,
                 expire_at=now + msg_buf.time_valid,
                 bid_addr=bid_addr,
                 was_sent=True,
@@ -2627,8 +2641,16 @@ class BasicSwap(BaseApp):
             ci_to = self.ci(coin_to)
 
             valid_for_seconds: int = extra_options.get('valid_for_seconds', 60 * 10)
-            bid_rate: int = extra_options.get('bid_rate', offer.rate)
-            amount_to: int = int((int(amount) * bid_rate) // ci_from.COIN())
+
+            if 'amount_to' in extra_options:
+                amount_to: int = extra_options['amount_to']
+                bid_rate: int = ci_from.make_int(amount_to / amount, r=1)
+            elif 'bid_rate' in extra_options:
+                bid_rate: int = extra_options.get('bid_rate', offer.rate)
+                amount_to: int = int((int(amount) * bid_rate) // ci_from.COIN())
+            else:
+                amount_to: int = offer.amount_to
+                bid_rate: int = ci_from.make_int(amount_to / amount, r=1)
 
             bid_created_at: int = self.getTime()
             if offer.swap_type != SwapTypes.XMR_SWAP:
@@ -2646,8 +2668,6 @@ class BasicSwap(BaseApp):
             reverse_bid: bool = self.is_reverse_ads_bid(coin_from)
             if reverse_bid:
                 reversed_rate: int = ci_to.make_int(amount / amount_to, r=1)
-                amount_from: int = int((int(amount_to) * reversed_rate) // ci_to.COIN())
-                ensure(abs(amount_from - amount) < 20, 'invalid bid amount')  # TODO: Tolerance?
 
                 msg_buf = ADSBidIntentMessage()
                 msg_buf.protocol_version = PROTOCOL_VERSION_ADAPTOR_SIG
@@ -2655,7 +2675,6 @@ class BasicSwap(BaseApp):
                 msg_buf.time_valid = valid_for_seconds
                 msg_buf.amount_from = amount
                 msg_buf.amount_to = amount_to
-                msg_buf.rate = bid_rate
 
                 bid_bytes = msg_buf.SerializeToString()
                 payload_hex = str.format('{:02x}', MessageTypes.ADS_BID_LF) + bid_bytes.hex()
@@ -2673,10 +2692,10 @@ class BasicSwap(BaseApp):
                     bid_id=xmr_swap.bid_id,
                     offer_id=offer_id,
                     amount=msg_buf.amount_to,
+                    amount_to=msg_buf.amount_from,
                     rate=reversed_rate,
                     created_at=bid_created_at,
                     contract_count=xmr_swap.contract_count,
-                    amount_to=msg_buf.amount_from,
                     expire_at=bid_created_at + msg_buf.time_valid,
                     bid_addr=bid_addr,
                     was_sent=True,
@@ -2700,7 +2719,7 @@ class BasicSwap(BaseApp):
             msg_buf.offer_msg_id = offer_id
             msg_buf.time_valid = valid_for_seconds
             msg_buf.amount = int(amount)  # Amount of coin_from
-            msg_buf.rate = bid_rate
+            msg_buf.amount_to = amount_to
 
             address_out = self.getReceiveAddressFromPool(coin_from, offer_id, TxTypes.XMR_SWAP_A_LOCK)
             if coin_from == Coins.PART_BLIND:
@@ -2763,10 +2782,10 @@ class BasicSwap(BaseApp):
                 bid_id=xmr_swap.bid_id,
                 offer_id=offer_id,
                 amount=msg_buf.amount,
-                rate=msg_buf.rate,
+                amount_to=msg_buf.amount_to,
+                rate=bid_rate,
                 created_at=bid_created_at,
                 contract_count=xmr_swap.contract_count,
-                amount_to=(msg_buf.amount * msg_buf.rate) // ci_from.COIN(),
                 expire_at=bid_created_at + msg_buf.time_valid,
                 bid_addr=bid_addr,
                 was_sent=True,
@@ -4537,12 +4556,13 @@ class BasicSwap(BaseApp):
         ensure(offer_data.coin_from != offer_data.coin_to, 'coin_from == coin_to')
 
         self.validateSwapType(coin_from, coin_to, offer_data.swap_type)
-        self.validateOfferAmounts(coin_from, coin_to, offer_data.amount_from, offer_data.rate, offer_data.min_bid_amount)
+        self.validateOfferAmounts(coin_from, coin_to, offer_data.amount_from, offer_data.amount_to, offer_data.min_bid_amount)
         self.validateOfferLockValue(offer_data.swap_type, coin_from, coin_to, offer_data.lock_type, offer_data.lock_value)
         self.validateOfferValidTime(offer_data.swap_type, coin_from, coin_to, offer_data.time_valid)
 
         ensure(msg['sent'] + offer_data.time_valid >= now, 'Offer expired')
 
+        offer_rate: int = ci_from.make_int(offer_data.amount_to / offer_data.amount_from, r=1)
         reverse_bid: bool = self.is_reverse_ads_bid(coin_from)
 
         if offer_data.swap_type == SwapTypes.SELLER_FIRST:
@@ -4593,7 +4613,8 @@ class BasicSwap(BaseApp):
                     coin_from=offer_data.coin_from,
                     coin_to=offer_data.coin_to,
                     amount_from=offer_data.amount_from,
-                    rate=offer_data.rate,
+                    amount_to=offer_data.amount_to,
+                    rate=offer_rate,
                     min_bid_amount=offer_data.min_bid_amount,
                     time_valid=offer_data.time_valid,
                     lock_type=int(offer_data.lock_type),
@@ -4806,16 +4827,16 @@ class BasicSwap(BaseApp):
         ensure(now <= offer.expire_at, 'Offer expired')
         self.validateBidValidTime(offer.swap_type, offer.coin_from, offer.coin_to, bid_data.time_valid)
         ensure(now <= msg['sent'] + bid_data.time_valid, 'Bid expired')
-        self.validateBidAmount(offer, bid_data.amount, bid_data.rate)
-
-        # TODO: Allow higher bids
-        # assert (bid_data.rate != offer['data'].rate), 'Bid rate mismatch'
 
         coin_to = Coins(offer.coin_to)
         ci_from = self.ci(offer.coin_from)
         ci_to = self.ci(coin_to)
+        bid_rate: int = ci_from.make_int(bid_data.amount_to / bid_data.amount, r=1)
+        self.validateBidAmount(offer, bid_data.amount, bid_rate)
+
+        # TODO: Allow higher bids
+        # assert (bid_data.rate != offer['data'].rate), 'Bid rate mismatch'
 
-        amount_to = int((bid_data.amount * bid_data.rate) // ci_from.COIN())
         swap_type = offer.swap_type
         if swap_type == SwapTypes.SELLER_FIRST:
             ensure(len(bid_data.pkhash_buyer) == 20, 'Bad pkhash_buyer length')
@@ -4823,7 +4844,7 @@ class BasicSwap(BaseApp):
             proof_utxos = ci_to.decodeProofUtxos(bid_data.proof_utxos)
             sum_unspent = ci_to.verifyProofOfFunds(bid_data.proof_address, bid_data.proof_signature, proof_utxos, offer_id)
             self.log.debug('Proof of funds %s %s', bid_data.proof_address, self.ci(coin_to).format_amount(sum_unspent))
-            ensure(sum_unspent >= amount_to, 'Proof of funds failed')
+            ensure(sum_unspent >= bid_data.amount_to, 'Proof of funds failed')
 
         elif swap_type == SwapTypes.BUYER_FIRST:
             raise ValueError('TODO')
@@ -4840,13 +4861,13 @@ class BasicSwap(BaseApp):
                 offer_id=offer_id,
                 protocol_version=bid_data.protocol_version,
                 amount=bid_data.amount,
-                rate=bid_data.rate,
+                amount_to=bid_data.amount_to,
+                rate=bid_rate,
                 pkhash_buyer=bid_data.pkhash_buyer,
                 proof_address=bid_data.proof_address,
                 proof_utxos=bid_data.proof_utxos,
 
                 created_at=msg['sent'],
-                amount_to=amount_to,
                 expire_at=msg['sent'] + bid_data.time_valid,
                 bid_addr=msg['from'],
                 was_received=True,
@@ -5116,7 +5137,8 @@ class BasicSwap(BaseApp):
         self.validateBidValidTime(offer.swap_type, offer.coin_from, offer.coin_to, bid_data.time_valid)
         ensure(now <= msg['sent'] + bid_data.time_valid, 'Bid expired')
 
-        self.validateBidAmount(offer, bid_data.amount, bid_data.rate)
+        bid_rate: int = ci_from.make_int(bid_data.amount_to / bid_data.amount, r=1)
+        self.validateBidAmount(offer, bid_data.amount, bid_rate)
 
         ensure(ci_to.verifyKey(bid_data.kbvf), 'Invalid chain B follower view key')
         ensure(ci_from.verifyPubkey(bid_data.pkaf), 'Invalid chain A follower public key')
@@ -5135,9 +5157,9 @@ class BasicSwap(BaseApp):
                 offer_id=offer_id,
                 protocol_version=bid_data.protocol_version,
                 amount=bid_data.amount,
-                rate=bid_data.rate,
+                amount_to=bid_data.amount_to,
+                rate=bid_rate,
                 created_at=msg['sent'],
-                amount_to=(bid_data.amount * bid_data.rate) // ci_from.COIN(),
                 expire_at=msg['sent'] + bid_data.time_valid,
                 bid_addr=msg['from'],
                 was_received=True,
@@ -5992,14 +6014,10 @@ class BasicSwap(BaseApp):
         self.validateBidValidTime(offer.swap_type, offer.coin_from, offer.coin_to, bid_data.time_valid)
         ensure(now <= msg['sent'] + bid_data.time_valid, 'Bid expired')
 
-        amount_from: int = bid_data.amount_from
-        amount_to: int = (bid_data.amount_from * bid_data.rate) // ci_to.COIN()
-        ensure(abs(amount_to - bid_data.amount_to) < 20, 'invalid bid amount_to')  # TODO: Tolerance?
-        reversed_rate: int = ci_from.make_int(amount_from / bid_data.amount_to, r=1)
-        amount_from_recovered: int = int((amount_to * reversed_rate) // ci_from.COIN())
-        ensure(abs(amount_from - amount_from_recovered) < 20, 'invalid bid amount_from')  # TODO: Tolerance?
-
-        self.validateBidAmount(offer, amount_from, bid_data.rate)
+        # ci_from/to are reversed
+        bid_rate: int = ci_to.make_int(bid_data.amount_to / bid_data.amount_from, r=1)
+        reversed_rate: int = ci_from.make_int(bid_data.amount_from / bid_data.amount_to, r=1)
+        self.validateBidAmount(offer, bid_data.amount_from, bid_rate)
 
         bid_id = bytes.fromhex(msg['msgid'])
 
@@ -6010,10 +6028,10 @@ class BasicSwap(BaseApp):
                 bid_id=bid_id,
                 offer_id=offer_id,
                 protocol_version=bid_data.protocol_version,
-                amount=amount_to,
+                amount=bid_data.amount_to,
+                amount_to=bid_data.amount_from,
                 rate=reversed_rate,
                 created_at=msg['sent'],
-                amount_to=amount_from,
                 expire_at=msg['sent'] + bid_data.time_valid,
                 bid_addr=msg['from'],
                 was_sent=False,
@@ -6044,7 +6062,7 @@ class BasicSwap(BaseApp):
             session = self.openSession()
             self.notify(NT.BID_RECEIVED, {'type': 'ads_reversed', 'bid_id': bid.bid_id.hex(), 'offer_id': bid.offer_id.hex()}, session)
 
-            options = {'reverse_bid': True, 'bid_rate': bid_data.rate}
+            options = {'reverse_bid': True, 'bid_rate': bid_rate}
             if self.shouldAutoAcceptBid(offer, bid, session, options=options):
                 delay = self.get_delay_event_seconds()
                 self.log.info('Auto accepting reverse adaptor-sig bid %s in %d seconds', bid.bid_id.hex(), delay)
@@ -6998,6 +7016,7 @@ class BasicSwap(BaseApp):
                     result[13] = amount_to
                     ci_from = self.ci(coin_from)
                     result[10] = ci_from.make_int(amount_to / amount_from, r=1)
+
                 rv.append(result)
             return rv
         finally:
diff --git a/basicswap/db.py b/basicswap/db.py
index 8e7f6ce..b8dce4f 100644
--- a/basicswap/db.py
+++ b/basicswap/db.py
@@ -11,7 +11,7 @@ from enum import IntEnum, auto
 from sqlalchemy.ext.declarative import declarative_base
 
 
-CURRENT_DB_VERSION = 22
+CURRENT_DB_VERSION = 23
 CURRENT_DB_DATA_VERSION = 4
 Base = declarative_base()
 
@@ -61,6 +61,7 @@ class Offer(Base):
     coin_from = sa.Column(sa.Integer)
     coin_to = sa.Column(sa.Integer)
     amount_from = sa.Column(sa.BigInteger)
+    amount_to = sa.Column(sa.BigInteger)
     rate = sa.Column(sa.BigInteger)
     min_bid_amount = sa.Column(sa.BigInteger)
     time_valid = sa.Column(sa.BigInteger)
diff --git a/basicswap/db_upgrades.py b/basicswap/db_upgrades.py
index a53032a..ae3f3cd 100644
--- a/basicswap/db_upgrades.py
+++ b/basicswap/db_upgrades.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 
-# Copyright (c) 2022-2023 tecnovert
+# Copyright (c) 2022-2024 tecnovert
 # Distributed under the MIT software license, see the accompanying
 # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
 
@@ -297,7 +297,9 @@ def upgradeDatabase(self, db_version):
             db_version += 1
             session.execute('ALTER TABLE offers ADD COLUMN proof_utxos BLOB')
             session.execute('ALTER TABLE bids ADD COLUMN proof_utxos BLOB')
-
+        elif current_version == 22:
+            db_version += 1
+            session.execute('ALTER TABLE offers ADD COLUMN amount_to INTEGER')
         if current_version != db_version:
             self.db_version = db_version
             self.setIntKVInSession('db_version', db_version, session)
diff --git a/basicswap/js_server.py b/basicswap/js_server.py
index aeb8b43..27fcebf 100644
--- a/basicswap/js_server.py
+++ b/basicswap/js_server.py
@@ -339,7 +339,10 @@ def js_bids(self, url_split, post_string: str, is_json: bool) -> bytes:
             extra_options = {
                 'valid_for_seconds': valid_for_seconds,
             }
-            if have_data_entry(post_data, 'bid_rate'):
+
+            if have_data_entry(post_data, 'amount_to'):
+                extra_options['amount_to'] = inputAmount(get_data_entry(post_data, 'amount_to'), ci_to)
+            elif have_data_entry(post_data, 'bid_rate'):
                 extra_options['bid_rate'] = ci_to.make_int(get_data_entry(post_data, 'bid_rate'), r=1)
             if have_data_entry(post_data, 'bid_amount'):
                 amount_from = inputAmount(get_data_entry(post_data, 'bid_amount'), ci_from)
@@ -493,10 +496,10 @@ def js_rate(self, url_split, post_string, is_json) -> bytes:
             amount_from = ci_from.format_amount(int((amt_to * rate) // ci_to.COIN()), r=1)
             return bytes(json.dumps({'amount_from': amount_from}), 'UTF-8')
 
-    amt_from = inputAmount(get_data_entry(post_data, 'amt_from'), ci_from)
-    amt_to = inputAmount(get_data_entry(post_data, 'amt_to'), ci_to)
+    amt_from: int = inputAmount(get_data_entry(post_data, 'amt_from'), ci_from)
+    amt_to: int = inputAmount(get_data_entry(post_data, 'amt_to'), ci_to)
 
-    rate = ci_to.format_amount(ci_from.make_int(amt_to / amt_from, r=1))
+    rate: int = ci_to.format_amount(ci_from.make_int(amt_to / amt_from, r=1))
     return bytes(json.dumps({'rate': rate}), 'UTF-8')
 
 
diff --git a/basicswap/messages.proto b/basicswap/messages.proto
index 87cf273..fb5815d 100644
--- a/basicswap/messages.proto
+++ b/basicswap/messages.proto
@@ -4,12 +4,13 @@ package basicswap;
 
 /* Step 1, seller -> network */
 message OfferMessage {
-    uint32 coin_from = 1;
-    uint32 coin_to = 2;
-    uint64 amount_from = 3;
-    uint64 rate = 4;
-    uint64 min_bid_amount = 5;
-    uint64 time_valid = 6;
+    uint32 protocol_version = 1;
+    uint32 coin_from = 2;
+    uint32 coin_to = 3;
+    uint64 amount_from = 4;
+    uint64 amount_to = 5;
+    uint64 min_bid_amount = 6;
+    uint64 time_valid = 7;
     enum LockType {
         NOT_SET = 0;
         SEQUENCE_LOCK_BLOCKS = 1;
@@ -17,20 +18,19 @@ message OfferMessage {
         ABS_LOCK_BLOCKS = 3;
         ABS_LOCK_TIME = 4;
     }
-    LockType lock_type = 7;
-    uint32 lock_value = 8;
-    uint32 swap_type = 9;
+    LockType lock_type = 8;
+    uint32 lock_value = 9;
+    uint32 swap_type = 10;
 
     /* optional */
-    string proof_address = 10;
-    string proof_signature = 11;
-    bytes pkhash_seller = 12;
-    bytes secret_hash = 13;
+    string proof_address = 11;
+    string proof_signature = 12;
+    bytes pkhash_seller = 13;
+    bytes secret_hash = 14;
 
-    uint64 fee_rate_from = 14;
-    uint64 fee_rate_to = 15;
+    uint64 fee_rate_from = 15;
+    uint64 fee_rate_to = 16;
 
-    uint32 protocol_version = 16;
     bool amount_negotiable = 17;
     bool rate_negotiable = 18;
 
@@ -39,34 +39,27 @@ message OfferMessage {
 
 /* Step 2, buyer -> seller */
 message BidMessage {
-    bytes offer_msg_id = 1;
-    uint64 time_valid = 2;          /* seconds bid is valid for */
-    uint64 amount = 3;              /* amount of amount_from bid is for */
-    uint64 rate = 4;
-    bytes pkhash_buyer = 5;         /* buyer's address to receive amount_from */
-    string proof_address = 6;
-    string proof_signature = 7;
-
-    uint32 protocol_version = 8;
+    uint32 protocol_version = 1;
+    bytes offer_msg_id = 2;
+    uint64 time_valid = 3;          /* seconds bid is valid for */
+    uint64 amount = 4;              /* amount of amount_from bid is for */
+    uint64 amount_to = 5;
+    bytes pkhash_buyer = 6;         /* buyer's address to receive amount_from */
+    string proof_address = 7;
+    string proof_signature = 8;
 
     bytes proof_utxos = 9;          /* 32 byte txid 2 byte vout, repeated */
 }
 
 /* For tests */
-message BidMessage_v1Deprecated {
-    bytes offer_msg_id = 1;
-    uint64 time_valid = 2;          /* seconds bid is valid for */
-    uint64 amount = 3;              /* amount of amount_from bid is for */
-    uint64 rate = 4;
-    bytes pkhash_buyer = 5;         /* buyer's address to receive amount_from */
-    string proof_address = 6;
-    string proof_signature = 7;
-
-    uint32 protocol_version = 8;
+message BidMessage_test {
+    uint32 protocol_version = 1;
+    bytes offer_msg_id = 2;
+    uint64 time_valid = 3;
+    uint64 amount = 4;
+    uint64 rate = 5;
 }
 
-
-
 /* Step 3, seller -> buyer */
 message BidAcceptMessage {
     bytes bid_msg_id = 1;
@@ -81,25 +74,23 @@ message OfferRevokeMessage {
 
 message BidRejectMessage {
     bytes bid_msg_id = 1;
-
     uint32 reject_code = 2;
 }
 
 message XmrBidMessage {
     /* MSG1L, F -> L */
-    bytes offer_msg_id = 1;
-    uint64 time_valid = 2;          /* seconds bid is valid for */
-    uint64 amount = 3;              /* amount of amount_from bid is for */
-    uint64 rate = 4;
+    uint32 protocol_version = 1;
+    bytes offer_msg_id = 2;
+    uint64 time_valid = 3;          /* seconds bid is valid for */
+    uint64 amount = 4;              /* amount of amount_from bid is for */
+    uint64 amount_to = 5;
 
-    bytes pkaf = 5;
+    bytes pkaf = 6;
 
-    bytes kbvf = 6;
-    bytes kbsf_dleag = 7;
+    bytes kbvf = 7;
+    bytes kbsf_dleag = 8;
 
-    bytes dest_af = 8;
-
-    uint32 protocol_version = 9;
+    bytes dest_af = 9;
 }
 
 message XmrSplitMessage {
@@ -112,24 +103,22 @@ message XmrSplitMessage {
 message XmrBidAcceptMessage {
     bytes bid_msg_id = 1;
 
-    bytes pkal = 3;
-
-    bytes kbvl = 4;
-    bytes kbsl_dleag = 5;
+    bytes pkal = 2;
+    bytes kbvl = 3;
+    bytes kbsl_dleag = 4;
 
     /* MSG2F */
-    bytes a_lock_tx = 6;
-    bytes a_lock_tx_script = 7;
-    bytes a_lock_refund_tx = 8;
-    bytes a_lock_refund_tx_script = 9;
-    bytes a_lock_refund_spend_tx = 10;
-    bytes al_lock_refund_tx_sig = 11;
+    bytes a_lock_tx = 5;
+    bytes a_lock_tx_script = 6;
+    bytes a_lock_refund_tx = 7;
+    bytes a_lock_refund_tx_script = 8;
+    bytes a_lock_refund_spend_tx = 9;
+    bytes al_lock_refund_tx_sig = 10;
 }
 
 message XmrBidLockTxSigsMessage {
     /* MSG3L */
     bytes bid_msg_id = 1;
-
     bytes af_lock_refund_spend_tx_esig = 2;
     bytes af_lock_refund_tx_sig = 3;
 }
@@ -137,7 +126,6 @@ message XmrBidLockTxSigsMessage {
 message XmrBidLockSpendTxMessage {
     /* MSG4F */
     bytes bid_msg_id = 1;
-
     bytes a_lock_spend_tx = 2;
     bytes kal_sig = 3;
 }
@@ -145,19 +133,16 @@ message XmrBidLockSpendTxMessage {
 message XmrBidLockReleaseMessage {
     /* MSG5F */
     bytes bid_msg_id = 1;
-
     bytes al_lock_spend_tx_esig = 2;
 }
 
 message ADSBidIntentMessage {
     /* L -> F Sent from bidder, construct a reverse bid */
-    bytes offer_msg_id = 1;
-    uint64 time_valid = 2;          /* seconds bid is valid for */
-    uint64 amount_from = 3;         /* amount of offer.coin_from bid is for */
-    uint64 amount_to = 4;           /* amount of offer.coin_to bid is for, equivalent to bid.amount */
-    uint64 rate = 5;                /* amount of offer.coin_from bid is for */
-
-    uint32 protocol_version = 6;
+    uint32 protocol_version = 1;
+    bytes offer_msg_id = 2;
+    uint64 time_valid = 3;          /* seconds bid is valid for */
+    uint64 amount_from = 4;         /* amount of offer.coin_from bid is for */
+    uint64 amount_to = 5;           /* amount of offer.coin_to bid is for, equivalent to bid.amount */
 }
 
 message ADSBidIntentAcceptMessage {
diff --git a/basicswap/messages_pb2.py b/basicswap/messages_pb2.py
index 7f889e4..74f08a2 100644
--- a/basicswap/messages_pb2.py
+++ b/basicswap/messages_pb2.py
@@ -1,6 +1,7 @@
 # -*- coding: utf-8 -*-
 # Generated by the protocol buffer compiler.  DO NOT EDIT!
 # source: messages.proto
+# Protobuf Python Version: 4.25.3
 """Generated protocol buffer code."""
 from google.protobuf import descriptor as _descriptor
 from google.protobuf import descriptor_pool as _descriptor_pool
@@ -13,7 +14,7 @@ _sym_db = _symbol_database.Default()
 
 
 
-DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0emessages.proto\x12\tbasicswap\"\xa6\x04\n\x0cOfferMessage\x12\x11\n\tcoin_from\x18\x01 \x01(\r\x12\x0f\n\x07\x63oin_to\x18\x02 \x01(\r\x12\x13\n\x0b\x61mount_from\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x16\n\x0emin_bid_amount\x18\x05 \x01(\x04\x12\x12\n\ntime_valid\x18\x06 \x01(\x04\x12\x33\n\tlock_type\x18\x07 \x01(\x0e\x32 .basicswap.OfferMessage.LockType\x12\x12\n\nlock_value\x18\x08 \x01(\r\x12\x11\n\tswap_type\x18\t \x01(\r\x12\x15\n\rproof_address\x18\n \x01(\t\x12\x17\n\x0fproof_signature\x18\x0b \x01(\t\x12\x15\n\rpkhash_seller\x18\x0c \x01(\x0c\x12\x13\n\x0bsecret_hash\x18\r \x01(\x0c\x12\x15\n\rfee_rate_from\x18\x0e \x01(\x04\x12\x13\n\x0b\x66\x65\x65_rate_to\x18\x0f \x01(\x04\x12\x18\n\x10protocol_version\x18\x10 \x01(\r\x12\x19\n\x11\x61mount_negotiable\x18\x11 \x01(\x08\x12\x17\n\x0frate_negotiable\x18\x12 \x01(\x08\"q\n\x08LockType\x12\x0b\n\x07NOT_SET\x10\x00\x12\x18\n\x14SEQUENCE_LOCK_BLOCKS\x10\x01\x12\x16\n\x12SEQUENCE_LOCK_TIME\x10\x02\x12\x13\n\x0f\x41\x42S_LOCK_BLOCKS\x10\x03\x12\x11\n\rABS_LOCK_TIME\x10\x04\"\xc9\x01\n\nBidMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x12\n\ntime_valid\x18\x02 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x14\n\x0cpkhash_buyer\x18\x05 \x01(\x0c\x12\x15\n\rproof_address\x18\x06 \x01(\t\x12\x17\n\x0fproof_signature\x18\x07 \x01(\t\x12\x18\n\x10protocol_version\x18\x08 \x01(\r\x12\x13\n\x0bproof_utxos\x18\t \x01(\x0c\"\xc1\x01\n\x17\x42idMessage_v1Deprecated\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x12\n\ntime_valid\x18\x02 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x14\n\x0cpkhash_buyer\x18\x05 \x01(\x0c\x12\x15\n\rproof_address\x18\x06 \x01(\t\x12\x17\n\x0fproof_signature\x18\x07 \x01(\t\x12\x18\n\x10protocol_version\x18\x08 \x01(\r\"V\n\x10\x42idAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x15\n\rinitiate_txid\x18\x02 \x01(\x0c\x12\x17\n\x0f\x63ontract_script\x18\x03 \x01(\x0c\"=\n\x12OfferRevokeMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x11\n\tsignature\x18\x02 \x01(\x0c\";\n\x10\x42idRejectMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x13\n\x0breject_code\x18\x02 \x01(\r\"\xb2\x01\n\rXmrBidMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x12\n\ntime_valid\x18\x02 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x0c\n\x04pkaf\x18\x05 \x01(\x0c\x12\x0c\n\x04kbvf\x18\x06 \x01(\x0c\x12\x12\n\nkbsf_dleag\x18\x07 \x01(\x0c\x12\x0f\n\x07\x64\x65st_af\x18\x08 \x01(\x0c\x12\x18\n\x10protocol_version\x18\t \x01(\r\"T\n\x0fXmrSplitMessage\x12\x0e\n\x06msg_id\x18\x01 \x01(\x0c\x12\x10\n\x08msg_type\x18\x02 \x01(\r\x12\x10\n\x08sequence\x18\x03 \x01(\r\x12\r\n\x05\x64leag\x18\x04 \x01(\x0c\"\x80\x02\n\x13XmrBidAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x0c\n\x04pkal\x18\x03 \x01(\x0c\x12\x0c\n\x04kbvl\x18\x04 \x01(\x0c\x12\x12\n\nkbsl_dleag\x18\x05 \x01(\x0c\x12\x11\n\ta_lock_tx\x18\x06 \x01(\x0c\x12\x18\n\x10\x61_lock_tx_script\x18\x07 \x01(\x0c\x12\x18\n\x10\x61_lock_refund_tx\x18\x08 \x01(\x0c\x12\x1f\n\x17\x61_lock_refund_tx_script\x18\t \x01(\x0c\x12\x1e\n\x16\x61_lock_refund_spend_tx\x18\n \x01(\x0c\x12\x1d\n\x15\x61l_lock_refund_tx_sig\x18\x0b \x01(\x0c\"r\n\x17XmrBidLockTxSigsMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12$\n\x1c\x61\x66_lock_refund_spend_tx_esig\x18\x02 \x01(\x0c\x12\x1d\n\x15\x61\x66_lock_refund_tx_sig\x18\x03 \x01(\x0c\"X\n\x18XmrBidLockSpendTxMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x17\n\x0f\x61_lock_spend_tx\x18\x02 \x01(\x0c\x12\x0f\n\x07kal_sig\x18\x03 \x01(\x0c\"M\n\x18XmrBidLockReleaseMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x1d\n\x15\x61l_lock_spend_tx_esig\x18\x02 \x01(\x0c\"\x8f\x01\n\x13\x41\x44SBidIntentMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x12\n\ntime_valid\x18\x02 \x01(\x04\x12\x13\n\x0b\x61mount_from\x18\x03 \x01(\x04\x12\x11\n\tamount_to\x18\x04 \x01(\x04\x12\x0c\n\x04rate\x18\x05 \x01(\x04\x12\x18\n\x10protocol_version\x18\x06 \x01(\r\"p\n\x19\x41\x44SBidIntentAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x0c\n\x04pkaf\x18\x02 \x01(\x0c\x12\x0c\n\x04kbvf\x18\x03 \x01(\x0c\x12\x12\n\nkbsf_dleag\x18\x04 \x01(\x0c\x12\x0f\n\x07\x64\x65st_af\x18\x05 \x01(\x0c\x62\x06proto3')
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0emessages.proto\x12\tbasicswap\"\xc0\x04\n\x0cOfferMessage\x12\x18\n\x10protocol_version\x18\x01 \x01(\r\x12\x11\n\tcoin_from\x18\x02 \x01(\r\x12\x0f\n\x07\x63oin_to\x18\x03 \x01(\r\x12\x13\n\x0b\x61mount_from\x18\x04 \x01(\x04\x12\x11\n\tamount_to\x18\x05 \x01(\x04\x12\x16\n\x0emin_bid_amount\x18\x06 \x01(\x04\x12\x12\n\ntime_valid\x18\x07 \x01(\x04\x12\x33\n\tlock_type\x18\x08 \x01(\x0e\x32 .basicswap.OfferMessage.LockType\x12\x12\n\nlock_value\x18\t \x01(\r\x12\x11\n\tswap_type\x18\n \x01(\r\x12\x15\n\rproof_address\x18\x0b \x01(\t\x12\x17\n\x0fproof_signature\x18\x0c \x01(\t\x12\x15\n\rpkhash_seller\x18\r \x01(\x0c\x12\x13\n\x0bsecret_hash\x18\x0e \x01(\x0c\x12\x15\n\rfee_rate_from\x18\x0f \x01(\x04\x12\x13\n\x0b\x66\x65\x65_rate_to\x18\x10 \x01(\x04\x12\x19\n\x11\x61mount_negotiable\x18\x11 \x01(\x08\x12\x17\n\x0frate_negotiable\x18\x12 \x01(\x08\x12\x13\n\x0bproof_utxos\x18\x13 \x01(\x0c\"q\n\x08LockType\x12\x0b\n\x07NOT_SET\x10\x00\x12\x18\n\x14SEQUENCE_LOCK_BLOCKS\x10\x01\x12\x16\n\x12SEQUENCE_LOCK_TIME\x10\x02\x12\x13\n\x0f\x41\x42S_LOCK_BLOCKS\x10\x03\x12\x11\n\rABS_LOCK_TIME\x10\x04\"\xce\x01\n\nBidMessage\x12\x18\n\x10protocol_version\x18\x01 \x01(\r\x12\x14\n\x0coffer_msg_id\x18\x02 \x01(\x0c\x12\x12\n\ntime_valid\x18\x03 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x04 \x01(\x04\x12\x11\n\tamount_to\x18\x05 \x01(\x04\x12\x14\n\x0cpkhash_buyer\x18\x06 \x01(\x0c\x12\x15\n\rproof_address\x18\x07 \x01(\t\x12\x17\n\x0fproof_signature\x18\x08 \x01(\t\x12\x13\n\x0bproof_utxos\x18\t \x01(\x0c\"s\n\x0f\x42idMessage_test\x12\x18\n\x10protocol_version\x18\x01 \x01(\r\x12\x14\n\x0coffer_msg_id\x18\x02 \x01(\x0c\x12\x12\n\ntime_valid\x18\x03 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x04 \x01(\x04\x12\x0c\n\x04rate\x18\x05 \x01(\x04\"V\n\x10\x42idAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x15\n\rinitiate_txid\x18\x02 \x01(\x0c\x12\x17\n\x0f\x63ontract_script\x18\x03 \x01(\x0c\"=\n\x12OfferRevokeMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x11\n\tsignature\x18\x02 \x01(\x0c\";\n\x10\x42idRejectMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x13\n\x0breject_code\x18\x02 \x01(\r\"\xb7\x01\n\rXmrBidMessage\x12\x18\n\x10protocol_version\x18\x01 \x01(\r\x12\x14\n\x0coffer_msg_id\x18\x02 \x01(\x0c\x12\x12\n\ntime_valid\x18\x03 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x04 \x01(\x04\x12\x11\n\tamount_to\x18\x05 \x01(\x04\x12\x0c\n\x04pkaf\x18\x06 \x01(\x0c\x12\x0c\n\x04kbvf\x18\x07 \x01(\x0c\x12\x12\n\nkbsf_dleag\x18\x08 \x01(\x0c\x12\x0f\n\x07\x64\x65st_af\x18\t \x01(\x0c\"T\n\x0fXmrSplitMessage\x12\x0e\n\x06msg_id\x18\x01 \x01(\x0c\x12\x10\n\x08msg_type\x18\x02 \x01(\r\x12\x10\n\x08sequence\x18\x03 \x01(\r\x12\r\n\x05\x64leag\x18\x04 \x01(\x0c\"\x80\x02\n\x13XmrBidAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x0c\n\x04pkal\x18\x02 \x01(\x0c\x12\x0c\n\x04kbvl\x18\x03 \x01(\x0c\x12\x12\n\nkbsl_dleag\x18\x04 \x01(\x0c\x12\x11\n\ta_lock_tx\x18\x05 \x01(\x0c\x12\x18\n\x10\x61_lock_tx_script\x18\x06 \x01(\x0c\x12\x18\n\x10\x61_lock_refund_tx\x18\x07 \x01(\x0c\x12\x1f\n\x17\x61_lock_refund_tx_script\x18\x08 \x01(\x0c\x12\x1e\n\x16\x61_lock_refund_spend_tx\x18\t \x01(\x0c\x12\x1d\n\x15\x61l_lock_refund_tx_sig\x18\n \x01(\x0c\"r\n\x17XmrBidLockTxSigsMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12$\n\x1c\x61\x66_lock_refund_spend_tx_esig\x18\x02 \x01(\x0c\x12\x1d\n\x15\x61\x66_lock_refund_tx_sig\x18\x03 \x01(\x0c\"X\n\x18XmrBidLockSpendTxMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x17\n\x0f\x61_lock_spend_tx\x18\x02 \x01(\x0c\x12\x0f\n\x07kal_sig\x18\x03 \x01(\x0c\"M\n\x18XmrBidLockReleaseMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x1d\n\x15\x61l_lock_spend_tx_esig\x18\x02 \x01(\x0c\"\x81\x01\n\x13\x41\x44SBidIntentMessage\x12\x18\n\x10protocol_version\x18\x01 \x01(\r\x12\x14\n\x0coffer_msg_id\x18\x02 \x01(\x0c\x12\x12\n\ntime_valid\x18\x03 \x01(\x04\x12\x13\n\x0b\x61mount_from\x18\x04 \x01(\x04\x12\x11\n\tamount_to\x18\x05 \x01(\x04\"p\n\x19\x41\x44SBidIntentAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x0c\n\x04pkaf\x18\x02 \x01(\x0c\x12\x0c\n\x04kbvf\x18\x03 \x01(\x0c\x12\x12\n\nkbsf_dleag\x18\x04 \x01(\x0c\x12\x0f\n\x07\x64\x65st_af\x18\x05 \x01(\x0c\x62\x06proto3')
 
 _globals = globals()
 _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -21,33 +22,33 @@ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'messages_pb2', _globals)
 if _descriptor._USE_C_DESCRIPTORS == False:
   DESCRIPTOR._options = None
   _globals['_OFFERMESSAGE']._serialized_start=30
-  _globals['_OFFERMESSAGE']._serialized_end=580
-  _globals['_OFFERMESSAGE_LOCKTYPE']._serialized_start=467
-  _globals['_OFFERMESSAGE_LOCKTYPE']._serialized_end=580
-  _globals['_BIDMESSAGE']._serialized_start=583
-  _globals['_BIDMESSAGE']._serialized_end=784
-  _globals['_BIDMESSAGE_V1DEPRECATED']._serialized_start=787
-  _globals['_BIDMESSAGE_V1DEPRECATED']._serialized_end=980
-  _globals['_BIDACCEPTMESSAGE']._serialized_start=982
-  _globals['_BIDACCEPTMESSAGE']._serialized_end=1068
-  _globals['_OFFERREVOKEMESSAGE']._serialized_start=1070
-  _globals['_OFFERREVOKEMESSAGE']._serialized_end=1131
-  _globals['_BIDREJECTMESSAGE']._serialized_start=1133
-  _globals['_BIDREJECTMESSAGE']._serialized_end=1192
-  _globals['_XMRBIDMESSAGE']._serialized_start=1195
-  _globals['_XMRBIDMESSAGE']._serialized_end=1373
-  _globals['_XMRSPLITMESSAGE']._serialized_start=1375
-  _globals['_XMRSPLITMESSAGE']._serialized_end=1459
-  _globals['_XMRBIDACCEPTMESSAGE']._serialized_start=1462
-  _globals['_XMRBIDACCEPTMESSAGE']._serialized_end=1718
-  _globals['_XMRBIDLOCKTXSIGSMESSAGE']._serialized_start=1720
-  _globals['_XMRBIDLOCKTXSIGSMESSAGE']._serialized_end=1834
-  _globals['_XMRBIDLOCKSPENDTXMESSAGE']._serialized_start=1836
-  _globals['_XMRBIDLOCKSPENDTXMESSAGE']._serialized_end=1924
-  _globals['_XMRBIDLOCKRELEASEMESSAGE']._serialized_start=1926
-  _globals['_XMRBIDLOCKRELEASEMESSAGE']._serialized_end=2003
-  _globals['_ADSBIDINTENTMESSAGE']._serialized_start=2006
-  _globals['_ADSBIDINTENTMESSAGE']._serialized_end=2149
-  _globals['_ADSBIDINTENTACCEPTMESSAGE']._serialized_start=2151
-  _globals['_ADSBIDINTENTACCEPTMESSAGE']._serialized_end=2263
+  _globals['_OFFERMESSAGE']._serialized_end=606
+  _globals['_OFFERMESSAGE_LOCKTYPE']._serialized_start=493
+  _globals['_OFFERMESSAGE_LOCKTYPE']._serialized_end=606
+  _globals['_BIDMESSAGE']._serialized_start=609
+  _globals['_BIDMESSAGE']._serialized_end=815
+  _globals['_BIDMESSAGE_TEST']._serialized_start=817
+  _globals['_BIDMESSAGE_TEST']._serialized_end=932
+  _globals['_BIDACCEPTMESSAGE']._serialized_start=934
+  _globals['_BIDACCEPTMESSAGE']._serialized_end=1020
+  _globals['_OFFERREVOKEMESSAGE']._serialized_start=1022
+  _globals['_OFFERREVOKEMESSAGE']._serialized_end=1083
+  _globals['_BIDREJECTMESSAGE']._serialized_start=1085
+  _globals['_BIDREJECTMESSAGE']._serialized_end=1144
+  _globals['_XMRBIDMESSAGE']._serialized_start=1147
+  _globals['_XMRBIDMESSAGE']._serialized_end=1330
+  _globals['_XMRSPLITMESSAGE']._serialized_start=1332
+  _globals['_XMRSPLITMESSAGE']._serialized_end=1416
+  _globals['_XMRBIDACCEPTMESSAGE']._serialized_start=1419
+  _globals['_XMRBIDACCEPTMESSAGE']._serialized_end=1675
+  _globals['_XMRBIDLOCKTXSIGSMESSAGE']._serialized_start=1677
+  _globals['_XMRBIDLOCKTXSIGSMESSAGE']._serialized_end=1791
+  _globals['_XMRBIDLOCKSPENDTXMESSAGE']._serialized_start=1793
+  _globals['_XMRBIDLOCKSPENDTXMESSAGE']._serialized_end=1881
+  _globals['_XMRBIDLOCKRELEASEMESSAGE']._serialized_start=1883
+  _globals['_XMRBIDLOCKRELEASEMESSAGE']._serialized_end=1960
+  _globals['_ADSBIDINTENTMESSAGE']._serialized_start=1963
+  _globals['_ADSBIDINTENTMESSAGE']._serialized_end=2092
+  _globals['_ADSBIDINTENTACCEPTMESSAGE']._serialized_start=2094
+  _globals['_ADSBIDINTENTACCEPTMESSAGE']._serialized_end=2206
 # @@protoc_insertion_point(module_scope)
diff --git a/basicswap/ui/page_offers.py b/basicswap/ui/page_offers.py
index 1d38bd4..7c05b26 100644
--- a/basicswap/ui/page_offers.py
+++ b/basicswap/ui/page_offers.py
@@ -142,6 +142,9 @@ def parseOfferFormData(swap_client, form_data, page_data, options={}):
         parsed_data['rate'] = ci_from.make_int(parsed_data['amt_to'] / parsed_data['amt_from'], r=1)
         page_data['rate'] = ci_to.format_amount(parsed_data['rate'])
 
+    if 'amt_to' not in parsed_data and 'rate' in parsed_data and 'amt_from' in parsed_data:
+        parsed_data['amt_to'] = int((parsed_data['amt_from'] * parsed_data['rate']) // ci_from.COIN())
+
     page_data['amt_var'] = True if have_data_entry(form_data, 'amt_var') else False
     parsed_data['amt_var'] = page_data['amt_var']
     page_data['rate_var'] = True if have_data_entry(form_data, 'rate_var') else False
@@ -308,6 +311,8 @@ def postNewOfferFromParsed(swap_client, parsed_data):
         extra_options['automation_id'] = parsed_data['automation_strat_id']
 
     swap_value = parsed_data['amt_from']
+    if parsed_data.get('amt_to', None) is not None:
+        extra_options['amount_to'] = parsed_data['amt_to']
     if parsed_data.get('subfee', False):
         ci_from = swap_client.ci(parsed_data['coin_from'])
         pi = swap_client.pi(swap_type)
@@ -358,7 +363,7 @@ def offer_to_post_string(self, swap_client, offer_id):
         'amt_from': ci_from.format_amount(offer.amount_from),
         'amt_bid_min': ci_from.format_amount(offer.min_bid_amount),
         'rate': ci_to.format_amount(offer.rate),
-        'amt_to': ci_to.format_amount((offer.amount_from * offer.rate) // ci_from.COIN()),
+        'amt_to': ci_to.format_amount(offer.amount_to),
         'validhrs': offer.time_valid // (60 * 60),
         'swap_type': strSwapType(offer.swap_type),
     }
@@ -579,7 +584,7 @@ def page_offer(self, url_split, post_string):
         'coin_from_ind': int(ci_from.coin_type()),
         'coin_to_ind': int(ci_to.coin_type()),
         'amt_from': ci_from.format_amount(offer.amount_from),
-        'amt_to': ci_to.format_amount((offer.amount_from * offer.rate) // ci_from.COIN()),
+        'amt_to': ci_to.format_amount(offer.amount_to),
         'amt_bid_min': ci_from.format_amount(offer.min_bid_amount),
         'rate': ci_to.format_amount(offer.rate),
         'lock_type': getLockName(offer.lock_type),
@@ -781,7 +786,7 @@ def page_offers(self, url_split, post_string, sent=False):
             ci_from.coin_name(),
             ci_to.coin_name(),
             ci_from.format_amount(o.amount_from),
-            ci_to.format_amount((o.amount_from * o.rate) // ci_from.COIN()),
+            ci_to.format_amount(o.amount_to),
             ci_to.format_amount(o.rate),
             'Public' if o.addr_to == swap_client.network_addr else o.addr_to,
             o.addr_from,
diff --git a/basicswap/ui/util.py b/basicswap/ui/util.py
index beea7c7..39f2fe6 100644
--- a/basicswap/ui/util.py
+++ b/basicswap/ui/util.py
@@ -162,12 +162,14 @@ def describeBid(swap_client, bid, xmr_swap, offer, xmr_offer, bid_events, edit_b
     ci_follower = ci_from if reverse_bid else ci_to
 
     bid_amount: int = bid.amount
+    bid_amount_to: int = bid.amount_to
     bid_rate: int = offer.rate if bid.rate is None else bid.rate
 
     initiator_role: str = 'offerer'  # Leader
     participant_role: str = 'bidder'  # Follower
     if reverse_bid:
         bid_amount = bid.amount_to
+        bid_amount_to = bid.amount
         bid_rate = ci_from.make_int(bid.amount / bid.amount_to, r=1)
         initiator_role = 'bidder'
         participant_role = 'offerer'
@@ -259,7 +261,7 @@ def describeBid(swap_client, bid, xmr_swap, offer, xmr_offer, bid_events, edit_b
         'coin_from': ci_from.coin_name(),
         'coin_to': ci_to.coin_name(),
         'amt_from': ci_from.format_amount(bid_amount),
-        'amt_to': ci_to.format_amount((bid_amount * bid_rate) // ci_from.COIN()),
+        'amt_to': ci_to.format_amount(bid_amount_to),
         'bid_rate': ci_to.format_amount(bid_rate),
         'ticker_from': ci_from.ticker(),
         'ticker_to': ci_to.ticker(),
diff --git a/doc/release-notes.md b/doc/release-notes.md
index 6d463de..9c009e4 100644
--- a/doc/release-notes.md
+++ b/doc/release-notes.md
@@ -1,3 +1,13 @@
+
+0.13.0
+==============
+
+- GUI v3.0
+- Bid and offer states change when expired.
+- bid amounts are specified directly and not constructed from rate.
+- Breaks compatibility with prior versions.
+
+
 0.12.7
 ==============
 
diff --git a/tests/basicswap/test_btc_xmr.py b/tests/basicswap/test_btc_xmr.py
index e548dad..9c093e7 100644
--- a/tests/basicswap/test_btc_xmr.py
+++ b/tests/basicswap/test_btc_xmr.py
@@ -185,7 +185,7 @@ class TestFunctions(BaseTest):
         bid0 = read_json_api(1800 + id_offerer, f'bids/{bid_id.hex()}')
         bid1 = read_json_api(1800 + id_bidder, f'bids/{bid_id.hex()}')
 
-        tolerance = 20 if reverse_bid else 0
+        tolerance = 1
         assert (bid0['ticker_from'] == ci_from.ticker())
         assert (bid1['ticker_from'] == ci_from.ticker())
         assert (bid0['ticker_to'] == ci_to.ticker())
diff --git a/tests/basicswap/test_other.py b/tests/basicswap/test_other.py
index a0ccdeb..98adce3 100644
--- a/tests/basicswap/test_other.py
+++ b/tests/basicswap/test_other.py
@@ -6,6 +6,7 @@
 # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
 
 import hashlib
+import random
 import secrets
 import unittest
 
@@ -41,7 +42,7 @@ from basicswap.util import (
 
 from basicswap.messages_pb2 import (
     BidMessage,
-    BidMessage_v1Deprecated,
+    BidMessage_test,
 )
 from basicswap.contrib.test_framework.script import hash160 as hash160_btc
 
@@ -310,6 +311,89 @@ class Test(unittest.TestCase):
         amount_to_recreate = int((amount_from * rate) // (10 ** scale_from))
         assert ('10.00000000' == format_amount(amount_to_recreate, scale_to))
 
+        coin_settings = {'rpcport': 0, 'rpcauth': 'none', 'walletrpcport': 0, 'walletrpcauth': 'none'}
+        coin_settings.update(self.REQUIRED_SETTINGS)
+        ci_xmr = XMRInterface(coin_settings, 'regtest')
+        ci_btc = BTCInterface(coin_settings, 'regtest')
+
+        for i in range(10000):
+
+            test_pairs = random.randint(0, 3)
+            if test_pairs == 0:
+                ci_from = ci_btc
+                ci_to = ci_xmr
+            elif test_pairs == 1:
+                ci_from = ci_xmr
+                ci_to = ci_btc
+            elif test_pairs == 2:
+                ci_from = ci_xmr
+                ci_to = ci_xmr
+            else:
+                ci_from = ci_btc
+                ci_to = ci_btc
+
+            test_range = random.randint(0, 5)
+            if test_range == 0:
+                amount_from = random.randint(10000, 1 * ci_from.COIN())
+            elif test_range == 1:
+                amount_from = random.randint(10000, 1000 * ci_from.COIN())
+            elif test_range == 2:
+                amount_from = random.randint(10000, 2100 * ci_from.COIN())
+            elif test_range == 3:
+                amount_from = random.randint(10000, 210000 * ci_from.COIN())
+            elif test_range == 4:
+                amount_from = random.randint(10000, 21000000 * ci_from.COIN())
+            else:
+                amount_from = random.randint(10000, 2100000000 * ci_from.COIN())
+
+            test_range = random.randint(0, 5)
+            if test_range == 0:
+                amount_to = random.randint(10000, 1 * ci_to.COIN())
+            elif test_range == 1:
+                amount_to = random.randint(10000, 1000 * ci_to.COIN())
+            elif test_range == 2:
+                amount_to = random.randint(10000, 2100 * ci_to.COIN())
+            elif test_range == 3:
+                amount_to = random.randint(10000, 210000 * ci_to.COIN())
+            elif test_range == 4:
+                amount_to = random.randint(10000, 21000000 * ci_to.COIN())
+            else:
+                amount_to = random.randint(10000, 2100000000 * ci_to.COIN())
+
+            offer_rate = ci_from.make_int(amount_to / amount_from, r=1)
+            amount_to_from_rate: int = int((int(amount_from) * offer_rate) // (10 ** scale_from))
+
+            scale_from = 24
+            offer_rate = make_int(amount_to, scale_from) // amount_from
+            amount_to_from_rate: int = int((int(amount_from) * offer_rate) // (10 ** scale_from))
+
+            if abs(amount_to - amount_to_from_rate) == 1:
+                offer_rate += 1
+
+            offer_rate_human_read: int = int(offer_rate // (10 ** (scale_from - ci_from.exp())))
+            amount_to_from_rate: int = int((int(amount_from) * offer_rate) // (10 ** scale_from))
+
+            if amount_to != amount_to_from_rate:
+                print('from exp, amount', ci_from.exp(), amount_from)
+                print('to exp, amount', ci_to.exp(), amount_to)
+                print('amount_to_from_rate', amount_to_from_rate)
+                raise ValueError('Bad amount_to')
+
+            scale_to = 24
+            reversed_rate = make_int(amount_from, scale_to) // amount_to
+
+            amount_from_from_rate: int = int((int(amount_to) * reversed_rate) // (10 ** scale_to))
+            if abs(amount_from - amount_from_from_rate) == 1:
+                reversed_rate += 1
+
+            amount_from_from_rate: int = int((int(amount_to) * reversed_rate) // (10 ** scale_to))
+
+            if amount_from != amount_from_from_rate:
+                print('from exp, amount', ci_from.exp(), amount_from)
+                print('to exp, amount', ci_to.exp(), amount_to)
+                print('amount_from_from_rate', amount_from_from_rate)
+                raise ValueError('Bad amount_from')
+
     def test_rfc2440(self):
         password = 'test'
         salt = bytes.fromhex('B7A94A7E4988630E')
@@ -330,14 +414,20 @@ class Test(unittest.TestCase):
     def test_protobuf(self):
         # Ensure old protobuf templates can be read
 
-        msg_buf = BidMessage_v1Deprecated()
+        msg_buf = BidMessage_test()
         msg_buf.protocol_version = 2
+        msg_buf.time_valid = 1024
         serialised_msg = msg_buf.SerializeToString()
 
         msg_buf_v2 = BidMessage()
         msg_buf_v2.ParseFromString(serialised_msg)
-
         assert (msg_buf_v2.protocol_version == 2)
+        assert (msg_buf_v2.time_valid == 1024)
+
+        # Decode only the first field
+        msg_buf_v2.ParseFromString(serialised_msg[:2])
+        assert (msg_buf_v2.protocol_version == 2)
+        assert (msg_buf_v2.time_valid == 0)
 
     def test_is_private_ip_address(self):
         assert (is_private_ip_address('localhost'))