Skip to content
LogoLogo

Peer-to-peer swap offers

Two people swap coins in one transaction: either both legs happen or nothing moves. There is no exchange, no custody and no fee beyond the network's. The maker shares an offer (a link or QR code), the taker signs its side and sends back an acceptance, and the maker completes it. The maker signs last, so an offer never executes without its final approval. Design and rules: Swap offers and coin lists.

Read and check an offer

Both SDKs decode the same arrektaswap: envelope and reject anything inconsistent (swap-decode, swap-version, swap-network, swap-terms, swap-expired). For an offer, the stated terms must match the PST itself.

import { decodeSwapEnvelope, swapOfferTerms } from 'arrekta-sdk'
 
const offer = decodeSwapEnvelope(link, { network: 'regtest', now: Math.floor(Date.now() / 1000) })
offer.give // { asset, amount }  what the maker gives (base units)
offer.want // { asset, amount }  what the maker asks for
swapOfferTerms(offer.pst)        // the same terms, recomputed from the transaction
import time
from arrekta_sdk import decode_swap_envelope, swap_offer_terms
 
offer = decode_swap_envelope(link, network="regtest", now=int(time.time()))
give, want = swap_offer_terms(offer.pst)   # == (offer.give, offer.want)

Make an offer

An offer is an unsigned PST with both construction flags set: the maker's inputs (what it gives), an output paying what it wants to its own address, and its change. Set validUntil (transaction version 2) so the swap cannot be mined after the offer expires.

import { PstFlags, createPst, encodeSwapEnvelope, swapOfferTerms } from 'arrekta-sdk'
 
const pst = createPst(tx /* version 2, validUntil = tip + N */, spentOutputs, 'regtest', {
  flags: PstFlags.INPUTS_MODIFIABLE | PstFlags.OUTPUTS_MODIFIABLE,
})
const { give, want } = swapOfferTerms(pst)
const link = encodeSwapEnvelope({ v: 1, kind: 'offer', network: 'regtest', expires, give, want, pst, memo: '10 GOLD for 500 ARRK' })
from arrekta_core.pst import FLAG_INPUTS_MODIFIABLE, FLAG_OUTPUTS_MODIFIABLE, Pst
from arrekta_sdk import SwapEnvelope, encode_swap_envelope, resolve_network, swap_offer_terms
 
pst = Pst.create(tx, spent_outputs, resolve_network("regtest").magic,
                 flags=FLAG_INPUTS_MODIFIABLE | FLAG_OUTPUTS_MODIFIABLE)
give, want = swap_offer_terms(pst)
link = encode_swap_envelope(SwapEnvelope("offer", "regtest", expires, give, want, pst, memo="10 GOLD for 500 ARRK"))

Take it, then complete it

The taker appends its inputs (paying want and the fee) and an output taking give (addPstInput / addPstOutput, or Pst.add_input / add_output), signs its inputs and returns a kind: 'accept' envelope. Before signing, the maker checks that the taker only appended:

import { swapAcceptanceMatches } from 'arrekta-sdk'
if (!swapAcceptanceMatches(myOffer.pst, acceptance.pst)) throw new Error('the acceptance changed the offer')
from arrekta_sdk import swap_acceptance_matches
assert swap_acceptance_matches(my_offer.pst, acceptance.pst)

then signs, finalizes and broadcasts. It also checks its own net change is exactly −give, +want.

In a wallet

@arrekta/wallet-core does all of this, including coin selection, fees, reserving the offered coins and previews with warnings for unverified or look-alike coins:

const offer = await maker.createSwapOffer({ give: { asset: 'GOLD', amount: '10' }, want: { asset: 'ARRK', amount: '5' } })
const take = await taker.prepareSwapTake(offer.offer)        // preview first
const { acceptance } = await taker.confirmSwapTake(take.id)  // signs the taker's side
const done = await maker.prepareSwapComplete(acceptance!)    // checks it, previews
const { txid } = await maker.confirmSwapComplete(done.id)    // signs and broadcasts

The desktop, mobile and extension wallets put this behind Swap: Market, New offer, Open a link, My swaps. To find a counterparty without sharing links by hand, list the offer on the market board.