Swaps
TESS trade against BTC, and against ETH, USDC and USDT on Ethereum, Base and Arbitrum, with atomic swaps: nobody, neither the foundation nor an exchange, holds the funds of another. Each side is locked on its own chain by a hash time-locked contract, and both are released by the same secret: either both parties are paid, or both get their funds back.
How a swap goes
A maker sells TESS with an offer; a taker buys them.
- The maker offers TESS on Tesserax (
pallet-swaps): an amount held from its account, a price in the smallest unit of the asset for one TESS, the chain and asset it wants, and its EVM address. - The taker picks a random secret, and locks the payment for the maker in the contract
TesseraxSwapof the chain, under the SHA-256 hash of the secret, for 24 hours. The lock names the offer and the Tesserax account of the taker. - The maker checks the lock: the asset and the amount the price asks, its address, the hash, and at least 12 hours left. It then fills the offer: the TESS are locked on Tesserax for the taker under the same hash, for 6 hours.
- The taker claims the TESS with the secret. The claim needs no signature and no fee: the secret authorizes it, so that a taker who has no TESS yet can make it.
- The secret is now public: anyone submits it to the contract, which pays the maker. The maker does it itself before the payment lock expires: it filled only a lock with 12 hours left, so it has at least 6 hours after the 6 hours of the trade.
If the maker never fills, the taker takes its payment back once the 24 hours are over. If the taker never claims, the TESS return to the offer after 6 hours, and the taker takes its payment back after 24. The chain of Tesserax cannot see the other chains: the maker checks the payment before filling. A maker filling wrongly only exposes its own TESS, and a taker never claims a trade that does not match its payment.
Bids: selling at once
Offers make a seller wait for a buyer. Bids go the other way: a buyer bids for TESS, and a seller, a miner cashing in its rewards first, sells into the bid whenever it wants, without waiting for anyone.
- The buyer bids on Tesserax: a price in the smallest unit of the asset for one TESS, the chain and asset it pays in, how many TESS it wants and the smallest sale. Only a storage deposit is held; the payment is locked sale by sale.
- The seller picks a random secret and sells into the bid (
sell): its TESS are locked on Tesserax for the buyer, under the SHA-256 hash of the secret, for 24 hours, with the EVM address or the Bitcoin key it is paid to. - The buyer, or its maker running for it, checks the sale has at least 12 hours left, locks the
payment for the seller under the same hash for 6 hours, in the same contract or script as
offers, and records the lock on Tesserax (
pay). - The seller checks the payment: the asset, the amount the price asks, its own address or key, the hash, at least an hour left, and on Bitcoin 2 confirmations. It then claims it with the secret, which is now public.
- The secret claims the TESS for the buyer (
claim_sale), without signature and without fee, so that a buyer without TESS can claim; the seller's wallet submits it right after being paid. Otherwise the buyer finds the secret in theClaimedevent of the contract, or in the witness of the Bitcoin claim, and submits it itself.
If the buyer never pays, the sale returns to the seller after 24 hours (refund_sale). If the
seller never claims the payment, the buyer takes it back after 6 hours, and the TESS return to
the seller after 24. The seller reveals the secret only by claiming a payment it checked, and the
payment expires at least 6 hours before the sale: the buyer always has the time to claim the TESS.
Chains and assets
| Chain | Assets | Chain id | Testnet of the Tesserax testnet |
|---|---|---|---|
| Bitcoin | BTC | Testnet4 | |
| Ethereum | ETH, USDC, USDT | 1 | Sepolia (ETH, USDC) |
| Base | ETH, USDC | 8453 | Base Sepolia (ETH, USDC) |
| Arbitrum | ETH, USDC, USDT | 42161 | Arbitrum Sepolia (ETH, USDC) |
Base and Arbitrum cost cents a transaction, Ethereum dollars. Token contracts are those of Circle for USDC and of Tether for USDT (USD₮0 on Arbitrum); tokens taking a fee on transfer are refused. Monero has no scripts, so no atomic swap is possible against it without elliptic curve cryptography on Tesserax: holders of XMR swap them for BTC first, with the existing XMR–BTC atomic swaps, then BTC for TESS.
TesseraxSwap is at 0xeA2f4f08D2A0f1972031B98De63f4B0395c256Fc on every chain, deployed
with the deterministic CREATE2 deployer (swaps/evm).
Bitcoin
Bitcoin has no contract: the payment is a P2WSH output of a script the taker writes, which pays the maker with the secret, or the taker after the timelock (24 hours, compared to the median time of the last blocks):
OP_IF
OP_SIZE 32 OP_EQUALVERIFY OP_SHA256 <hash> OP_EQUALVERIFY <maker key>
OP_ELSE
<timelock> OP_CHECKLOCKTIMEVERIFY OP_DROP <taker key>
OP_ENDIF
OP_CHECKSIG
The payout of a Bitcoin offer is the compressed public key of the maker. The transaction locking
a payment also sends 330 satoshis to the P2WPKH address of that key, and carries an OP_RETURN
output announcing the trade: TR, the offer, the Tesserax account of the taker, the hash, the
timelock and the refund key of the taker, 111 bytes. The maker watches its own address only.
The refund key is apart from the wallet paying: browser wallets, such as the Bitcoin account of
MetaMask, sign only transactions they finalize alone, which a refund of the script is not. The
web wallet derives the refund key from the seed of the account and the hash, and signs the
refund itself. The command line wallet, which holds its Bitcoin key, announces in 78 bytes
(TX, without the key): its refund key is the key of the first input. OP_RETURN outputs of
111 bytes are relayed by default since Bitcoin Core 30; nodes of older versions or filtering ones
keep only those of 80 bytes at most. The maker rebuilds the script, checks it is the one of the output, and fills once
the transaction has 2 confirmations: an unconfirmed transaction can be replaced. The payment lock
on Tesserax is the outpoint of the script, 36 bytes. The maker claims with the secret the taker
revealed, to the same address.
Using it
In the web wallet, the Market page: buyers pick an offer and pay from their browser EVM wallet (MetaMask, Rabby or any EIP-1193 wallet), whose keys never reach Tesserax; the wallet claims the TESS as soon as the maker fills, and takes the payment back if it never does. Sellers create offers there and run the maker of their account, which fills them while the wallet is open and lists the payments to collect. Offers in BTC are paid to a Bitcoin key the wallet derives from the account: its maker collects them by itself, to the address chosen with the offer. Holders of TESS, miners first, also sell there into the open bids: the wallet locks the TESS, waits for the buyer's payment, checks it and claims it, in BTC with the key it derives from the account, or in ETH, USDC and USDT through the browser EVM wallet, which pays the gas of the claim.
With the command line wallet, which runs a maker without a browser and pays in BTC; the EVM key is a file holding a private key in hexadecimal, which pays the locks and the gas, and the Bitcoin key a file holding a private key in WIF, which pays from its P2WPKH address:
# Makers: offer 10,000 TESS at 0.05 USDC on Base, paid to an EVM address.
tesserax-wallet swap offer alice --chain base --asset usdc --price 0.05 --amount 10000 \
--min 100 --payout 0x...
# Run the maker: it fills the offer as payments come, collects them, takes back expired trades.
tesserax-wallet swap make alice --evm-key-file maker.key
tesserax-wallet swap cancel alice 3
# Takers: list the offers, buy 500 TESS of offer 3. The account receiving them needs no TESS.
tesserax-wallet swap offers
tesserax-wallet swap buy bob 3 500 --evm-key-file taker.key
# If the wait was interrupted, or the maker never filled:
tesserax-wallet swap claim bob 0x<lock>
tesserax-wallet swap refund-payment 0x<lock> --evm-key-file taker.key
# In BTC: the offer is paid to the key of maker.wif, the buy from the address of taker.wif.
tesserax-wallet swap offer alice --chain bitcoin --asset btc --price 0.000002 --amount 10000 \
--btc-key-file maker.wif
tesserax-wallet swap make alice --btc-key-file maker.wif --evm-key-file maker.key
tesserax-wallet swap buy bob 4 500 --btc-key-file taker.wif
tesserax-wallet swap refund-payment <txid>:<vout> --btc-key-file taker.wif
Bitcoin is read and written through the Esplora API of mempool.space; --esplora gives another.
Bids, from the command line:
# Buyers: bid for 10,000 TESS at 0.05 USDC on Base, in sales of at least 10 TESS, and run the
# maker, which pays each sale from the EVM key and claims its TESS.
tesserax-wallet swap bid alice --chain base --asset usdc --price 0.05 --amount 10000 --min 10
tesserax-wallet swap make alice --evm-key-file buyer.key
tesserax-wallet swap cancel-bid alice 2
# Sellers: list the bids, sell 500 TESS into bid 2. The EVM key receives the payment and pays
# the gas of its claim, a few cents on Base.
tesserax-wallet swap bids
tesserax-wallet swap sell bob 2 500 --evm-key-file seller.key
# If the wait was interrupted, or the buyer never paid:
tesserax-wallet swap collect-sale 7 --evm-key-file seller.key
tesserax-wallet swap refund-sale bob 7
# In BTC: the sale is paid to the key of seller.wif, which claims the payment to its address.
tesserax-wallet swap sell bob 3 500 --btc-key-file seller.wif --create-btc-key
A maker keeps a bid open by itself with --keep-bid chain:asset:price:amount:min: it bids for
amount TESS again whenever its open bids for that payment want less than min. Its payments of
sales stay in its state file until their TESS are claimed or the payment taken back. On Bitcoin,
it bids only for what its key can pay: the bitcoin left once its open bids and unpaid sales are
paid, with the notice and the fee of every sale. It logs the address of its key on start; send
the bitcoin there.
A maker can keep an offer open by itself: with --keep-offer chain:asset:price:amount:min, it
offers amount TESS again whenever its open offers for that payment hold less than min, while
the account has them. --create-btc-key creates the key of --btc-key-file on the first start,
readable by its owner only. The demo maker of the testnet runs this way
(testnet.md):
tesserax-wallet swap make alice --btc-key-file maker.wif --create-btc-key \
--keep-offer bitcoin:btc:0.00001:1000:10
The secret of a buy is saved in the wallet directory before the payment is locked, and deleted
once the TESS are claimed: losing it before the claim only delays the refund. A buy in BTC also
saves its script, which takes the payment back. The Rust SDK has the
same operations: tesserax_sdk::swaps, and tesserax_sdk::swaps::evm with the swaps feature;
the TypeScript SDK client.swaps and EvmSwaps, with any viem wallet client; the Python SDK
client.swaps and EvmSwaps, with web3.py. In BTC, tesserax_sdk::swaps::bitcoin, the Bitcoin
module of the TypeScript SDK, and tesserax.bitcoin of the Python SDK build the same
transactions: the tests of the Rust SDK verify those of the other two with Bitcoin Core. Both refuse to lock
a payment on a chain where the contract is not deployed.
Testing on one's own machine
A development chain, anvil for the EVM side and Bitcoin Core in regtest for the Bitcoin side run
a whole swap without any public network. swaps/bitcoin/esplora-regtest.mjs serves the Esplora
API the wallets read, over the regtest node:
tesserax --dev --tmp --mine --author <address>
anvil &
RPC_URL=http://127.0.0.1:8545 swaps/evm/deploy.sh --private-key <a key of anvil>
bitcoind -regtest -txindex=1 -fallbackfee=0.0001 -server -datadir=<dir> -daemon
node swaps/bitcoin/esplora-regtest.mjs --datadir <dir> # http://127.0.0.1:3002
tesserax-wallet swap make alice --btc-key-file maker.wif --btc-network regtest \
--evm-local http://127.0.0.1:8545 --evm-key-file maker.key
The web wallet takes VITE_DEVELOPMENT_BITCOIN_NETWORK=regtest and
VITE_ESPLORA_URL=http://127.0.0.1:3002, the command line wallet --btc-network regtest.
Blocks of regtest are mined on demand (bitcoin-cli -regtest generatetoaddress), and its clock
moved with setmocktime, to reach the timelock of a refund.
Market data
The explorer serves the prices, volumes, order book and trades of the market for price
aggregators, in the format of CoinGecko, and the supply of TESS: /api/coingecko/tickers,
/api/coingecko/orderbook?ticker_id=TESS_USDC, /api/supply/circulating and the others of
explorer/README.md. They count the trades their taker
claimed, the ones whose payment the maker can collect.
Risks
- A maker must be online to fill: a taker whose payment is not filled waits 24 hours for its refund. Offers show how many trades a maker filled and completed.
- The taker holds an option for the time the TESS are locked, 6 hours: it may not claim if the price moved. Makers price it, and the short lock limits it.
- Blocks of Tesserax are final 60 blocks deep, about 18 minutes, well within the 6 hours of a trade; makers fill payments the EVM chain already included, and Bitcoin payments 2 blocks deep.
Bids add their own:
- A buyer must be online to pay a sale: a seller whose sale is not paid gets its TESS back after 24 hours. Bids show how many sales a buyer received and completed.
- The buyer holds an option for the time between a sale and its payment: it may not pay if the price moved. The seller holds one for the 6 hours of the payment: it may not claim. Neither loses funds, only time.
- A seller paid on an EVM chain needs a little of its native token for the gas of its claim; a seller paid in BTC pays the fee from the payment itself.