# Atomiq Docs > Atomiq is a fully trustless cross-chain DEX enabling swaps between Bitcoin/Lightning and smart chains (Solana, Starknet, EVM) using a Bitcoin light client, submarine swaps (HTLCs), and a Request-for-Quote Liquidity Provider network. Two integration surfaces exist: the TypeScript SDK (preferred for JavaScript/TypeScript environments) and a public or self-hosted REST API (use when the SDK cannot run — non-JS runtimes or environments without local persistence). - [Atomiq Docs](https://docs.atomiq.exchange/index.md) ## overview ### actors Atomiq is designed as a permissionless system where multiple independent parties interact to enable trustless cross-chain swaps. The main participants are the User, Liquidity Provider (LP), and Watchtower (also referred to as a relayer). Each plays a distinct role across both Bitcoin L1 and Lightning swaps, while all operations remain non-custodial and verifiable through smart contracts, cryptography, and Bitcoin consensus. - [Actors in the Atomiq Protocol](https://docs.atomiq.exchange/overview/actors.md): Atomiq is designed as a permissionless system where multiple independent parties interact to enable trustless cross-chain swaps. The main participants are the User, Liquidity Provider (LP), and Watchtower (also referred to as a relayer). Each plays a distinct role across both Bitcoin L1 and Lightning swaps, while all operations remain non-custodial and verifiable through smart contracts, cryptography, and Bitcoin consensus. ### contracts Atomiq's smart contracts are deployed across multiple chains, with two distinct architectural approaches depending on the target platform. - [Contracts](https://docs.atomiq.exchange/overview/contracts.md): Atomiq's smart contracts are deployed across multiple chains, with two distinct architectural approaches depending on the target platform. - [Contract Addresses](https://docs.atomiq.exchange/overview/contracts/contract-addresses.md): Summary of all contract addresses used by atomiq across different chains. The contracts are classified based on which Bitcoin network they support: - [EVM & Starknet Contracts](https://docs.atomiq.exchange/overview/contracts/evm-starknet.md): The EVM and Starknet contract deployments share a modular architecture — the protocol logic is decomposed into small, composable contract modules that can be combined and upgraded independently. The same architectural design is implemented on both platforms, with each module fulfilling an identical role. - [Solana Contracts](https://docs.atomiq.exchange/overview/contracts/solana.md): The Solana deployment uses a monolithic architecture, where the protocol is implemented as two standalone Anchor programs — a swap program and a BTC relay program. Unlike the modular EVM & Starknet contracts, all swap logic (HTLC, PrTLC, and transaction verification) lives within a single swap program rather than being split into separate handler contracts. The UTXO-controlled vault primitive is not implemented on Solana, meaning Solana can only process legacy swaps in the Bitcoin → Solana direction. ### core-primitives These pages cover the low-level mechanisms that make Atomiq's trustless swap protocol possible. They define how Bitcoin state is verified on-chain, how funds are escrowed across chains, and how settlement can be enforced without relying on a trusted intermediary. - [Core Primitives](https://docs.atomiq.exchange/overview/core-primitives.md): These pages cover the low-level mechanisms that make Atomiq's trustless swap protocol possible. They define how Bitcoin state is verified on-chain, how funds are escrowed across chains, and how settlement can be enforced without relying on a trusted intermediary. - [Bitcoin light client (on-chain)](https://docs.atomiq.exchange/overview/core-primitives/bitcoin-light-client.md): The Bitcoin light client (also called bitcoin relay) is a smart contract deployed on smart chains (Solana, Starknet, EVM, etc.) that verifies and stores Bitcoin block headers using the proof-of-work consensus algorithm. It acts as a permissionless & trustless oracle of Bitcoin's state — anyone can submit block headers and their validity is verified entirely on-chain. No trusted third party, signer set, or attestation is required. - [HTLC (hash-time locked contract)](https://docs.atomiq.exchange/overview/core-primitives/htlc.md): An HTLC is an escrow smart contract between two parties that uses a cryptographic hash to link transactions across independent blockchains. The swap protocol uses a secret and its hash (where the hash is calculated sha256(secret)) to create an all-or-nothing outcome: either both parties successfully exchange assets, or both safely refund through time-bound recovery paths. - [PrTLC (proof-time locked contract)](https://docs.atomiq.exchange/overview/core-primitives/prtlc.md): A PrTLC is an escrow smart contract between two parties that uses a proof of a Bitcoin transaction—provided through the on-chain Bitcoin light client—to enable conditional release of funds on the smart chain (Solana, Starknet, EVM). The protocol replaces the hash-based condition of traditional HTLCs with a proof-lock tied to a Bitcoin transaction confirming on the Bitcoin blockchain, creating an all-or-nothing outcome: either the counterparty sends the required Bitcoin transaction and proves it to claim the escrowed assets, or the funds are refunded after a timeout. - [UTXO-controlled vault](https://docs.atomiq.exchange/overview/core-primitives/utxo-controlled-vault.md): A UTXO-controlled vault is a smart contract on the smart chain that holds LP liquidity and authorizes withdrawals solely based on verified Bitcoin transactions, using the on-chain Bitcoin light client to confirm UTXO states and prevent front-running. By importing Bitcoin's deterministic UTXO ordering to the smart chain, the vault ensures a tamper-proof withdrawal queue, where each update spends the previous vault UTXO and creates a new one, chaining transactions in a linear, verifiable sequence enforced by Bitcoin consensus. ### protocol-overview Atomiq facilitates trustless cross-chain swaps through a combination of on-chain smart contract logic and off-chain coordination, ensuring atomic execution without intermediaries. Users initiate swaps by requesting quotes from a decentralized network of Liquidity Providers (LPs) via an off-chain request-for-quote (RFQ) system. This allows competitive pricing and zero slippage while keeping the actual settlement fully on-chain and trustless. - [Protocol Overview](https://docs.atomiq.exchange/overview/protocol-overview.md): Atomiq facilitates trustless cross-chain swaps through a combination of on-chain smart contract logic and off-chain coordination, ensuring atomic execution without intermediaries. Users initiate swaps by requesting quotes from a decentralized network of Liquidity Providers (LPs) via an off-chain request-for-quote (RFQ) system. This allows competitive pricing and zero slippage while keeping the actual settlement fully on-chain and trustless. ### swaps Swaps between Bitcoin and smart chains are inherently asymmetric — Bitcoin cannot execute smart contracts, so each direction requires a different approach. Smart chain → Bitcoin swaps use PrTLCs where the LP proves it sent BTC, while Bitcoin → Smart chain swaps use UTXO-controlled vaults where a single cooperatively-signed Bitcoin transaction atomically settles both sides. Lightning swaps follow a symmetric HTLC pattern, linking Lightning payment secrets to smart chain contracts. - [Swaps](https://docs.atomiq.exchange/overview/swaps.md): Swaps between Bitcoin and smart chains are inherently asymmetric — Bitcoin cannot execute smart contracts, so each direction requires a different approach. Smart chain → Bitcoin swaps use PrTLCs where the LP proves it sent BTC, while Bitcoin → Smart chain swaps use UTXO-controlled vaults where a single cooperatively-signed Bitcoin transaction atomically settles both sides. Lightning swaps follow a symmetric HTLC pattern, linking Lightning payment secrets to smart chain contracts. - [Bitcoin → Smart chains](https://docs.atomiq.exchange/overview/swaps/bitcoin-sc-new.md): This is the current protocol for swapping on-chain Bitcoin to smart chain tokens. It uses the UTXO-controlled vault primitive, where the LP deposits liquidity into a vault on the smart chain whose withdrawals are controlled by Bitcoin transactions (verified through the Bitcoin light client). The user and LP cooperatively sign a single Bitcoin transaction that atomically sends BTC to the LP and authorizes the smart chain payout to the user. Unlike the legacy PrTLC-based approach, users don't need any smart chain balance upfront, the LP doesn't lock funds per-swap, and watchtowers are a UX convenience rather than a security requirement. - [Bitcoin → Solana (legacy)](https://docs.atomiq.exchange/overview/swaps/bitcoind-sc-legacy.md): This is the legacy protocol only used on Solana, superseded by the UTXO-controlled vault based Bitcoin → Smart chain swap on all the other chains which eliminates the cold-start problem and watchtower dependency. - [Lightning → Solana (legacy)](https://docs.atomiq.exchange/overview/swaps/lightning-sc-legacy.md): This is the legacy protocol only used on Solana, superseded by the new Lightning → Smart chain swap on all the other chains, which eliminates the need for users to hold smart chain tokens upfront. - [Lightning → Smart chain](https://docs.atomiq.exchange/overview/swaps/lightning-sc-new.md): This is the current protocol for receiving smart chain tokens via a Lightning payment. It uses an HTLC created by the LP on the smart chain, solving the "cold start" problem of the legacy approach — users don't need any smart chain balance to receive funds. After the LP locks the HTLC, the user broadcasts the payment secret over Nostr, and incentivized watchtowers claim the funds on the user's behalf, earning a small fee. The user can always self-claim as a fallback. - [Smart chain → Bitcoin](https://docs.atomiq.exchange/overview/swaps/sc-bitcoin.md): This swap uses a PrTLC (proof-time locked contract) to enable trustless swaps from smart chain tokens to on-chain Bitcoin. The user locks tokens in a PrTLC on the smart chain, and the LP claims them by proving — through the Bitcoin light client — that it sent the agreed BTC amount to the recipient's Bitcoin address. If the LP fails to deliver, the user can unilaterally refund after the timelock expires. A cooperative refund path also allows the LP to release the user's funds immediately if the payment cannot be completed, without waiting for the timeout. - [Smart chain → Lightning](https://docs.atomiq.exchange/overview/swaps/sc-lightning.md): This swap enables paying a Lightning Network invoice using smart chain (Solana, Starknet, EVM) tokens. It works by linking a Lightning payment to an on-chain HTLC escrow: the user locks tokens in an HTLC on the smart chain using the Lightning invoice's payment hash, and the LP attempts to route and pay the Lightning payment. When the recipient settles the invoice they reveal the hash preimage (secret), which the LP then uses to claim the funds from the HTLC escrow. If the payment fails, the LP can cooperatively refund the user immediately, or the user can self-refund after the timelock expires. ## guides ### lps LP nodes provide liquidity for the swaps, handle cross-chain swaps, determine prices & earn swap fees. Reputation is also tracked for every LP and increases with every successfully processed swap and decreases for failed swaps (ones which resulted in the client having to refund after a timeout). - [Liquidity provider nodes (LPs)](https://docs.atomiq.exchange/guides/lps.md): LP nodes provide liquidity for the swaps, handle cross-chain swaps, determine prices & earn swap fees. Reputation is also tracked for every LP and increases with every successfully processed swap and decreases for failed swaps (ones which resulted in the client having to refund after a timeout). - [Localhost Tunnel (Pinggy)](https://docs.atomiq.exchange/guides/lps/pinggy-tunnel.md): You only need to do this in case your machine running the LP node software doesn't have a static & public IP address accessible from the public internet. This is usually the case if you want to run the LP node at your home, which then runs in a local network and is not accessible from the outside world because of NAT. - [Running LP node](https://docs.atomiq.exchange/guides/lps/running-lp-node.md): LP node runs in docker containers & it is fully separated from your other programs, there is also no need to install anything other than docker on the main system. ## sdk-guide The Atomiq SDK is a TypeScript multichain client for building trustless swaps between smart chains and Bitcoin, both on-chain and over Lightning. This page is the entry point for the SDK docs and helps you choose the right path through the documentation, whether you are setting up the SDK for the first time, implementing a specific swap family, building the surrounding quote UI, or hardening the integration for production. - [SDK Guide](https://docs.atomiq.exchange/sdk-guide.md): The Atomiq SDK is a TypeScript multichain client for building trustless swaps between smart chains and Bitcoin, both on-chain and over Lightning. This page is the entry point for the SDK docs and helps you choose the right path through the documentation, whether you are setting up the SDK for the first time, implementing a specific swap family, building the surrounding quote UI, or hardening the integration for production. ### advanced This section is for integrations that go beyond the default SDK flow. It focuses on production concerns around transaction control, runtime behavior, real-time state, and persistence. - [Advanced](https://docs.atomiq.exchange/sdk-guide/advanced.md): This section is for integrations that go beyond the default SDK flow. It focuses on production concerns around transaction control, runtime behavior, real-time state, and persistence. - [Configuration](https://docs.atomiq.exchange/sdk-guide/advanced/configuration.md): Customize the swapper instance with advanced options. - [Events](https://docs.atomiq.exchange/sdk-guide/advanced/events.md): The SDK emits events on both individual swaps and the top-level swapper. Use them for real-time UI state, pending swap tracking, analytics, LP monitoring, and route-limit refreshes. - [Manual Transactions](https://docs.atomiq.exchange/sdk-guide/advanced/manual-transactions.md): Use the txs*() methods when your app needs to sign and broadcast smart-chain transactions outside the SDK's built-in signer flow. This is useful for manual signing flows (e.g. hardware wallets or custom custody solutions) or apps where transaction approval is handled by another application layer. - [Storage](https://docs.atomiq.exchange/sdk-guide/advanced/storage.md): The SDK persists initiated swaps and a small amount of chain-specific helper data. In browser environments this works out of the box. In Node.js or custom environments you need to provide storage backends explicitly. ### quick-start This section is the shortest path from an empty project to a working swap. The guides here help you pick the right runtime, initialize the SDK, connect wallets or signers, request a quote, and execute the route. - [Quick Start](https://docs.atomiq.exchange/sdk-guide/quick-start.md): This section is the shortest path from an empty project to a working swap. The guides here help you pick the right runtime, initialize the SDK, connect wallets or signers, request a quote, and execute the route. - [Creating Quotes](https://docs.atomiq.exchange/sdk-guide/quick-start/creating-quotes.md): Every swap starts with a quote. Call swapper.swap() to create one — this contacts LPs, finds the best rate, and returns a swap object you can inspect before executing the swap. - [Executing Swaps](https://docs.atomiq.exchange/sdk-guide/quick-start/executing-swaps.md): After you create and inspect a quote with swapper.swap(...), the usual next step is swap.execute(...). - [Quick Start – Browser](https://docs.atomiq.exchange/sdk-guide/quick-start/quick-start-browser.md): This guide covers installing the Atomiq SDK in a browser and its chain-specific connectors and walks you through setting up and initializing the Atomiq SDK. - [Quick Start – Node.js](https://docs.atomiq.exchange/sdk-guide/quick-start/quick-start-nodejs.md): This guide covers installing the Atomiq SDK in Node.js and its chain-specific connectors and walks you through setting up and initializing the Atomiq SDK. ### swap-management This section covers what happens after a swap has already been created and persisted. These pages are for retrieving past swaps, recovering them after app restarts, and handling the cases where a swap needs follow-up outside the normal high-level flow. - [Swap Management](https://docs.atomiq.exchange/sdk-guide/swap-management.md): This section covers what happens after a swap has already been created and persisted. These pages are for retrieving past swaps, recovering them after app restarts, and handling the cases where a swap needs follow-up outside the normal high-level flow. - [Claiming](https://docs.atomiq.exchange/sdk-guide/swap-management/claiming.md): For swaps in the Bitcoin (on-chain or Lightning) → Smart Chain direction, the SDK normally handles claiming automatically via watchtower services. - [Historical Swaps](https://docs.atomiq.exchange/sdk-guide/swap-management/historical-swaps.md): The SDK persists all swaps locally, allowing you to retrieve them later by ID. - [Refunds](https://docs.atomiq.exchange/sdk-guide/swap-management/refunds.md): When a swap in the Smart Chain → Bitcoin direction fails, you can refund your tokens back to your wallet, this can happen when: ### swaps This section helps you choose the guide for the exact swap your app is executing. Each page covers one swap family in detail, including the protocol behind it, the required wallets or signers, the manual execution path, and the recovery actions that matter when the automatic flow does not fully finish. - [Swap Guides](https://docs.atomiq.exchange/sdk-guide/swaps.md): This section helps you choose the guide for the exact swap your app is executing. Each page covers one swap family in detail, including the protocol behind it, the required wallets or signers, the manual execution path, and the recovery actions that matter when the automatic flow does not fully finish. - [Bitcoin → Smart Chain](https://docs.atomiq.exchange/sdk-guide/swaps/btc-to-smart-chain.md): Swap Bitcoin L1 (on-chain) to Starknet or EVM tokens. These swaps are based on the UTXO-controlled vault primitive and verified through the on-chain Bitcoin light client. - [Lightning → Smart Chain](https://docs.atomiq.exchange/sdk-guide/swaps/lightning-to-smart-chain.md): Swap Bitcoin Lightning to Starknet or EVM tokens. These swaps are based on an LP-initiated HTLC on the destination chain. After the LP receives the Lightning payment, it creates the HTLC and the user broadcasts the payment secret over Nostr so watchtowers can claim on the user's behalf. The user can always self-claim as a fallback. - [Smart Chain → BTC/Lightning](https://docs.atomiq.exchange/sdk-guide/swaps/smart-chain-to-btc.md): Swap smart chain tokens (Solana, Starknet & EVM tokens) to Bitcoin L1 (on-chain) or Lightning L2. These swaps are based on the HTLC (for lightning) & PrTLC (for on-chain) primitives. - [Bitcoin → Solana](https://docs.atomiq.exchange/sdk-guide/swaps/solana/btc-to-solana.md): Swap Bitcoin L1 (on-chain) to Solana tokens. Solana still uses the legacy PrTLC based Bitcoin → smart chain flow, verified through the on-chain Bitcoin light client. Unlike the newer UTXO-controlled vault protocol used on Starknet and EVM, the user must first initialize a destination-side swap escrow on Solana and lock a SOL deposit before sending BTC. - [Lightning → Solana](https://docs.atomiq.exchange/sdk-guide/swaps/solana/lightning-to-solana.md): Swap Bitcoin Lightning to Solana tokens. Solana still uses the legacy HTLC based Lightning → smart chain flow. The user pays a Lightning invoice first, then manually initializes and claims the destination-side HTLC on Solana, revealing the secret that lets the LP settle the Lightning payment. ### utilities The Utilities section is for the helper APIs you use while assembling a swap form or route selection flow. Rather than walking through a single swap end to end, these pages focus on the smaller decisions that happen around it: parsing user input, choosing valid token pairs, understanding the route, checking limits, and estimating spendable balance. In UI terms, this includes parsing Bitcoin addresses, BOLT11 invoices, LNURLs, Lightning addresses, and smart-chain addresses, building route-aware token selectors, and adapting the form based on the selected swap protocol. - [Utilities](https://docs.atomiq.exchange/sdk-guide/utilities.md): The Utilities section is for the helper APIs you use while assembling a swap form or route selection flow. Rather than walking through a single swap end to end, these pages focus on the smaller decisions that happen around it: parsing user input, choosing valid token pairs, understanding the route, checking limits, and estimating spendable balance. In UI terms, this includes parsing Bitcoin addresses, BOLT11 invoices, LNURLs, Lightning addresses, and smart-chain addresses, building route-aware token selectors, and adapting the form based on the selected swap protocol. - [Address Parser](https://docs.atomiq.exchange/sdk-guide/utilities/address-parser.md): swapper.Utils.parseAddress() & swapper.Utils.parseAddressSync() are the SDK's unified parsers for user-entered destination and source fields. It is useful anywhere you accept free-form input before calling swapper.swap(...), because the same field may contain a Bitcoin address, a BOLT11 invoice, an LNURL, a Lightning address, or a smart chain address. - [Supported Tokens](https://docs.atomiq.exchange/sdk-guide/utilities/supported-tokens.md): The SDK exposes route discovery helpers on the swapper instance so you can build token selectors, validate route availability before quoting, and resolve stored token identifiers back into typed token objects. - [Swap Limits](https://docs.atomiq.exchange/sdk-guide/utilities/swap-limits.md): swapper.getSwapLimits() returns the currently known minimum and maximum bounds for a given token pair. This is useful for amount validation and min/max hints in quote forms. - [Swap Types](https://docs.atomiq.exchange/sdk-guide/utilities/swap-types.md): swapper.getSwapType() is the SDK's protocol classifier for a token pair. Given a source Token and destination Token, it tells you which swap protocol the SDK will use. This is useful when you want to branch UI, show capability flags such as gas drop support, or understand which swap flow a later swapper.swap(...) quote will follow. - [Wallet Balance](https://docs.atomiq.exchange/sdk-guide/utilities/wallet-balance.md): swapper.Utils exposes fee-aware balance helpers for both smart chain wallets and Bitcoin wallets. ## rest-api-guide The Atomiq REST API is an HTTP interface for the Atomiq cross-chain DEX, covering trustless swaps between Bitcoin / Lightning and smart chains (Starknet, Solana, Botanix, Citrea, Alpen, Goat). - [REST API Guide](https://docs.atomiq.exchange/rest-api-guide.md): The Atomiq REST API is an HTTP interface for the Atomiq cross-chain DEX, covering trustless swaps between Bitcoin / Lightning and smart chains (Starknet, Solana, Botanix, Citrea, Alpen, Goat). ### bitcoin-and-lightning Most of the API looks the same across all supported chains. Bitcoin on-chain and Lightning are the exceptions — PSBTs, BOLT11 invoices, LNURL, and Lightning preimages are Bitcoin-world concepts the smart-chain side doesn't share. This page groups everything direction-specific in one place. - [Bitcoin & Lightning](https://docs.atomiq.exchange/rest-api-guide/bitcoin-and-lightning.md): Most of the API looks the same across all supported chains. Bitcoin on-chain and Lightning are the exceptions — PSBTs, BOLT11 invoices, LNURL, and Lightning preimages are Bitcoin-world concepts the smart-chain side doesn't share. This page groups everything direction-specific in one place. ### creating-and-executing This page covers the core swap lifecycle that every direction follows: create → poll → sign → submit → repeat until finished. Bitcoin- and Lightning-specific details (PSBT building, LNURL, preimage reveal) are split into Bitcoin & Lightning Specifics. - [Creating & Executing a Swap](https://docs.atomiq.exchange/rest-api-guide/creating-and-executing.md): This page covers the core swap lifecycle that every direction follows: create → poll → sign → submit → repeat until finished. Bitcoin- and Lightning-specific details (PSBT building, LNURL, preimage reveal) are split into Bitcoin & Lightning Specifics. ### listing-swaps The API keeps a persistent record of every created swap, except the expired non-executed swaps (which are purged from the database). These endpoints allow you to get either the subset of swaps which are currently pending/ongoing or a list of all swaps executed by a given wallet address. This is useful for the "Pending swaps" screen, the post-app-restart recovery flow and for simply showing the history of all the swaps the given user has completed. - [Listing Swaps](https://docs.atomiq.exchange/rest-api-guide/listing-swaps.md): The API keeps a persistent record of every created swap, except the expired non-executed swaps (which are purged from the database). These endpoints allow you to get either the subset of swaps which are currently pending/ongoing or a list of all swaps executed by a given wallet address. This is useful for the "Pending swaps" screen, the post-app-restart recovery flow and for simply showing the history of all the swaps the given user has completed. ### quoting These endpoints let you build a complete swap form with dynamic token pickers, "Send Max" button, amount limit validation and address parsing. You usually use them before creating the swap, to constrain the tokens and amounts the user is allowed to swap. For the bare-minimum create and execute flow, see Creating & Executing a Swap. - [Quoting Swaps](https://docs.atomiq.exchange/rest-api-guide/quoting.md): These endpoints let you build a complete swap form with dynamic token pickers, "Send Max" button, amount limit validation and address parsing. You usually use them before creating the swap, to constrain the tokens and amounts the user is allowed to swap. For the bare-minimum create and execute flow, see Creating & Executing a Swap. ### run-locally Most integrators will talk to the public Atomiq REST API. This section is for teams that need to host the API themselves — to keep quote traffic on their own infrastructure, run against testnets, control rate limits centrally, or combine it with a custom auth layer. - [Run REST API Locally](https://docs.atomiq.exchange/rest-api-guide/run-locally.md): Most integrators will talk to the public Atomiq REST API. This section is for teams that need to host the API themselves — to keep quote traffic on their own infrastructure, run against testnets, control rate limits centrally, or combine it with a custom auth layer. - [Configuration](https://docs.atomiq.exchange/rest-api-guide/run-locally/configuration.md): The service reads its entire runtime config from a single YAML file. The path is selected by the CONFIG_PATH environment variable; the bundled docker-compose.yml sets it to /src/config/config.yaml, which corresponds to ./config/config.yaml on the host. ## Optional - [Atomiq SDK on npm](https://www.npmjs.com/package/@atomiqlabs/sdk): TypeScript SDK package for integrating Atomiq swaps. - [Atomiq SDK code examples](https://github.com/atomiqlabs/atomiq-sdk-demo): TypeScript SDK integration examples. - [Atomiq REST API — OpenAPI 3.1 spec (JSON)](https://docs.atomiq.exchange/rest-api-reference/openapi.json): Machine-readable OpenAPI specification for the Atomiq REST API. Use this as the source of truth for endpoint shapes, parameters, and error responses when generating client code. - [Atomiq REST API code examples](https://github.com/atomiqlabs/atomiq-api-docker/tree/main/scripts): REST API integration examples. - [Atomiq GitHub organization](https://github.com/atomiqlabs): All Atomiq protocol, SDK, and contract repositories. --- # Full Documentation Content # Liquidity provider nodes (LPs) LP nodes provide liquidity for the swaps, handle cross-chain swaps, determine prices & earn swap fees. Reputation is also tracked for every LP and increases with every successfully processed swap and decreases for failed swaps (ones which resulted in the client having to refund after a timeout). Anyone can become an LP in atomiq.exchange protocol by running the atomiq LP node software, creating a network of LPs. All LP nodes are included in the central LP node registry. ## Request for quote (RFQ) atomiq.exchange operates on the request for quote (RFQ) order model. This works by sending an order request (e.g. I want to swap 100USDC to BTC) to the network of LPs, for which client gets quotes from multiple LPs able to process the swap, client then goes through the quotes and selects the best fitting one (this can be based on price, LP node's reputation or liquidity). tip This selection process is abstracted away from the end user and automated in our SDK and dApp. Once client is happy with the quote, he commits to it by signing it and sending it as a Solana transaction, initiating the swap. info Quotes are signed by the LP, such that they are binding and the user can readily execute them. ## Requirements for running an LP node LP node needs to provide liquidity on the Smart chains (Solana, Starknet, EVM, etc.) and/or Bitcoin side to be able to facilitate swaps. It is recommended to deposit assets on both sides so the LP node can process swaps going both ways (Bitcoin -> Smart chain & Smart chain -> Bitcoin). info Note that if an LP node provides liquidity e.g. only in SOL it is only possible for it to only process BTC -> SOL swaps at first (since it has no BTC to payout for SOL -> BTC swaps). info The liquidity of the node will be redistributed based on swaps it processes (if LP node executes BTC -> SOL swap, the LP will end up with more BTC and less SOL, since for the swap it receives BTC and has to pay out SOL). ### Software LP node needs to have access to Smart chain blockchains (via RPC), Bitcoin blockchain and Lightning network to be able to process swaps. * Bitcoin node (bitcoind) - to provide access to Bitcoin blockchain * Lightning network node (lnd) - to provide a Bitcoin wallet and access to the Bitcoin Lightning network * atomiq Relayer/Watchtower - to make sure bitcoin light client on the Smart chain is synchronized so atomiq LP is able to claim on-chain swaps * atomiq LP - main software processing swaps All of these are part of the atomiq node docker images—see [Running LP node](https://docs.atomiq.exchange/guides/lps/running-lp-node.md) for installation guide. --- # Localhost Tunnel (Pinggy) warning You only need to do this in case your machine running the LP node software doesn't have a static & public IP address accessible from the public internet. This is usually the case if you want to run the LP node at your home, which then runs in a local network and is not accessible from the outside world because of NAT. Pinggy is a service which gives you a static public domain with the traffic tunneled directly to your server. You therefore don't need a public IP address when running the LP node and can run the LP node on home networks. ## Creating account To use the pinggy tunnel you first need to create an account and then get the Pro subscription there (this costs $3 per month, with a free trial for 7 days) Head over to [Pinggy website](https://dashboard.pinggy.io/register) and create an account there. Once registered go over to [subscriptions](https://dashboard.pinggy.io/subscriptions) and subscribe to the Pro plan. ## Tunnel setup After registering the account and subscribing to the Pro subscription you need to head over to the [subdomains](https://dashboard.pinggy.io/subdomains). Copy the **Access token** & **Subdomain** from the page, should look like this: ![Pinggy dashboard](https://3413090771-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FQKYJLT6LdI5sTgcaMspD%2Fuploads%2FjTW8itmHxtfovcQPkdzi%2Fobr%C3%A1zok.png?alt=media\&token=04eabbb4-edaa-43a2-911c-387e6a57bbdd) So in this case the access token (*eCC...*) & subdomain (*zrgaulsdkx.a.pinggy.link*) ### Running the tunnel We will now use the copied access token & subdomain to setup the tunnel on the LP node. The following command will run the tunnel inside a docker image (replace the *\* with your own access token copied in the previous step) ``` sudo docker run -d --restart=unless-stopped --net=host -t pinggy/pinggy -p 443 -R0:localhost:443 -o StrictHostKeyChecking=no -o ServerAliveInterval=30 -t +tls@pro.pinggy.io ``` ### LP configuration We need to configure the LP node to use a different TLS challenge mechanism and also configure the correct subdomain to be used. To do so we edit the LP node's configuration - this is stored at `config/intermediary/config.yaml` (for mainnet) or `config-testnet/intermediary/config.yaml` (for testnet). Search for the `SSL_AUTO` section in the config, should look like this: config.yaml ``` #Automatic SSL certificate provisioning config SSL_AUTO: #HTTP listen port to list for ACME challenges HTTP_LISTEN_PORT: 80 #DNS proxy to use - mapping the server's IP address to a domain DNS_PROXY: "nodes.atomiq.exchange" ``` Replace the whole section with the following (be sure to copy in your subdomain you've got in the pinggy dashboard) config.yaml ``` #Automatic SSL certificate provisioning config SSL_AUTO: #Use different TLS challenge method ACME_METHOD: "tls-alpn-01" #DNS domain to use FULL_DNS_DOMAIN: "" ``` *** After this you are done with the pinggy setup and can [continue the LP node setup](https://docs.atomiq.exchange/guides/lps/running-lp-node.md#installation). --- # Running LP node LP node runs in docker containers & it is fully separated from your other programs, there is also no need to install anything other than docker on the main system. ## Pre-requisites * A linux based machine (preferrably ubuntu 20.04 or 22.04) and SSH (command line) access * Testnet requirements: 4GB of RAM, 200GB SSD storage * Mainnet requirements: 6GB of RAM, 1TB SSD storage * Machine either needs to have a public IP address and be accessible from the public internet or you need to use the [Pinggy tunnel](https://docs.atomiq.exchange/guides/lps/pinggy-tunnel.md) to forward traffic to your local machine info We can recommend using [Contabo](https://contabo.com/en/vps) for VPS hosting, the following instances are recommended: * Testnet: [Storage VPS 1](https://contabo.com/en/storage-vps/storage-vps-10) (€4.50 per month w/o VAT) * Mainnet: [Storage VPS 3](https://contabo.com/en/storage-vps/storage-vps-30) (€14.00 per month w/o VAT) ## Preparations ### Installing docker & docker-compose Install docker ``` sudo apt update sudo apt install -y docker.io ``` Install docker-compose ``` sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose sudo chmod +x /usr/local/bin/docker-compose ``` ### Setup firewall Open ports 22 (SSH), 80 (HTTP), 443 (REST for mainnet) & 8443 (REST for testnet) in the firewall ``` sudo ufw allow 22 sudo ufw allow 80 sudo ufw allow 443 sudo ufw allow 8443 sudo ufw enable ``` ## Installation Download the latest atomiq node archive ``` wget https://atomiqbeta.blob.core.windows.net/node/atomiq-node.tar.gz ``` Unpack the archive ``` sudo tar -xvzf atomiq-node.tar.gz ``` warning If you don't have a server with public ip address you need to [setup the Pinggy tunnel](https://docs.atomiq.exchange/guides/lps/pinggy-tunnel.md) now. This creates a secure tunnel and allows your server to be accessible from the public internet - this is required such that your node is able to accept and respond to RFQ swap requests from clients. Run the setup script - this will walk you through setting an environment (mainnet/testnet) and node's wallet ``` sudo ./setup.bash ``` Once this completes, it runs all the required software inside docker containers, you can check that all of them are running with ``` sudo docker container list ``` ## Interacting with the node ### CLI You can interact with the atomiq node through a CLI (command line interface), this uses a simple TCP connection over netcat to communicate with the LP node: ``` ./lp-cli ## Or ./lp-cli-testnet for testnet ``` ### Stop and start The full atomiq stack will run automatically on machine boot after installation, should you need to stop or start it you can use the following commands Stop the atomiq stack with (will stop the containers): ``` sudo ./stop-mainnet.bash ## Or ./stop-testnet.bash for testnet ``` Start the atomiq stack back up with (will reset the containers, if they are still running) ``` sudo ./start-mainnet.bash ## Or ./start-testnet.bash for testnet ``` ## Using CLI CLI is the way to send command to your running atomiq node. To get an overview of all the available command type in 'help' ``` > help Available commands: status : Fetches the current status of the bitcoin RPC, LND gRPC & intermediary application getaddress : Gets the SmartChains & Bitcoin address of the node getbalance : Gets the balances of the node transfer : Transfer wallet balance to an external address deposit : Deposits smartchain wallet balance to an LP Vault withdraw : Withdraw LP Vault balance to node's SmartChain wallet getreputation : Checks the LP node's reputation stats plugins : Shows the list of loaded plugins geturl : Returns the URL of the node (only works when SSL_AUTO mode is used) register : Registers the URL of the node to the public LP node registry (only works when SSL_AUTO mode is used) listswaps : Lists all swaps in progress splitutxos : Splits funds to a bunch of smaller utxos consolidateutxos : Consolidates small UTXOs listvaults : Lists created spv vaults createvaults : Creates new spv vaults depositvault : Deposits funds to the specific spv vault withdrawvault : Withdraw funds from the specific spv vault airdrop : Requests an airdrop of SOL tokens (only works on devnet!) Use 'help ' for usage examples, description & help around a specific command! ``` ### Monitoring synchronization status We need to wait for the bitcoin node to sync up to the network (download whole bitcoin blockchain, this takes few hours on testnet & up to a day on mainnet). We can monitor the status of the sync progress with the status command ``` > status { "smartChains": { "SOLANA": { "rpcStatus": "ready", "funds": "0.000000000", "ticker": "SOL", "hasEnoughFunds": true }, "STARKNET": { "rpcStatus": "ready", "funds": "0.000000000000000000", "ticker": "STRK", "hasEnoughFunds": true } }, "bitcoinRpc": { "status": "verifying blockchain", "verificationProgress": "6.8971%", // <---- We can see the sync up/verification progress here "syncedHeaders": 2812116, "syncedBlocks": 549068 }, "bitcoinWallet": { "status": "offline" }, "lightningWallet": null, "lpNodeStatus": "wait_btc_rpc" // <---- We can see LP node status here, once it's "ready" the node is synced and ready to roll } ``` ### Depositing funds While the node is syncing we can already deposit funds to the node, using ‘getaddress’ command we get the Smart chains (Solana, Starknet, EVM, etc.) & Bitcoin deposit addresses ``` > getaddress { "addresses": { "SOLANA": "3X8UXqRF1sUQrkmtmdH2gHCuwgn1eVJpyiXAnPQzdcmy", "STARKNET": "0x07616a5e3dc18e97b3310d8aba0bacb14ab389a4078442d9658be9616feeff3f", "bitcoin": "tb1qz3wac0jz0u4muz0egzf986pukgp69ej3ftn2ck" } } ``` After funds are deposited to the wallets we can track the balance with the ‘getbalance’ command **(BTC balances only show up after bitcoin node is synced)** info The balances on the smart chains (Solana, Starknet, EVM, etc.) are split across wallet balances (these are not used for trading) - 'nonTradingWalletBalances' & trading vault balances (these are actively used for processing swaps) - 'tradingVaultBalances' ``` > getbalance { "nonTradingWalletBalances": { "SOLANA": { "SOL": { "balance": "1.494565198", "decimals": 9 } }, "STARKNET": { "ETH": { "balance": "0.000000000000000000", "decimals": 18 }, "STRK": { "balance": "1120.053239098453593203", "decimals": 18 } } }, "tradingVaultBalances": { "SOLANA": { "SOL": { "balance": "0.000000000", "decimals": 9 } }, "STARKNET": { "ETH": { "balance": "0.000000000000000000", "decimals": 18 }, "STRK": { "balance": "0.000000000000000000", "decimals": 18 } } }, "tradingBitcoinBalance": { "error": "bitcoin wallet not ready" } } ``` To make the smart chains funds available for processing swaps we have to deposit them to the LP vault (this doesn’t have to be done with bitcoin assets) - repeat this for all the assets you want to be traded ‘deposit \ \’. The assets are always specified in the following format: '\-\', e.g. STARKNET-STRK or SOLANA-SOL ``` > deposit SOLANA-SOL 1 Transaction sent, signature: 4PxwU42k2xocYtspd8uAjNZahjU1YZSv6E4dXMziJRq9Ajd8ecjY8GQn8XXqGoJn6vxZ7F8W6qexMcTci3EuMda6 waiting for confirmation... { "success": true, "message": "Deposit transaction confirmed", "txId": "4PxwU42k2xocYtspd8uAjNZahjU1YZSv6E4dXMziJRq9Ajd8ecjY8GQn8XXqGoJn6vxZ7F8W6qexMcTci3EuMda6" } ``` Now we can check that the assets are really deposited and used for trading ``` > getbalance { "nonTradingWalletBalances": { "SOLANA": { "SOL": { "balance": "0.494565198", "decimals": 9 } }, "STARKNET": { "ETH": { "balance": "0.000000000000000000", "decimals": 18 }, "STRK": { "balance": "620.053239098453593203", "decimals": 18 } } }, "tradingVaultBalances": { "SOLANA": { "SOL": { "balance": "1.000000000", "decimals": 9 } }, "STARKNET": { "ETH": { "balance": "0.000000000000000000", "decimals": 18 }, "STRK": { "balance": "500.000000000000000000", "decimals": 18 } } }, "tradingBitcoinBalance": { "error": "bitcoin wallet not ready" } } ``` danger **It is important that you always keep some balance of native chain token (SOL for Solana, STRK for Starknet) in your wallet (non-trading) - this is used to cover the transaction fees for executing the swaps. Keeping at least 0.2 SOL and 200 STRK is recommended.** ### Waiting for sync For the LP node to start being operational you will have to wait till the underlying bitcoin node finishes synchronizing the bitcoin blockchain. You can check see the synchronization progress in the verification progress field. Once your node is synced up and ready the LP node status should show `"ready"` ``` > status { "smartChains": { "SOLANA": { "rpcStatus": "ready", "funds": "0.494565198", "ticker": "SOL", "hasEnoughFunds": true }, "STARKNET": { "rpcStatus": "ready", "funds": "620.053239098453593203", "ticker": "STRK", "hasEnoughFunds": true } }, "bitcoinRpc": { "status": "ready", "verificationProgress": "99.9994%", // <---- Synchronization progress "syncedHeaders": 2812116, "syncedBlocks": 2812116 }, "bitcoinWallet": { "status": "ready" }, "lightningWallet": null, "lpNodeStatus": "ready" // <---- We can see that our node is ready now! } ``` ### Testing the LP node After the node is synced up we can test the node via the atomiq frontend, to do this you first need to get the URL of your LP node ``` > geturl { "url": "https://83-32-155-32.nodes.atomiq.exchange:443" } ``` To make the atomiq frontend access your node you can use the following frontend URL and replace `` with the URL obtained by executing the `geturl` command: ``` https://app.atomiq.exchange/?UNSAFE_LP_URL= ``` This will force the frontend to connect only to your LP node ### Registering LP node To be able to process swaps of other atomiq users your node needs to be registered in the atomiq LP node registry. After confirming the swaps through the LP node work in the previous step, we can now send a request to register our node in the central LP registry with the `register` command. Please be sure to include an e-mail where we can contact you in case there is something wrong with your node ``` > register atomiq@example.com { "success": true, "status": "created", "message": "LP registration request created", "url": "https://github.com/adambor/SolLightning-registry/pull/3" } ``` We will now review your node (check if it is reachable & try swapping through it), you can monitor your node's approval/disapproval status by issuing the `register` command again ``` > register atomiq@example.com { "success": true, "status": "checking", "message": "LP registration status: pending", "githubPR": "https://github.com/adambor/SolLightning-registry/pull/3" } ``` tip Once your node is approved to be listed in the LP registry you will start processing user's swaps! ## Updating To update the node to the latest version of the docker images you can run the following Download the latest atomiq node archive ``` wget https://atomiqbeta.blob.core.windows.net/node/atomiq-node.tar.gz -O atomiq-node.tar.gz ``` Unpack & run the update script (this will automatically install the new package versions and restart all the docker containers) ``` tar -zxvf atomiq-node.tar.gz update.bash && sudo ./update.bash ``` ## Configuration Your atomiq node comes pre-configured with reasonable default, but in case you want to change the configuration you can find in `config/intermediary/config.yaml` (for mainnet) or `config-testnet/intermediary/config.yaml` (for testnet) folders. info You might want to change the RPC URLs, and use dedicated ones (from e.g. [Helius](https://www.helius.dev/) (Solana)- reasonable free tier, or [Alchemy](https://www.alchemy.com/rpc-api) (Starknet, EVM) - a reasonable pay-as-you go tier) - see the `SOLANA` and `STARKNET` section. Or change the minimums/maximums or fees charged for the swaps - see the `ONCHAIN` and `ONCHAIN_SPV` section. Default mainnet configuration: config.yaml ``` #Solana RPC SOLANA: #If the LP node has below this amount it will stop processing new swaps MIN_NATIVE_RESERVE: 0.1 #Solana RPC URL to use, it is recommended to use a dedicated RPC endpoint from e.g. Helius (recommended), Quicknode, etc. RPC_URL: "https://api.mainnet-beta.solana.com" #Maximum fee in micro lamport/CU to use for transactions MAX_FEE_MICRO_LAMPORTS: 250000 #File with the wallet mnemonic seed MNEMONIC_FILE: "/mnt/share/wallet/mnemonic.txt" #Static tip (in lamports) to add to every transaction STATIC_TIP: 50000 #Jito transaction relayer configuration #JITO: # PUBKEY: "DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL" # ENDPOINT: "https://frankfurt.mainnet.block-engine.jito.wtf/api/v1/transactions" #Starknet RPC STARKNET: #If the LP node has below this amount it will stop processing new swaps MIN_NATIVE_RESERVE: 100 #Starknet RPC URL RPC_URL: "https://starknet.api.onfinality.io/public/rpc/v0_9" #Can also optionally specify an additional WS URL - allows for faster swap processing #WS_URL: "" #Gas price limits (higher bound) MAX_L1_FEE_GWEI: 20000000 MAX_L2_FEE_GWEI: 4000000 MAX_L1_DATA_FEE_GWEI: 10000000 #Starknet chain CHAIN: "MAIN" #File with wallet mnemonic seed MNEMONIC_FILE: "/mnt/share/wallet/mnemonic.txt" #Starknet quotes timeout AUTHORIZATION_TIMEOUT: 90 BOTANIX: #If the LP node has below this amount it will stop processing new swaps MIN_NATIVE_RESERVE: 0.0002 #Botanix RPC URL, can either be an http or ws RPC RPC_URL: "wss://rpc.botanixlabs.com/ws" #Maximum block range that the underlying RPC provider supports MAX_LOGS_BLOCK_RANGE: 900 #Fees MAX_FEE_GWEI: 2 FEE_TIP_GWEI: 0.001 #Botanix chain CHAIN: "MAINNET" #File with wallet mnemonic seed MNEMONIC_FILE: "/mnt/share/wallet/mnemonic.txt" #Botanix quotes timeout AUTHORIZATION_TIMEOUT: 90 #Required APY option premium to be paid by the users as security deposit with BTC -> Solana swaps SECURITY_DEPOSIT_APY: 80 #Bitcoin RPC BITCOIND: PROTOCOL: "http" PORT: 8332 HOST: "bitcoind" RPC_USERNAME: "user1" RPC_PASSWORD: "ThisIsAPassword" NETWORK: "mainnet" #LND RPC LND: MNEMONIC_FILE: "/mnt/share/wallet/mnemonic.txt" MNEMONIC_BIRTHDAY_FILE: "/mnt/share/wallet/mnemonic-birthday.txt" WALLET_PASSWORD_FILE: "/mnt/share/wallet/password.txt" CERT_FILE: "/mnt/share/lnd/tls.cert" MACAROON_FILE: "/mnt/share/lnd/admin.macaroon" HOST: "lnd" PORT: 10009 #LN setup, uncomment these lines if you want to enable lightning network support for your node #LN: # #Total swap fee is calculated as BASE_FEE + {SWAP_VALUE}*FEE_PERCENTAGE # #Base fee (in BTC) to be paid by every swap # BASE_FEE: 0.00000010 # #Fee (in %) to be charged on the swaps # FEE_PERCENTAGE: 0.3 # # #Minimum swappable amount in BTC (for lightning network swaps) # MIN: 0.00001000 # #Maximum swappable amount in BTC (for lightning network swaps) # MAX: 0.01000000 # # #Allow lightning network swaps that cannot be probed first # ALLOW_NON_PROBABLE_SWAPS: true # #Allow lightning network swaps where the LN invoice has a very short expiry # ALLOW_LN_SHORT_EXPIRY: true # #Set expiration for lightning swaps invoices # INVOICE_EXPIRY_SECONDS: 90 # # #Maximum amounts for swap for gas # GAS_MAX: # STARKNET: 5 # SOLANA: 0.03 # BOTANIX: 0.00005 # # MAX_INFLIGHT_SWAPS: 100 # MAX_INFLIGHT_AUTO_SWAPS: 20 #On-chain setup ONCHAIN: #Total swap fee is calculated as BASE_FEE + {SWAP_VALUE}*FEE_PERCENTAGE #Base fee (in BTC) to be paid by every swap BASE_FEE: 0.00000150 #Fee (in %) to be charged on the swaps FEE_PERCENTAGE: 0.3 #Minimum swappable amount in BTC (for on-chain swaps) MIN: 0.00010000 #Maximum swappable amount in BTC (for on-chain swaps) MAX: 0.05000000 #Network fee buffer (in %), we charge the client this much more on the network # fee to accomodate for the possible fee rate increases in the near future NETWORK_FEE_ADD_PERCENTAGE: 25 MAX_INFLIGHT_SWAPS: 100 #On-chain setup BTC -> Starknet/EVM (new protocol - not BTC -> Solana!!!) ONCHAIN_SPV: MNEMONIC_FILE: "/mnt/share/wallet/mnemonic.txt" #Total swap fee is calculated as BASE_FEE + {SWAP_VALUE}*FEE_PERCENTAGE #Base fee (in BTC) to be paid by every swap BASE_FEE: 0.00000150 #Fee (in %) to be charged on the swaps FEE_PERCENTAGE: 0.3 #Minimum swappable amount in BTC (for on-chain swaps) MIN: 0.00010000 #Maximum swappable amount in BTC (for on-chain swaps) MAX: 0.05000000 #Maximum "gas drop" for different chains GAS_MAX: STARKNET: 5 SOLANA: 0.05 BOTANIX: 0.00005 MAX_INFLIGHT_SWAPS: 100 #Tradable assets setup ASSETS: BBTC: chains: BOTANIX: address: "0x0000000000000000000000000000000000000000" decimals: 18 securityDepositAllowed: true spvVaultMultiplier: 1000000000 pricing: "$fixed-100000000" WBTC: chains: SOLANA: #Address of the token address: "3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh" #Decimal places decimals: 8 STARKNET: address: "0x03fe2b97c1fd336e750087d68b9b867997fd64a2661ff3ca5a7c771641e8e7ac" decimals: 8 spvVaultMultiplier: 1 #Pricing source (using Binance), you can invert the pair by prepending "!" pricing: "WBTCBTC" USDC: chains: SOLANA: address: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" decimals: 6 pricing: "!BTCUSDC" USDT: chains: SOLANA: address: "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB" decimals: 6 pricing: "!BTCUSDT" SOL: chains: SOLANA: address: "So11111111111111111111111111111111111111112" decimals: 9 securityDepositAllowed: true pricing: "SOLBTC" ETH: chains: STARKNET: address: "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7" decimals: 18 securityDepositAllowed: true spvVaultMultiplier: 1000000000 pricing: "ETHBTC" STRK: chains: STARKNET: address: "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d" decimals: 18 securityDepositAllowed: true spvVaultMultiplier: 1000000000 pricing: "STRKUSDT;!BTCUSDT" #CLI - command line interface config CLI: #CLI bind address ADDRESS: "0.0.0.0" #CLI TCP port PORT: 40221 #REST RFQ API config REST: #REST API bind address ADDRESS: "0.0.0.0" #REST API port PORT: 443 #Automatic SSL certificate provisioning config SSL_AUTO: #HTTP listen port to list for ACME challenges HTTP_LISTEN_PORT: 80 #DNS proxy to use - mapping the server's IP address to a domain DNS_PROXY: "nodes.atomiq.exchange" #Node extensions/plugins PLUGINS: atomiq-archiver: "atomiq-archiver@latest" ``` --- # Actors in the Atomiq Protocol Atomiq is designed as a permissionless system where multiple independent parties interact to enable trustless cross-chain swaps. The main participants are the User, Liquidity Provider (LP), and Watchtower (also referred to as a relayer). Each plays a distinct role across both Bitcoin L1 and Lightning swaps, while all operations remain non-custodial and verifiable through smart contracts, cryptography, and Bitcoin consensus. ![Protocol Overview](/assets/images/atomiq-overview-diagrams-fff726c42d2b804682ba1a57d9552beb.svg) ## User The User is initiating a cross-chain swap. They request quotes off-chain directly from Liquidity Providers (LPs) via the RFQ system through the Atomiq SDK, which automatically selects and accepts the best available offer on the client-side. The user then signs and broadcasts the necessary transaction(s) on the source chain to commit funds for the swap. This involves depositing funds into a smart contract [PrTLC](https://docs.atomiq.exchange/overview/core-primitives/prtlc.md) or [HTLC](https://docs.atomiq.exchange/overview/core-primitives/htlc.md) escrow on the smart chain (for **Smart chain → Bitcoin/Lightning** swaps), co-signing a Bitcoin transaction with the LP to move funds to the LP's address on Bitcoin (for **Bitcoin → Smart chain** swaps), or paying a Lightning invoice invoice (for **Lightning → Smart chain**). In every case, the user retains custody unless the agreed cryptographic settlement conditions are met. Users do not need to remain online for the full duration of the swap—watchtowers can handle settlement if required. In case of any failure or non-cooperation, the user's funds can be refunded, with refund mechanics depending on swap direction: * In **Smart chain → Bitcoin/Lightning** swaps, if the counterparty fails to deliver Bitcoin, the user (or a watchtower) can manually trigger a refund from the smart chain escrow after the timeout period. * In **Bitcoin → Smart chain** swaps, there is no refund mechanism needed—if the Bitcoin transaction is not confirmed (e.g., due to a double-spend or replacement in the mempool), the funds never leave the user's wallet, and no swap occurs on the smart chain side. * In **Lightning → Smart chain** swaps, if the counterparty fails to create an HTLC on the destination side, the Lightning network payment eventually times out and funds are automatically returned to the user. This ensures users retain full control and never risk permanent loss from non-cooperation. ## Liquidity Provider (LP) Liquidity Providers are market makers who give out quotes and facilitate swaps. Anyone can permissionlessly run an LP node and join the LP network. LPs compete in the off-chain RFQ system to offer the best prices to users. They provide the necessary liquidity on the destination chain to fulfill the swap. For Bitcoin L1 swaps, they leverage the Atomiq protocol's novel [PrTLCs](https://docs.atomiq.exchange/overview/core-primitives/prtlc.md) and [UTXO-controlled vaults](https://docs.atomiq.exchange/overview/core-primitives/utxo-controlled-vault.md) primitives. These protect LPs by solving the free option problem—ensuring they are not exposed to one-sided risk if the user chooses not to complete the swap in a timely manner and instead exploits the optionality provided by the swap timeouts. For Lightning L2 swaps, the established [HTLC](https://docs.atomiq.exchange/overview/core-primitives/htlc.md) mechanism is used. In both cases, settlement between the user and LP is enforced by the protocol's smart contracts and cryptographic conditions, such that it only occurs when both sides fulfill their obligations. ## Watchtower Watchtowers are optional, permissionless helpers that monitor swaps and perform settlements on behalf of users when conditions are met. They improve user experience by removing the need for constant online presence (eliminating user liveness requirements) in Bitcoin L1 swaps. When a watchtower successfully settles a swap (e.g., by submitting the required bitcoin transaction proof to the smart contract), it earns a small fee from the swap proceeds. On a high level the watchtowers: 1. Observe the creation of a swap on-chain (the user must explicitly opt-in) 2. Monitor subsequent Bitcoin blocks for the required transaction 3. Once the Bitcoin transaction is found, they wait for the required number of confirmations 4. After sufficient confirmations, they claim the swap funds to the user's account and receive the settlement fees Because settlement logic is fully on-chain and verifiable, users remain in full control and face no additional trust assumptions: watchtowers simply accelerate execution and improve swap UX without custody or decision-making power. info Watchtowers are also used for [**Lightning → Smart chain**](https://docs.atomiq.exchange/overview/swaps/lightning-sc-new.md) swaps, where users broadcast the HTLC swap secret over **Nostr** and let the watchtowers claim the funds on their behalf. This delegates the settlement transaction fee payment to the watchtowers, allowing users with no Smart chain balance to execute swaps. *** These three roles work together in a decentralized, open manner: no single party is trusted, and atomicity is enforced purely through cryptography, consensus verification, and smart contract escrows. Whether the swap uses Bitcoin L1 [PrTLCs](https://docs.atomiq.exchange/overview/core-primitives/prtlc.md) and [UTXO-controlled vaults](https://docs.atomiq.exchange/overview/core-primitives/utxo-controlled-vault.md) swap primitives, or the established Lightning L2 [HTLCs](https://docs.atomiq.exchange/overview/core-primitives/htlc.md) swap primitive, the structure preserves Atomiq's strong security guarantees while reducing the need for constant online presence and making the protocol accessible for everyday users. --- # Contracts Atomiq's smart contracts are deployed across multiple chains, with two distinct architectural approaches depending on the target platform. ## Solana The Solana contracts use a **monolithic architecture** — the BTC relay and swap logic are implemented as standalone Anchor programs. This is the older contract design, originally built as the first implementation of the Atomiq protocol. **[Solana Contracts →](https://docs.atomiq.exchange/overview/contracts/solana/.md)** *** ## EVM & Starknet The EVM and Starknet contracts use a **modular architecture** — the protocol logic is decomposed into small, composable contract modules (BTC relay, escrow manager, claim/refund handlers, etc.) that can be combined and upgraded independently. This is the newer contract design, enabling greater flexibility and reuse across chains. **[EVM & Starknet Contracts →](https://docs.atomiq.exchange/overview/contracts/evm-starknet/.md)** *** --- # Contract Addresses Summary of all contract addresses used by atomiq across different chains. The contracts are classified based on which Bitcoin network they support: * **Mainnet**: Bitcoin mainnet, live deployment * **Testnet3**: Bitcoin older testnet3 for testing * **Testnet4**: New Bitcoin testnet4 network for testing warning Not every chain supports both bitcoin testnets (3 and 4)! ## Solana * Mainnet * Testnet3 (Solana Devnet) | Contract | Address | | --------- | ---------------------------------------------- | | BTC Relay | `3KHSHFpEK6bsjg3bqcxQ9qssJYtRCMi2S9TYVe4q6CQc` | | Swaps | `4hfUykhqmD7ZRvNh1HuzVKEY7ToENixtdUKZspNDCrEM` | | Contract | Address | | --------- | ---------------------------------------------- | | BTC Relay | `3KHSHFpEK6bsjg3bqcxQ9qssJYtRCMi2S9TYVe4q6CQc` | | Swaps | `4hfUykhqmD7ZRvNh1HuzVKEY7ToENixtdUKZspNDCrEM` | ## Starknet * Mainnet * Testnet3 (Starknet Sepolia) * Testnet4 (Starknet Sepolia) | Contract | Address | | --------------------------- | -------------------------------------------------------------------- | | BTC Relay | `0x057b14a4231b82f1e525ff35a722d893ca3dd2bde0baa6cee97937c5be861dbc` | | Escrow Manager | `0x04f278e1f19e495c3b1dd35ef307c4f7510768ed95481958fbae588bd173f79a` | | SPV Swap Vault | `0x01932042992647771f3d0aa6ee526e65359c891fe05a285faaf4d3ffa373e132` | | Hashlock Claim Handler | `0x07b74b50a883ebee262b6db0e3c0c697670c6f30e3d610e75faf33a89c46aa2a` | | Output Claim Handler | `0x02c45a81c4a48d0645a0a199e620061e8a55dcc9c2b5946d050eaeeddba64e9a` | | Nonced Output Claim Handler | `0x0019b5480dd7ed8ded10a09437b0a7a30b8997b4ef139deb24ff8c86f995d84f` | | TxID Claim Handler | `0x016c2db2b03f39cf4fd7f871035000f66b62307d9983056e33a38315da8a44dc` | | Timelock Refund Handler | `0x06a59659990c2aefbf7239f6d911617b3ae60b79cb3364f3bd242a6ca8f4f4f7` | | Contract | Address | | --------------------------- | -------------------------------------------------------------------- | | BTC Relay | `0x068601c79da2231d21e015ccfd59c243861156fa523a12c9f987ec28eb8dbc8c` | | Escrow Manager | `0x017bf50dd28b6d823a231355bb25813d4396c8e19d2df03026038714a22f0413` | | SPV Swap Vault | `0x02d581ea838cd5ca46ba08660eddd064d50a0392f618e95310432147928d572e` | | Hashlock Claim Handler | `0x04a57ea54d4637c352aad1bbee046868926a11702216a0aaf7eeec1568be2d7b` | | Output Claim Handler | `0x051bef6f5fd12e2832a7d38653bdfc8eb84ba7eb7a4aada5b87ef38a9999cf17` | | Nonced Output Claim Handler | `0x050e50eacd16da414f2c3a7c3570fd5e248974c6fe757d41acbf72d2836fa0a1` | | TxID Claim Handler | `0x04c7cde88359e14b6f6f779f8b9d8310cee37e91a6f143f855ae29fab33c396e` | | Timelock Refund Handler | `0x034b8f28b3ca979036cb2849cfa3af7f67207459224b6ca5ce2474aa398ec3e7` | | Contract | Address | | --------------------------- | -------------------------------------------------------------------- | | BTC Relay | `0x0099b63f39f0cabb767361de3d8d3e97212351a51540e2687c2571f4da490dbe` | | Escrow Manager | `0x017bf50dd28b6d823a231355bb25813d4396c8e19d2df03026038714a22f0413` | | SPV Swap Vault | `0x02d581ea838cd5ca46ba08660eddd064d50a0392f618e95310432147928d572e` | | Hashlock Claim Handler | `0x04a57ea54d4637c352aad1bbee046868926a11702216a0aaf7eeec1568be2d7b` | | Output Claim Handler | `0x051bef6f5fd12e2832a7d38653bdfc8eb84ba7eb7a4aada5b87ef38a9999cf17` | | Nonced Output Claim Handler | `0x050e50eacd16da414f2c3a7c3570fd5e248974c6fe757d41acbf72d2836fa0a1` | | TxID Claim Handler | `0x04c7cde88359e14b6f6f779f8b9d8310cee37e91a6f143f855ae29fab33c396e` | | Timelock Refund Handler | `0x034b8f28b3ca979036cb2849cfa3af7f67207459224b6ca5ce2474aa398ec3e7` | ## Botanix * Mainnet * Testnet3 (Botanix Testnet) | Contract | Address | | --------------------------- | -------------------------------------------- | | Execution Contract | `0x71Bc44F3F7203fC1279107D924e418F02b0d4029` | | Escrow Manager | `0x9a027B5Bf43382Cc4A5134d9EFD389f61ece27B9` | | BTC Relay | `0xe8be24CF21341c9567664009a8a82C9Dc1eE90D6` | | SPV Swap Vault | `0xe510D5781C6C849284Fb25Dc20b1684cEC445C8B` | | Timelock Refund Handler | `0x44aC0f0677C88e2c0B2FEc986b70E3b9A224f553` | | Hashlock Claim Handler | `0xfFA842529977a40A3fdb988cdDC9CB5c39bAcF26` | | TxID Claim Handler | `0xa2698D2fBE3f7c74cCca428a5fd968411644C641` | | Output Claim Handler | `0x62a718348081F9CF9a8E3dF4B4EA6d6349991ad9` | | Nonced Output Claim Handler | `0xb0226bAC3BD30179fb66A43cEA212AbBC988e004` | | Contract | Address | | --------------------------- | -------------------------------------------- | | Execution Contract | `0xe510D5781C6C849284Fb25Dc20b1684cEC445C8B` | | Escrow Manager | `0xfFA842529977a40A3fdb988cdDC9CB5c39bAcF26` | | BTC Relay | `0xba7E78011909e3501027FBc226a04DCC837a555D` | | SPV Swap Vault | `0x9a027B5Bf43382Cc4A5134d9EFD389f61ece27B9` | | Timelock Refund Handler | `0xEf227Caf24681FcEDa5fC26777B81964D404e239` | | Hashlock Claim Handler | `0xBe8C784b03F0c6d54aC35a4D41bd6CF2EDb6e012` | | TxID Claim Handler | `0x65faec5DC334bf2005eC2DFcf012da87a832f1F0` | | Output Claim Handler | `0x4699450973c21d6Fe09e36A8A475EaE4D78a3137` | | Nonced Output Claim Handler | `0xfd0FbA128244f502678251b07dEa0fb4EcE959F3` | ## Citrea * Mainnet * Testnet4 (Citrea Testnet) | Contract | Address | | --------------------------- | -------------------------------------------- | | Execution Contract | `0x6a373b6Adad83964727bA0fa15E22Be05173fc12` | | Escrow Manager | `0xc98Ef084d3911C8447DBbE4dDa18bC2c9bB0584e` | | BTC Relay | `0x11ac854A68830d61af975063c91602f878C36fA6` | | SPV Swap Vault | `0x5bb0C725939cB825d1322A99a3FeB570097628c3` | | Timelock Refund Handler | `0x2920EE496693A5027249a027A6FD3F643E743745` | | Hashlock Claim Handler | `0x8A44f1995a54fD976c904Cccf9EbaB49c3182eb3` | | TxID Claim Handler | `0x59A54378B6bA9C21ba66487C6A701D702baDEabE` | | Output Claim Handler | `0x32EB4DbDdC31e19ba908fecc7cae03F0d04F01Fa` | | Nonced Output Claim Handler | `0xaB2D14745362B26a732dD8B7F95daAE3D2914bBF` | | Contract | Address | | --------------------------- | -------------------------------------------- | | Execution Contract | `0x9e289512965A0842b342A6BB3F3c41F22a555Cfe` | | Escrow Manager | `0xBbf7755b674dD107d59F0650D1A3fA9C60bf6Fe6` | | BTC Relay | `0x00D122E9f9766cd81a38D2dd44f9AFfb94c67Af7` | | SPV Swap Vault | `0x9Bf990C6088F716279797a602b05941c40591533` | | Timelock Refund Handler | `0x4699450973c21d6Fe09e36A8A475EaE4D78a3137` | | Hashlock Claim Handler | `0x1120e1Eb3049148AeBEe497331774BfE1f6c174D` | | TxID Claim Handler | `0xf61D1da542111216337FeEA5586022130D468842` | | Output Claim Handler | `0xBe8C784b03F0c6d54aC35a4D41bd6CF2EDb6e012` | | Nonced Output Claim Handler | `0x65faec5DC334bf2005eC2DFcf012da87a832f1F0` | ## Alpen * Mainnet * Testnet3 (Alpen Testnet) * Testnet4 (Alpen Testnet) info Not yet deployed on Alpen mainnet. | Contract | Address | | --------------------------- | -------------------------------------------- | | Execution Contract | `0x32EB4DbDdC31e19ba908fecc7cae03F0d04F01Fa` | | Escrow Manager | `0x2920EE496693A5027249a027A6FD3F643E743745` | | BTC Relay | `0x59A54378B6bA9C21ba66487C6A701D702baDEabE` | | SPV Swap Vault | `0xaB2D14745362B26a732dD8B7F95daAE3D2914bBF` | | Timelock Refund Handler | `0x9a027B5Bf43382Cc4A5134d9EFD389f61ece27B9` | | Hashlock Claim Handler | `0x3887B02217726bB36958Dd595e57293fB63D5082` | | TxID Claim Handler | `0xe8be24CF21341c9567664009a8a82C9Dc1eE90D6` | | Output Claim Handler | `0x71Bc44F3F7203fC1279107D924e418F02b0d4029` | | Nonced Output Claim Handler | `0xe510D5781C6C849284Fb25Dc20b1684cEC445C8B` | | Contract | Address | | --------------------------- | -------------------------------------------- | | Execution Contract | `0xa2698D2fBE3f7c74cCca428a5fd968411644C641` | | Escrow Manager | `0xb0226bAC3BD30179fb66A43cEA212AbBC988e004` | | BTC Relay | `0xfFA842529977a40A3fdb988cdDC9CB5c39bAcF26` | | SPV Swap Vault | `0x62a718348081F9CF9a8E3dF4B4EA6d6349991ad9` | | Timelock Refund Handler | `0xA6E5eBF158cDFC4A5B3694495FB26ecadb1378eb` | | Hashlock Claim Handler | `0x44aC0f0677C88e2c0B2FEc986b70E3b9A224f553` | | TxID Claim Handler | `0xCBd9bcfb4b47F0A98948eD4d7Dcc872433989d57` | | Output Claim Handler | `0x50dFF49039c0e45eCb8040fC986fA24B4dda787D` | | Nonced Output Claim Handler | `0xFB5582400a527342a7D32491D7e756Ba3C346FE7` | ## GOAT Network * Mainnet * Testnet3 (GOAT Testnet) * Testnet4 (GOAT Testnet) info Not yet deployed on GOAT Network mainnet. | Contract | Address | | --------------------------- | -------------------------------------------- | | Execution Contract | `0xe8be24CF21341c9567664009a8a82C9Dc1eE90D6` | | Escrow Manager | `0xe510D5781C6C849284Fb25Dc20b1684cEC445C8B` | | BTC Relay | `0x3887B02217726bB36958Dd595e57293fB63D5082` | | SPV Swap Vault | `0x71Bc44F3F7203fC1279107D924e418F02b0d4029` | | Timelock Refund Handler | `0xb0226bAC3BD30179fb66A43cEA212AbBC988e004` | | TxID Claim Handler | `0xfFA842529977a40A3fdb988cdDC9CB5c39bAcF26` | | Hashlock Claim Handler | `0x9a027B5Bf43382Cc4A5134d9EFD389f61ece27B9` | | Output Claim Handler | `0xa2698D2fBE3f7c74cCca428a5fd968411644C641` | | Nonced Output Claim Handler | `0x62a718348081F9CF9a8E3dF4B4EA6d6349991ad9` | | Contract | Address | | --------------------------- | -------------------------------------------- | | Execution Contract | `0x4f7d86C870F28ac30C8fa864Ee04264D7dD03847` | | Escrow Manager | `0x3FbbA0eb82cf1247cbf92B3D51641226310F0Ca5` | | BTC Relay | `0xEeD58871C24d24C49554aF8B65Dd86eD8ed778D3` | | SPV Swap Vault | `0x8a80A68f8bA1732015A821b5260fEF8040a844b7` | | Timelock Refund Handler | `0x0Ff4792d4F792c5B3678f08c18e7cE1974880e48` | | Hashlock Claim Handler | `0x122962B30c46Ef188Dd598a76647c2DBbE1E914e` | | TxID Claim Handler | `0x4737C793a1f86a375BAad0D96134bEd64f246693` | | Output Claim Handler | `0xa2b2d8CD8D1a9200Ac0970523FdfFcbD94aE54B6` | | Nonced Output Claim Handler | `0x3BfF76308e6DEaCaEfdF5a928d6a3082Ab55bf58` | --- # EVM & Starknet Contracts The EVM and Starknet contract deployments share a **modular architecture** — the protocol logic is decomposed into small, composable contract modules that can be combined and upgraded independently. The same architectural design is implemented on both platforms, with each module fulfilling an identical role. ![EVM \& Starknet smart contract architecture](/assets/images/EVM-starknet-smart-contracts-c7af7cee72ac09684301a1abd32504b4.svg) ## BTC Relay (Bitcoin Light Client) [EVM → contracts/btc\_relay](https://github.com/atomiqlabs/atomiq-contracts-evm/tree/main/contracts/btc_relay) | [Starknet → packages/btc\_relay](https://github.com/atomiqlabs/atomiq-contracts-starknet/tree/main/packages/btc_relay) A permissionless, trustless Bitcoin SPV light client deployed on-chain. It verifies and stores Bitcoin block headers using proof-of-work validation, serving as a **trustless oracle of Bitcoin state**. Anyone can submit block headers — the contract verifies their validity regardless of who submits them. The relay validates consensus rules (PoW difficulty, difficulty adjustments, previous block hash, timestamps) and handles forks automatically by adopting the chain with the greatest cumulative chainwork. Block header data is stored efficiently — on EVM as calldata with only a hash fingerprint on-chain, and on Starknet as events with Poseidon hash fingerprints in storage. Bitcoin relay contract is used for all Bitcoin on-chain claim handlers (PrTLCs) and the UTXO-controlled vault contract. ## Escrow Manager [EVM → contracts/escrow\_manager](https://github.com/atomiqlabs/atomiq-contracts-evm/tree/main/contracts/escrow_manager) | [Starknet → packages/escrow\_manager](https://github.com/atomiqlabs/atomiq-contracts-starknet/tree/main/packages/escrow_manager) The central contract for escrow based swaps (HTLCs & PrTLCs). It handles initialization, claiming, refunding, and cooperative closing of escrows. Each escrow specifies a claim handler contract address with committed claim data and a refund handler contract address with committed refund data. All [claim handlers](#claim-handlers) implement a common `IClaimHandler` interface and all [refund handlers](#refund-handlers) implement `IRefundHandler`. These are then used as a predicate to verify validity of the claim and refund. Concretely, the caller calls the escrow claim function with the `witness` (a data to satisfy the claim condition), internally the escrow manager calls a claim handler's `claim` function with the: a) claim data committed during escrow initialization & b) caller provided `witness`. The claim handler than asserts the validity of the `witness` - the can range from a simple hashing (for hashlocks), to parsing a bitcoin transaction and verifying it through the BTC Relay contract. ### Reputation Tracking The escrow contract records swap outcomes (success, failed, cooperative refund) per LP per token, enabling users to identify reliable liquidity providers. ### LP vault To prevent LP always having to deposit funds into the contract (and incurring the ERC20 transfer gas cost) the contract allows the LP to hold balance inside the contract—the contracts tracks this balance in a single mapping. This way when an escrow is created using LP's funds no ERC20 transfer occurs and funds are instead taken from the internal balance of the LP. ## Claim Handlers Claim handlers are used by the Escrow Manager contract to verify whether a condition required to pay out funds from the escrow has been met. ### Hashlock Claim Handler [EVM → contracts/hashlock\_claim\_handler](https://github.com/atomiqlabs/atomiq-contracts-evm/tree/main/contracts/hashlock_claim_handler) | [Starknet → packages/hashlock\_claim\_handler](https://github.com/atomiqlabs/atomiq-contracts-starknet/tree/main/packages/hashlock_claim_handler) Validates claims based on knowledge of a SHA-256 preimage, asserts that the hash of the `witness` equals to the committed claim data. Used for **Lightning Network swaps** (both directions), where the preimage links the on-chain escrow to the off-chain Lightning payment. ### Bitcoin Output Claim Handler [EVM → contracts/btc\_output\_claim\_handler](https://github.com/atomiqlabs/atomiq-contracts-evm/tree/main/contracts/btc_output_claim_handler) | [Starknet → packages/btc\_output\_claim\_handler](https://github.com/atomiqlabs/atomiq-contracts-starknet/tree/main/packages/btc_output_claim_handler) Verifies that a specific Bitcoin transaction output exists and is confirmed on the canonical chain. The `witness` contains the full bitcoin transaction, the handler parses it, verifies the transaction's output script & amount matches the committed claim data, and verifies transaction's inclusion Merkle proof via the BTC relay. Used for **Legacy Bitcoin on-chain → Smart chain** swaps. ### Bitcoin Nonced Output Claim Handler [EVM → contracts/btc\_nonced\_output\_claim\_handler](https://github.com/atomiqlabs/atomiq-contracts-evm/tree/main/contracts/btc_nonced_output_claim_handler) | [Starknet → packages/btc\_nonced\_output\_claim\_handler](https://github.com/atomiqlabs/atomiq-contracts-starknet/tree/main/packages/btc_nonced_output_claim_handler) Same as the output claim handler, but additionally verifies a nonce encoded in the Bitcoin transaction's `locktime` and `nSequence` fields. The nonce uniquely identifies each swap and prevents replay attacks. Used for **Smart chain → Bitcoin on-chain** swaps. ### Bitcoin TxID Claim Handler [EVM → contracts/btc\_txid\_claim\_handler](https://github.com/atomiqlabs/atomiq-contracts-evm/tree/main/contracts/btc_txid_claim_handler) | [Starknet → packages/btc\_txid\_claim\_handler](https://github.com/atomiqlabs/atomiq-contracts-starknet/tree/main/packages/btc_txid_claim_handler) Verifies that a specific Bitcoin transaction ID is confirmed on the canonical chain. Currently unused — reserved for future swap types such as Ordinals, Runes, or RGB. ## Refund Handlers Similar to claim handlers, the refund handlers are used by the Escrow Manager to determine whether an escrow can be refunded and funds returned to the offerer. ### Timelock Refund Handler [EVM → contracts/timelock\_refund\_handler](https://github.com/atomiqlabs/atomiq-contracts-evm/tree/main/contracts/timelock_refund_handler) | [Starknet → packages/timelock\_refund\_handler](https://github.com/atomiqlabs/atomiq-contracts-starknet/tree/main/packages/timelock_refund_handler) Validates refunds after a specified expiry timestamp. This is the universal refund handler used by **all current swap types** — it ensures the offerer can reclaim funds if the counterparty fails to claim within the timeout period. ## SPV Swap Vault [EVM → contracts/spv\_swap\_vault](https://github.com/atomiqlabs/atomiq-contracts-evm/tree/main/contracts/spv_swap_vault) | [Starknet → packages/spv\_swap\_vault](https://github.com/atomiqlabs/atomiq-contracts-starknet/tree/main/packages/spv_swap_vault) Implements the [UTXO-controlled vault](https://docs.atomiq.exchange/overview/core-primitives/utxo-controlled-vault.md) primitive for **Bitcoin on-chain → Smart chain** swaps. The vault holds LP liquidity on the smart chain, and withdrawals are authorized solely by verified Bitcoin transactions using the BTC relay. The vault tracks a specific Bitcoin UTXO as its ownership reference. Each withdrawal must spend the current UTXO and produce a new one, enforcing a strict linear sequence of operations via Bitcoin's consensus rules. Withdrawal parameters (recipient, amounts, fees) are encoded in the Bitcoin transaction's `OP_RETURN` output and `nSequence` fields. The vault supports **liquidity fronting** (third parties can front funds before Bitcoin confirmation for a fee) and **caller fees** (watchtower incentives for submitting proofs). ## Execution Contract [EVM → contracts/execution\_contract](https://github.com/atomiqlabs/atomiq-contracts-evm/tree/main/contracts/execution_contract) | [Starknet → packages/execution\_contract](https://github.com/atomiqlabs/atomiq-contracts-starknet/tree/main/packages/execution_contract) Allows scheduling and executing arbitrary smart contract calls as part of a swap. When a swap includes an execution action, the claimed funds are transferred to the execution contract instead of directly to the recipient. Any third party can then execute the scheduled action and earn an execution fee. An execution action contains only a hash of the actual to-be-executed contract calls. Hence, the contract calls have to be kept and transferred off-chain to the watchtower network, which then provide these contract calls to the execution contract when executing the action. This enables use cases like swapping Bitcoin directly into a DeFi position — e.g., the swap output can be routed into an AMM swap or a lending deposit in a single flow. If the execution fails or the execution action data is lost, funds can always be refunded by the original recipient and anyone can refund after an expiry period. ## Utilities ### Bitcoin Utilities [EVM → contracts/btc\_utils](https://github.com/atomiqlabs/atomiq-contracts-evm/tree/main/contracts/btc_utils) | [Starknet → packages/btc\_utils](https://github.com/atomiqlabs/atomiq-contracts-starknet/tree/main/packages/btc_utils) Low-level Bitcoin protocol utilities used by claim handlers and the SPV vault: transaction parsing, Merkle proof verification, endianness conversion, and compact size encoding. ### Token Utilities [EVM → contracts/transfer\_utils](https://github.com/atomiqlabs/atomiq-contracts-evm/tree/main/contracts/transfer_utils) | [Starknet → packages/erc20\_utils](https://github.com/atomiqlabs/atomiq-contracts-starknet/tree/main/packages/erc20_utils) Unified interface for ERC20 and native token transfers, used by the escrow manager, SPV vault, and execution contract. ## Swap Type Overview Different swap directions use different combinations of claim and refund handlers with the Escrow Manager contract. The only exception being the **Bitcoin on-chain → Smart chain** swap direction, which uses the UTXO-controlled vault primitive implemented in the SPV Swap Vault contract. Swaps using Escrow Manager contract: | Swap Direction | Claim Handler | Refund Handler | | ----------------------------------------- | ------------- | -------------- | | Lightning → Smart chain | Hashlock | Timelock | | Smart chain → Lightning | Hashlock | Timelock | | *Legacy Bitcoin on-chain → Smart chain*\* | Output | Timelock | | Smart chain → Bitcoin on-chain | Nonced Output | Timelock | info \* *Legacy Bitcoin on-chain → Smart chain* swaps have been replaced with the new Bitcoin on-chain → Smart chain swap protocol built on the UTXO-controlled vault primitive, these swaps are handled by the SPV Swap Vault contract. --- # Solana Contracts The Solana deployment uses a **monolithic architecture**, where the protocol is implemented as two standalone Anchor programs — a swap program and a BTC relay program. Unlike the [modular EVM & Starknet contracts](https://docs.atomiq.exchange/overview/contracts/evm-starknet/.md), all swap logic ([HTLC](https://docs.atomiq.exchange/overview/core-primitives/htlc.md), [PrTLC](https://docs.atomiq.exchange/overview/core-primitives/prtlc.md), and transaction verification) lives within a single swap program rather than being split into separate handler contracts. The [UTXO-controlled vault](https://docs.atomiq.exchange/overview/core-primitives/utxo-controlled-vault.md) primitive is not implemented on Solana, meaning Solana can only process legacy swaps in the Bitcoin → Solana direction. This is the original contract design, built as the first implementation of the Atomiq protocol. ## BTC Relay (Bitcoin Light Client) **Source code**: [atomiq-contracts-solana/btcrelay](https://github.com/atomiqlabs/atomiq-contracts-solana/tree/main/btcrelay) A permissionless, trustless Bitcoin SPV light client deployed on-chain. It verifies and stores Bitcoin block headers using proof-of-work validation, serving as a **trustless oracle of Bitcoin state**. Anyone can submit block headers — the contract verifies their validity regardless of who submits them. The relay validates consensus rules (PoW difficulty, difficulty adjustments, previous block hash, timestamps) and handles forks automatically by adopting the chain with the greatest cumulative chainwork. Block headers are stored in a **ring buffer of 250 blocks** (meaning only the last 250 blocks can be used for verification). The relay stores only a single 32-byte SHA-256 commitment hash per block — full header data is emitted as a program event. Bitcoin relay program is used for verifying PrTLC claim conditions (bitcoin block inclusion merkle proofs) by the swap program. ### Fork Handling Short forks (< 6 bitcoin blocks) are resolved in a single transaction. Longer forks use a separate `ForkState` PDA that accumulates headers until the fork overtakes the main chain. ## Swap Program **Source code**: [atomiq-contracts-solana/swaps](https://github.com/atomiqlabs/atomiq-contracts-solana/tree/main/swaps) Handles all swap execution — initialization, claiming, refunding, and cooperative closing. Unlike the modular EVM/Starknet approach where claim/refund logic is in separate handler contracts, the swap program contains all verification logic internally and dispatches based on swap type. ### BTC Relay Integration The swap program interacts with the BTC Relay to verify Bitcoin transaction proofs for PrTLC based swaps via Solana's instruction introspection rather than traditional CPI calls: 1. The BTC relay's `verify_transaction` instruction is included as a **prior instruction** in the same transaction as the swap claim 2. The swap program reads and validates the prior instruction's data and accounts to confirm the Bitcoin transaction was verified This pattern avoids CPI overhead while maintaining the verification chain ``` Solana transaction for PrTLC swap claim (providing an on-chain Bitcoin transaction and merkle proof): Instruction 0: BTC Relay → verify_transaction(txid, merkle_proof, ...) Instruction 1: Swap Program → claimer_claim(secret) └─ reads instruction 0 to confirm BTC relay verification ``` ### Swap Types The program supports four swap types, each with its own claim verification path: | Type | Name | Used For | | ---- | --------------- | ------------------------------------------ | | 0 | **HTLC** | Lightning Network swaps in both directions | | 1 | **Chain** | Bitcoin → Solana | | 2 | **ChainNonced** | Solana → Bitcoin | | 3 | **ChainTxhash** | Nothing (Future extension) | #### Claim Verification 1. **HTLC (Lightning)**: The claimer provides a secret. The program hashes it with SHA-256 and compares against the stored payment hash. For BTC Relay based swap types, the program checks that a prior `verify_transaction` instruction from the BTC relay program exists in the same transaction — confirming the Bitcoin transaction (identified by its transaction hash) is included in a confirmed block. 1. **Chain (Bitcoin → Solana)**: The claimer provides the full Bitcoin transaction data. The program parses it, verifies the transaction's output script & amount matches the payment hash (i.e. sha256(*scriptPubKey*, *amountSats*) = *paymentHash*), then computes the transaction hash to be checked via the BTC Relay 2. **ChainNonced (Solana → Bitcoin)**: The claimer provides the full Bitcoin transaction data. The program parses it, verifies the transaction's output script, amount and nonce (extracted from the Bitcoin transaction's `locktime` and `nSequence`) matches the payment hash (i.e. sha256(*scriptPubKey*, *amountSats*, *nonce*) = *paymentHash*), then computes the transaction hash to be checked via the BTC Relay 3. **ChainTxhash**: Payment hash is an explicit Bitcoin transaction hash that is verified through the BTC relay to be included in a confirmed block ### Refund Verification Swap escrow refunds are triggered in two ways: * **Timeout**: the offerer is able to refund unilaterally after the current timestamp exceeds the escrow's expiry, or the Bitcoin blockheight exceeds a threshold (verified via BTC Relay) * **Cooperative close**: the claimer signs a refund authorization (verified via ED25519 signature introspection) allowing the offerer to refund before the timeout. ### Reputation Tracking The program tracks LP reputation in a `UserAccount` PDA per LP per token, recording success/failure/cooperative-close counts and volumes for each swap type. ## Comparison with EVM & Starknet | Aspect | Solana | EVM & Starknet | | --------------------- | ------------------------------------ | ------------------------------------ | | Architecture | Monolithic — single swap program | Modular — separate handler contracts | | Claim logic | Internal dispatch by swap type | External claim handler contracts | | Refund logic | Internal timeout + cooperative close | External refund handler contracts | | Adding new swap types | Requires program upgrade | Deploy a new handler contract | | Bitcoin verification | Instruction introspection | Direct contract calls | | UTXO-controlled vault | *Not implemented* | Separate SPV Swap Vault contract | --- # Core Primitives These pages cover the low-level mechanisms that make Atomiq's trustless swap protocol possible. They define how Bitcoin state is verified on-chain, how funds are escrowed across chains, and how settlement can be enforced without relying on a trusted intermediary. This section introduces these foundational primitives before moving into higher-level swap flows and contract architecture ## [📄️Hash-time locked contract (HTLC)](https://docs.atomiq.exchange/overview/core-primitives/htlc.md) [Established HTLC primitive used for Lightning Network swaps.](https://docs.atomiq.exchange/overview/core-primitives/htlc.md) ## [📄️Bitcoin Light Client](https://docs.atomiq.exchange/overview/core-primitives/bitcoin-light-client.md) [Verifies Bitcoin block headers on the smart chain.](https://docs.atomiq.exchange/overview/core-primitives/bitcoin-light-client.md) ## [📄️Proof-time locked contract (PrTLC)](https://docs.atomiq.exchange/overview/core-primitives/prtlc.md) [Light client-based HTLC-like escrow used for Bitcoin on-chain swaps.](https://docs.atomiq.exchange/overview/core-primitives/prtlc.md) ## [📄️UTXO-controlled Vault](https://docs.atomiq.exchange/overview/core-primitives/utxo-controlled-vault.md) [Enables PSBT-based swaps between Bitcoin and Smart chains.](https://docs.atomiq.exchange/overview/core-primitives/utxo-controlled-vault.md) --- # Bitcoin light client (on-chain) The Bitcoin light client (also called **bitcoin relay**) is a smart contract deployed on smart chains (Solana, Starknet, EVM, etc.) that verifies and stores Bitcoin block headers using the proof-of-work consensus algorithm. It acts as a **permissionless & trustless oracle** of Bitcoin's state — anyone can submit block headers and their validity is verified entirely on-chain. No trusted third party, signer set, or attestation is required. This contract is the cryptographic anchor of the Atomiq protocol. It enables swap contracts to verify that a specific Bitcoin transaction was confirmed, which is the foundation for both [PrTLCs](https://docs.atomiq.exchange/overview/core-primitives/prtlc.md) (proof-time locked contracts) and [UTXO-controlled vaults](https://docs.atomiq.exchange/overview/core-primitives/utxo-controlled-vault.md) — the two primitives upon which all Bitcoin on-chain swaps are built. ## What is a light client? Light clients are blockchain clients that don't verify full blocks by re-executing transactions. Instead, they only verify the **consensus mechanism** of the blockchain network (in Bitcoin's case, proof-of-work). They trust the consensus of the network to attest to the validity of the transactions included in the blockchain. This approach was first proposed in the Bitcoin whitepaper under the name *simplified payment verification (SPV)*. Light clients only need to store the relatively small block headers (\~80 bytes each, see [Bitcoin block header structure](https://developer.bitcoin.org/reference/block_chain.html#block-headers) for details), yet can verify any transaction's inclusion through a short Merkle proof. Each block contains a **merkle root**, that is the key field for bitcoin transaction verification. It commits (using a merkle tree) to every transaction in the block, enabling efficient proofs of transaction inclusion via merkle proofs. The computational cost of verifying a Bitcoin block header is minimal — just 2 SHA256 invocations, some bitshifting, and comparisons. This makes them well-suited for running inside a smart contract on another chain, where storage and computation are expensive. The light client needs to verify on average 144 block headers per day (one every \~10 minutes). ## Proof-of-work verification To verify Bitcoin's consensus, the light client runs the following checks on each submitted block header: * **Proof-of-Work difficulty target** — the `nBits` field matches the previous block's difficulty, or is adjusted correctly according to the difficulty adjustment algorithm (every 2016 blocks) * **Link to previous block** — the `prev_block_hash` equals the hash of the current chain tip (the latest stored block header) * **Proof-of-work** — the block header's double-SHA256 hash is lower than the difficulty target defined by the `nBits` field * **Timestamp bounds** — the timestamp is strictly larger than the median of the last 11 block headers and at most 2 hours ahead of the current system time If all checks pass, the block header is accepted and stored, extending the verified chain. ## Handling forks Blockchain forks naturally occur when multiple miners find a block at roughly the same time. The Bitcoin network uses the **highest chainwork** fork selection rule — the chain with the greatest cumulative proof-of-work wins and becomes the canonical chain. The on-chain light client replicates this behavior. When a fork is submitted, the contract compares the total chainwork (sum of difficulty across all blocks) of each fork and accepts the one with the highest cumulative chainwork as the main canonical chain. For edge cases where the fork length exceeds the smart chain (Solana, Starknet, EVM) transaction size limit, the relay contract supports piece-by-piece fork submission across multiple transactions. The submitted blocks are merged into the canonical chain once the fork's chainwork exceeds that of the current main chain. ## Relayers Since smart contracts cannot access Bitcoin's P2P network directly, the light client relies on **relayers** to submit the latest block headers. The role of a relayer is permissionless — anyone can submit block headers (including the LP and the user) as the contract verifies their validity regardless of who submits them. Since submitting a block header incurs a transaction fee on the smart chain, relayers must be economically motivated to do so. In Atomiq, any swap participant has a vested economic interest in keeping the light client synchronized — [LPs](https://docs.atomiq.exchange/overview/actors.md#liquidity-provider-lp) need it to unlock capital, [Watchtowers](https://docs.atomiq.exchange/overview/actors.md#watchtower) earn fees for settling swaps, and [Users](https://docs.atomiq.exchange/overview/actors.md#user) can always submit headers themselves to self-settle. ## Implementations * [Solana bitcoin light client program](https://github.com/atomiqlabs/atomiq-contracts-solana/tree/main/btcrelay) * [Starknet bitcoin light client contract](https://github.com/atomiqlabs/atomiq-contracts-starknet/tree/main/packages/btc_relay) * [EVM bitcoin light client contract](https://github.com/atomiqlabs/atomiq-contracts-evm/tree/main/contracts/btc_relay) --- # HTLC (hash-time locked contract) An HTLC is an escrow smart contract between two parties that uses a cryptographic hash to link transactions across independent blockchains. The swap protocol uses a **secret** and its **hash** (where the hash is calculated ***sha256(secret)***) to create an all-or-nothing outcome: either both parties successfully exchange assets, or both safely refund through time-bound recovery paths. HTLCs are the foundational primitive behind traditional atomic swaps and the Lightning Network. While Atomiq uses more advanced primitives ([PrTLCs](https://docs.atomiq.exchange/overview/core-primitives/prtlc.md) and [UTXO-controlled vaults](https://docs.atomiq.exchange/overview/core-primitives/utxo-controlled-vault.md)) for its Bitcoin on-chain swap protocol, the HTLC submarine swaps are still used for [Lightning Network swaps](https://docs.atomiq.exchange/overview/swaps/.md#bitcoin-lightning-l2-swaps). ## Mechanism An HTLC is created between an **offerer**—funding the escrow, and a **claimer**—recipient of the escrowed funds, with two parameters: * A **hashlock** — specified by the hash ***P***, ensures that funds can be claimed by the **claimer** only when they reveal a **secret** on-chain, such that it hashes the to specified hashlock: ***P***=sha256(***secret***). Once the ***secret*** is revealed on one chain, the counterparty can observe it and use it to claim on the other chain. This is the happy path — the swap completes successfully on both sides. * A **timelock** — specified by the timeout ***T***, ensures that the **offerer** can reclaim funds after the **timelock** expires: ***T***>*CurrentTime*. This is the sad path — used when the counterparty fails to cooperate — and ensures that funds are never permanently locked. The escrow also allows the **offerer** to reclaim funds from the escrow using the cooperative refund path by providing a valid signature from the **claimer** that authorizes a premature cooperative refund without waiting for the timelock. ## Swap mechanism In Lightning Network swaps with smart chains (commonly referred to as submarine swaps), HTLCs bridge the on-chain smart contract environment with the off-chain Lightning layer. One side locks funds in a smart contract HTLC on the smart chain, while the counterparty creates a corresponding Lightning HTLC (typically as part of an invoice or routed payment). When the claimer reveals the secret to claim on one side, the revelation is immediately usable on the other side to complete the swap. *** While HTLCs are not an ideal construction for general cross-chain swaps due to their inherent limitations, pairing them with the Lightning Network at least partially mitigates the key drawbacks. Lightning’s fast settlement times—payments are typically confirmed in seconds—significantly reduce the user liveness requirement. Crucially, HTLCs achieve fully trustless and non-custodial execution: neither party needs to hand over control of funds to a third party, and the outcome is enforced purely by cryptography and on-chain consensus rules. --- # PrTLC (proof-time locked contract) A PrTLC is an escrow smart contract between two parties that uses a proof of a Bitcoin transaction—provided through the on-chain Bitcoin light client—to enable conditional release of funds on the smart chain (Solana, Starknet, EVM). The protocol replaces the hash-based condition of traditional [HTLCs](https://docs.atomiq.exchange/overview/core-primitives/htlc.md) with a proof-lock tied to a Bitcoin transaction confirming on the Bitcoin blockchain, creating an all-or-nothing outcome: either the counterparty sends the required Bitcoin transaction and proves it to claim the escrowed assets, or the funds are refunded after a timeout. PrTLCs are a novel primitive introduced by Atomiq to enable trustless [Smart chain → Bitcoin swaps](https://docs.atomiq.exchange/overview/swaps/sc-bitcoin.md), addressing key limitations of [HTLCs](https://docs.atomiq.exchange/overview/core-primitives/htlc.md) such as user liveness requirements and the free option problem. They leverage on-chain Bitcoin light client verification for secure cross-chain settlement. While PrTLCs are central to Atomiq's core protocol for this direction, the reverse (Bitcoin → smart chain) primarily uses [UTXO-controlled vaults](https://docs.atomiq.exchange/overview/core-primitives/utxo-controlled-vault.md), with PrTLCs serving as a legacy option. This primitive is used for [Smart chain → Bitcoin swaps](https://docs.atomiq.exchange/overview/swaps/sc-bitcoin.md) and [legacy Bitcoin → Smart chain swaps](https://docs.atomiq.exchange/overview/swaps/bitcoind-sc-legacy.md). ## Mechanism A PrTLC is created between an **offerer**—who funds the escrow with the source chain assets, and a **claimer**—who is the intended recipient upon fulfilling the condition. It uses the following parameters: * A **proof-lock** ensures that funds can be claimed by the claimer only when they provide a proof that a Bitcoin transaction sending the agreed BTC amount to the **offerer**'s Bitcoin address has been confirmed. This proof consists of the raw transaction and a merkle proof of inclusion, the smart contract then checks the transaction and verifies that it has been confirmed in a Bitcoin block using the Bitcoin light client. Upon successful verification, the escrow releases the funds to the claimer. * A **timelock** ensures that the offerer can reclaim funds after the timeout expires if the proof-lock condition is not met. In this case, the offerer receives the escrowed amount as a refund. The escrow also supports a cooperative close path, where both parties can mutually agree to cancel early via signatures. In this scenario, the offerer reclaims the escrowed amount immediately, and any associated reward is returned without penalty. ## Transaction verification The escrow has to verify that a bitcoin transaction sends exact BTC amount to the specified address (identified by the bitcoin output script). The raw Bitcoin transaction data are submitted to the smart contract, which parses it, inspects its structure—verifying outputs, amounts, scripts, and other required conditions—before confirming its inclusion in a block via the Bitcoin light client. This enables precise, programmable validation directly in the contract. ## Swap mechanism In Atomiq swaps using PrTLCs, one party locks assets in the PrTLC escrow on the smart chain, while the counterparty must send a specific Bitcoin transaction (paying the agreed amount to the designated address) and prove its confirmation through the light client to claim the escrowed funds. If the Bitcoin transaction is not sent, confirmed and submitted before the timeout, the **offerer** can reclaim the locked assets. ## Limitations Using PrTLCs to swap in the Bitcoin → Smart chain direction, necessitate that the LPs pre-lock the swap funds in a PrTLC escrow before the user sends the Bitcoin transaction. If LPs were to do this "for free", it would open up a denial-of-service vector, where users could request the LP to pre-lock liquidity for many swaps and not execute any of them. The LP would end up with all the liquidity locked-up till the timeout, unable to process more swaps and earning nothing in return. The solution for PrTLC based [legacy Bitcoin → Smart chain swaps](https://docs.atomiq.exchange/overview/swaps/bitcoind-sc-legacy.md) was for the user to put up some deposit on the destination smart chain, that would be taken by the LP, should the user not execute the swap. This however introduces a "cold start" problem, where users have to hold funds on the destination chain prior to swapping, making on-boarding of new users impossible with such as system. Due to this limitation, the use of PrTLCs in the Bitcoin → Smart chain direction was discontinued (only Solana still uses this legacy approach) and PrTLCs were superseded by the [UTXO-controlled vaults](https://docs.atomiq.exchange/overview/core-primitives/utxo-controlled-vault.md) based swaps in the Bitcoin → Smart chain direction, which resolve the "cold start" problem and the need for the LPs to pre-lock capital. --- # UTXO-controlled vault A UTXO-controlled vault is a smart contract on the smart chain that holds LP liquidity and authorizes withdrawals solely based on verified Bitcoin transactions, using the on-chain [Bitcoin light client](https://docs.atomiq.exchange/overview/core-primitives/bitcoin-light-client.md) to confirm UTXO states and prevent front-running. By importing Bitcoin's deterministic UTXO ordering to the smart chain, the vault ensures a tamper-proof withdrawal queue, where each update spends the previous vault UTXO and creates a new one, chaining transactions in a linear, verifiable sequence enforced by Bitcoin consensus. UTXO-controlled vaults are a novel primitive introduced by Atomiq to enable trustless Bitcoin → smart chain swaps, overcoming limitations of using PrTLCs in the [legacy Bitcoin → Smart chain](https://docs.atomiq.exchange/overview/swaps/bitcoind-sc-legacy.md) direction, such as the "cold start" problem (users needing smart chain tokens upfront), timeout dependencies, and—most critically—the need for LPs to pre-lock liquidity into per-swap escrows. This primitive is central to Atomiq's core protocol for the [Bitcoin → smart chain](https://docs.atomiq.exchange/overview/swaps/bitcoin-sc-new.md) direction, complementing PrTLCs for the reverse flow. ## Intuition To resolve [the issue with the use of PrTLCs in Bitcoin → Smart chain](https://docs.atomiq.exchange/overview/core-primitives/prtlc.md#limitations) direction—having to pre-lock liquidity—the UTXO-controlled vaults were introduced. Core idea is to let the LP create a single vault from which multiple swaps could be executed—pre-lock once, execute multiple swaps. This removes the need to always pre-lock new liquidity for every swap. However a naive implementation of it would be completely insecure — i.e. if we allow the LP to withdraw funds from the vault, the LP can do so right after user intitiates a swap and already sends the bitcoin transaction, draining all the funds in the vault before the user could be made whole. Making the vault deposit-only, such that withdrawals can only be processed by sending BTC amount to the LP address (as verified by the light client), can slightly mitigate this vulnerability. However, it introduces additional issue on the bitcoin side — if the user sends the BTC amount now, what guarantee do they have that when their bitcoin transaction confirms the vault will still have enough balance to make them whole? This creates an inherent race-condition as anyone can front-run the user's bitcoin transaction by paying a higher fee and getting funds first, leaving no funds left for the user. UTXO-controlled vaults enforce the order of withdrawals and prevent race-conditions by assigning the vault a specific ownership UTXO, where every withdrawal authorized by the Bitcoin transaction needs to spend this ownership UTXO as one of its inputs and create a new ownership UTXO for the vault, that is to be used for subsequent withdrawals. This enforces a strict order of withdrawals as the ownership UTXO can only be spent once (i.e. only one withdrawal can be processed at a time), creating a new UTXO, which then the subsequent withdrawal uses. ## Mechanism The vault operates by tracking a specific Bitcoin UTXO as its ownership reference, ensuring that only transactions spending this UTXO can update its state and withdraw funds from the vault. To guarantee orderly withdrawals, the vault requires that each withdrawal transaction includes the current vault UTXO as an input. This makes double-spending impossible under Bitcoin rules, enforcing a single, linear chain of updates. Each valid transaction produces a new UTXO as its first output, which becomes the vault's next reference. The smart contract verifies this via the Bitcoin light client, checking the raw transaction, Merkle proof, and required confirmations before executing the encoded action (e.g., releasing funds to a user) and advancing the UTXO pointer. The vault specifies the number of Bitcoin confirmations needed for finality. Submitted transactions are parsed on-chain to validate structures, outputs, and data-carrier payloads before light client confirmation. LPs deposit assets into the vault on the smart chain, providing pooled liquidity for multiple swaps. Withdrawals are triggered by Bitcoin transactions carrying data instructions (e.g., "transfer X tokens to Y address"), verified on-chain. This decouples liquidity from individual swaps, allowing LPs to serve unlimited users without per-swap locks. The primitive requires no timeouts or cooperative refund paths, if the Bitcoin transaction is not confirmed (e.g., due to a double-spend or replacement in the mempool), the funds never leave the user's wallet, and no swap occurs on the smart chain side. ## Example ![Diagram showcasing updating of vault UTXO](/assets/images/utxo-vault-state-7955c81188d4f41d4ef37ca0e4bfb22e.svg) The diagram above illustrates how the UTXO-controlled vault maintains state synchronization between Bitcoin and the smart chain vault through UTXO chaining. Starting from the left, Vault **State 0** on the smart chain references ownership **UTXO U0** on Bitcoin with a balance of 10 WBTC. **Bitcoin TXA** consumes **UTXO U0** as input and produces **UTXO UA,0** as its first output, while including a data-carrier opcode specifying the action: *1 WBTC to Carol*. This transaction is verified by the light client on the smart chain, authorizing the action. As a result, the vault updates to **State 1**: ownership now references **UTXO UA,0**, and the balance reduces to 9 WBTC after releasing 1 WBTC to Carol. Next, **Bitcoin TXB** consumes **UTXO UA,0** as input and produces **UTXO UB,0** as output, with a data-carrier action: *2 WBTC to Dave*. Again, verified via the light client, this advances the vault to **State 2**: ownership shifts to UTXO **UTXO UB,0**, and the balance drops to 7 WBTC after the withdrawal to Dave. This chaining ensures only one valid update can occur at a time, as double-spending the current UTXO is impossible under Bitcoin rules. ![Diagram showing that double-spending of UTXO is impossible](/assets/images/utxo-chain-999511ac7c5be9e582aecc665cca3822.svg) ## Swap Mechanism In Atomiq swaps using UTXO-controlled vaults, the LP's vault holds the destination chain assets, while the user and LP collaboratively construct a Bitcoin transaction (via PSBT) that simultaneously transfers BTC from the user to the LP and encodes a withdrawal instruction for the user's assets from the vault. Once broadcast and confirmed on Bitcoin, the transaction is submitted to the smart chain vault for verification via the light client. If valid, the vault releases the funds atomically; if not (e.g., due to non-broadcast or double-spend), no funds move on either side. This achieves atomicity at the Bitcoin protocol level, with watchtowers able to submit proofs permissionlessly for fees, removing any liveness needs. ![Diagram showcasing a UTXO-controlled vault based Bitcoin → Smart chain swap](/assets/images/utxo-swap-diagram-340f1e0e6e19f994190c5c900ae4750e.svg) *** UTXO-controlled vaults bind smart chain state directly to Bitcoin UTXOs and consensus (via the light client) and deliver fully trustless and non-custodial execution: LPs retain efficient liquidity management, users face no upfront costs or risks, and outcomes are enforced by verifiable proofs and contract logic. --- # Protocol Overview Atomiq facilitates trustless cross-chain swaps through a combination of on-chain smart contract logic and off-chain coordination, ensuring atomic execution without intermediaries. Users initiate swaps by requesting quotes from a decentralized network of Liquidity Providers (LPs) via an off-chain request-for-quote (RFQ) system. This allows competitive pricing and zero slippage while keeping the actual settlement fully on-chain and trustless. ![RFQ based Swap Flow](/assets/images/rfq-7571d9a389aa5b528be5f0b650ef8e4b.svg) ## Swap Flow and Settlement Once a user accepts a quote, the swap proceeds with atomic guarantees enforced by protocol-specific primitives. For swaps from smart chains to Bitcoin, funds are escrowed in a smart contract vault on the smart chain side (Solana, Starknet, EVM). Settlement occurs only when the counterparty demonstrates successful delivery of Bitcoin, verified through the protocol's mechanisms. If cooperation fails at any point, escrowed funds are automatically refunded to the original owner after a predefined timeout period. ## Core Settlement Mechanisms The protocol relies on Bitcoin's proof-of-work consensus, verified directly on the smart chain via an on-chain [Bitcoin light client](https://docs.atomiq.exchange/overview/core-primitives/bitcoin-light-client.md). This light client serves as a permissionless oracle of bitcoin state, maintaining and validating Bitcoin block headers submitted by any participant. It enables secure verification of Bitcoin-side transactions on the smart chain without trusted intermediaries. Two novel primitives handle the primary swap directions and address longstanding limitations in traditional atomic swaps: * [Proof-time Locked Contracts (PrTLCs)](https://docs.atomiq.exchange/overview/core-primitives/prtlc.md) manage [Smart chain → Bitcoin swaps](https://docs.atomiq.exchange/overview/swaps/sc-bitcoin.md), replacing conventional HTLC hashlocks with bitcoin light client based transaction proofs. This eliminates the free option problem for liquidity providers and removes user liveness requirements, as anyone can permissionlessly settle the swap using light client transaction proofs. * [UTXO-controlled vaults](https://docs.atomiq.exchange/overview/core-primitives/utxo-controlled-vault.md) secure [Bitcoin → Smart chain swaps](https://docs.atomiq.exchange/overview/swaps/bitcoin-sc-new.md), enabling atomic execution through bitcoin light client based transaction proofs and UTXO-chaining. This enables swaps without LPs having to pre-lock liquidity on the Smart chain side, while eliminating the free option problem and the user liveness requirement (anyone can settle using light client transaction proofs). These primitives eliminate liveness dependencies for users—anyone, including specialized [watchtowers](https://docs.atomiq.exchange/overview/actors.md#watchtower), can permissionlessly settle swaps on behalf of participants while earning fees for doing so. ## Lightning Network Integration For swaps involving the Bitcoin Lightning Network, Atomiq incorporates established [Hash-time locked contracts (HTLC)](https://docs.atomiq.exchange/overview/core-primitives/htlc.md) based mechanisms, similar to submarine swaps to ensure atomicity in a similar trust-minimized manner. While HTLC swaps are not an ideal construction for cross-chain swaps, using it with the lightning network at least partially mitigates their drawbacks as the user livneness requirement is significantly reduced thanks to Lightning’s fast settlement times—payments are typically confirmed in seconds. *** This architecture delivers practical, everyday usability while preserving full non-custodial security: users retain control of their funds throughout, and settlement is enforced purely by code and verifiable blockchain consensus. --- # Swaps Swaps between Bitcoin and smart chains are inherently asymmetric — Bitcoin cannot execute smart contracts, so each direction requires a different approach. Smart chain → Bitcoin swaps use [PrTLCs](https://docs.atomiq.exchange/overview/core-primitives/prtlc.md) where the LP proves it sent BTC, while Bitcoin → Smart chain swaps use [UTXO-controlled vaults](https://docs.atomiq.exchange/overview/core-primitives/utxo-controlled-vault.md) where a single cooperatively-signed Bitcoin transaction atomically settles both sides. Lightning swaps follow a symmetric [HTLC](https://docs.atomiq.exchange/overview/core-primitives/htlc.md) pattern, linking Lightning payment secrets to smart chain contracts. info The protocol has evolved over time — the Bitcoin (on-chain and lightning) → Solana swaps use legacy protocol designs, while the other chains support newer swap protocols. ## Bitcoin On-chain (L1) swaps On-chain swaps interact directly with the Bitcoin base layer. They use the [Bitcoin light client](https://docs.atomiq.exchange/overview/core-primitives/bitcoin-light-client.md) to verify Bitcoin transactions on the smart chain, secured by [PrTLCs](https://docs.atomiq.exchange/overview/core-primitives/prtlc.md) and [UTXO-controlled vaults](https://docs.atomiq.exchange/overview/core-primitives/utxo-controlled-vault.md). | Swap | Primitive | Description | | ---------------------------------------------------------------------------------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Smart chain → Bitcoin](https://docs.atomiq.exchange/overview/swaps/sc-bitcoin.md) | PrTLC | User funds a PrTLC escrow and LP claims funds upon proving that it sent BTC to the user | | [Bitcoin → Smart chain](https://docs.atomiq.exchange/overview/swaps/bitcoin-sc-new.md) | UTXO-controlled vault | Single cooperatively signed bitcoin transaction between LP and user settles both sides of the swap | | [Bitcoin → Solana (legacy)](https://docs.atomiq.exchange/overview/swaps/bitcoind-sc-legacy.md) | PrTLC | Legacy protocol where the user initiates the creation of a PrTLC on the smart chain (by pulling the funds from the LP), sends BTC to LP's address and watchtower automatically settle the PrTLC after the Bitcoin transaction confirms | ## Bitcoin Lightning (L2) swaps Lightning swaps use the Lightning Network for faster, cheaper transfers. They leverage [HTLCs](https://docs.atomiq.exchange/overview/core-primitives/htlc.md) to atomically link Lightning payments with smart chain contract state. | Swap | Primitive | Description | | ------------------------------------------------------------------------------------------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Smart chain → Lightning](https://docs.atomiq.exchange/overview/swaps/sc-lightning.md) | HTLC | Escrow based swap, where LP requires the knowledge of the secret preimage obtained by paying a lightning network invoice | | [Lightning → Smart chain](https://docs.atomiq.exchange/overview/swaps/lightning-sc-new.md) | LP-initiated HTLC | User initiates the lightning network invoice payment, the LP funds an HTLC on the smart chain, user broadcasts the secret preimage over Nostr and watchtowers claim the HTLC on user's behalf while revealing the secret preimage on the smart chain | | [Lightning → Solana (legacy)](https://docs.atomiq.exchange/overview/swaps/lightning-sc-legacy.md) | User-inititated HTLC | Legacy protocol where the user initiates the creation of an HTLC on the smart chain (by pulling the funds from the LP) and has to claim the HTLC manually (no watchtowers are used) | --- # Bitcoin → Smart chains This is the current protocol for swapping on-chain Bitcoin to smart chain tokens. It uses the [UTXO-controlled vault](https://docs.atomiq.exchange/overview/core-primitives/utxo-controlled-vault.md) primitive, where the LP deposits liquidity into a vault on the smart chain whose withdrawals are controlled by Bitcoin transactions (verified through the [Bitcoin light client](https://docs.atomiq.exchange/overview/core-primitives/bitcoin-light-client.md)). The user and LP cooperatively sign a single Bitcoin transaction that atomically sends BTC to the LP and authorizes the smart chain payout to the user. Unlike the [legacy PrTLC-based approach](https://docs.atomiq.exchange/overview/swaps/bitcoind-sc-legacy.md), users don't need any smart chain balance upfront, the LP doesn't lock funds per-swap, and watchtowers are a UX convenience rather than a security requirement. The LP (liquidity provider) creates a UTXO-controlled vault on the smart chain, and uses a small (dust) UTXO that he owns as the initial UTXO. It's important to note that setting up the UTXO-controlled vault is not done on a per-swap basis and is instead done just once when the LP sets up their LP node. User and LP can then cooperatively construct and sign a single Bitcoin transaction (PSBT) that atomically (in a single transaction): * **Spends** the vault's current UTXO * **Sends** BTC from the user's inputs to the LP's wallet * **Encodes** the withdrawal data (recipient, amount, watchtower reward) in an `OP_RETURN` output Both parties sign with `SIGHASH_ALL`, committing to the full transaction — so it either confirms as-is (both sides get their assets) or doesn't confirm at all (nothing happens). This makes the swap atomic at the Bitcoin protocol level, without requiring any escrow contract or timelock. Once the transaction gets enough confirmations, anyone (a [watchtower](https://docs.atomiq.exchange/overview/actors.md#watchtower), the LP, or the user themselves) can submit the transaction data to the smart chain, where the vault verifies it through the light client and pays out the tokens to the user. Watchtowers earn a small reward for this service, but they are purely a UX convenience — the user can always claim independently. ![Diagram showcasing a UTXO-controlled vault based Bitcoin → Smart chain swap](/assets/images/utxo-swap-diagram-340f1e0e6e19f994190c5c900ae4750e.svg) ## Liquidity fronting In case users want to get their funds sooner than the confirmations required in the vault, they can specify a fronting fee/tip in the additional transaction data. The liquidity fronters can then process the swaps sooner (sending the assets to the user) - e.g. with the btc transaction having just 1 confirmation, and later reclaim the value they fronted (+ fronting fee) from the vault - e.g. when the bitcoin transaction gains 3 confirmations. info Liquidity fronting is not yet available on mainnet nor testnet ## Parties * **User** - has bitcoin and smart chain wallets and wants to swap bitcoin to smart chain asset * **LP** - handling the swap and providing the liquidity, needs to have a deposit only vault set up on the smart chain * **Watchtower** - submitting the bitcoin transaction data on the smart chain on behalf of the user * **Liquidity fronter** - fronting the liquidity for swaps ## Process 1. **User** sends an RFQ (request for quote) to the **LP** indicating the requested input (or output) amount of assets 2. **LP** constructs a PSBT (not signed yet) spending the latest vault UTXO, and adds 3 outputs: 1. Small (dust) output as a next vault UTXO 2. OP\_RETURN data specifying the output amount and recipient 3. Output to LP's wallet with the swap's input amount 3. **User** verifies the returned PSBT: 1. Validly spends latest vault UTXO and that UTXO is unspent 2. Specifies the correct (i.e. requested) output amount and recipient in the OP\_RETURN data 3. Sends the expected BTC amount to the LP's wallet 4. There is still enough balance in the vault on the smart chain to honor this swap 4. **User** then adds his own input UTXOs to fund the transaction (user is also expected to cover the transaction fee), and optionally also adds a change output 5. **User** signs this adjusted PSBT and sends it to the **LP** 6. **LP** re-verifies that the user hasn't changed any of the **LP** specified outputs and inputs 7. **LP** signs the transaction and broadcasts it to the bitcoin network #### a) With liquidity fronting > ...the transaction confirms on the bitcoin network, but doesn't get enough confirmations to be eligible for vault withdrawal yet (i.e. gets 1 confirmation) 8. A **Liquidity fronter** fronts the liquidity (minus the fronting fee) through the smart contract to the **User**, the smart contract saves this fact and later redirects the claim to the **Liquidity fronter** > ...the transaction now gets enough confirmations to be eligible for vault withdrawal (i.e. gets 3 confirmations) 9. A **Liquidity fronter** submits the bitcoin transaction data to smart chain, where it's verified and the output amount is paid out to the **Liquidity fronter** #### b) Without liquidity fronting > ...the transaction confirms on the bitcoin network and gets required number of confirmations 9. A **User**, **LP** or **Watchtower** submits the bitcoin transaction data to smart chain, where it's verified and the output amount is paid out from the vault to the **User**. Whoever submits the data gets the watchtower reward - if specified in the transaction data (i.e. can also be set to 0). ## Swap sequence diagram ![UTXO-controlled vault based Bitcoin → Smart chain flow diagram](/assets/images/frombtc-new-swap-flow-diagram-4e2305f9ca1010167f82c8e73074cab6.svg) --- # Bitcoin → Solana (legacy) info This is the legacy protocol only used on Solana, superseded by the [UTXO-controlled vault based Bitcoin → Smart chain swap](https://docs.atomiq.exchange/overview/swaps/bitcoin-sc-new.md) on all the other chains which eliminates the cold-start problem and watchtower dependency. This swap uses a [PrTLC](https://docs.atomiq.exchange/overview/core-primitives/prtlc.md) in the reverse direction: the LP locks tokens on the smart chain, and the user sends BTC on-chain. A watchtower (or the user) then proves the Bitcoin payment through the [Bitcoin light client](https://docs.atomiq.exchange/overview/core-primitives/bitcoin-light-client.md) and claims the smart chain tokens. Because the user must post a bond and needs an existing smart chain balance to initiate the swap, this approach suffers from the "cold start" problem — users cannot onboard without already holding smart chain tokens. ## Parties * **User** - has bitcoin and smart chain wallets and wants to swap bitcoin to smart chain asset, also needs to have funds on the smart chain to inititate the swap. * **LP** - handling the swap and providing the liquidity, locks-up smart chain tokens in the PrTLC escrow and receives a Bitcoin on-chain payment * **Watchtower** - submitting the bitcoin transaction data on the smart chain on behalf of the user, while earning the watchtower fee ## Process 1. **User** sends an RFQ (request for quote) to the **LP** indicating the requested input (or output) amount of assets 2. **LP** responds with the swap fees, bond amount (that the user needs to lockup on the smart chain side to guarantee the execution of the swap) and the signed authorization, allowing the **User** to create a PrTLC on the smart chain by pulling funds from the **LP**'s balance 3. **User** reviews the returned fee and sends a transaction creating the PrTLC on the Smart chain using the signed **LP** authorization to fund the PrTLC with **LP**'s balance, while the **User** also deposits the slashable bond and watchtower reward. The PrTLC has the following spend conditions: 1. Pays the funds to the **User**, upon verifying a Bitcoin transaction (via the light client) that sends the exact pre-agreed amount of BTC to the **LP**'s Bitcoin address, the slashable bond is returned back to the **User** and watchtower reward is paid to the caller (an entity that sent the transaction to settle the PrTLC) 2. Returns the funds back to the **LP** after a timeout, along with the slashable bond from the user - as this implies the user didn't execute the swap info The **LP** needs to make sure to not use the same Bitcoin address for multiple swaps, as this might lead to the **User** using the same transaction to claim funds from multiple PrTLC escrows. Therefore, the **LP** has to generate a new unique Bitcoin address for every swap (e.g. via Hierarchical Deterministic - HD wallets). ### a) Successful swap > ...the initiation transaction confirms on the smart chain 4. **User** sends the Bitcoin transaction, sending an exact amount of BTC to the **LP**'s bitcoin address > ...the transaction confirms on the bitcoin network and gets required number of confirmations 5. A **User** or **Watchtower** submits the Bitcoin transaction data to smart chain, where it's verified and the output amount along with the slashable bond is paid out from the PrTLC escrow to the **User**. Whoever submits the data gets the watchtower reward (initially deposited by the **User** on the smart chain) ### b) Failed swap 4. No payment arrived in **LP**'s bitcoin address until the timeout, therefore **LP** refunds its funds back from the PrTLC escrow, keeping the slashable bond originally deposited by the **User** ## Swap sequence diagram ![Legacy Bitcoin on-chain → Solana swap process](/assets/images/frombtc-diagram-865a22011ceec004826c23c17b3526f2.svg) --- # Lightning → Solana (legacy) info This is the legacy protocol only used on Solana, superseded by the [new Lightning → Smart chain swap](https://docs.atomiq.exchange/overview/swaps/lightning-sc-new.md) on all the other chains, which eliminates the need for users to hold smart chain tokens upfront. This swap uses an [HTLC](https://docs.atomiq.exchange/overview/core-primitives/htlc.md) to receive smart chain tokens via a Lightning payment. The user generates a secret preimage and payment hash, the LP creates a Lightning invoice with that hash, and the payer sends the Lightning payment. The user then locks up LP funds in an HTLC on the smart chain, and subsequently claims it by revealing the secret preimage — which also allows the LP to settle the Lightning payment. Unlike the current protocol, users must have native smart chain token balance in their destination wallet to pay for the initiate and claim transaction, creating a "cold start" barrier for new users. ## Parties * **User** - has bitcoin lightning network and smart chain wallets and wants to swap bitcoin (on Lightning) to smart chain asset, also needs to have funds on the smart chain to inititate the swap. * **LP** - handling the swap and providing the liquidity, locks-up smart chain tokens in the HTLC escrow and receives a lightning network payment ## Process 1. **User** generates a secret preimage and uses it to generate a payment hash—i.e. payment hash = sha256(secret) 2. **User** sends an RFQ (request for quote) to the **LP** indicating the requested input (or output) amount of assets, while sending over the payment hash 3. **LP** creates a BOLT11 bitcoin lightning invoice using the **User**-provided payment hash, with the amount specified, then returns it along with the swap fee and bond amount (that the user needs to lockup on the smart chain side to guarantee the execution of the swap after the HTLC is created) 4. **User** reviews the returned fees and checks that the returned lightning invoice uses the correct payment hash, then initiates a lightning network payment to the **LP** via the invoice 5. **LP** receives an incoming lightning network payment from the **User**, but cannot settle it because it doesn't know the secret preimage, yet 6. **User** queries the **LP** to obtain the signed authorization, allowing the **User** to create an HTLC on the smart chain (with the same payment hash) by pulling funds from the **LP**'s balance. 7. **User** sends a transaction creating the HTLC on the Smart chain using the signed **LP** authorization to fund the PrTLC with **LP**'s balance, while the **User** also deposits the slashable bond. The HTLC has the following spend conditions: 1. Pays the funds to the **User**, if it's able to provide a valid secret that, when hashed produces HTLC payment hash—i.e. payment hash = sha256(secret), the slashable bond is also returned back to the **User** 2. Returns the funds back to the **LP** after a timeout, along with the slashable bond from the user - as this implies the user didn't execute the swap info HTLC timeout is determined by the **LP** based on lightning invoice's *min\_cltv\_delta* field - the minimal timeout delta for last lightning network HTLC (last hop of the lightning network payment) as the **LP** needs to have a knowledge of the secret preimage before then to successfully settle the lightning network payment and claim the funds. ### a) Successful swap > ...the initiation transaction confirms on the smart chain 8. **User** submits a second smart chain transaction that claims the funds from the HTLC escrow by revealing the secret preimage, the slashable bond is also returned back to the **User** 9. **LP** observes this transaction on Solana and uses the revealed *secret S* to settle the lightning network payment. ### b) Failed swap > ...the HTLC was never claimed by User, hence no secret preimage was revealed 8. **LP** waits till the HTLC timeout and refunds its funds back from the HTLC escrow, keeping the slashable bond originally deposited by the **User** 9. **LP** cancels the incoming lightning network HTLC or lets it expire, this returns the lightning network funds back to the **User** ## Swap sequence diagram ![Legacy Bitcoin Lightning → Smart chain swap process](/assets/images/frombtcln-diagram-a15be9f43f2db997b4d0a270766a7452.svg) --- # Lightning → Smart chain This is the current protocol for receiving smart chain tokens via a Lightning payment. It uses an [HTLC](https://docs.atomiq.exchange/overview/core-primitives/htlc.md) created by the LP on the smart chain, solving the "cold start" problem of the [legacy approach](https://docs.atomiq.exchange/overview/swaps/lightning-sc-legacy.md) — users don't need any smart chain balance to receive funds. After the LP locks the HTLC, the user broadcasts the payment secret over [Nostr](https://github.com/nostr-protocol/nostr), and incentivized watchtowers claim the funds on the user's behalf, earning a small fee. The user can always self-claim as a fallback. The new swap protocol addresses the drawback of the legacy one, mainly around user UX and the "cold start" problem - inability to onboard users onto the smart chain without them holding smart chain tokens first. This is accomplished by letting the LP initiate the HTLC on the smart chain side and leveraging watchtowers to settle the HTLC. ## Parties * **User** - has bitcoin lightning network and smart chain wallets and wants to swap bitcoin (on Lightning) to smart chain asset, also needs to have funds on the smart chain to inititate the swap. * **LP** - handling the swap and providing the liquidity, locks-up smart chain tokens in the HTLC escrow and receives a lightning network payment * **Watchtower** - registers swaps, listens to swap secrets over **Nostr** and claims the swap on behalf of the user, while earning the watchtower reward ## Process 1. **User** generates a secret preimage and uses it to generate a payment hash—i.e. payment hash = sha256(secret) 2. **User** sends an RFQ (request for quote) to the **LP** indicating the requested input (or output) amount of assets and a watchtower reward (this will be funded by the **LP** and taken from the swap amount), while sending over the payment hash 3. **LP** creates a BOLT11 bitcoin lightning invoice using the **User**-provided payment hash, with the amount specified, then returns it along with the swap fee 4. **User** reviews the returned fees and checks that the returned lightning invoice uses the correct payment hash, then initiates a lightning network payment to the **LP** via the invoice 5. **LP** receives an incoming lightning network payment from the **User**, but cannot settle it because it doesn't know the secret preimage, yet 6. **LP** funds an HTLC on the Smart chain, the HTLC has the following spend conditions: 1. Pays the funds to the **User**, if it's able to provide a valid secret that, when hashed produces HTLC payment hash—i.e. payment hash = sha256(secret) 2. Returns the funds back to the **LP** after a timeout info HTLC timeout is determined by the **LP** based on lightning invoice's *min\_cltv\_delta* field - the minimal timeout delta for last lightning network HTLC (last hop of the lightning network payment) as the **LP** needs to have a knowledge of the secret preimage before then to successfully settle the lightning network payment and claim the funds. > ...the initiation transaction confirms on the smart chain 7. **Watchtower** observes the HTLC created on the Smart chain and registers it internally ### a) Successful swap 8. **User** verifies that the created HTLC on the smart chain fits the agreed-to conditions (amount, watchtower fees, etc.) and then broadcasts the secret preimage over **Nostr** 9. **Watchtower** will observe the **Nostr** message and submit an HTLC claim transaction on the smart chain, claiming the funds from the HTLC to the **User** and receiving the watchtower fee info If no watchtower claims on behalf of the user, the user can always claim by itself! 10. **LP** observes the transaction settling the HTLC on the Smart chain and uses the revealed secret preimage to settle the lightning network payment ### b) Failed swap > ...the HTLC was never claimed, hence no secret preimage was revealed 8. **LP** waits till the HTLC timeout and refunds its funds back from the HTLC escrow 9. **LP** lets the incoming lightning network HTLC expire, this returns the lightning network funds back to the **User** after a delay ## Swap sequence diagram ![Bitcoin Lightning → Smart chain swap process](/assets/images/nostr-frombtcln-diagram-92df87c917fa05367d84b2dc340d788e.svg) --- # Smart chain → Bitcoin This swap uses a [PrTLC (proof-time locked contract)](https://docs.atomiq.exchange/overview/core-primitives/prtlc.md) to enable trustless swaps from smart chain tokens to on-chain Bitcoin. The user locks tokens in a PrTLC on the smart chain, and the LP claims them by proving — through the [Bitcoin light client](https://docs.atomiq.exchange/overview/core-primitives/bitcoin-light-client.md) — that it sent the agreed BTC amount to the recipient's Bitcoin address. If the LP fails to deliver, the user can unilaterally refund after the timelock expires. A cooperative refund path also allows the LP to release the user's funds immediately if the payment cannot be completed, without waiting for the timeout. ## Parties * **User** - has smart chain and bitcoin wallets and wants to swap smart chain token (e.g. SOL on Solana, WBTC on Starknet) to bitcoin * **LP** - handling the swap and providing the liquidity, sends a Bitcoin on-chain payment and receives funds from the PrTLC escrow ## Process 1. **User** sends an RFQ (request for quote) to the **LP** indicating the requested input (or output) amount of assets, and the bitcoin address to receive the BTC at. Also sends a unique nonce that will be used to tag the bitcoin transaction and prevent replay attacks 2. **LP** returns the swap fee and Bitcoin network fee along with the signed authorization allowing the **User** to initiate the PrTLC escrow 3. **User** reviews the returned fees and sends a transaction to construct a PrTLC on the Smart chain. The PrTLC has the following spend conditions: 1. Pays the funds to the **LP**, upon verifying a Bitcoin transaction (via the light client) that sends the exact pre-agreed amount of BTC to the **User**'s bitcoin address and is tagged with the nonce provided by the **User** to prevent replay attacks 2. Returns the funds back to the **User** after a timeout 3. Returns the funds back to the **User**, upon verifying a refund authorization signed by the **LP**—this allows an instant co-operative refund info The **User** needs to make sure that the **LP** cannot use an already existing bitcoin transaction to claim funds from the PrTLC escrow—i.e. if the user already received 1 BTC to its address, and initiates the swap of 1 BTC, the LP could use the already existing Bitcoin transaction to claim the funds from the PrTLC escrow. To prevent this, it is required that each Bitcoin transaction is tagged with a random 7-byte nonce (provided by the **User**), where: * 3 least significant bytes prefixed with 0xFF are being used as the *nSequence* field for ALL transaction inputs * 4 most significant bytes being treated as integer, adding `500,000,000` to that integer and using it as *locktime* field for the transaction. The PrTLC escrow asserts that the Bitcoin transaction uses the pre-agreed to nonce by parsing the *nSequence* and *locktime* fields. ### a) Successful swap 4. **LP** observes the creation of PrTLC on the Smart chain and proceeds to send a Bitcoin transaction, paying out BTC funds to the **User** > ...the transaction confirms on the bitcoin network and gets required number of confirmations 5. **LP** submits the Bitcoin transaction data to smart chain, where it's verified and the escrowed funds are paid out to the **LP** ### b) Failed swap 4. **LP** is unable to send the Bitcoin transaction to the **User**—e.g. **LP** ran out of funds in the meantime 5. **User** sends a request to the **LP**, the **LP** returns a signed refund message, that allows the **User** to refund the PrTLC before a timeout (cooperative close) 6. **User** uses the signed refund message to refund its funds from the PrTLC before a timeout ### c) LP went offline > ...LP is unresponsive 4. **User** waits until the timeout and then refunds its funds back from the PrTLC ## Swap sequence diagram ![Smart chain → Bitcoin on-chain swap process](/assets/images/tobtc-diagram-c1d00c47a94479dc376461093a3998e7.svg) --- # Smart chain → Lightning This swap enables paying a Lightning Network invoice using smart chain (Solana, Starknet, EVM) tokens. It works by linking a Lightning payment to an on-chain [HTLC](https://docs.atomiq.exchange/overview/core-primitives/htlc.md) escrow: the user locks tokens in an HTLC on the smart chain using the Lightning invoice's payment hash, and the LP attempts to route and pay the Lightning payment. When the recipient settles the invoice they reveal the hash preimage (secret), which the LP then uses to claim the funds from the HTLC escrow. If the payment fails, the LP can cooperatively refund the user immediately, or the user can self-refund after the timelock expires. ## Requirements * Lightning network invoice (BOLT11) has to have a fixed amount * Lightning network payment recipient needs to be online at the time of payment ## Parties * **User** - has smart chain wallet and wants to swap smart chain token (e.g. SOL on Solana, WBTC on Starknet) to lightning * **LP** - handling the swap and providing the liquidity, sends the lightning network payment and receives funds from the HTLC escrow * **Recipient** - recipient of the lightning network payment, can be the **User**, but also any other party which the **User** wishes to pay over the lightning network, only needs to provide a standard BOLT11 lightning network invoice ## Process 1. **Recipient** creates a standard lightning network invoice with fixed amount—generated a secret preimage and used its hash to generate the BOLT11 invoice 2. **User** sends an RFQ (request for quote) to the **LP** indicating the to be paid lightning network invoice 3. **LP** checks whether the lightning network invoice is payable by probing its destination and returns the swap fee and lightning network routing fee along with the signed authorization allowing the **User** to initiate the HTLC escrow on the smart chain 4. **User** reviews the returned fees and sends a transaction to construct an HTLC on the Smart chain with the same payment hash as the lightning network invoice. The HTLC has the following spend conditions: 1. Pays the funds to the **LP**, if it's able to provide a valid secret that, when hashed produces HTLC payment hash—i.e. payment hash = sha256(secret) 2. Returns the funds back to the **User** after a timeout 3. Returns the funds back to the **User**, upon verifying a refund authorization signed by the **LP**—this allows an instant co-operative refund info HTLC timeout is determined by the **User** and is a trade-off between likelihood of payment being successful and locking the funds for shorter periods in case the **LP** becomes unresponsive. * Larger timeout means more payment paths can be considered (more hops on the lightning network), increases the likelihood of payment being successfully routed, but will also lead to longer lock periods when **LP** is unresponsive * Smaller timeout means less payment paths can be considered (less hops on the lightning network) and increases the likelihood of payment routing failures, but will also lead to shorter lock periods when **LP** is unresponsive 5. **LP** observes the creation of the HTLC on the Smart chain and proceeds to attempt a payment of the lightning network invoice ### a) Successful swap 6. **Recipient** receives the lightning network payment and settles it by revealing the secret preimage 7. **LP** claims the escrowed funds from the HTLC using the secret preimage ### b) Failed swap 6. The payment was unsuccessful—**Recipient** did not reveal the secret preimage 7. **User** sends a request to the **LP**, the **LP** returns a signed refund message, that allows the **User** to refund the HTLC before a timeout (cooperative close) 8. **User** uses the signed refund message to refund its funds from the HTLC before a timeout ### c) LP went offline 6. **User** waits until the timeout and then refunds its funds back from the HTLC ## Swap sequence diagram ![Smart chain→ Bitcoin Lightning swap process](/assets/images/tobtcln-diagram-5bbd484f9227208667ba496ecbc4498c.svg) --- # REST API Guide The **Atomiq REST API** is an HTTP interface for the Atomiq cross-chain DEX, covering trustless swaps between **Bitcoin / Lightning** and smart chains (**Starknet, Solana, Botanix, Citrea, Alpen, Goat**). You can either self-host the REST API on your own infrastructure using Docker (refer to **[Run REST API Locally](https://docs.atomiq.exchange/rest-api-guide/run-locally/.md)**), or use the public API provided by Atomiq: * Mainnet: `https://mainnet.swaps-api.atomiq.exchange/` * Testnet4: `https://testnet4.swaps-api.atomiq.exchange/` warning By using the REST API you fully trust the backend to correctly validate swap data and serve properly constructed transactions to the clients. A compromised backend might drain your user's funds. Prefer the **[Atomiq SDK](https://docs.atomiq.exchange/sdk-guide/.md)**, which verifies everything locally and doesn't rely on trusted external APIs. Use the REST API only when you cannot use the SDK: non-JS runtimes or environments without local persistence. The API is **non-custodial**: it never holds user keys. All signing happens in the client wallet — the API builds unsigned transactions and submits signed ones. tip * Looking for exact request / response shapes? → **[REST API Reference](https://docs.atomiq.exchange/rest-api-reference/atomiq-rest-api)** * **Machine-readable OpenAPI 3.1 spec:** [`/rest-api-reference/openapi.json`](https://docs.atomiq.exchange/assets/files/openapi-ebfbede72ebedbfda4880a81d0cadbd6.json) — for code generators, AI agents, and tooling. ## Conventions used throughout * **Base URL** — examples use `https://mainnet.swaps-api.atomiq.exchange` (the hosted public API). Replace with `https://testnet4.swaps-api.atomiq.exchange` for testnet, or with `http://localhost:3000` when running the self-hosted container. * **`GET` vs `POST`** — `GET` endpoints read parameters from the query string, `POST` endpoints from a JSON body. The rest of this guide assumes you're familiar with the shared vocabulary described here. ### Token identifiers Tokens are identified by a single string in the form `-`. Typical values: | Token ID | Meaning | | ----------------------------------------------------------- | ---------------------------------- | | `BITCOIN-BTC` | On-chain Bitcoin | | `LIGHTNING-BTC` | Bitcoin over the Lightning Network | | `STARKNET-STRK`, `STARKNET-ETH`, `STARKNET-` | Starknet native and ERC-20 tokens | | `SOLANA-SOL`, `SOLANA-` | Solana native and SPL tokens | | `CITREA-CBTC`, `BOTANIX-BTC`, `ALPEN-BTC`, `GOAT-BTC` | Supported EVM chains | You don't need to hard-code this list. Use [`GET /getSupportedTokens`](https://docs.atomiq.exchange/rest-api-reference/get-supported-tokens) and [`GET /getSwapCounterTokens`](https://docs.atomiq.exchange/rest-api-reference/get-swap-counter-tokens) to enumerate what the current LP network supports — see [Quoting Swaps](https://docs.atomiq.exchange/rest-api-guide/quoting.md). ### Amounts and BigInt-as-string Numerical integer values (like TypeScript's `bigint`) appear in this API as [**BigIntString**](https://docs.atomiq.exchange/rest-api-reference/schemas/bigintstring) (e.g. `"150000"`, `"1500000000000000000"`), because JSON cannot safely encode arbitrary-precision integers as native numbers. info The SDK accepts human-readable values as strings and base unit values as `bigint`s, since the REST API cannot differentiate between these types (both would be string-serialized) only base unit amounts are supported by the API! Every monetary amount returned by the API uses the [`ApiAmount`](https://docs.atomiq.exchange/rest-api-reference/schemas/apiamount) shape: ``` { "amount": "0.00003", // decimal-formatted for display "rawAmount": "3000", // base units as a decimal string "decimals": 8, // token decimals "symbol": "BTC", // ticker "chain": "BITCOIN" // chain identifier } ``` When sending amounts to the API, for example when creating a swap with [`POST /createSwap`](https://docs.atomiq.exchange/rest-api-reference/create-swap), always pass the **raw base-unit string** — not a decimal, not a number. `"150000"` for 0.0015 BTC, not `0.0015`. ### Error shape All errors are JSON. For `4xx` responses the body is `{ "error": "" }`. Transient `5xx` errors should be retried with backoff. Rate-limit (`429`) responses may include additional fields depending on how the API is deployed — the self-hosted container returns `retryAfter` in seconds inside the body, while hosted deployments typically expose `Retry-After` only as a standard HTTP header. Treat both as advisory and back off accordingly. ## What this guide covers | Section | What you'll learn | | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **[Creating & Executing a Swap](https://docs.atomiq.exchange/rest-api-guide/creating-and-executing.md)** | Create a swap, poll `currentAction`, sign PSBTs or smart-chain transactions, submit signed payloads, render execution steps, and stop at terminal states. | | **[Quoting Swaps](https://docs.atomiq.exchange/rest-api-guide/quoting.md)** | Build the pre-swap form: list supported tokens, narrow compatible counter-tokens, estimate spendable balance, validate limits, and parse recipient addresses. | | **[Listing Swaps](https://docs.atomiq.exchange/rest-api-guide/listing-swaps.md)** | Restore pending swaps after restart, list signer history, resume polling from list results, and handle refunds. | | **[Bitcoin & Lightning Specifics](https://docs.atomiq.exchange/rest-api-guide/bitcoin-and-lightning.md)** | Handle Bitcoin PSBT variants, Lightning HTLC-based hashes and swap secret reveal, BOLT11 invoices, LNURLs, and gas drops. | | **[Run REST API Locally](https://docs.atomiq.exchange/rest-api-guide/run-locally/.md)** | Self-host `atomiq-api-docker` with Docker Compose and configure chains, LP connectivity, storage, rate limits, and server settings. | --- # Bitcoin & Lightning Most of the API looks the same across all supported chains. Bitcoin on-chain and Lightning are the exceptions — PSBTs, BOLT11 invoices, LNURL, and Lightning preimages are Bitcoin-world concepts the smart-chain side doesn't share. This page groups everything direction-specific in one place. If you're new to the flow, read [Creating & Executing a Swap](https://docs.atomiq.exchange/rest-api-guide/creating-and-executing.md) first — this page extends the patterns there. ## Bitcoin PSBTs When a swap needs a Bitcoin payment from the user's wallet (e.g. `BITCOIN-BTC → STARKNET-STRK`), [`GET /getSwapStatus`](https://docs.atomiq.exchange/rest-api-reference/get-swap-status) returns a `SignPSBT` action. The `txs` array always contains PSBTs of a single variant — either already-funded (`FUNDED_PSBT`) or raw (`RAW_PSBT`): ``` { "type": "SignPSBT", "name": "Deposit on Bitcoin", "chain": "BITCOIN", "description": "Sign the funding transaction to commit the deposit.", "txs": [ { "type": "FUNDED_PSBT", "psbtHex": "70736274ff01…", "psbtBase64": "cHNidP8B…", "signInputs": [0, 2], // which input indices to sign "feeRate": 3.5 // sats / vB, matches what's encoded in the PSBT } ] } ``` * **`FUNDED_PSBT`** — inputs are already selected and populated by the API. The client just signs the indices listed in `signInputs` and submits back. * **`RAW_PSBT`** — the client needs to add its own inputs (e.g. using its wallet's coin-selection) before signing. Carries an additional `in1sequence` field whose value must be preserved on the corresponding input; every other input can use default sequence. info The API needs the bitcoin wallet details to properly build `FUNDED_PSBT` variant of PSBTs, if not provided a `RAW_PSBT` will be returned and you have to run the coinselection on the client-side. Pass `bitcoinAddress` and `bitcoinPublicKey` (hex-encoded) on **every** [`GET /getSwapStatus`](https://docs.atomiq.exchange/rest-api-reference/get-swap-status) call. You can also optionally pass a `bitcoinFeeRate` parameter to set the bitcoin fee rate (sats/vB) that the transaction will use, if omitted the current economical fee rate is used. ``` curl "https://mainnet.swaps-api.atomiq.exchange/getSwapStatus?swapId= \ &bitcoinAddress=bc1q… \ &bitcoinPublicKey=02… \ &bitcoinFeeRate=10" ``` Submit the signed PSBT as hex or base64 via [`POST /submitTransaction`](https://docs.atomiq.exchange/rest-api-reference/submit-transaction): ``` { "swapId": "…", "signedTxs": ["70736274ff01…signed…"] } ``` ## Lightning Network Lightning network swaps allow you to specify an additional set of fields when creating the swap through [`POST /createSwap`](https://docs.atomiq.exchange/rest-api-reference/create-swap): | Field | Direction | Purpose | | --------------------------------- | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `paymentHash` | **Lightning → Smart chain** | Client-supplied payment hash so you retain the preimage. Hex string. See [Lightning → Smart chain swaps](#lightning--smart-chain-swaps). | | `lightningInvoiceDescription` | **Lightning → Smart chain** | Optional custom description to include in the generated BOLT11 invoice. | | `lightningInvoiceDescriptionHash` | **Lightning → Smart chain** | Optional description-hash to use in the generated BOLT11 invoice, hex-encoded. | | `lightningPaymentHTLCTimeout` | **Smart chain → Lightning** | Optional HTLC timeout override in seconds. Longer = more routing hops available; shorter = faster lockup release if the LP misbehaves. | ### Lightning → Smart chain swaps Swaps in the **Lightning → Smart chain** direction use the **HTLC swap protocol**, which requires the user to create a swap secret and paymentHash pair and to later reveal the swap secret once prompted to. Therefore you have to: 1. Generate a cryptographically secure random 32-byte buffer, this is your swap secret, then hash this secret using sha256 hash function to derive a payment hash. Store the swap secret securely for later reveal (i.e. in the browser local storage). ``` const secret: Buffer = crypto.randomBytes(32); const paymentHash: string = crypto.createHash("sha256").update(secret).digest("hex"); // Store the preimage, i.e. in browser storage ``` 2. Pass the hex-encoded payment hash to the [`POST /createSwap`](https://docs.atomiq.exchange/rest-api-reference/create-swap) API as a `paymentHash` parameter. 3. Wait till [`GET /getSwapStatus`](https://docs.atomiq.exchange/rest-api-reference/get-swap-status) returns the `requiresSecretReveal: true` prompting the user to reveal the swap secret: ``` { /* ... */ "requiresSecretReveal": true, "currentAction": null } ``` 4. Reveal the swap secret by calling [`GET /getSwapStatus`](https://docs.atomiq.exchange/rest-api-reference/get-swap-status) with the secret in the string, the API then broadcasts the secret over Nostr for automatic settlement, or returns signing actions for manual settlement: ``` curl "https://mainnet.swaps-api.atomiq.exchange/getSwapStatus?swapId=&secret=" ``` ### BOLT11 invoices BOLT11 invoices are the most common way of receiving lightning network payments, usually supported by all the wallets. They are single-use only, so need to be re-generated after every swap to the specific invoice. warning The atomiq API only supports invoices which contain an explicit satoshis (BTC) amount, invoices without an amount are not supported. This means for **Smart chain → Lightning** routes using BOLT11 invoices as the destination address only `EXACT_OUT` quoting is supported. ### LNURLs and static internet identifiers LNURLs come in two variants: * LNURL-pay — allow dynamic payments with optional min/max bounds, descriptions, repeated payments and also support `EXACT_IN` swaps (unlike BOLT11 invoices) * LNURL-withdraw — can be used as source for **Lightning → Smart chain** swaps, allows the user to "pull" amount from the address * Static internet identifiers (i.e. lightning "e-mail" addresses: `joe@example.com`) — can be used interchangeably as LNURL-pay Since both of these LNURL types use the same format you should first resolve them using the [`GET /parseAddress`](https://docs.atomiq.exchange/rest-api-reference/parse-address) endpoint (see [Quoting Swaps → Parse the destination address](https://docs.atomiq.exchange/rest-api-guide/quoting.md#parse-the-destination-address)), this returns the information about what type of the LNURL it is, along with the min/max values and descriptions. You can then pass these LNURLs directly to [`POST /createSwap`](https://docs.atomiq.exchange/rest-api-reference/create-swap): * LNURL-withdraw as `srcAddress` when `srcToken = LIGHTNING-BTC`. * LNURL-pay as `dstAddress` when `dstToken = LIGHTNING-BTC`. info While the atomiq API supports the parsing and automatic handling of the LNURLs, you should always (if possible) prefer to resolve and handle them yourself on the client-side. This removes the trust from the backend to resolve the LNURLs honestly. ### Settle the swap with LNURL-withdraw link For **Lightning -> Smart chain** which are in a state where they request a payment of the BOLT11 invoice by the user (`currentAction` is `SendToAddress`, stage `PR_CREATED`), you can instead use the LNURL-withdraw link to settle the swap by calling [`POST /settleWithLnurl`](https://docs.atomiq.exchange/rest-api-reference/settle-with-lnurl): ``` curl -X POST "https://mainnet.swaps-api.atomiq.exchange/settleWithLnurl" \ -H "Content-Type: application/json" \ -d '{ "swapId": "a3cabd3ef93...", "lnurlWithdraw": "lnurl1..." }' ``` info If the swap was already created with an LNURL-withdraw link (i.e. [`POST /createSwap`](https://docs.atomiq.exchange/rest-api-reference/create-swap) received the LNURL-withdraw as `srcAddress`), omit the `lnurlWithdraw` parameter. Passing it in that case is a validation error. Response: ``` { "paymentHash": "" } ``` --- # Creating & Executing a Swap This page covers the core swap lifecycle that every direction follows: **create → poll → sign → submit → repeat** until finished. Bitcoin- and Lightning-specific details (PSBT building, LNURL, preimage reveal) are split into [Bitcoin & Lightning Specifics](https://docs.atomiq.exchange/rest-api-guide/bitcoin-and-lightning.md). tip This page covers the minimum needed to create and execute a swap. If you're building a complete swap form (e.g. token pickers, "Max" button, amount limit validation, recipient address parsing), see [Quoting Swaps](https://docs.atomiq.exchange/rest-api-guide/quoting.md) first to populate the form inputs. ## Create the swap Use [`POST /createSwap`](https://docs.atomiq.exchange/rest-api-reference/create-swap) to create a new swap: ``` curl -X POST "https://mainnet.swaps-api.atomiq.exchange/createSwap" \ -H "Content-Type: application/json" \ -d '{ "srcToken": "BITCOIN-BTC", "dstToken": "STARKNET-STRK", "amount": "150000", "amountType": "EXACT_IN", "srcAddress": "bc1q…", "dstAddress": "0x0123…" }' ``` For **Bitcoin / Lightning → Smart chain** swaps, you can also request a destination-chain gas drop by passing `gasAmount`; see [Gas-drop](#gas-drop). This creates a swap of 0.0015 BTC (150,000 sats) to STRK token. Note that `amount` is in base units (sats for Bitcoin, wei for EVM, etc.), and the amount type can be either `EXACT_IN` or `EXACT_OUT`, based on whether the `amount` field specifies an input amount or output amount. For some swap types (e.g. Lightning → Smart chain and Bitcoin → Smart chain) the `srcAddress` can be left empty. Lightning network swaps For **Lightning → Smart chain** swaps, you also need to create a swap secret and paymentHash pair. First generate a cryptographically secure random 32-byte buffer, this is your swap secret, then hash this secret using sha256 hash function to derive a payment hash. Store the swap secret securely for later reveal (i.e. in the browser local storage). ``` const secret: Buffer = crypto.randomBytes(32); const paymentHash: string = crypto.createHash("sha256").update(secret).digest("hex"); // Store the preimage, i.e. in browser storage ``` Then pass the hex-encoded payment hash to the [`POST /createSwap`](https://docs.atomiq.exchange/rest-api-reference/create-swap) API as a `paymentHash` parameter. The response is a **swap record** — the same shape every subsequent [`GET /getSwapStatus`](https://docs.atomiq.exchange/rest-api-reference/get-swap-status) call will return (see the REST API Reference for the full response schema): ``` { "swapId": "54cbe34e2ee1bf...", "swapType": "SPV_VAULT_FROM_BTC", "state": { "number": 1, "name": "CREATED", "description": "..." }, "quote": { "inputAmount": { "amount": "0.0015", "rawAmount": "150000", ... }, "outputAmount": { "amount": "4.21", "rawAmount": "4210000000000000000", ... }, "fees": { "swap": { ... }, "networkOutput": { ... } }, "expiry": 1713360000000, "outputAddress": "0x0123..." }, "createdAt": 1713359700000, "steps": [ ... ] } ``` ### Gas-drop For **Bitcoin / Lightning → Smart chain** swaps you can optionally request a small portion of the swap amount to be swapped into destination chain's native gas token (e.g. STRK on Starknet, CBTC on Citrea, ...). This is useful for providing new wallets with some native token balance to cover transaction fees, such that they can start interacting with other dApps on the network immediately. To request a gas drop set the `gasAmount` parameter (base-unit string, same convention as `amount`) when creating the swap with [`POST /createSwap`](https://docs.atomiq.exchange/rest-api-reference/create-swap), the gas drop is usually not available when already swapping to the native token of the chain (it is anyway only useful when you swap to a non-native token): ``` curl -X POST "https://mainnet.swaps-api.atomiq.exchange/createSwap" \ -H "Content-Type: application/json" \ -d '{ "srcToken": "BITCOIN-BTC", "dstToken": "STARKNET-WBTC", "amount": "150000", "amountType": "EXACT_IN", "srcAddress": "bc1q…", "dstAddress": "0x0123…", "gasAmount": "10000000000000000000" }' ``` This creates a swap of 150,000 sats (0.0015 BTC) from Bitcoin into WBTC and also adds a 10 STRK gas drop on Starknet. The 10 STRK gas drop value is subtracted from the output WBTC amount (as part of the swap value was converted into the STRK). info Requesting gas-drop also affects spendable-balance math; see [Quoting Swaps → Spendable balance](https://docs.atomiq.exchange/rest-api-guide/quoting.md#estimate-spendable-balance). ## Poll for the current action tip The full reference implementation of the polling loop, handling all the actions in TypeScript can be found at [`scripts/process-swap.ts`](https://github.com/atomiqlabs/atomiq-api-docker/blob/main/scripts/process-swap.ts) in the `atomiq-api-docker` repo. The swap process might include multiple actions that must be executed sequentially, you should therefore call [`GET /getSwapStatus`](https://docs.atomiq.exchange/rest-api-reference/get-swap-status) in a polling loop, which returns the current action that the user should execute. ``` curl "https://mainnet.swaps-api.atomiq.exchange/getSwapStatus?swapId=" ``` Bitcoin swaps For **Bitcoin → Smart chain** swaps, pass `bitcoinAddress` + `bitcoinPublicKey` on **every** call as the API needs them to build funded PSBTs. ``` curl "https://mainnet.swaps-api.atomiq.exchange/getSwapStatus?swapId=&bitcoinAddress=&bitcoinPublicKey=" ``` The response extends the swap record with four boolean flags and a `currentAction`, which indicates the action, that has to be executed by the user. ``` { /* ... all swap-record fields ... */ "isFinished": false, "isSuccess": false, "isFailed": false, "isExpired": false, "currentAction": { "type": "SignPSBT", /* ... */ }, "requiresSecretReveal": false } ``` When `isFinished === true`, stop polling. See [Terminal states](#terminal-states) below for what the other three flags mean. Lightning secret reveal For **Lightning → Smart chain** swaps, the `requiresSecretReveal` parameter indicates when the swap secret should be revealed. To reveal the swap secret, include the `secret` parameter in the subsequent [`GET /getSwapStatus`](https://docs.atomiq.exchange/rest-api-reference/get-swap-status) calls: ``` curl "https://mainnet.swaps-api.atomiq.exchange/getSwapStatus?swapId=&secret=" ``` The returned `currentAction`, uses the [`SerializedAction`](https://docs.atomiq.exchange/rest-api-reference/schemas/serializedaction) schema, and is one of the following four shapes, which correspond to types that the client should branch on: ### `SignPSBT` Reference: [SerializedActionSignPSBT](https://docs.atomiq.exchange/rest-api-reference/schemas/serializedactionsignpsbt) Here, the API has built a Bitcoin PSBT for the user's deposit and needs the client's wallet to sign the PSBT, then return the signed PSBT back to the API. ``` { "type": "SignPSBT", "chain": "BITCOIN", "name": "Deposit on Bitcoin", "description": "Sign the funding transaction to commit the deposit.", "pollTimeSeconds": 5, "txs": [ { "type": "FUNDED_PSBT", "psbtHex": "70736274ff0100…ac0000000000", "psbtBase64": "cHNidP8BAH0CAAA…rAAAAAAA=", "signInputs": [0, 2], "feeRate": 3.5 } ] } ``` For each PSBT in `action.txs`, sign the inputs listed in `signInputs` with the user's Bitcoin wallet and post the signed PSBTs back to [`POST /submitTransaction`](https://docs.atomiq.exchange/rest-api-reference/submit-transaction), for the API to broadcast the transaction. I.e. in this example the client should parse the PSBT from its `psbtHex` or `psbtBase64`, sign inputs with indexes 0 and 2. info In case you didn't supply `bitcoinAddress` + `bitcoinPublicKey` on `GET /getSwapStatus` the endpoint, returns the `RAW_PSBT` objects in `action.txs` instead of `FUNDED_PSBT`, and the client must add its own inputs (coin-selection) before signing — see [Bitcoin & Lightning → PSBT signing](https://docs.atomiq.exchange/rest-api-guide/bitcoin-and-lightning.md#bitcoin-psbts). Submit each signed PSBT as a **hex or base64 encoded** string. Order of the PSBTs in `signedTxs` must match the order in `action.txs`: ``` curl -X POST "https://mainnet.swaps-api.atomiq.exchange/submitTransaction" \ -H "Content-Type: application/json" \ -d '{ "swapId": "0x9f3c…", "signedTxs": ["70736274ff01..."] }' ``` The response contains the broadcasted Bitcoin transaction IDs: ``` { "txHashes": ["a1b2c3d4e5f6…"] } ``` ### `SignSmartChainTransaction` Reference: [SerializedActionSignSmartChainTransaction](https://docs.atomiq.exchange/rest-api-reference/schemas/serializedactionsignsmartchaintransaction) Here, the API has pre-built smart chain (i.e. non-Bitcoin: EVM, Solana, Starknet, etc.) transactions and needs the user's smart chain wallet to sign them. ``` { "type": "SignSmartChainTransaction", "chain": "SOLANA", "name": "Commit on Solana", "description": "Sign and submit the escrow-funding transaction.", "pollTimeSeconds": 3, "txs": [ "{\"tx\":\"01a4f1b2...\",\"signers\":[\"7c983c019e...\"],\"lastValidBlockheight\":345678901}" ] } ``` Each entry of `action.txs` is a serialized unsigned transaction whose shape depends on `action.chain`: #### Solana Uses a **JSON-stringified envelope**, with the shape `{ tx, signers, lastValidBlockheight }` where `tx` is a hex-encoded legacy `Transaction`, `signers` is an array of hex-encoded secret keys for any ephemeral co-signers that also need to sign the transaction, and `lastValidBlockheight` is the transaction expiration block height. Use the `partialSign()`, as the LP may already have co-signed the transaction (see the code example below). #### Starknet Uses a **JSON-stringified envelope**, with the shape `{ type, tx, details, ... }`. `type` is either `INVOKE` (regular call) or `DEPLOY_ACCOUNT` (first-time account deployment). For `INVOKE`, build the signed invocation from `tx` + `details`. For `DEPLOY_ACCOUNT`, you need to build the deployment payload yourself (the API can't know your account class) using the supplied `details` (fees, nonce). Convert every `resourceBounds.*.*` value to `BigInt` before signing. Populate `parsed.signed` and re-stringify (use a BigInt → string replacer). #### EVM (Botanix / Citrea / Alpen / Goat) Uses a hex-serialized unsigned EVM transaction, with pre-populated nonce and fees, ready to be signed by the EVM wallet. *** Code example handling the smart chain signing action: ``` import {Transaction as SolanaTransaction} from "@solana/web3.js"; import {Transaction as EthersTransaction} from "ethers"; const signedTxs: string[] = []; for (const tx of action.txs) { switch (action.chain) { case "SOLANA": { const parsed = JSON.parse(tx); const extraSigners = parsed.signers.map((k: string) => Keypair.fromSecretKey(Buffer.from(k, "hex")), ); const solTx = SolanaTransaction.from(Buffer.from(parsed.tx, "hex")); solTx.lastValidBlockHeight = parsed.lastValidBlockheight; solTx.partialSign(solanaWallet, ...extraSigners); // preserves LP sig signedTxs.push(solTx.serialize().toString("hex")); break; } case "STARKNET": { const parsed = JSON.parse(tx); // Coerce resourceBounds back to BigInt for (const t in parsed.details.resourceBounds) for (const p in parsed.details.resourceBounds[t]) parsed.details.resourceBounds[t][p] = BigInt(parsed.details.resourceBounds[t][p]); if (parsed.type === "INVOKE") { parsed.signed = await starknetWallet.buildInvocation(parsed.tx, parsed.details); } else if (parsed.type === "DEPLOY_ACCOUNT") { // API can't know your account class — build the deployment payload yourself. parsed.signed = await starknetWallet.buildAccountDeployPayload( starknetWalletDeploymentPayload, parsed.details, ); } signedTxs.push(JSON.stringify(parsed, (_, v) => typeof v === "bigint" ? v.toString() : v)); break; } default: { // EVM chains const parsedTx = EthersTransaction.from(tx); signedTxs.push(await evmWallet.signTransaction(parsedTx)); break; } } } ``` After signing, submit the signed payloads. Encoding per chain: hex-encoded signed transaction for Solana and EVM; JSON-stringified envelope (with `signed` populated) for Starknet. ``` curl -X POST "https://mainnet.swaps-api.atomiq.exchange/submitTransaction" \ -H "Content-Type: application/json" \ -d '{ "swapId": "9f3c...", "signedTxs": ["21b12eea..."] }' ``` info Smart chain transaction submission endpoint usually waits for the smart chain transaction to confirm before responding, ensure you call the [`POST /submitTransaction`](https://docs.atomiq.exchange/rest-api-reference/submit-transaction) with a long enough timeout, to accommodate transaction confirmation delays. The response contains transaction IDs of the submitted and broadcasted transactions: ``` { "txHashes": ["3KZv8Fq..."] } ``` ### `SendToAddress` Reference: [SerializedActionSendToAddress](https://docs.atomiq.exchange/rest-api-reference/schemas/serializedactionsendtoaddress) Here, the user has to pay to either an on-chain Bitcoin address or a BOLT11 Lightning invoice. ``` { "type": "SendToAddress", "name": "Pay Lightning invoice", "description": "Pay the BOLT11 invoice from any Lightning wallet.", "pollTimeSeconds": 5, "expectedTimeSeconds": 60, "txs": [ { "name": "Lightning payment", "address": "lnbc1500n1p3z7...", "hyperlink": "lightning:lnbc1500n1p3z7...", "amount": { "amount": "0.00015", "symbol": "BTC" } } ] } ``` Show the `address` and `amount` from each entry of `action.txs` to the user (or use a `hyperlink`, which can be displayed as a QR code, or used as clickable deeplink) and let them pay from whichever wallet they like. Keep polling [`GET /getSwapStatus`](https://docs.atomiq.exchange/rest-api-reference/get-swap-status); the server detects the incoming payment and advances the state automatically. info A Lightning → Smart chain swap can also be settled via LNURL-withdraw instead of asking the user to pay an invoice — use the [LNURL-withdraw settlement](https://docs.atomiq.exchange/rest-api-guide/bitcoin-and-lightning.md#settle-the-swap-with-lnurl-withdraw-link) instead. ### `Wait` Reference: [SerializedActionWait](https://docs.atomiq.exchange/rest-api-reference/schemas/serializedactionwait) Here, there's nothing actionable right now — the API is waiting for confirmations, a counterparty action, or a timer to elapse. ``` { "type": "Wait", "name": "Waiting for confirmations", "description": "1/3 confirmations seen. ETA ~30 min.", "expectedTimeSeconds": 1800, "pollTimeSeconds": 30 } ``` Wait `pollTimeSeconds` and call [`GET /getSwapStatus`](https://docs.atomiq.exchange/rest-api-reference/get-swap-status) again. Use `expectedTimeSeconds` to render an ETA in the UI. ## Terminal states A swap is **finished** when `isFinished === true`. The terminal state is described by three additional flags: | Flag | Meaning | | ----------- | -------------------------------------------------------------------------------- | | `isSuccess` | The swap completed successfully and the user received the destination token. | | `isFailed` | The swap failed after funds were committed — typically results in a refund flow. | | `isExpired` | The quote expired before the user paid. No on-chain state was created. | Once `isFinished` is true, stop polling. These flags are mutually exclusive. ## Lifecycle overview ![REST API swap lifecycle — create, poll, sign, submit until finished](/assets/images/swap-lifecycle-3b0df6a4c3f3935fc1ffb425a86fcc31.svg) ## Swap execution steps Every swap record carries a `steps` array alongside the action-level state, which is a **UX hint** describing the swap as a linear sequence of stages — good for rendering the swap progress. The *actionable* state still lives in `currentAction`. Individual steps in the array use the [`SwapExecutionStep`](https://docs.atomiq.exchange/rest-api-reference/schemas/swapexecutionstep) schema, which always contains a `status` field, plus additional fields based on the type: | `type` | Meaning | Statuses | Additional fields | | --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | | [`Setup`](https://docs.atomiq.exchange/rest-api-reference/schemas/swapexecutionstepsetup) | Destination-side setup (e.g. creating the destination HTLC / escrow). | `awaiting`, `completed`, `soft_expired`, `expired` | `setupTxId` | | [`Payment`](https://docs.atomiq.exchange/rest-api-reference/schemas/swapexecutionsteppayment) | The user's payment that initiates or funds the swap on the source side. | `inactive`, `awaiting`, `received`, `confirmed`, `soft_expired`, `expired` | `initTxId`, `settleTxId`, `confirmations?` (BTC-only) | | [`Settlement`](https://docs.atomiq.exchange/rest-api-reference/schemas/swapexecutionstepsettlement) | Payout / settlement on the destination side. | `inactive`, `waiting_lp`, `awaiting_automatic`, `awaiting_manual`, `soft_settled`, `soft_expired`, `settled`, `expired` | `initTxId`, `settleTxId` | | [`Refund`](https://docs.atomiq.exchange/rest-api-reference/schemas/swapexecutionsteprefund) | Source-side refund path after a failed swap. | `inactive`, `awaiting`, `refunded` | `refundTxId` | ``` // Example step — Bitcoin payment being confirmed { "type": "Payment", "side": "source", "chain": "BITCOIN", "title": "Bitcoin deposit", "description": "Waiting for 3 block confirmations.", "status": "received", "confirmations": { "current": 1, "target": 3, "etaSeconds": 1200 }, "initTxId": "a1b2…" } ``` ## Next Steps ### Bitcoin & Lightning Handling Bitcoin-specific bits (PSBT inputs, fee rates), Lightning invoices, and LNURL settlement. **[Bitcoin & Lightning Specifics →](https://docs.atomiq.exchange/rest-api-guide/bitcoin-and-lightning.md)** *** ### Listing Swaps Listing swap history, pending swaps that need user attention, resuming mid-flight swaps after restart, and handling refunds. **[Listing Swaps →](https://docs.atomiq.exchange/rest-api-guide/listing-swaps.md)** *** --- # Listing Swaps The API keeps a persistent record of every created swap, except the expired non-executed swaps (which are purged from the database). These endpoints allow you to get either the subset of swaps which are currently pending/ongoing or a list of all swaps executed by a given wallet address. This is useful for the "Pending swaps" screen, the post-app-restart recovery flow and for simply showing the history of all the swaps the given user has completed. | Endpoint | Use for | | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | [`GET /listPendingSwaps`](https://docs.atomiq.exchange/rest-api-reference/list-pending-swaps) | Only swaps that are currently ongoing, might also require an action from the user to continue executions. | | [`GET /listSwaps`](https://docs.atomiq.exchange/rest-api-reference/list-swaps) | Full swap history for a signer — e.g. a "Transactions" tab. | Both endpoints take the same query parameters: | Query param | Required | Notes | | ----------- | -------- | ------------------------------------------------------------------------------------------ | | `signer` | ✓ | Smart-chain signer address whose swaps to return. | | `chainId` | optional | Filter to a single chain (`SOLANA`, `STARKNET`, `CITREA`, …). Omit to include every chain. | They also both return the same [`ListSwapOutput`](https://docs.atomiq.exchange/rest-api-reference/schemas/listswapsoutput) shape (an array of swap records with the four terminal flags and with the `currentAction` field): ``` [ { "swapId": "…", "swapType": "FROM_BTC", "state": { "number": 3, "name": "CLAIM_COMMITED", "description": "…" }, "quote": { /* ... */ }, "createdAt": 1713359700000, "steps": [ /* ... */ ], "isFinished": false, "isSuccess": false, "isFailed": false, "isExpired": false }, /* ... */ ] ``` ## List pending swaps You can query the [`GET /listPendingSwaps`](https://docs.atomiq.exchange/rest-api-reference/list-pending-swaps) endpoint on app startup to restore the polling loops of pending swaps. ``` curl "https://mainnet.swaps-api.atomiq.exchange/listPendingSwaps?signer=0x0123…" ``` The response is an array of ongoing swaps that still need user action. Each entry should be in a non-terminal state with `isFinished: false`, the `currentAction` field is not populated by this endpoint. ### Processing pending swaps To process the pending swaps, use the usual [polling loop](https://docs.atomiq.exchange/rest-api-guide/creating-and-executing.md#poll-for-the-current-action) from the **Creating & Executing a Swap** section. Because every swap record includes the full `quote`, `state`, `steps`, and (via [`GET /getSwapStatus`](https://docs.atomiq.exchange/rest-api-reference/get-swap-status)) `currentAction`, the client does not need its own persistence layer — just remember the `signer` address and whatever pairing you have with the user account: ``` // on app start const pending = await get("/listPendingSwaps", { signer }); for (const swap of pending) { // resume each one with the same loop you use for fresh swaps await pollUntilFinished(swap.swapId); } ``` tip If your wallet supports multiple accounts, call [`GET /listPendingSwaps`](https://docs.atomiq.exchange/rest-api-reference/list-pending-swaps) **per account** and aggregate the results. There is no endpoint that merges across signers. ### Refunds and expired quotes The API exposes refunds through the same `currentAction` machinery as the happy path — you don't need a separate endpoint. When a swap enters a refundable state, the polling loop using [`GET /getSwapStatus`](https://docs.atomiq.exchange/rest-api-reference/get-swap-status) will get either: * **`SignSmartChainTransaction`** with `name: "Refund"` — sign the refund transaction and submit it back via [`POST /submitTransaction`](https://docs.atomiq.exchange/rest-api-reference/submit-transaction). * **`Wait`** — if the refund is automatic (LP-settled), no client action is needed. For **expired quotes** — swaps where the quote elapsed before the user paid — `isExpired` becomes `true`, `isFinished` becomes `true`, and no on-chain state was ever created. Safe to drop from the pending list and silently discard from local UI state. ## List all swaps Use [`GET /listSwaps`](https://docs.atomiq.exchange/rest-api-reference/list-swaps) to query all swaps (completed and pending) for a given signer. ``` curl "https://mainnet.swaps-api.atomiq.exchange/listSwaps?signer=0x0123…&chainId=STARKNET" ``` Uses the same `signer` and optional `chainId` query parameters as [`GET /listPendingSwaps`](https://docs.atomiq.exchange/rest-api-reference/list-pending-swaps). The response includes the full swap history for the signer, with the same record shape and terminal flags. ## UI patterns A few small conventions that come up in real integrations: * **Badge count** — `(await get("/listPendingSwaps", { signer })).length`. * **Transactions list** — [`GET /listSwaps`](https://docs.atomiq.exchange/rest-api-reference/list-swaps), sorted by `createdAt desc`, grouped by `state.name` or by the first / last `step.title`. * **Per-swap detail screen** — once the user taps an entry, switch to the per-swap polling loop ([Creating & Executing](https://docs.atomiq.exchange/rest-api-guide/creating-and-executing.md)) using the `swapId` from the list. * **Multi-chain aggregation** — call without `chainId`; the returned records already include `quote.inputAmount.chain` / `quote.outputAmount.chain` to label which chain each one belongs to. ## Next Steps ### Creating & Executing Use the same polling and action-handling loop when resuming pending swaps from a list response. **[Creating & Executing a Swap →](https://docs.atomiq.exchange/rest-api-guide/creating-and-executing.md)** *** ### Quoting Swaps Build the token, amount, and recipient-address form before creating new swaps. **[Quoting Swaps →](https://docs.atomiq.exchange/rest-api-guide/quoting.md)** --- # Quoting Swaps These endpoints let you build a complete swap form with dynamic token pickers, "Send Max" button, amount limit validation and address parsing. You usually use them before creating the swap, to constrain the tokens and amounts the user is allowed to swap. For the bare-minimum create and execute flow, see [Creating & Executing a Swap](https://docs.atomiq.exchange/rest-api-guide/creating-and-executing.md). | Endpoint | Purpose | | ------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- | | [`GET /getSupportedTokens`](https://docs.atomiq.exchange/rest-api-reference/get-supported-tokens) | Populate the initial source/destination token pickers. | | [`GET /getSwapCounterTokens`](https://docs.atomiq.exchange/rest-api-reference/get-swap-counter-tokens) | After one side is picked, show only tokens that actually route with it. | | [`GET /getSpendableBalance`](https://docs.atomiq.exchange/rest-api-reference/get-spendable-balance) | Maximum spendable balance (considering network fees) - i.e. a "Send Max" button. | | [`GET /getSwapLimits`](https://docs.atomiq.exchange/rest-api-reference/get-swap-limits) | Min / max bounds for the amount field. | | [`GET /parseAddress`](https://docs.atomiq.exchange/rest-api-reference/parse-address) | Validate and classify the destination address; can also pre-pick the swap type. | Together, these let you build a swap form that never lets the user assemble an impossible quote. info Supported tokens list, swap counter tokens and swap limits might change over time as LPs join/leave the network, refresh them as necessary (i.e. once every 5 minutes) ## List supported tokens Call [`GET /getSupportedTokens`](https://docs.atomiq.exchange/rest-api-reference/get-supported-tokens) with `side=INPUT` (source picker) or `side=OUTPUT` (destination picker), to get the tokens available as a swap source or destination. ``` curl "https://mainnet.swaps-api.atomiq.exchange/getSupportedTokens?side=INPUT" ``` The response is an array of [`ApiToken`](https://docs.atomiq.exchange/rest-api-reference/schemas/apitoken) objects: ``` { "id": "STARKNET-STRK", // use this exact string elsewhere in the API "chainId": "STARKNET", "ticker": "STRK", "name": "Starknet Token", "decimals": 18, "address": "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d" } ``` The `id` field of the token is its canonical identifier (in the form of `-`), which is used throughout the API. ## Find compatible counter-tokens Once the user has picked one side (say `BITCOIN-BTC` as input), call [`GET /getSwapCounterTokens`](https://docs.atomiq.exchange/rest-api-reference/get-swap-counter-tokens) to discover which tokens actually have liquidity as the other side: ``` curl "https://mainnet.swaps-api.atomiq.exchange/getSwapCounterTokens?token=BITCOIN-BTC&side=INPUT" ``` `side` describes how the caller's `token` is being used. `side=INPUT` means "the user is spending BTC, show me what they can receive." `side=OUTPUT` means "the user wants to receive X, show me what they can pay with." The response is the same [`ApiToken[]`](https://docs.atomiq.exchange/rest-api-reference/schemas/apitoken) shape as [`GET /getSupportedTokens`](https://docs.atomiq.exchange/rest-api-reference/get-supported-tokens). Using this list for the *destination* picker avoids showing combinations the LP network can't actually quote. *** tip Call [`GET /getSupportedTokens`](https://docs.atomiq.exchange/rest-api-reference/get-supported-tokens) **once** at startup to populate the source and destination token picker, then call [`GET /getSwapCounterTokens`](https://docs.atomiq.exchange/rest-api-reference/get-swap-counter-tokens) when the user selects either the source or destination token to narrow down the token picker on the other side. You don't need to re-fetch when the user changes the amount. ## Estimate spendable balance [`GET /getSpendableBalance`](https://docs.atomiq.exchange/rest-api-reference/get-spendable-balance) returns the balance a wallet can *actually spend* on a swap after accounting for chain fees. Usable as an upper limit on the swap source amount or the amount you want behind a "Send Max" button. ``` curl "https://mainnet.swaps-api.atomiq.exchange/getSpendableBalance?wallet=0x0123…&token=STARKNET-STRK" ``` Lightning network balances **Lightning balances are not supported** by this endpoint. For `LIGHTNING-BTC`, the API will throw. Lightning is an off-chain system with no public information about balances. Query parameters (see [`GetSpendableBalanceInput`](https://docs.atomiq.exchange/rest-api-reference/schemas/getspendablebalanceinput) for the authoritative schema): | Query param | Required | Notes | | ------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `wallet` | ✓ | Address to inspect. | | `token` | ✓ | Token identifier (e.g. `BITCOIN-BTC`, `STARKNET-STRK`). | | `feeRate` | optional | Manual fee-rate override. Chain-specific format defined by [`SpendableBalanceFeeRate`](https://docs.atomiq.exchange/rest-api-reference/schemas/spendablebalancefeerate) — see [Fee-rate format](#fee-rate-format). | | `feeMultiplier` | optional, number | Multiplier applied to the auto-fetched fee rate. Mutually exclusive with `feeRate`. | | **Bitcoin-only** | | | | `gasDrop` | optional, boolean | For `BITCOIN-BTC`-source swaps: include gas-drop transaction overhead in the estimate. | | `targetChain` | optional | Destination chain identifier (`STARKNET`, `SOLANA`, ...). Only for `BITCOIN-BTC` balances, changes the estimate because different targets may use different swap protocols, and have different Bitcoin network fees. | | `minBitcoinFeeRate` | optional, number | Floor for the fetched Bitcoin fee rate (sats/vB). Only for `BITCOIN-BTC` | Response shape ([`GetSpendableBalanceOutput`](https://docs.atomiq.exchange/rest-api-reference/schemas/getspendablebalanceoutput)): ``` { "balance": { "amount": "0.00243", "rawAmount": "243000", "decimals": 8, "symbol": "BTC", "chain": "BITCOIN" }, "feeRate": 3.5 // only when estimating BTC on Bitcoin } ``` `balance` follows the [`ApiAmount`](https://docs.atomiq.exchange/rest-api-reference/schemas/apiamount) shape used throughout the API. ### Fee-rate format `feeRate` is chain-specific. The authoritative format is the [`SpendableBalanceFeeRate`](https://docs.atomiq.exchange/rest-api-reference/schemas/spendablebalancefeerate) schema, but in brief: | Chain | Format | Example | | -------------------------------------- | ------------------------------------------------------------- | ------------------------------ | | **Bitcoin** | Positive number, sats/vB | `3.5` | | **Solana** | `[;]` | `"10000;500"` | | **Starknet** | `,,;v3` (in Fri per gas) | `"1000000000,1000000000,0;v3"` | | **EVM** (Botanix, Citrea, Alpen, Goat) | `,` (in Wei per gas) | `"30000000000,2000000000"` | *** tip Re-run this call whenever the target chain or `gasDrop` toggle changes — both affect the estimate, then check periodically as fee rate does change over time. You don't need to re-run it every keystroke in the amount field. ## Get swap limits Once both tokens are chosen, call [`GET /getSwapLimits`](https://docs.atomiq.exchange/rest-api-reference/get-swap-limits) to fetch the current min/max for the pair: ``` curl "https://mainnet.swaps-api.atomiq.exchange/getSwapLimits?srcToken=BITCOIN-BTC&dstToken=STARKNET-STRK" ``` Response ([`GetSwapLimitsOutput`](https://docs.atomiq.exchange/rest-api-reference/schemas/getswaplimitsoutput)) contains two [`GetSwapLimitsSide`](https://docs.atomiq.exchange/rest-api-reference/schemas/getswaplimitsside) entries, each carrying [`ApiAmount`](https://docs.atomiq.exchange/rest-api-reference/schemas/apiamount) `min` / `max` values: ``` { "input": { "min": { /* ApiAmount */ }, "max": { /* ApiAmount */ } }, "output": { "min": { /* ApiAmount */ }, "max": { /* ApiAmount */ } } } ``` Both `input` and `output` limits are returned so you can enforce the constraint on whichever side the user is editing — relevant when `amountType` is `EXACT_IN` vs `EXACT_OUT` (see [`ExactAmountType`](https://docs.atomiq.exchange/rest-api-reference/schemas/exactamounttype)). `max` is optional and may be absent when the LP network does not enforce a hard ceiling. info Limits can change as LPs join/leave the network, re-fetch the swap limits as needed (e.g. every 5 minutes) ## Parse the destination address [`GET /parseAddress`](https://docs.atomiq.exchange/rest-api-reference/parse-address) accepts anything the user might reasonably paste into an address field: * Bitcoin on-chain addresses, also [BIP-21](https://en.bitcoin.it/wiki/BIP_0021) bitcoin payment URIs - `bitcoin:...` * BOLT11 invoices, also lightning deeplinks - `lightning:...` * LNURL-pay ([LUD-6](https://github.com/lnurl/luds/blob/luds/06.md)), LNURL-withdraw ([LUD-3](https://github.com/lnurl/luds/blob/luds/03.md)) links and lightning static internet identifiers ([LUD-16](https://github.com/lnurl/luds/blob/luds/16.md)) * Smart-chain addresses (Starknet, Solana, EVM) Returns the parsed `type` plus structured data (see [`ParseAddressOutput`](https://docs.atomiq.exchange/rest-api-reference/schemas/parseaddressoutput) for the full response shape). info Be sure to URL-encode the address in the GET request parameter, as bitcoin/lightning payment URIs might contain non-URL-safe characters. ``` curl "https://mainnet.swaps-api.atomiq.exchange/parseAddress?address=lnbc510400n1p4qm..." ``` ``` { "address": "lnbc510400n1p4qm...", "type": "LIGHTNING", "amount": { "amount": "0.00051040", "rawAmount": "51040", "decimals": 8, "symbol": "BTC", "chain": "LIGHTNING" } } ``` `type` is one of `BITCOIN`, `LIGHTNING`, `LNURL`, `SOLANA`, `STARKNET`, `CITREA`, … (or other supported chains). For BOLT11 invoices and Bitcoin URIs with `amount` parameters, the response may include an [`ApiAmount`](https://docs.atomiq.exchange/rest-api-reference/schemas/apiamount) `amount` field (and `min` / `max` for LNURL). Use these to pre-populate the amount field if the user pasted a request that already carries one. Two ways to use the result: * **Validate the recipient field.** Show a chip ("Bitcoin address" / "Lightning invoice" / …) once `type` resolves; surface an error if the call throws. * **Auto-pick the destination token / swap type.** The returned `type` tells you which destination chain the user actually pasted, so you can switch the destination picker to e.g. `BITCOIN-BTC` when they paste an on-chain address, or `LIGHTNING-BTC` when they paste a BOLT11 invoice / LNURL link. When `type` is `LNURL`, the response includes an [`ApiLNURL`](https://docs.atomiq.exchange/rest-api-reference/schemas/apilnurl) object that's either an [`ApiLNURLPay`](https://docs.atomiq.exchange/rest-api-reference/schemas/apilnurlpay) or [`ApiLNURLWithdraw`](https://docs.atomiq.exchange/rest-api-reference/schemas/apilnurlwithdraw) variant — see [Bitcoin & Lightning → LNURL](https://docs.atomiq.exchange/rest-api-guide/bitcoin-and-lightning.md#lnurls-and-static-internet-identifiers) for how to use the resolved payload. ## Putting it together A minimal quote form flow: ``` // on mount const inputTokens = await get("/getSupportedTokens", { side: "INPUT" }); populateSourcePicker(inputTokens); // on source-token change const outputTokens = await get("/getSwapCounterTokens", { token: srcToken.id, side: "INPUT", }); populateDestinationPicker(outputTokens); // on destination-address paste const parsed = await get("/parseAddress", { address: pastedRecipient }); pickDestinationTokenFromType(parsed.type); // on "Max" click const { balance } = await get("/getSpendableBalance", { wallet: srcAddress, token: srcToken.id, targetChain: dstToken.chainId, gasDrop: userWantsGasDrop, }); setAmountField(balance.rawAmount); // on both selected (or on amount focus) const limits = await get("/getSwapLimits", { srcToken: srcToken.id, dstToken: dstToken.id, }); setAmountBounds(limits.input.min.rawAmount, limits.input.max?.rawAmount); ``` ## Next Steps ### Creating & Executing Once the form passes validation, create the swap and run the polling/signing loop until it reaches a terminal state. **[Creating & Executing a Swap →](https://docs.atomiq.exchange/rest-api-guide/creating-and-executing.md)** *** ### Bitcoin & Lightning Handle PSBT details, Lightning invoices, LNURL flows, preimage reveal, and gas-drop behavior for Bitcoin-specific routes. **[Bitcoin & Lightning Specifics →](https://docs.atomiq.exchange/rest-api-guide/bitcoin-and-lightning.md)** --- # Run REST API Locally Most integrators will talk to the **public Atomiq REST API**. This section is for teams that need to host the API themselves — to keep quote traffic on their own infrastructure, run against testnets, control rate limits centrally, or combine it with a custom auth layer. The service is published as [`atomiqlabs/atomiq-api-docker`](https://github.com/atomiqlabs/atomiq-api-docker) — a thin, stateful HTTP layer over the Atomiq SDK that embeds one `SwapperApi` instance, persists swap state in SQLite, and exposes the same endpoints documented in the [REST API Reference](https://docs.atomiq.exchange/rest-api-reference/atomiq-rest-api). `atomiq-api-docker` is a single stateful container. It talks outbound to: * **Atomiq LP nodes** — to fetch RFQ quotes and coordinate HTLC / SPV-vault setup. * **Smart-chain RPCs** — read-only, for transaction simulation, account lookups, and broadcast. * **Bitcoin mempool / fee APIs** — for fee estimation and PSBT building. Clients talk to it inbound over HTTP(S). The container never holds user keys — signing happens in the **client wallet**; the API only generates unsigned transactions and submits signed ones. ![System architecture](/assets/images/docker-swap-backend-729688caaa4c14ff929f7fbe5140dd56.svg) A typical deployment has the wallet backend running the container on an internal network and terminating TLS on it directly (or behind a reverse proxy). ## Prerequisites * Docker 24+ with the Docker Compose plugin (`docker compose` v2). * RPC endpoints for the smart chains you want to enable. Mainnet Bitcoin is configured by network name only (no RPC). ## 1. Build the Image ``` git clone https://github.com/atomiqlabs/atomiq-api-docker.git cd atomiq-api-docker ./build.sh ``` The final image is Alpine-based, \~280 MB. ## 2. Create a `config.yaml` Start from the bundled example (lives in `config/`): ``` cp config/config.yaml.example config/config.yaml ``` Minimum viable config (testnet, public access, no TLS): ``` port: 3000 logLevel: info starknetRpc: "https://rpc.starknet.lava.build/" solanaRpc: "https://api.devnet.solana.com" botanixRpc: null citreaRpc: null alpenRpc: null goatRpc: null bitcoinNetwork: TESTNET cors: origin: "*" rateLimit: windowMs: 60000 maxRequests: 200 auth: - type: none name: "Public" ``` See [Configuration](https://docs.atomiq.exchange/rest-api-guide/run-locally/configuration.md) for the full schema. ### Setting up RPC endpoints Before the service can talk to a smart chain, you need to give it an RPC URL for that chain. Three common options: 1. **Public / community RPCs** — free endpoints like `https://api.mainnet-beta.solana.com` or `https://rpc.starknet.lava.build/`. Easiest to start with, but typically rate-limited and not reliable enough for production. 2. **Hosted providers** — services like Alchemy, Infura, QuickNode, Helius (Solana), Lava, etc. give you a private URL with a generous free tier and paid plans once traffic grows. 3. **Self-hosted node** — run your own full node and point the API at it. Most control, most operational overhead. Whichever you pick, paste the URL into the matching key in `config.yaml`. Leave a key out, or set it to `null`, to disable that chain entirely. A few things to double-check: * Make sure the **network of each RPC matches the rest of your config** — e.g. don't combine a mainnet Solana RPC with `bitcoinNetwork: TESTNET`. See the SDK [quick-start guide](https://docs.atomiq.exchange/sdk-guide/quick-start/.md) for the supported network combinations. * **Mainnet Bitcoin** is configured by network name only (`bitcoinNetwork: MAINNET`) and does not need an RPC. ## 3. Run Use the bundled `docker-compose.yml`: ``` docker compose up -d # if this doesn't work, try: docker-compose up -d ``` This starts the service on port `3000`, mounts `./config` read-only into `/src/config`, and persists the SQLite swap databases in the host `./storage` directory so they survive container restarts. The bundled compose file also sets `CONFIG_PATH=/src/config/config.yaml` and `STORAGE_DIR=/src/storage`. See [Configuration → Persistence](https://docs.atomiq.exchange/rest-api-guide/run-locally/configuration.md#persistence) for details. You can tail the logs with: ``` docker compose logs -f ``` On startup you should see: ``` Initializing SwapperApi... SwapperApi initialized. Chains: STARKNET, SOLANA, ... atomiq-api listening on port 3000 POST /createSwap GET /listSwaps ... ``` tip If port `3000` is already in use on your host, change the host-side port in `docker-compose.yml` by editing the `ports` mapping. For example, to expose the API on port `8080`: ``` services: atomiq-api: ports: - "8080:3000" ``` ## 4. Smoke Test ``` curl "http://localhost:3000/getSupportedTokens?side=INPUT" ``` You should see an array of token objects. If the list is non-empty, the LP network is reachable and quoting is working. ## Configuration Full configuration details, including `config.yaml` schema, auth paths, TLS, reverse proxy, persistence, and security notes. **[Configuration →](https://docs.atomiq.exchange/rest-api-guide/run-locally/configuration.md)** *** --- # Configuration The service reads its entire runtime config from a single YAML file. The path is selected by the `CONFIG_PATH` environment variable; the bundled `docker-compose.yml` sets it to `/src/config/config.yaml`, which corresponds to `./config/config.yaml` on the host. ## Top-level keys | Key | Type | Default | Description | | --------------------------------------------------------------------------------- | -------------------------------------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------- | | `port` | number | **required** | TCP port the server binds to. | | `logLevel` | `error` \| `warn` \| `info` \| `debug` | `info` | `info` = morgan HTTP logs; `debug` = verbose per-request log line incl. IP, XFF, UA. | | `bitcoinNetwork` | `MAINNET` \| `TESTNET` \| `TESTNET3` \| `TESTNET4` | **required** | Which Bitcoin network the SDK connects to. `TESTNET` is an alias for `TESTNET3`. | | `starknetRpc` / `solanaRpc` / `botanixRpc` / `citreaRpc` / `alpenRpc` / `goatRpc` | string or null | null (disabled) | RPC URL per smart chain. Omit / set to null to disable that chain. | | `swapsSyncIntervalSeconds` | number | 300 | Interval between background `SwapperApi.sync()` calls (purges expired swaps, refreshes state). | | `reloadLpIntervalSeconds` | number | 300 | Interval between background LP reloads (re-discovers dropped LPs). | | `cors` | object or null | null (disabled) | Passed through to the [`cors`](https://github.com/expressjs/cors) middleware. | | `rateLimit` | `{ windowMs, maxRequests }` | **required** | Global fallback rate limit (applied when an auth path does not override). | | `auth` | array | **required**, non-empty | Ordered list of auth paths — see [Authentication](#authentication). | | `https` | `{ keyPath, certPath }` or null | null (HTTP) | TLS config. Paths are resolved relative to the config file. | | `trustProxy` | boolean | `false` | When running the API behind a reverse proxy, set to `true` to properly parse the client's IP address. | ## Authentication `auth` is an **ordered array** — the first entry that matches a request wins. Each entry can optionally set its own `rateLimit` (or `null` to disable rate limiting entirely on that path). Three entry types: ``` auth: # 1. Shared-secret API key - type: apiKey name: "Privileged clients" apiKey: "replace-with-long-random-secret" header: x-api-key # optional, default x-api-key rateLimit: null # null = no rate limit on this path # 2. JWT signed by an external auth service - type: jwt name: "Premium Users" publicKey: "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----" algorithms: [RS256] # or [ES256], etc. claims: # optional — all claims must match user_tier: "swapper" rateLimit: windowMs: 60000 maxRequests: 200 # 3. Open fallback (uses global rateLimit) - type: none name: "Public" ``` ### API-key auth Any client that can present the configured shared secret in a header (default `x-api-key`) is authorized on this path. ``` GET /listSwaps?signer=0x... HTTP/1.1 x-api-key: replace-with-long-random-secret ``` Treat the API key as a shared secret. Whether you ship it to a trusted backend, embed it in a first-party frontend, or hand it to an operator depends on your threat model — just never expose it to clients you do not control. ### JWT auth Requests are authorized by a signed JWT in the `Authorization: Bearer ` header. The service: 1. Verifies the JWT signature against the public key configured in `config.yaml` using the algorithms listed in `algorithms`. 2. Enforces the standard `exp` claim. 3. Checks any additional required claims listed under `claims` on the auth entry. How the JWT is minted and delivered to the caller is out of scope for this service — issue it from any auth system that can sign with a key pair whose public half you paste into `config.yaml`. `claims` supports two forms: * Exact match: `{ user_tier: "swapper" }` — the JWT payload must contain a matching scalar value. * Array-includes: `{ permissions: { includes: "swap_permission" } }` — the JWT payload must contain an array that includes the given value. #### Generating a test key pair + JWT A helper script is bundled for local testing: ``` npx ts-node --project tsconfig.scripts.json \ scripts/generate-jwt.ts \ '{"payload":{"sub":"demo","user_tier":"swapper"},"options":{"expiresIn":"1h"}}' ``` It prints: * A freshly generated ES256 (P-256) private key (PEM). * The matching public key in both multi-line and single-line PEM form — paste the single-line form into `auth[].publicKey` in `config.yaml`. * A signed JWT you can use immediately with `Authorization: Bearer ...`. Pass an existing private key as the second argument to sign with your own key instead. ### Public / no-auth `type: none` matches any request. Put it last if you want to offer anonymous access; omit it if all traffic must be authenticated. ## Rate limiting Uses in-memory bucketing per client IP, with a fixed window. * Each auth entry can set its own `rateLimit: { windowMs, maxRequests }` or explicitly `null` (no limit — typical for `apiKey` traffic). * If an auth entry has **no** `rateLimit` key, the **global** `rateLimit` from the top level applies. * Exceeding the limit returns `429 { error: "Rate limit exceeded", retryAfter }`. ## HTTPS and certificate reload Set `https` in the config to run TLS directly: ``` https: keyPath: "./tls/server.key" certPath: "./tls/server.cert" ``` Both paths are resolved relative to the `config.yaml` file. With the bundled compose layout that means you can keep the certificate, key, or symlinks to them under `config/tls/` and mount the whole `config/` directory into the container read-only. The server watches both files with a 1 s poll interval. On any change it schedules a 60 s-delayed reload (debounced) via `server.setSecureContext(...)` — Node keeps serving existing connections during the swap. This is designed to work cleanly with Let's Encrypt / certbot renewal hooks: the renewal hook writes both files, the server picks them up within a minute without a restart. ## Running behind a reverse proxy Set `trustProxy` if you run the API behind a reverse proxy and want to correctly resolve client IP addresses (important for rate limiting): ``` trustProxy: true ``` You can also let the reverse proxy handle TLS and omit the `https` section from the API config. ## Persistence The container writes SQLite files into the directory pointed to by the `STORAGE_DIR` environment variable. The bundled compose file sets it to `/src/storage`, mapped to `./storage` on the host: * `CHAIN_atomiqsdk-1-.sqlite3` — one per active smart chain; swap state for that chain. * `STORE_.sqlite3` — additional SDK state (e.g. `solAccounts`). The bundled `docker-compose.yml` uses two bind mounts so swap state and config live as plain files in the project directory: | Host path | Container path | Mode | Purpose | | ----------- | -------------- | ---------- | ------------------------------------- | | `./config` | `/src/config` | read-only | `config.yaml` and (optionally) `tls/` | | `./storage` | `/src/storage` | read-write | SQLite swap state | Back up `./storage/` to preserve in-flight swaps across host migrations. ## Background maintenance timers `atomiq-api-docker` runs two timers (configurable): | Timer | Interval key | Default | Purpose | | --------- | -------------------------- | ------- | ------------------------------------------------------------------------------------------------------- | | Swap sync | `swapsSyncIntervalSeconds` | 300 s | Calls `SwapperApi.sync()`. Refreshes state for active swaps and purges expired swaps from the local DB. | | LP reload | `reloadLpIntervalSeconds` | 300 s | Re-discovers Atomiq LPs, so a dropped LP can rejoin the quote pool without restarting the container. | Errors in either timer are logged and the timer continues. ## Error handling All endpoints return JSON. Errors come in two shapes: | Status | Body | Meaning | | ------ | ------------------------------------------------------------- | ------------------------------------------------- | | 400 | `{ "error": "" }` | Validation error or SDK rejected the request. | | 401 | `{ "error": "Unauthorized" }` | No auth entry matched. | | 429 | `{ "error": "Rate limit exceeded", "retryAfter": }` | Per-IP, per-auth-path or global bucket exhausted. | Rate-limit state is per IP. If you use a reverse proxy in front set `trustProxy: true` in the config. ## Security notes * **JWT `exp`** — the JWT auth path enforces `exp`, so you can tune expiration of the JWT. * **Public key rotation** — changing `auth[].publicKey` requires a restart. Plan a rollover window by temporarily listing both the old and new key as two JWT auth entries. * **CORS** — `origin: "*"` is fine for public endpoints, but in production you should restrict it to your wallet front-end origin(s). --- # SDK Guide The Atomiq SDK is a TypeScript multichain client for building trustless swaps between smart chains and Bitcoin, both on-chain and over Lightning. This page is the entry point for the SDK docs and helps you choose the right path through the documentation, whether you are setting up the SDK for the first time, implementing a specific swap family, building the surrounding quote UI, or hardening the integration for production. tip These resources complement the guides in this section: * For runnable end-to-end examples in Node.js, see the [atomiq-sdk-demo](https://github.com/atomiqlabs/atomiq-sdk-demo) repository. * For exact SDK classes, methods, enums, interfaces, and type signatures, see the [SDK API Reference](https://docs.atomiq.exchange/sdk-reference). * For the low-level protocol background behind these integration guides, see [Protocol Overview](https://docs.atomiq.exchange/overview/protocol-overview.md) and [Swaps](https://docs.atomiq.exchange/overview/swaps/.md). ## Where To Start ### First Working Swap Start with [Quick Start](https://docs.atomiq.exchange/sdk-guide/quick-start/.md) to choose Browser or Node.js, initialize the swapper, and connect wallets or signers. Then continue with [Creating Quotes](https://docs.atomiq.exchange/sdk-guide/quick-start/creating-quotes.md) and [Executing Swaps](https://docs.atomiq.exchange/sdk-guide/quick-start/executing-swaps.md) to get the default high-level flow running end to end. ### Handling Edge Cases Use [Swap Management](https://docs.atomiq.exchange/sdk-guide/swap-management/.md) once your app needs to recover saved swaps after restart or interruption. In practice, this is a core part of a production integration rather than an optional extra: apps should expose clear recovery paths for swaps that did not finish automatically, including surfacing refund or claim actions when saved swaps still need user attention. ### Building the Swap UI Once the basic flow works, use [Utilities](https://docs.atomiq.exchange/sdk-guide/utilities/.md) to handle the smaller decisions around quoting: parse Bitcoin addresses, BOLT11 invoices, LNURLs, and smart-chain addresses, populate token selectors from the supported routes, inspect the `SwapType`, enforce route-specific limits, and calculate spendable balances for "Max" actions. ### Implementing Specific Swap Families Use [Swap Guides](https://docs.atomiq.exchange/sdk-guide/swaps/.md) when your app needs the details of the exact swap it is executing. These pages cover the direction-specific signer and wallet inputs, LNURL variants, manual execution paths, recovery actions, and the distinction between the standard Starknet and EVM flows and the legacy inbound Solana flows. ### Advanced Runtime Control Use [Advanced](https://docs.atomiq.exchange/sdk-guide/advanced/.md) when the default SDK flow is already working but the app needs more control over how it runs, such as handing smart-chain transactions off to external signers, subscribing to runtime events, customizing storage, or tuning swapper configuration. ## Sections ### Quick Start Set up the SDK in Browser or Node.js, initialize the swapper, and follow the shared path from setup to quoting and execution. **[Quick Start →](https://docs.atomiq.exchange/sdk-guide/quick-start/.md)** *** ### Swap Guides Find the exact swap family your app is implementing, including standard Bitcoin and Lightning routes, LNURL variants, and the legacy Solana inbound flows. **[Swap Guides →](https://docs.atomiq.exchange/sdk-guide/swaps/.md)** *** ### Utilities Build the quote form and route-selection layer around swaps with helpers for address parsing, supported tokens, swap classification, amount limits, and spendable balances. **[Utilities →](https://docs.atomiq.exchange/sdk-guide/utilities/.md)** *** ### Swap Management Recover saved swaps from storage and handle the cases where a refund or claim action is still required after restart or interruption. **[Swap Management →](https://docs.atomiq.exchange/sdk-guide/swap-management/.md)** *** ### Advanced Adapt the SDK to more complex runtimes with manual transaction signing, event subscriptions, storage customization, and advanced swapper configuration. **[Advanced →](https://docs.atomiq.exchange/sdk-guide/advanced/.md)** *** --- # Advanced This section is for integrations that go beyond the default SDK flow. It focuses on production concerns around transaction control, runtime behavior, real-time state, and persistence. info If you are still setting up the SDK or learning the swap lifecycle, start with [Creating Quotes](https://docs.atomiq.exchange/sdk-guide/quick-start/creating-quotes.md) and [Executing Swaps](https://docs.atomiq.exchange/sdk-guide/quick-start/executing-swaps.md) first. The advanced pages assume that part is already familiar. ## Usage Most integrations use the advanced features once the standard quote and execution flow is already in place and the remaining work is about adapting the SDK to the application's runtime: * Use [Manual Transactions](https://docs.atomiq.exchange/sdk-guide/advanced/manual-transactions.md) when your app needs to get smart-chain transactions from the SDK, hand them off to an external signer or wallet flow, and then let the SDK observe the result afterwards, such as with hardware wallets, custody systems, or a separate approval layer. * Use [Configuration](https://docs.atomiq.exchange/sdk-guide/advanced/configuration.md) when you need to customize how the swapper runs, including RPC setup, LP discovery behavior, pricing hooks, runtime flags, and chain-specific options. * Use [Events](https://docs.atomiq.exchange/sdk-guide/advanced/events.md) when your UI or backend needs to react to live swap state updates, LP discovery, or limit changes, for example to show pending swaps, update route availability, or drive notifications. * Use [Storage](https://docs.atomiq.exchange/sdk-guide/advanced/storage.md) when you need to understand what the SDK persists or provide a custom storage backend, especially outside browser environments or when integrating with your own database layer. ## Topics ### Manual Transactions Use the `txs*()` methods to sign and broadcast smart-chain transactions outside the SDK, for example with hardware wallets, custom custody, or a separate approval layer. **[Manual Transactions →](https://docs.atomiq.exchange/sdk-guide/advanced/manual-transactions.md)** *** ### Configuration Tune how the swapper is initialized, including RPCs, LP discovery, storage hooks, pricing, runtime flags, and per-chain options. **[Configuration →](https://docs.atomiq.exchange/sdk-guide/advanced/configuration.md)** *** ### Events Subscribe to swap state, LP discovery, and swap-limit updates so UIs and services can react in real time. **[Events →](https://docs.atomiq.exchange/sdk-guide/advanced/events.md)** *** ### Storage Understand what the SDK persists, how the default browser setup works, and how to provide Node.js, React Native, or custom storage backends. **[Storage →](https://docs.atomiq.exchange/sdk-guide/advanced/storage.md)** *** --- # Configuration Customize the swapper instance with advanced options. tip See the complete working example: [setup.ts](https://github.com/atomiqlabs/atomiq-sdk-demo/blob/main/src/setup.ts) ## Basic Configuration The `chains` object is typed from the initializer list passed to `SwapperFactory`. With Solana, Starknet, and Citrea initializers, `SOLANA`, `STARKNET`, and `CITREA` are typed as [SolanaSwapperOptions](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-solana/src/type-aliases/SolanaSwapperOptions), [StarknetOptions](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-starknet/src/type-aliases/StarknetOptions), and [CitreaOptions](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-evm/src/type-aliases/CitreaOptions), respectively. ``` const swapper = Factory.newSwapper({ chains: { SOLANA: { rpcUrl: "https://api.mainnet-beta.solana.com" }, // SolanaSwapperOptions STARKNET: { rpcUrl: "https://starknet-mainnet.public.blastapi.io/rpc/v0_8" }, // StarknetOptions CITREA: { rpcUrl: "https://rpc.citrea.xyz" } // CitreaOptions }, bitcoinNetwork: BitcoinNetwork.MAINNET }); ``` ## Top-Level Options | Parameter | Type | Description | | ------------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `chains` | `GetAllOptions` | Required. The keys are derived from the initializers passed to `SwapperFactory`, and each value is that initializer's option type. | | `intermediaryUrl` | `string \| string[]` | Manually pin one or more LP URLs instead of relying on registry discovery. | | `registryUrl` | `string` | Registry URL used to discover LPs. Defaults to the SDK's per-network registry. | | `defaultTrustedIntermediaryUrl` | `string` | Trusted LP used for gas swaps. Defaults to the SDK's per-network value. | | `getRequestTimeout` | `number` | HTTP GET timeout in milliseconds for SDK requests. | | `postRequestTimeout` | `number` | HTTP POST timeout in milliseconds for SDK requests. | | `defaultAdditionalParameters` | `{[key: string]: any}` | Extra parameters included when requesting quotes from intermediaries. | | `messenger` | `Messenger` | Data propagation messenger used for watchtower broadcasting. Defaults to the SDK's Nostr-based messenger. | ### Storage | Parameter | Type | Description | | ------------------ | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | `swapStorage` | `(storageName: string) => IUnifiedStorage<...>` | Swap persistence backend. Defaults to IndexedDB in browser environments. | | `chainStorageCtor` | `(storageName: string) => IStorageManager` | Key-value storage backend used by chain integrations. Defaults to browser local storage. | | `storagePrefix` | `string` | Prefix used for swap-storage names. `SwapperFactory.newSwapper()` defaults this to `atomiqsdk-${bitcoinNetwork}-`. | #### SQLite Storage Example (Node.js) ``` import {SqliteStorageManager, SqliteUnifiedStorage} from "@atomiqlabs/storage-sqlite"; const swapper = Factory.newSwapper({ ... swapStorage: storageName => new SqliteUnifiedStorage(`${storageName}.sqlite3`), chainStorageCtor: storageName => new SqliteStorageManager(`${storageName}.sqlite3`) }); ``` When you run outside the browser, provide both `swapStorage` and `chainStorageCtor`. ### Pricing | Parameter | Type | Description | | ------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | `pricingFeeDifferencePPM` | `bigint` | Maximum allowed difference between LP pricing and market pricing, in parts-per-million. Defaults to `10000n` (1%). | | `getPriceFn` | `(tickers: string[], abortSignal?: AbortSignal) => Promise` | Custom pricing callback. Return USD prices in the same order as the input `tickers`. | #### Custom Pricing API Your custom pricing callback must return `Promise`, with results in the same order as the input tickers. ``` const swapper = Factory.newSwapper({ ... getPriceFn: async (tickers: string[], abortSignal?: AbortSignal) => { const response = await fetch( `https://my-api.com/prices?tickers=${tickers.join(",")}`, {signal: abortSignal} ); const data = await response.json(); return tickers.map(ticker => data[ticker].usd); } }); ``` ### Bitcoin network and RPC | Parameter | Type | Description | | ---------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `bitcoinNetwork` | `BitcoinNetwork` | Defaults to `BitcoinNetwork.MAINNET`. Controls both the Bitcoin network and the canonical smart-chain deployments selected by the chain initializers | | `mempoolApi` | `MempoolApi \| MempoolBitcoinRpc \| string \| string[]` | Custom mempool API client or URL(s). If omitted, the SDK uses built-in URLs for the selected Bitcoin network. | ### Runtime Flags | Parameter | Type | Description | | ------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `noTimers` | `boolean` | Disable automatic tick timers. If you set this, call `swapper._syncSwaps()` periodically yourself, this updates the state of the swaps. | | `noEvents` | `boolean` | Disable on-chain event subscriptions. If you set this, call `swapper._syncSwaps()` periodically yourself, this updates the state of the swaps. | | `noSwapCache` | `boolean` | Always load swaps from persistent storage instead of using the in-memory `WeakRef` cache. Set this if you access the same swap storage from multiple instances! | | `dontCheckPastSwaps` | `boolean` | Skip past-swap checking during `init()`. If you set this, call `swapper._syncSwaps()` manually on startup to synchronize the swaps. | | `dontFetchLPs` | `boolean` | Skip LP discovery during `init()`. LP metadata is fetched lazily later when needed. | | `saveUninitializedSwaps` | `boolean` | Persist swaps before they are initialized, i.e. `commit()`, `execute()`, or `waitTillPayment()` is called. Useful for backend systems, where one endpoint creates a quote and other endpoint then provides the swap transactions/status. | | `automaticClockDriftCorrection` | `boolean` | Checks for clock drift during `init()` and corrects `Date.now()` if the local clock is far off. Having invalid local time can lead to quotes displaying as expired before they actually expire! | *** Here is the full configuration example as passed to the `Factory.newSwapper()` function: ``` const swapper = Factory.newSwapper({ chains: { SOLANA: { rpcUrl: solanaRpc }, STARKNET: { rpcUrl: starknetRpc }, CITREA: { rpcUrl: citreaRpc } }, intermediaryUrl: ["https://lp-1.example.com", "https://lp-2.example.com"], registryUrl: "https://registry.example.com", defaultTrustedIntermediaryUrl: "https://trusted-lp.example.com", getRequestTimeout: 15000, postRequestTimeout: 20000, defaultAdditionalParameters: { clientId: "my-app", referralCode: "PARTNER123" }, //Bitcoin network and RPC bitcoinNetwork: BitcoinNetwork.MAINNET, mempoolApi: new MempoolApi("https://mempool.space"), //Storage swapStorage: storageName => new SqliteUnifiedStorage(`${storageName}.sqlite3`), chainStorageCtor: storageName => new SqliteStorageManager(`${storageName}.sqlite3`), storagePrefix: "my-app-mainnet-", //Pricing pricingFeeDifferencePPM: 20000n, getPriceFn: customPriceGetter, //Runtime flags noTimers: true, noEvents: true, noSwapCache: true, dontCheckPastSwaps: true, dontFetchLPs: true, saveUninitializedSwaps: true, automaticClockDriftCorrection: true }); ``` ## Chain Configuration Each `chains.` entry is the options type exposed by that chain initializer. ### Solana `chains.SOLANA` is [`SolanaSwapperOptions`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-solana/src/type-aliases/SolanaSwapperOptions). | Parameter | Type | Description | | -------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | | `rpcUrl` | `string \| Connection` | Required. Solana RPC URL or a prebuilt `Connection`. | | `dataAccountStorage` | `IStorageManager` | Storage backend for ephemeral Solana data-submission accounts. If omitted, the initializer uses `chainStorageCtor("solAccounts")`. | | `retryPolicy` | `SolanaRetryPolicy` | Retry policy for Solana RPC calls and transaction submission. If omitted, the initializer uses `{ transactionResendInterval: 1000 }`. | | `btcRelayContract` | `string` | Override the canonical BTC Relay program address. | | `swapContract` | `string` | Override the canonical swap program address. | | `fees` | `SolanaFees` | Custom Solana fee API implementation. | ``` const swapper = Factory.newSwapper({ chains: { SOLANA: { rpcUrl: "https://api.mainnet-beta.solana.com", //Or `new Connection("https://api.mainnet-beta.solana.com", "confirmed")` dataAccountStorage: new SqliteStorageManager('solAccountsStore.sqlite3'), retryPolicy: {...}, btcRelayContract: "", swapContract: "", fees: new SolanaFees(...) }, ... }, ... }); ``` ### Starknet `chains.STARKNET` is [`StarknetOptions`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-starknet/src/type-aliases/StarknetOptions). | Parameter | Type | Description | | ---------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `rpcUrl` | `string \| Provider` | Required. Starknet RPC URL or a prebuilt provider. | | `wsUrl` | `string \| WebSocketChannel` | Optional websocket endpoint or channel for realtime subscriptions. | | `retryPolicy` | `{maxRetries?: number, delay?: number, exponential?: boolean}` | Retry policy for RPC calls. | | `chainId` | `constants.StarknetChainId` | Optional chain ID override. If omitted, the initializer uses `SN_MAIN` for Bitcoin mainnet and `SN_SEPOLIA` for non-mainnet networks. | | `swapContract` | `string` | Override the Escrow Manager contract address. | | `swapContractDeploymentHeight` | `number` | Override the swap contract deployment height used as the event-query genesis. | | `btcRelayContract` | `string` | Override the BTC Relay contract address. | | `btcRelayContractDeploymentHeight` | `number` | Override the BTC Relay deployment height used as the event-query genesis. | | `spvVaultContract` | `string` | Override the SPV Vault manager contract address. | | `spvVaultContractDeploymentHeight` | `number` | Override the SPV Vault deployment height used as the event-query genesis. | | `handlerContracts` | `{ refund?: { timelock?: string }, claim?: { [type in ChainSwapType]?: string } }` | Optional refund and claim handler overrides. Use `refund.timelock` to override the refund handler and `claim[ChainSwapType]` to override individual claim handlers. | | `fees` | `StarknetFees` | Custom Starknet fee API implementation. | | `starknetConfig` | `StarknetConfig` | Advanced Starknet configuration passed into the chain interface. | ``` const swapper = Factory.newSwapper({ chains: { STARKNET: { rpcUrl: "https://starknet-mainnet.public.blastapi.io/rpc/v0_8", //Or `new RpcProvider({ nodeUrl: "https://starknet-mainnet.public.blastapi.io/rpc/v0_8" })` wsUrl: "wss://starknet-mainnet.public.blastapi.io/ws", retryPolicy: {...}, chainId: constants.StarknetChainId.SN_MAIN, swapContract: "", swapContractDeploymentHeight: 123456, btcRelayContract: "", btcRelayContractDeploymentHeight: 123456, spvVaultContract: "", spvVaultContractDeploymentHeight: 123456, handlerContracts: { refund: { timelock: "" }, claim: {...} }, fees: new StarknetFees(...), starknetConfig: {...} }, ... }, ... }); ``` ### EVM Chains Every EVM initializer exposes its own options type built on [`EVMOptions`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-evm/src/type-aliases/EVMOptions). For example, `chains.CITREA` uses [`CitreaOptions`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-evm/src/type-aliases/CitreaOptions), which is `EVMOptions<"MAINNET" | "TESTNET4", CitreaFees>`. Shared `EVMOptions` fields: | Parameter | Type | Description | | ------------------------------ | --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `rpcUrl` | `string \| JsonRpcApiProvider` | Required. EVM RPC URL or a prebuilt provider. | | `retryPolicy` | `EVMRetryPolicy` | Retry policy for EVM RPC calls. | | `chainType` | `string` | EVM network variant for that initializer. The exact union depends on the chain. For Citrea, it is `"MAINNET" \| "TESTNET4"`. | | `swapContract` | `string` | Override the Escrow Manager contract address. | | `swapContractDeploymentHeight` | `number` | Override the swap contract deployment height used as the event-query genesis. | | `btcRelayContract` | `string` | Override the BTC Relay contract address. | | `btcRelayDeploymentHeight` | `number` | Override the BTC Relay deployment height used as the event-query genesis. | | `spvVaultContract` | `string` | Override the SPV Vault manager contract address. | | `spvVaultDeploymentHeight` | `number` | Override the SPV Vault deployment height used as the event-query genesis. | | `handlerContracts` | `{ refund?: { timelock?: string }, claim?: { [type in ChainSwapType]?: string } }` | Optional refund and claim handler overrides. Use `refund.timelock` to override the refund handler and `claim[ChainSwapType]` to override individual claim handlers. | | `fees` | `EVMFees` | Custom EVM fee API implementation for that chain. Generally uses the [EVMFees](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-evm/src/classes/EVMFees) class, only for Citrea the custom [CitreaFees](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-evm/src/classes/CitreaFees) class is used, because of additional state diff fees. | | `evmConfig` | `Partial>` | Advanced EVM configuration. Those omitted fields are controlled by the initializer itself. | For Citrea specifically, `chainType` is derived automatically from `bitcoinNetwork` on `MAINNET` and `TESTNET4`. On other Bitcoin networks, set it explicitly. ``` const swapper = Factory.newSwapper({ chains: { CITREA: { rpcUrl: "https://rpc.citrea.xyz", //Or `new JsonRpcProvider("https://rpc.citrea.xyz")` retryPolicy: {...}, chainType: "MAINNET", swapContract: "", swapContractDeploymentHeight: 123456, btcRelayContract: "", btcRelayDeploymentHeight: 123456, spvVaultContract: "", spvVaultDeploymentHeight: 123456, handlerContracts: { refund: { timelock: "" }, claim: {...} }, fees: new CitreaFees(...), evmConfig: {...} }, ... }, ... }); ``` ## Debug Logging Verbosity of the atomiq packages is controlled by the global `atomiqLogLevel` flag. Set it at any time before initiating the SDK to control the log verbosity level. ``` // Enable verbose logging global.atomiqLogLevel = 3; ``` | Level | Description | | ----- | ------------------- | | 0 | No logs | | 1 | Errors only | | 2 | Warnings and errors | | 3 | Verbose (all logs) | --- # Events The SDK emits events on both individual swaps and the top-level `swapper`. Use them for real-time UI state, pending swap tracking, analytics, LP monitoring, and route-limit refreshes. tip See the complete working example: [utils/events.ts](https://github.com/atomiqlabs/atomiq-sdk-demo/blob/main/src/utils/events.ts) ## Event Emitters The SDK uses the standard javascript [`EventEmitter`](https://nodejs.org/api/events.html#class-eventemitter) pattern in both places - individual swaps and the overall `Swapper` instance: * `swapper` itself extends `EventEmitter`, so you can subscribe directly with `swapper.on(...)`. * Each `ISwap` exposes its own `EventEmitter` on `swap.events`, using the same `on(...)` / `off(...)` API. Here is a list of the SDK emitted events on the individual swaps and on the `Swapper` instance: | Emitter | Event | Payload | Description | | -------------- | ------------------- | ---------------- | ------------------------------------- | | `ISwap.events` | `swapState` | `ISwap` | The specific swap changed state | | `Swapper` | `swapState` | `ISwap` | Any tracked swap changed state | | `Swapper` | `swapLimitsChanged` | none | Swap minimum / maximum limits changed | | `Swapper` | `lpsAdded` | `Intermediary[]` | New LPs were discovered | | `Swapper` | `lpsRemoved` | `Intermediary[]` | Known LPs were removed | Example snippet showing how to add new event listeners using the `.on()` method and remove existing listeners using the `.off()` method. ``` import {ISwap, Intermediary} from "@atomiqlabs/sdk"; // Create functions that will fire on events const handleSwapState = (updatedSwap: ISwap) => { console.log("Swap state changed:", updatedSwap.getStateInfo().name); }; const handleLpsAdded = (lps: Intermediary[]) => { console.log("New LPs discovered:", lps.length); }; // Subscribe to relevant events on the swapper instance directly swapper.on("swapState", handleSwapState); // Fires for all swap state changes swapper.on("lpsAdded", handleLpsAdded); // Or subscribe directly on a specific swap instance swap.events.on("swapState", handleSwapState); // Fires only when the specific swap's state changes // Later, unsubscribe with the same handler reference swapper.off("swapState", handleSwapState); swapper.off("lpsAdded", handleLpsAdded); swap.events.off("swapState", handleSwapState); ``` For the meaning of individual swap states, see the swap-specific guides under [Swap Tutorials](https://docs.atomiq.exchange/sdk-guide/swaps/.md). ## Listening to a Single Swap Each swap exposes an `events` emitter. The event payload is the current swap instance, so the usual pattern is to re-read the state through `getState()` or `getStateInfo()`. ``` import {ISwap} from "@atomiqlabs/sdk"; const handleSwapState = (updatedSwap: ISwap) => { const state = updatedSwap.getState(); const info = updatedSwap.getStateInfo(); console.log("Swap ID:", updatedSwap.getId()); console.log("State:", state, info.name); console.log("Description:", info.description); }; swap.events.on("swapState", handleSwapState); // Later, unsubscribe swap.events.off("swapState", handleSwapState); ``` tip This is the right choice when a screen or component is focused on a single known swap. ## Listening on the Whole Swapper The top-level `swapper` aggregates swap state changes across all currently tracked swaps and also emits system-level events for LP discovery and swap minimum / maximum limit updates. ### Global swap state updates `swapState` triggers when any of the tracked swaps changes state, allowing you to track states of all currently active swaps. This is useful for updating the pending swaps list or for notifications. ``` swapper.on("swapState", (updatedSwap) => { const info = updatedSwap.getStateInfo(); console.log( `Swap ${updatedSwap.getId()} changed to ${info.name}: ${info.description}` ); }); ``` ### Swap limit updates `swapLimitsChanged` is emitted when the known bounds for one or more routes change. This can happen because: * new LPs were discovered or existing LPs disappeared. * an LP returned updated min/max bounds after a quote failed as out of range. ``` swapper.on("swapLimitsChanged", () => { const limits = swapper.getSwapLimits(srcToken, dstToken); updateLimitUI(limits); }); ``` info For more details on swap limits, see [Swap Limits](https://docs.atomiq.exchange/sdk-guide/utilities/swap-limits.md). ### LP discovery updates If your UI exposes LP status or route availability, you can also listen for LP discovery events directly: ``` swapper.on("lpsAdded", (lps) => { console.log("LPs added:", lps.map(lp => lp.url)); }); swapper.on("lpsRemoved", (lps) => { console.log("LPs removed:", lps.map(lp => lp.url)); }); ``` info In most apps, `swapLimitsChanged` is enough for route refreshes. The LP events are mainly useful when you want to show LP availability explicitly or log discovery changes. ## Best Practices * always unsubscribe listeners when a component unmounts or when you stop tracking a swap. * treat the emitted swap object in `swapState` event as the source of truth and re-read derived values through getters such as `getStateInfo()`, `getInput()`, or `getOutput()`. * use `swapLimitsChanged` to dynamically update your UI with newer swap minimum / maximum limits ## API Reference * [ISwap.events](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ISwap#events) - Per-swap event emitter * [getState](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ISwap#getstate) - Get current numeric state * [getStateInfo](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ISwap#getstateinfo) - Get human-readable state info * [Swapper](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/Swapper) - Top-level SDK client exposing global events * [getSwapLimits](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/Swapper#getswaplimits) - Re-read route limits after `swapLimitsChanged` ## Next Steps ### Swap Limits Use `swapLimitsChanged` together with the route-bound helpers when building quote forms. **[Swap Limits →](https://docs.atomiq.exchange/sdk-guide/utilities/swap-limits.md)** *** --- # Manual Transactions Use the `txs*()` methods when your app needs to sign and broadcast smart-chain transactions outside the SDK's built-in signer flow. This is useful for manual signing flows (e.g. hardware wallets or custom custody solutions) or apps where transaction approval is handled by another application layer. tip * [smartchain-to-btc/swapAdvancedSolana.ts](https://github.com/atomiqlabs/atomiq-sdk-demo/blob/main/src/smartchain-to-btc/swapAdvancedSolana.ts) * [smartchain-to-btc/swapAdvancedStarknet.ts](https://github.com/atomiqlabs/atomiq-sdk-demo/blob/main/src/smartchain-to-btc/swapAdvancedStarknet.ts) * [smartchain-to-btc/swapAdvancedEVM.ts](https://github.com/atomiqlabs/atomiq-sdk-demo/blob/main/src/smartchain-to-btc/swapAdvancedEVM.ts) * [smartchain-to-btcln/swapAdvancedSolana.ts](https://github.com/atomiqlabs/atomiq-sdk-demo/blob/main/src/smartchain-to-btcln/swapAdvancedSolana.ts) * [btc-to-smartchain/swapAdvancedEVM.ts](https://github.com/atomiqlabs/atomiq-sdk-demo/blob/main/src/btc-to-smartchain/swapAdvancedEVM.ts) ## How Manual Execution Works Each action method has a transaction-producing counterpart and a matching wait method: | Action | Manual transaction method | Wait after broadcasting | | ------------------ | ------------------------- | ----------------------- | | `commit()` | `txsCommit()` | `waitTillCommited()` | | `claim()` | `txsClaim()` | `waitTillClaimed()` | | `refund()` | `txsRefund()` | `waitTillRefunded()` | | `commitAndClaim()` | `txsCommitAndClaim()` | `waitTillClaimed()` | The manual transaction flow is always the same: 1. Create the swap quote normally with `swapper.swap(...)`. 2. Call the relevant `txs*()` method to get the smart-chain transactions. 3. Sign and broadcast the returned transactions with your external wallet flow. 4. Call the matching `waitTill*()` method so the SDK can observe the result, update storage, and emit swap state changes. ``` // 1. Get the smart-chain transactions for the next action const txsCommit = await swap.txsCommit(); // 2. Sign and broadcast them with your own wallet flow ... // 3. Let the SDK observe the result and update swap state await swap.waitTillCommited(); ``` info `txs*()` only replaces the smart-chain signing and broadcasting step. Bitcoin-side and Lightning-side steps such as `waitForBitcoinTransaction()`, `getFundedPsbt()`, or `waitForPayment()` stay the same as in the regular swap tutorials. ## Sending Transactions by Chain The returned transaction shape depends on the smart chain. The same pattern applies whether you are calling `txsCommit()`, `txsClaim()`, `txsRefund()`, or `txsCommitAndClaim()`. * Solana * Starknet * EVM For Solana, each returned [SolanaTx](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-solana/src/type-aliases/SolanaTx) contains a `tx` and any additional `signers` that the SDK needs to attach before your wallet signs. ``` import {Connection} from "@solana/web3.js"; const connection = new Connection(rpcUrl); const txsCommit = await swap.txsCommit(); // Sign helper keypairs required by the SDK first txsCommit.forEach(({tx, signers}) => { if (signers.length > 0) { tx.sign(...signers); } }); // Then let the external wallet sign the transactions const signedTransactions = await wallet.signAllTransactions( txsCommit.map(({tx}) => tx) ); // IMPORTANT: Broadcast sequentially for (const tx of signedTransactions) { const signature = await connection.sendRawTransaction(tx.serialize()); // Wait for confirmation before sending the next transaction await connection.confirmTransaction(signature); } // Make sure you wait till the SDK processes the transaction await swap.waitTillCommited(); ``` For Starknet, the returned [StarknetTx](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-starknet/src/type-aliases/StarknetTx) objects tell you whether to execute a normal invoke or deploy an account first. ``` const txsCommit = await swap.txsCommit(); // Starknet has two transaction types: INVOKE and DEPLOY_ACCOUNT // SDK automatically detects if account is not deployed and adds a DEPLOY_ACCOUNT transaction // IMPORTANT: Broadcast sequentially for (const tx of txsCommit) { // You may also sign all the transactions first and then broadcast them if (tx.type === "INVOKE") { await account.execute(tx.tx, tx.details); } if (tx.type === "DEPLOY_ACCOUNT") { await account.deployAccount(tx.tx, tx.details); } } // Make sure you wait till the SDK processes the transaction await swap.waitTillCommited(); ``` For EVM chains, the returned objects use the `ethers` [TransactionRequest](https://docs.ethers.org/v6/api/providers/#TransactionRequest) shape. ``` const txsCommit = await swap.txsCommit(); // IMPORTANT: Broadcast sequentially for (const tx of txsCommit) { // You may also sign all the transactions first and then broadcast them const response = await wallet.sendTransaction(tx); // Wait for confirmation before sending the next transaction await response.wait(); } // Make sure you wait till the SDK processes the transaction await swap.waitTillCommited(); ``` warning Ensure that you broadcast the transactions sequentially, always waiting for the previous one to confirm before sending the next one. Otherwise, you might expose yourself to the risk of losing funds especially during HTLC swaps where the pre-image **must** only be revealed after the HTLC is initiated. ## Skip Checks for Fresh Quotes If you call `txsCommit()` immediately after a fresh quote is created, you can skip the redundant init-signature checks: ``` const txsCommit = await swap.txsCommit(true); ``` Use `skipChecks=true` only when you are executing the quote right away. Do not reuse it for older swaps that may have already expired or changed state. ## Common Mistakes * forgetting to call the matching `waitTill*()` method after broadcasting transactions. * sending returned transactions out of order when the swap flow depends on sequential confirmation. * treating `txs*()` as a replacement for Bitcoin or Lightning payment steps. It only replaces the smart-chain side. * using `skipChecks=true` on stale quotes. ## API Reference * [txsCommit](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ToBTCSwap#txscommit) - Get smart-chain commit transactions * [txsClaim](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCSwap#txsclaim) - Get smart-chain claim transactions * [txsRefund](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ToBTCSwap#txsrefund) - Get smart-chain refund transactions * [txsCommitAndClaim](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCLNSwap#txscommitandclaim) - Get combined commit and claim transactions * [waitTillCommited](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ToBTCSwap#waittillcommited) - Wait for commit to be observed by the SDK * [waitTillClaimed](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCSwap#waittillclaimed) - Wait for claim or automatic settlement * [waitTillRefunded](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ToBTCSwap#waittillrefunded) - Wait for refund to be observed by the SDK ## Next Steps ### Swap Tutorials Use the route-specific swap guides to see where each manual transaction step fits into the full swap flow. **[Swap Tutorials →](https://docs.atomiq.exchange/sdk-guide/swaps/.md)** *** ### Claiming and Refunds Manual transaction signing is commonly paired with recovery flows for past swaps. **[Swap Management →](https://docs.atomiq.exchange/sdk-guide/swap-management/.md)** *** --- # Storage The SDK persists initiated swaps and a small amount of chain-specific helper data. In browser environments this works out of the box. In Node.js or custom environments you need to provide storage backends explicitly. Storage is what enables: * recovering swaps after app restarts. * swap getters like `getSwapById()` and `getAllSwaps()`. * refund and claim recovery flows after app restarts. * long-running apps that need durable swap history. ## What the SDK Stores The swapper uses two separate storage backends: | Option | Purpose | Browser default | | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | | `swapStorage` | Indexed swap database used for swap history that allows querying data based on indexes | Indexed DB | | `chainStorageCtor` | General-purpose key-value storage used by chain integrations for auxiliary data (e.g. Solana ephemeral data storage accounts are saved here) | Local Storage | info By default, only initiated swaps are persisted. If you need to retrieve quotes that were created but not yet committed or executed, enable `saveUninitializedSwaps` in [Configuration](https://docs.atomiq.exchange/sdk-guide/advanced/configuration.md#runtime-flags). info If multiple SDK instances or processes access the same swap database, also set `noSwapCache: true` so each read is refreshed from persistent storage instead of the in-memory `WeakRef` cache. ## Default Setups The SDK supports browser-based environments out of the box, React Native environments with the `@atomiqlabs/storage-rn-async` adapter, and backend Node.JS environments with the atomiq-built SQLite storage backend. If you use a different storage backend (i.e. other SQL or NoSQL databases) check the [Implementing Custom Storage](#implementing-custom-storage) section. * Browser * Node.js * React Native In the browser, no extra storage configuration is required. The SDK uses IndexedDB for swaps and browser local storage for chain-level key-value data. ``` const swapper = Factory.newSwapper({ chains: { SOLANA: {rpcUrl: solanaRpc}, STARKNET: {rpcUrl: starknetRpc} }, bitcoinNetwork: BitcoinNetwork.MAINNET }); ``` In Node.js, provide both `swapStorage` and `chainStorageCtor`. The recommended backend is SQLite provided by the `@atomiqlabs/storage-sqlite` npm package: ``` npm install @atomiqlabs/storage-sqlite@latest ``` ``` import {SqliteStorageManager, SqliteUnifiedStorage} from "@atomiqlabs/storage-sqlite"; const swapper = Factory.newSwapper({ ... swapStorage: storageName => new SqliteUnifiedStorage(`${storageName}.sqlite3`), chainStorageCtor: storageName => new SqliteStorageManager(`${storageName}.sqlite3`) }); ``` warning If you run outside the browser, do not provide only one of the two hooks. The SDK expects both persistent swap storage and chain-level key-value storage to exist. In React Native, provide both `swapStorage` and `chainStorageCtor` using the `@atomiqlabs/storage-rn-async` package. This adapter uses `@react-native-async-storage/async-storage` under the hood, because React Native does not provide the browser IndexedDB storage used by default in web environments. ``` npm install @atomiqlabs/storage-rn-async ``` ``` import {RNAsyncStorageManager, RNAsyncUnifiedStorage} from "@atomiqlabs/storage-rn-async"; const swapper = Factory.newSwapper({ ... // React Native does not provide the browser IndexedDB storage // used by default in web environments, so provide AsyncStorage-backed adapters. swapStorage: storageName => new RNAsyncUnifiedStorage(storageName), chainStorageCtor: storageName => new RNAsyncStorageManager(storageName) }); ``` warning `RNAsyncUnifiedStorage` builds the SDK's indexed storage behavior on top of the key-value AsyncStorage, so indexing is handled in memory. In React Native, this is naturally a client-side, per-device storage setup. The practical limit is therefore the size of the local dataset on that device, so this is suitable for per-device datasets with fewer than roughly `10,000` saved swaps. ## Implementing Custom Storage Use custom storage when you want the SDK to persist into your existing backend, such as AWS DynamoDB, Azure Cosmos DB, PostgreSQL, MySQL, MongoDB Atlas, or another hosted database service. info If you already have a simple key-value store and do not want to implement both interfaces from scratch, you can use the [`@atomiqlabs/storage-memory-indexed-kv`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-storage-memory-indexed-kv/src) wrapper, which can use any simple key-value storage backend (synchronous or async). It keeps the indexes required for `query()` operations in memory and serializes writes through a single queue. It is meant for single-user, client-side style datasets, not shared multi-user backends or large server-side swap databases. `Factory.newSwapper()` expects two storage hooks: * `swapStorage(storageName)` for the indexed swap database, returning an [`IUnifiedStorage`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/interfaces/IUnifiedStorage) implementation. * `chainStorageCtor(storageName)` for the chain-specific key-value storage, returning an [`IStorageManager`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/interfaces/IStorageManager) implementation. The `storageName` argument is the SDK namespace for that storage instance, which depends on the current environment (i.e. mainnet, testnet or regtest). Depending on your backend, you can use it as a table name, collection name, partition key prefix, tenant key, or any other logical namespace. ``` const swapper = Factory.newSwapper({ ... // Use `storageName` for e.g. the table name swapStorage: storageName => new MyUnifiedStorage({ databaseName: "atomiq_swaps", tableName: storageName }), // This backend is for chain-level key-value data, not swap history. chainStorageCtor: storageName => new MyStorageManager({ databaseName: "atomiq_chain_state", tableName: storageName }) }); ``` tip For an example storage backend integration, you can check out how the SQLite storage library implements the storage interfaces: * [`SqliteUnifiedStorage`](https://github.com/atomiqlabs/atomiq-storage-sqlite/blob/main/src/SqliteUnifiedStorage.ts) for the indexed `IUnifiedStorage` implementation. * [`SqliteStorageManager`](https://github.com/atomiqlabs/atomiq-storage-sqlite/blob/main/src/SqliteStorageManager.ts) for the `IStorageManager` implementation. ### `IUnifiedStorage` `swapStorage` must return an implementation of [`IUnifiedStorage`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/interfaces/IUnifiedStorage). This is the swap database used by the SDK. Treat it as an indexed JSON document store keyed by swap `id`, with additional indexes based on the statically typed `UnifiedSwapStorageIndexes` and `UnifiedSwapStorageCompositeIndexes` arguments passed to `init()`. Your backend needs to support all of the following: * upserts by `id`, because `save()` and `saveAll()` are used both when swaps are first created and when existing swaps are updated later. * deletes by `id` for cleanup and expired swap removal. * equality queries over the indexes declared in `init(indexes, compositeIndexes)`. Your backend needs to implement the following interface: ``` import { IUnifiedStorage, QueryParams, UnifiedStoredObject, UnifiedSwapStorageCompositeIndexes, UnifiedSwapStorageIndexes, } from "@atomiqlabs/sdk"; // Shown as a concrete class shell for documentation purposes. // In your implementation, replace these empty bodies with real logic. class MyUnifiedStorage implements IUnifiedStorage< UnifiedSwapStorageIndexes, UnifiedSwapStorageCompositeIndexes > { // These inputs are statically typed, so you can either create the schema here // or validate a schema that was provisioned earlier by migrations / DDL. init( indexes: UnifiedSwapStorageIndexes, compositeIndexes: UnifiedSwapStorageCompositeIndexes ): Promise {} // `params` is OR-of-ANDs: // [[{key: "type", value: 1}, {key: "state", value: [2, 3]}], [{key: "id", value: "swap-123"}]] // means: // (type = 1 AND state IN [2, 3]) OR (id = "swap-123") query(params: QueryParams[][]): Promise {} // Upsert by `value.id`. The SDK calls this when a swap is first persisted // and again when that same swap changes state. save(value: UnifiedStoredObject): Promise {} // Same semantics as repeated `save()`, just batched for efficiency. saveAll(values: UnifiedStoredObject[]): Promise {} // Delete by `value.id`. remove(value: UnifiedStoredObject): Promise {} // Same semantics as repeated `remove()`, just batched for efficiency. removeAll(values: UnifiedStoredObject[]): Promise {} } ``` #### Indexes `init(indexes, compositeIndexes)` tells your backend exactly which lookup patterns the SDK will use. These arguments are statically typed as [`UnifiedSwapStorageIndexes`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/UnifiedSwapStorageIndexes) and [`UnifiedSwapStorageCompositeIndexes`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/UnifiedSwapStorageCompositeIndexes), so the set of supported index shapes is known ahead of time. That means you can pre-create the database schema manually with migrations, raw SQL, or cloud-database provisioning tools, and then have `init()` only validate or open it. The SDK still passes both index lists to `init()` so implementations that prefer auto-provisioning can create them there on first run. The SDK currently provides these single-field indexes through [`UnifiedSwapStorageIndexes`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/UnifiedSwapStorageIndexes): | Key | Type | Unique | Nullable | | ------------- | -------- | ------ | -------- | | `id` | `string` | ☑ | ☐ | | `escrowHash` | `string` | ☑ | ☑ | | `type` | `number` | ☐ | ☐ | | `initiator` | `string` | ☐ | ☐ | | `state` | `number` | ☐ | ☐ | | `paymentHash` | `string` | ☐ | ☑ | The SDK currently provides these composite indexes through [`UnifiedSwapStorageCompositeIndexes`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/UnifiedSwapStorageCompositeIndexes): | Keys | Unique | | -------------------------------- | ------ | | `["initiator", "id"]` | ☐ | | `["type", "state"]` | ☐ | | `["type", "paymentHash"]` | ☐ | | `["type", "initiator", "state"]` | ☐ | #### Queries `query()` receives `QueryParams[][]`, where [`QueryParams`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/QueryParams) is `{key: string, value: any | any[]}`. A single `QueryParams` entry matches one field (identified by its `key` parameter), with the `value` being: * an exact (non-array) value, which means "match this exact value", similar to SQL's `=` operator * an array, which means "match any of these values for the same key", similar to SQL's `IN (...)` The full `QueryParams[][]` input then forms an OR-of-ANDs query: the outer array is the OR layer, with each inner array being one AND group, and each group contains one or more `QueryParams` conditions. Here is a comparison between various input arguments and their SQL equivalents: | Query Input | SQL Equivalent | | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | | `[[{key: "id", value: "swap-123"}]]` | `id = "swap-123"` | | `[[{key: "type", value: 1}, {key: "state", value: [2, 3]}]]` | `type = 1 AND state IN (2, 3)` | | `[[{key: "type", value: 1}, {key: "state", value: 2}], [{key: "type", value: 4}, {key: "initiator", value: "alice"}]]` | `(type = 1 AND state = 2) OR (type = 4 AND initiator = "alice")` | info You should expect the SDK to construct queries only from the declared single-field and composite indexes. For composite lookups, the keys will be passed in the exact order in which that composite index is defined. For example, the composite index `["type", "state"]` will be queried as `[{key: "type", ...}, {key: "state", ...}]`, not in any other order. Your backend should therefore optimize for these exact lookup shapes; a full scan may still be useful as a defensive fallback, but it should not be the expected production path. #### Save and Remove Operations `save()` and `saveAll()` must behave like upserts, not insert-only writes. The SDK uses them for both initial persistence and later state transitions of the same swap, always keyed by the serialized object's `id` field. `remove()` and `removeAll()` are the delete counterparts. They should remove the stored swap by that same `id` key, including any related index entries your backend maintains. As with the save methods, the `All` variants should have the same semantics as repeated single-item operations, just implemented more efficiently when your backend supports batch writes or deletes. ### `IStorageManager` `chainStorageCtor` must return an [`IStorageManager`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/interfaces/IStorageManager) implementation for general key-value storage used by the chain integrations. This is not the swap history database. It is a typed key-value store for auxiliary chain data. The key behavioral requirement here is that `loadData()` must load every stored record for that namespace, recreate typed objects, and populate the in-memory `data` map. Chain integrations may call `loadData()` once during init and then read directly from `storage.data` afterwards. This can be a class or any object that satisfies the following interface: ``` import {StorageObject} from "@atomiqlabs/sdk"; export interface IStorageManager { // In-memory cache of stored objects, keyed by id/hash. data: { [key: string]: T }; // Initializes the storage backend. init(): Promise; // Saves an object to storage. This is used for both new records and updates. // `StorageObject`s implement `serialize()`, which returns a JSON-stringify-safe object. saveData(hash: string, object: T): Promise; // Removes an object from storage. removeData(hash: string): Promise; // Loads all stored objects, recreates them via `new type(...)`, // returns them, and also repopulates `data`. loadData(type: new (data: any) => T): Promise; // Optional batch delete optimization. removeDataArr?(keys: string[]): Promise; // Optional batch upsert optimization. saveDataArr?(values: {id: string, object: T}[]): Promise; } ``` Important details: * `StorageObject` instances implement a `serialize()` function, which returns a JSON-stringify-safe object * `saveData()` is used for both creating new records and updating existing ones, so implement it as an upsert * keep `data` synchronized with every successful save or delete. * `loadData()` must read every stored key-value pair, deserialize it with `new type(serializedValue)`, return those objects, and also repopulate `data`, keyed by the stored `id`. * `saveDataArr()` and `removeDataArr()` are optional batch helpers. Implement them only if your backend has efficient batch APIs; otherwise, leaving them undefined is fine. ## API Reference * [SwapperFactory](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperFactory) - Creates swapper instances and accepts the storage hooks * [IUnifiedStorage](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/interfaces/IUnifiedStorage) - Swap storage interface * [UnifiedSwapStorageIndexes](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/UnifiedSwapStorageIndexes) - Static single-field index definition passed to `IUnifiedStorage.init()` * [UnifiedSwapStorageCompositeIndexes](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/UnifiedSwapStorageCompositeIndexes) - Static composite index definition passed to `IUnifiedStorage.init()` * [Historical Swaps](https://docs.atomiq.exchange/sdk-guide/swap-management/historical-swaps.md) - Retrieving persisted swaps * [Configuration](https://docs.atomiq.exchange/sdk-guide/advanced/configuration.md) - Runtime flags such as `saveUninitializedSwaps`, `noSwapCache`, and `storagePrefix` ## Next Steps ### Configuration For more runtime options affecting swapper behavior, continue with the configuration guide. **[Configuration →](https://docs.atomiq.exchange/sdk-guide/advanced/configuration.md)** *** ### Quick Start Continue with the main quick start index to pick the setup flow for your environment. **[Quick Start →](https://docs.atomiq.exchange/sdk-guide/quick-start/.md)** *** --- # Quick Start This section is the shortest path from an empty project to a working swap. The guides here help you pick the right runtime, initialize the SDK, connect wallets or signers, request a quote, and execute the route. info The Quick Start pages are meant for the default high-level SDK flow. If your integration later needs custom persistence, event handling, or manual transaction signing, continue with the [Advanced](https://docs.atomiq.exchange/sdk-guide/advanced/.md) section after the basic flow is working. ## Usage Most integrations use this section in the following order: ### Environment Setup * Use [Quick Start - Browser](https://docs.atomiq.exchange/sdk-guide/quick-start/quick-start-browser.md) for browser-based web apps that connect user wallets directly through wallet adapters, injected wallets, or browser wallet connection flows. * Use [Quick Start - Node.js](https://docs.atomiq.exchange/sdk-guide/quick-start/quick-start-nodejs.md) for Node.js backends that require explicit storage setup to run the SDK outside the browser and typically use keypair-based signers. ### Swap Flow 1. Start with [Creating Quotes](https://docs.atomiq.exchange/sdk-guide/quick-start/creating-quotes.md) once you have the swapper instance initialized and are ready to get your first swap quote. 2. Continue with [Executing Swaps](https://docs.atomiq.exchange/sdk-guide/quick-start/executing-swaps.md) once you are ready to move from inspecting the quote to the full swap flow and need to understand the expected signer or wallet inputs for each swap protocol. ## Network Availability Not all chains are available on all Bitcoin testnets. Use the table below to pick the right `bitcoinNetwork` setting for your chain combination: | Chain | Mainnet | Testnet3 | Testnet4 | | ---------------- | ------------ | -------- | -------- | | **Solana** | Mainnet-beta | Devnet | - | | **Starknet** | Mainnet | Sepolia | Sepolia | | **Citrea** | Mainnet | - | Testnet | | **Botanix** | Mainnet | Testnet | - | | **Alpen** | - | Testnet | Testnet | | **GOAT Network** | - | Testnet | Testnet | The `bitcoinNetwork` setting determines both the Bitcoin network and which smart chain network is used by the chain initializers. tip * **Bitcoin Testnet/Testnet4** - Use a [Bitcoin testnet faucet](https://bitcoinfaucet.uo1.net/) or [Bitcoin testnet4 faucet](https://mempool.space/testnet4/faucet) * **Solana Devnet** - Use Solana CLI's `solana airdrop 1` or the [Solana faucet](https://faucet.solana.com/) * **Starknet Sepolia** - Use the [Starknet faucet](https://starknet-faucet.vercel.app/) * **Botanix Testnet** - Use the [Botanix faucet](https://faucet.botanixlabs.dev/) * **Alpen Testnet** - Use the [Alpen CLI](https://docs.alpenlabs.io/welcome/using-the-alpen-cli#user-content-request-btc) ## Topics ### Quick Start - Browser Install the browser SDK packages, initialize the swapper, and connect supported browser wallets. **[Quick Start - Browser →](https://docs.atomiq.exchange/sdk-guide/quick-start/quick-start-browser.md)** *** ### Quick Start - Node.js Install the Node.js SDK packages, configure storage, initialize the swapper, and construct server-side signers. **[Quick Start - Node.js →](https://docs.atomiq.exchange/sdk-guide/quick-start/quick-start-nodejs.md)** *** ### Creating Quotes Request quotes from LPs and inspect the resulting swap object before moving on to execution. **[Creating Quotes →](https://docs.atomiq.exchange/sdk-guide/quick-start/creating-quotes.md)** *** ### Executing Swaps Execute the swap and understand which signer or wallet type is needed for each route direction. **[Executing Swaps →](https://docs.atomiq.exchange/sdk-guide/quick-start/executing-swaps.md)** *** --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; # Creating Quotes Every swap starts with a quote. Call `swapper.swap()` to create one — this contacts LPs, finds the best rate, and returns a swap object you can inspect before executing the swap. ``` import {SwapAmountType} from "@atomiqlabs/sdk"; const swap = await swapper.swap( fromToken, // Source token (e.g. Tokens.BITCOIN.BTC, Tokens.STARKNET.STRK) toToken, // Destination token (e.g. Tokens.CITREA.CBTC, Tokens.BITCOIN.BTCLN) amount, // Amount as string (in human readable format - "0.0001") or bigint (in base units - 10000n) amountType, // SwapAmountType.EXACT_IN (specifying exact input amount) or SwapAmountType.EXACT_OUT (specifying exact output amount) sourceAddress, // Source wallet address [not required for BTC/Lightning source] destinationAddress, // Destination wallet address, Lightning invoice, or LNURL options? // Optional additional options, e.g. { gasAmount } ); ``` info Preferably use the `Tokens..` notation for `fromToken` and `toToken` for to get automatic swap type inference. You can alternatively also use ticker strings such as `"BTC"`, `"BTC-LN"`, `"WBTC"`, or chain + ticker combination, such as `"BITCOIN-BTC"`, `"LIGHTNING-BTC"`, `"STARKNET-WBTC"`, `"SOLANA-WBTC"`, `"CITREA-CBTC"`. But these don't automatically infer the swap type of the returned quote. You can use the [isSwapType](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/functions/isSwapType) type-guard to narrow down the type in those cases. ## Example ``` const swap = await swapper.swap( Tokens.STARKNET.STRK, // Correct token notation for automatic swap type inference Tokens.BITCOIN.BTC, // Correct token notation for automatic swap type inference "0.0001", // Amount as human readable string SwapAmountType.EXACT_OUT, // Receive exactly 0.0001 BTC starknetSigner.getAddress(), // Sender wallet address, in this case Starknet wallet address "bc1q..." // Destination address, in this case a Bitcoin on-chain address ); // Type of swap will get correctly infered as `ToBTCSwap` ``` ## Inspecting the Quote Once quote is created you can check the amounts, fees, pricing. This is fixed after a quote is received and valid until quote expiry. Atomiq protocol uses an RFQ model so there is no additional slippage! ### Amounts Amounts are returned as [TokenAmount](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/TokenAmount) objects, making it easy to print them out, show them in human-readable format, get the amount in base units or estimate their value in USD-terms. ``` swap.getInput() // Total input amount (including fees) swap.getInputWithoutFee() // Input amount excluding fees swap.getOutput() // Output amount you'll receive ``` ### Fees Fees are returned as [Fee](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/Fee) objects, making it easy to get the fee denominated in input/output (source & destination) tokens or in USD. Relevant fee types returned by the `getFeeBreakdown()` function use the [FeeType](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/enumerations/FeeType) enum. ``` import {FeeType} from "@atomiqlabs/sdk"; swap.getFee().amountInSrcToken // Total fee in source token swap.getFee().amountInDstToken // Total fee in destination token // Detailed fee breakdown for (const fee of swap.getFeeBreakdown()) { console.log(`${FeeType[fee.type]}: ${fee.fee.amountInSrcToken}`); } ``` ### Pricing Return the information about the swap price, the current market price and the difference between the two. ``` const priceInfo = swap.getPriceInfo(); priceInfo.swapPrice // Effective swap price priceInfo.marketPrice // Current market price priceInfo.difference // Difference between swap and market price ``` ### Quote Expiry Every created quote has an expiry time until which it needs to be executed, trying to execute the quote after expiry will lead to reverted transactions. ``` swap.getQuoteExpiry() // Timestamp (ms) when the quote expires ``` ### Checking Swap State Check the current lifecycle state of the swap, the main state-check helpers are: * `swap.isQuoteExpired()` - whether the quote has definitely expired and cannot be initiated anymore * `swap.isQuoteSoftExpired()` - whether the quote is close enough to expiry that there might not be enough time buffer left to execute it, it is usually good practice to show the quote as expired to the user at this point already * `swap.isFinished()` - whether the swap reached a terminal state (can be either success or fail) * `swap.isSuccessful()` - whether the swap finished successfully * `swap.isFailed()` - whether the swap failed, for example because it was refunded ``` if (swap.isQuoteExpired()) { throw new Error("Quote expired"); } if (swap.isFinished()) { if (swap.isSuccessful()) console.log("Swap finished successfully"); if (swap.isFailed()) console.log("Swap finished in a failed state"); } ``` info If you need the raw swap type specific state value, use [`getState()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ISwap#getstate). If you want something human-readable, use [`getStateInfo()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ISwap#getstateinfo). ## EXACT\_IN vs EXACT\_OUT * **EXACT\_IN** — You specify how much to spend, the SDK calculates what you'll receive * **EXACT\_OUT** — You specify how much to receive, the SDK calculates the cost ``` // Spend exactly 100 STRK const swap = await swapper.swap( Tokens.STARKNET.STRK, Tokens.BITCOIN.BTC, "100", SwapAmountType.EXACT_IN, starknetSigner.getAddress(), "bc1q..." ); // Receive exactly 10,000 sats const swap = await swapper.swap( Tokens.STARKNET.STRK, Tokens.BITCOIN.BTC, 10000n, SwapAmountType.EXACT_OUT, starknetSigner.getAddress(), "bc1q..." ); ``` ## Gas Drop info This is so far not supported on **Solana**, which still uses legacy swap protocol. When swapping from Bitcoin on-chain or lightning to smart chains (i.e. Starknet, EVM chains) and swapping to non-native token (e.g. WBTC on Starknet), you can additionally request to receive some amount of native token along with your swap - this ensures that you can have some native token ready to cover gas fees when doing your first transactions on the destination chain. ``` // Swapping 0.0001 BTC to WBTC on Starknet and requesting to also receive 1 STRK gas drop const swap = await swapper.swap( Tokens.BITCOIN.BTC, Tokens.STARKNET.WBTC, "0.0001", SwapAmountType.EXACT_IN, undefined, // Not required when using Bitcoin or Lightning as source starknetSigner.getAddress(), // Destination Starknet signer address { gasAmount: "1" // Request 1 STRK in addition as gas drop } ); swap.getGasDropOutput() // Amount of native token gas drop ``` When using `SwapAmountType.EXACT_IN`, the value of the gas drop gets deducted from the output token amount by the LP, while with `SwapAmountType.EXACT_OUT` the gas token value gets added to the input token amount. ## Swap Options The `options` argument of `swapper.swap(...)` lets you fine-tune how a quote is created. In most cases you can omit it entirely, but it is useful when you want to request a gas drop, customize Lightning routing limits, attach invoice metadata, or relax certain safety checks for advanced flows. The exact option set depends on the swap type, so the overview below groups the available non-deprecated options by swap direction. info Swaps from Bitcoin on-chain (L1) and Lightning (L2) to **Solana** still use legacy protocols and are grouped together in the `Legacy (Solana)` tab. > No swap creation options are currently exposed for this swap type. | Name | Type | Description | | ------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `expirySeconds` | `number` | HTLC expiration timeout, in seconds, offered to the LP. Larger values allow more Lightning routes, but also keep funds locked for longer if the LP does not cooperate.
Default: 5 days. | | `maxRoutingFeePercentage` | `number` | Caps the percentage component of the maximum Lightning routing fee.
Default: `0.2` (0.2% of the swap value). | | `maxRoutingBaseFee` | `bigint` | Caps the base component of the maximum Lightning routing fee, in sats.
Default: `10` sats. | | `comment` | `string` | Optional LNURL-pay comment. Only applies when the destination is an LNURL-pay endpoint and must fit the service's `commentAllowed` limit.
Default: none. | | Name | Type | Description | | -------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `gasAmount` | `bigint \| string` | Optional extra native-token gas drop to receive on the destination chain. `bigint` is in base units, `string` is a human-readable decimal amount. Do not use it when swapping to the native token itself.
Default: no gas drop. | | `maxAllowedBitcoinFeeRate` | `number` | Upper bound, in sats/vB, on the LP-required minimum Bitcoin fee rate for the funding transaction.
Default: computed dynamically as `10 + currentBitcoinFeeRate * 1.5`. | | `unsafeZeroWatchtowerFee` | `boolean` | Attaches zero watchtower fee to the swap. This can make automatic settlement unattractive, so you may need to settle manually with `swap.claim(...)`.
Default: `false`. | | `feeSafetyFactor` | `number` | Multiplier used when estimating the watchtower fee. Higher values make automatic settlement more attractive.
Default: `1.25`. | | Name | Type | Description | | ------------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `paymentHash` | `Buffer \| string` | Lets you supply your own payment hash instead of letting the SDK generate it. When set, the swap will not settle automatically until you later provide the preimage/secret manually during claim or waiting.
Default: generated automatically by the SDK. | | `description` | `string` | Optional Lightning invoice description. Keep it below 500 UTF-8 bytes.
Default: none. | | `descriptionHash` | `Buffer \| string` | Optional Lightning invoice description hash, useful when the invoice is exposed through LNURL-pay.
Default: none. | | `gasAmount` | `bigint \| string` | Optional extra native-token gas drop to receive on the destination chain. `bigint` is in base units, `string` is a human-readable decimal amount. Do not use it when swapping to the native token itself.
Default: no gas drop. | | `unsafeSkipLnNodeCheck` | `boolean` | Skips checking whether the LP's Lightning node has enough channel liquidity for the swap.
Default: `false`. | | `unsafeZeroWatchtowerFee` | `boolean` | Attaches zero watchtower fee to the swap. This can prevent automatic settlement and leave you with a manual `swap.claim(...)` flow.
Default: `false`. | | `feeSafetyFactor` | `number` | Multiplier used when estimating the watchtower fee. Higher values make automatic settlement more attractive.
Default: `1.25`. | ### BTC → Solana | Name | Type | Description | | ------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `unsafeZeroWatchtowerFee` | `boolean` | Attaches zero watchtower fee to the swap. This can make automatic settlement unattractive, so you may need to settle manually with `swap.claim(...)`.
Default: `false`. | | `feeSafetyFactor` | `number \| bigint` | Multiplier used when estimating the watchtower fee. Higher values make automatic settlement more attractive. Also accepts `bigint` for legacy compatibility.
Default: `1.5`. | ### Lightning → Solana | Name | Type | Description | | ----------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `paymentHash` | `Buffer \| string` | Lets you supply your own payment hash instead of letting the SDK generate it. If you do this, you must later reveal the preimage manually during claim.
Default: generated automatically by the SDK. | | `description` | `string` | Optional Lightning invoice description. Keep it below 500 UTF-8 bytes.
Default: none. | | `descriptionHash` | `Buffer \| string` | Optional Lightning invoice description hash, useful when the invoice is exposed through LNURL-pay.
Default: none. | | `unsafeSkipLnNodeCheck` | `boolean` | Skips checking whether the LP's Lightning node has enough channel liquidity for the swap.
Default: `false`. | ## Next Steps ### Executing the Swap Guides you through executing various swap types using the high-level `execute()` function. **[Executing Swaps →](https://docs.atomiq.exchange/sdk-guide/quick-start/executing-swaps.md)** *** ### Swap Types Lower-level swap-specific guides providing in-depth explanations and manual swap signing flows for different swap types. **[Swap Types →](https://docs.atomiq.exchange/sdk-guide/swaps/.md)** *** --- # Executing Swaps After you create and inspect a quote with `swapper.swap(...)`, the usual next step is `swap.execute(...)`. `execute()` is the high-level execution path used by the SDK. It is state-aware, so it resumes from the current swap state, performs the next on-chain or off-chain action, and then waits for automatic settlement when the protocol supports it. Generally the `execute()` function follows this syntax: ``` import {isRefundableSwap, isClaimableSwap} from "@atomiqlabs/sdk"; const result = await swap.execute(/* source signer or wallet */, /* optional callbacks */, /* optional options */); if (result === false) { // In case of Smart chain → BTC/Lightning swaps: // - the swap has failed and the user can refund with swap.refund(...) if(isRefundableSwap(swap)) await swap.refund(/* source chain signer or wallet - always a smart chain signer */); // In case of BTC/Lightning → Smart chain swaps: // - the swap wasn't settled automatically, call swap.claim(...) to settle the swap manually with the destination chain signer. if(isClaimableSwap(swap)) await swap.claim(/* destination chain signer or wallet - always a smart chain signer */); } ``` The high-level flow is shared across swap types, but the `execute(...)` signature is not identical everywhere, in general the swaps: * take a signer/wallet on the source chain * return a `boolean` indicating whether the execution was successful or not (in this case the user should either `refund()` on the source chain or `claim()` on the destination chain manually). tip If you need low-level control instead of `execute()` or want to get raw transactions and PSBTs that you sign and send manually, see the respective swap pages. ## Swap Types An overview of `execute()` function arguments for various swap types info Swaps from Bitcoin on-chain (L1) and Lightning (L2) to **Solana** use legacy swap protocols and are described below in the [Legacy Swaps](#legacy-swaps) section. * Smart chain → BTC/Lightning * BTC → Smart chain * Lightning → Smart chain > Swap classes: [ToBTCSwap](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ToBTCSwap) and [ToBTCLNSwap](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ToBTCLNSwap) Same flow is shared for both, Smart chain → BTC & Smart chain → Lightning swaps. Typical usage of the [execute()](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/IToBTCSwap#execute) function: ``` // Returns whether the swap was successfully executed by the LP, if not the user can refund const swapSuccess = await swap.execute(srcSigner, callbacks?, options?); // Refund on failure, this returns the funds back to the signer's wallet if (!swapSuccess) await swap.refund(srcSigner); ``` Type of the signer is dependent on the source network: * For Solana → BTC swaps: [SolanaSigner](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-solana/src/classes/SolanaSigner), or native `@coral-xyz/anchor` [Wallet](https://solana-foundation.github.io/anchor/ts/classes/Wallet.html) * For Starknet → BTC swaps: [StarknetSigner](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-starknet/src/classes/StarknetSigner), [StarknetBrowserSigner](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-starknet/src/classes/StarknetBrowserSigner), or native `starknet` [Account](https://starknetjs.com/docs/API/classes/Account) * For EVM → BTC swaps: [EVMSigner](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-evm/src/classes/EVMSigner), or native `ethers` [Signer](https://docs.ethers.org/v6/api/providers/#Signer) ### Callbacks | Name | Type | Description | | ------------------------------ | ----------------------------------- | ------------------------------------------------------------- | | `onSourceTransactionSent` | `(sourceTxId: string) => void` | Fired when the source smart-chain commit transaction is sent. | | `onSourceTransactionConfirmed` | `(sourceTxId: string) => void` | Fired when the source smart-chain transaction is confirmed. | | `onSwapSettled` | `(destinationTxId: string) => void` | Fired when the BTC or Lightning payment is sent. | ### Options | Name | Type | Description | | --------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `abortSignal` | `AbortSignal` | Cancels execution or waiting. | | `paymentCheckIntervalSeconds` | `number` | Polling interval used while waiting for the LP to process the payment. | | `maxWaitTillSwapProcessedSeconds` | `number` | Maximum time to wait for the LP to process the swap. Defaults to `120`. If the LP doesn't process the swap an `Error` is thrown, you can then get the swap expiration time with `swap.getExpiry()` after which you can refund unilaterally. | info For more details about the swap, swap states, going through swap steps manually or getting the transactions to sign and send outside the SDK refer to the respective [Smart Chain → Bitcoin swap](https://docs.atomiq.exchange/sdk-guide/swaps/smart-chain-to-btc.md). > Swap classes: [SpvFromBTCSwap](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SpvFromBTCSwap) Typical usage of the [execute()](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SpvFromBTCSwap#execute) function: ``` // Returns whether the swap was successfully executed automatically, if not the user can claim manually const automaticallySettled = await swap.execute(bitcoinWallet, callbacks?, options?); // Claim on failure, this settles the funds to the destination chain signer if (!automaticallySettled) await swap.claim(destinationSmartChainSigner); ``` Type of the source wallet can be: * A class extending the abstract [IBitcoinWallet](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/interfaces/IBitcoinWallet), e.g. the built-in [SingleAddressBitcoinWallet](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SingleAddressBitcoinWallet) * A simple object implementing [MinimalBitcoinWalletInterfaceWithSigner](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/MinimalBitcoinWalletInterfaceWithSigner) type: ``` const bitcoinWallet: MinimalBitcoinWalletInterfaceWithSigner = { address: "bc1p..", // Wallet address publicKey: "03030cbd...", // Wallet public key signPsbt: (psbtToSign: {psbt: Transaction, psbtHex: string, psbtBase64: string}, signInputs: number[]) => { ... // Sign the inputs specified by `signInputs` of the provided PSBT here. return signedPsbt; // Can be in base64 or hex serialized format, or `@scure/bitcoin-js` Transaction object. } } ``` ### Callbacks | Name | Type | Description | | --------------------------------------- | ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | | `onSourceTransactionSent` | `(sourceTxId: string) => void` | Fired when the BTC swap transaction is sent. | | `onSourceTransactionConfirmationStatus` | `(sourceTxId?: string, confirmations?: number, targetConfirations?: number, etaMs?: number) => void` | Progress callback while waiting for the BTC transaction to confirm. | | `onSourceTransactionConfirmed` | `(sourceTxId: string) => void` | Fired once the BTC transaction is confirmed. | | `onSwapSettled` | `(destinationTxId: string) => void` | Fired when the destination-chain settlement succeeds automatically or is fronted. | ### Options | Name | Type | Description | | --------------------------------------- | ------------- | ---------------------------------------------------------------------------- | | `feeRate` | `number` | Bitcoin fee rate to use when the SDK sends the BTC transaction. | | `abortSignal` | `AbortSignal` | Cancels execution or waiting. | | `btcTxCheckIntervalSeconds` | `number` | Polling interval while waiting for the BTC transaction to confirm. | | `maxWaitTillAutomaticSettlementSeconds` | `number` | Maximum time to wait for automatic destination settlement. Defaults to `60`. | info For more details about the swap, swap states, going through swap steps manually or getting the raw PSBT to sign outside the SDK refer to the respective [Bitcoin → Smart Chain swap](https://docs.atomiq.exchange/sdk-guide/swaps/btc-to-smart-chain.md). > Swap classes: [FromBTCLNAutoSwap](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCLNAutoSwap) Typical usage of the [execute()](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCLNAutoSwap#execute) function: ``` // Returns whether the swap was successfully executed automatically, if not the user can claim manually const automaticallySettled = await swap.execute(lightningWalletOrLnurlWithdraw?, callbacks?, options?); // Claim on failure, this settles the funds to the destination chain signer if (!automaticallySettled) await swap.claim(destinationSmartChainSigner); ``` Type of the lightning network source can be: * A simple object implementing [MinimalLightningNetworkWalletInterface](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/MinimalLightningNetworkWalletInterface): ``` const lightningWallet: MinimalLightningNetworkWalletInterface = { payInvoice: (bolt11PaymentRequest: string) => { ... // Pay the provided bolt11 payment request return bolt11PaymentHash; //Return the hexadecimal payment hash } } ``` * LNURL-withdraw as `string` or [LNURLWithdraw](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/LNURLWithdraw) object from [SwapperUtils.parseAddress()](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils#parseaddress) * Pass `null` or `undefined` if the invoice will be paid externally: ``` const bolt11PaymentInvoice = swap.getAddress(); // Returns the lightning network invoice to be displayed and paid by the user const lightningPaymentDeeplink = swap.getHyperlink(); // Or get a `lightning:` deeplink that can be displayed as QR code // Display the invoice and/or deeplink in a QR code to the user // Call execute without a wallet for it to wait till an external payment arrives const automaticallySettled = await swap.execute(null, callbacks?, options?); ``` ### Callbacks | Name | Type | Description | | ----------------------------- | ----------------------------------- | ------------------------------------------------------------------- | | `onSourceTransactionReceived` | `(sourceTxId: string) => void` | Fired when the Lightning payment is received by the LP. | | `onSwapSettled` | `(destinationTxId: string) => void` | Fired when the destination-chain settlement succeeds automatically. | ### Options | Name | Type | Description | | --------------------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `abortSignal` | `AbortSignal` | Cancels execution or waiting. | | `lightningTxCheckIntervalSeconds` | `number` | Polling interval while waiting for the Lightning payment to arrive. | | `maxWaitTillAutomaticSettlementSeconds` | `number` | Maximum time to wait for automatic destination settlement. Defaults to `60`. | | `secret` | `string` | Secret/pre-image to broadcast to watchtowers. Usually only needed when the swap was recovered or the pre-image was generated outside the SDK. | info For more details about the swap, swap states, going through swap steps manually or getting the transactions to sign and send outside the SDK refer to the respective [Lightning → Smart Chain swap](https://docs.atomiq.exchange/sdk-guide/swaps/lightning-to-smart-chain.md). ### Legacy Swaps These swaps use the legacy protocol in the Bitcoin/Lightning → Solana direction, and require a destination **Solana** wallet to sign and send transactions. * BTC → Solana * Lightning → Solana > Swap classes: [FromBTCSwap](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCSwap) Typical usage of the [execute()](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCSwap#execute) function: ``` // Returns whether the swap was successfully executed automatically, if not the user can claim manually const automaticallySettled = await swap.execute(dstSigner, bitcoinWallet?, callbacks?, options?); // Claim on failure, this settles the funds to the destination chain signer if (!automaticallySettled) await swap.claim(dstSigner); ``` Type of the destination `dstSigner` signer: [SolanaSigner](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-solana/src/classes/SolanaSigner), or native `@coral-xyz/anchor` [Wallet](https://solana-foundation.github.io/anchor/ts/classes/Wallet.html) Type of the source bitcoin wallet can be: * A class extending the abstract [IBitcoinWallet](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/interfaces/IBitcoinWallet), e.g. the built-in [SingleAddressBitcoinWallet](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SingleAddressBitcoinWallet) * A simple object implementing [MinimalBitcoinWalletInterfaceWithSigner](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/MinimalBitcoinWalletInterfaceWithSigner) type: ``` const bitcoinWallet: MinimalBitcoinWalletInterfaceWithSigner = { address: "bc1p..", // Wallet address publicKey: "03030cbd...", // Wallet public key signPsbt: (psbtToSign: {psbt: Transaction, psbtHex: string, psbtBase64: string}, signInputs: number[]) => { ... // Sign the inputs specified by `signInputs` of the provided PSBT here. return signedPsbt; // Can be in base64 or hex serialized format, or `@scure/bitcoin-js` Transaction object. } } ``` ### Callbacks | Name | Type | Description | | --------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | | `onDestinationCommitSent` | `(destinationCommitTxId: string) => void` | Fired when the destination-chain commit/open-address transaction is sent. | | `onSourceTransactionSent` | `(sourceTxId: string) => void` | Fired when the BTC funding transaction is sent. | | `onSourceTransactionConfirmationStatus` | `(sourceTxId?: string, confirmations?: number, targetConfirations?: number, etaMs?: number) => void` | Progress callback while waiting for the BTC transaction to confirm. | | `onSourceTransactionConfirmed` | `(sourceTxId: string) => void` | Fired once the BTC transaction is confirmed. | | `onSwapSettled` | `(destinationTxId: string) => void` | Fired when the destination-chain settlement is completed automatically. | ### Options | Name | Type | Description | | --------------------------------------- | ------------- | ---------------------------------------------------------------------------- | | `feeRate` | `number` | Bitcoin fee rate to use when the SDK sends the BTC transaction. | | `abortSignal` | `AbortSignal` | Cancels execution or waiting. | | `btcTxCheckIntervalSeconds` | `number` | Polling interval while waiting for the BTC transaction to confirm. | | `maxWaitTillAutomaticSettlementSeconds` | `number` | Maximum time to wait for automatic destination settlement. Defaults to `60`. | info For more details about the swap, swap states, going through swap steps manually or getting the transactions to sign and send outside the SDK refer to the respective [Bitcoin → Smart Chain swap](https://docs.atomiq.exchange/sdk-guide/swaps/btc-to-smart-chain.md). > Swap classes: [FromBTCLNSwap](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCLNSwap) Typical usage of the [execute()](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCLNSwap#execute) function: ``` // Executes the full legacy flow, always returns `true`, so there is not edge-case to cover await swap.execute(dstSigner, lightningWalletOrLnurlWithdraw?, callbacks?, options?); ``` Type of the destination `dstSigner` signer: [SolanaSigner](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-solana/src/classes/SolanaSigner), or native `@coral-xyz/anchor` [Wallet](https://solana-foundation.github.io/anchor/ts/classes/Wallet.html) Type of the lightning network source can be: * A simple object implementing [MinimalLightningNetworkWalletInterface](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/MinimalLightningNetworkWalletInterface): ``` const lightningWallet: MinimalLightningNetworkWalletInterface = { payInvoice: (bolt11PaymentRequest: string) => { ... // Pay the provided bolt11 payment request return bolt11PaymentHash; //Return the hexadecimal payment hash } } ``` * LNURL-withdraw as `string` or [LNURLWithdraw](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/LNURLWithdraw) object from [SwapperUtils.parseAddress()](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils#parseaddress) * Pass `null` or `undefined` if the invoice will be paid externally: ``` const bolt11PaymentInvoice = swap.getAddress(); // Returns the lightning network invoice to be displayed and paid by the user const lightningPaymentDeeplink = swap.getHyperlink(); // Or get a `lightning:` deeplink that can be displayed as QR code // Display the invoice and/or deeplink in a QR code to the user // Call execute without a wallet for it to wait till an external payment arrives const automaticallySettled = await swap.execute(null, callbacks?, options?); ``` ### Callbacks | Name | Type | Description | | ----------------------------- | ----------------------------------------- | --------------------------------------------------------------------------- | | `onSourceTransactionReceived` | `(sourceTxId: string) => void` | Fired when the Lightning payment is received by the LP. | | `onDestinationCommitSent` | `(destinationCommitTxId: string) => void` | Fired when the destination-chain commit transaction is sent. | | `onDestinationClaimSent` | `(destinationClaimTxId: string) => void` | Fired when the destination-chain claim transaction is sent. | | `onSwapSettled` | `(destinationTxId: string) => void` | Fired when the destination-chain funds are claimed and the swap is settled. | ### Options | Name | Type | Description | | ----------------------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `abortSignal` | `AbortSignal` | Cancels execution or waiting. | | `lightningTxCheckIntervalSeconds` | `number` | Polling interval while waiting for the Lightning payment to arrive. | | `delayBetweenCommitAndClaimSeconds` | `number` | Optional delay inserted between sending commit and claim transaction on the destination network. | | `secret` | `string` | Override the swap secret/pre-image. Usually only needed when the swap was recovered or the pre-image was generated outside the SDK. | info For more details about the swap, swap states, going through swap steps manually or getting the transactions to sign and send outside the SDK refer to the respective [Lightning → Smart Chain swap](https://docs.atomiq.exchange/sdk-guide/swaps/lightning-to-smart-chain.md). ## Summary of Failure Modes and Fallbacks When `execute()` returns `false` and does not fully finish the swap automatically: | Swap family | Failure Mode | Manual fallback | | -------------------------------- | --------------------------------- | ------------------------------- | | Smart chain → BTC L1 / Lightning | LP didn't process the swap | `swap.refund(sourceSigner)` | | BTC L1 → Starknet / EVM | Swap wasn't settled automatically | `swap.claim(destinationSigner)` | | Lightning → Starknet / EVM | Swap wasn't settled automatically | `swap.claim(destinationSigner)` | ### Legacy Swaps | Swap family | Failure Mode | Manual fallback | | --------------------------- | --------------------------------- | ------------------------------- | | BTC L1 → Solana (legacy) | Swap wasn't settled automatically | `swap.claim(destinationSigner)` | | Lightning → Solana (legacy) | None | - | ## Next Steps ### Detailed Swap Type Descriptions Explain each swap type in-depth, go through manually processing the swap & explain various swap states. **[Swap Types →](https://docs.atomiq.exchange/sdk-guide/swaps/.md)** *** ### Utilities SDK-exposed utilities allow you to parse addresses (including LNURL links), fetch token balances, list supported tokens and swap types. **[Utilities →](https://docs.atomiq.exchange/sdk-guide/utilities/.md)** *** ### Swap Management Retrieve & query past swaps, handle refundable and claimable swaps. **[Swap Management →](https://docs.atomiq.exchange/sdk-guide/swap-management/.md)** *** --- # Quick Start – Browser This guide covers installing the Atomiq SDK in a browser and its chain-specific connectors and walks you through setting up and initializing the Atomiq SDK. ## Core SDK Install the main SDK package: ``` npm install @atomiqlabs/sdk@latest ``` ### Chain Connectors The SDK supports multiple chains. Install only the chain connectors your project needs, and mix and match them as required: ``` npm install @atomiqlabs/chain-solana@latest npm install @atomiqlabs/chain-starknet@latest npm install @atomiqlabs/chain-evm@latest ``` ## Setup Not all chains support both Bitcoin testnets (testnet3 & testnet4), check the [network availability](https://docs.atomiq.exchange/sdk-guide/quick-start/.md#network-availability). * Mainnet * Testnet3 * Testnet4 Set your RPC URLs: ``` const solanaRpc = "https://api.mainnet-beta.solana.com"; const starknetRpc = "https://rpc.starknet.lava.build/"; //Alternatively: https://starknet.api.onfinality.io/public or https://api.zan.top/public/starknet-mainnet const citreaRpc = "https://rpc.mainnet.citrea.xyz"; ``` Create a swapper factory with your desired chain support. Use `as const` so TypeScript can properly infer the types: ``` import {SolanaInitializer} from "@atomiqlabs/chain-solana"; import {StarknetInitializer} from "@atomiqlabs/chain-starknet"; import {CitreaInitializer} from "@atomiqlabs/chain-evm"; import {BitcoinNetwork, TypedSwapper, SwapperFactory, TypedTokens} from "@atomiqlabs/sdk"; // Define chains you want to support const chains = [SolanaInitializer, StarknetInitializer, CitreaInitializer] as const; type SupportedChains = typeof chains; // Create the swapper factory const Factory = new SwapperFactory(chains); // Get the tokens for the supported chains const Tokens: TypedTokens = Factory.Tokens; // Create one swapper instance for your entire app, and use that instance for all your swaps. const swapper: TypedSwapper = Factory.newSwapper({ chains: { SOLANA: { rpcUrl: solanaRpc // Can also pass Connection object }, STARKNET: { rpcUrl: starknetRpc // Can also pass Provider object }, CITREA: { rpcUrl: citreaRpc, // Can also pass JsonRpcProvider object } }, bitcoinNetwork: BitcoinNetwork.MAINNET }); // Initialize the swapper await swapper.init(); ``` info On testnets, only native tokens are generally supported (e.g. SOL, STRK, cBTC). Set your testnet RPC URLs: ``` const solanaRpc = "https://api.devnet.solana.com"; const starknetRpc = "https://rpc.starknet-testnet.lava.build/"; ``` Create a swapper factory with your desired chain support: ``` import {SolanaInitializer} from "@atomiqlabs/chain-solana"; import {StarknetInitializer} from "@atomiqlabs/chain-starknet"; import {BitcoinNetwork, TypedSwapper, SwapperFactory, TypedTokens} from "@atomiqlabs/sdk"; const chains = [SolanaInitializer, StarknetInitializer] as const; type SupportedChains = typeof chains; const Factory = new SwapperFactory(chains); const Tokens: TypedTokens = Factory.Tokens; const swapper: TypedSwapper = Factory.newSwapper({ chains: { SOLANA: { rpcUrl: solanaRpc // Can also pass Connection object }, STARKNET: { rpcUrl: starknetRpc // Can also pass Provider object } }, bitcoinNetwork: BitcoinNetwork.TESTNET3 }); await swapper.init(); ``` info On testnets, only native tokens are generally supported (e.g. SOL, STRK, cBTC). Set your testnet RPC URLs: ``` const starknetRpc = "https://rpc.starknet-testnet.lava.build/"; const citreaRpc = "https://rpc.testnet.citrea.xyz"; ``` Create a swapper factory with your desired chain support: ``` import {StarknetInitializer} from "@atomiqlabs/chain-starknet"; import {CitreaInitializer} from "@atomiqlabs/chain-evm"; import {BitcoinNetwork, TypedSwapper, SwapperFactory, TypedTokens} from "@atomiqlabs/sdk"; const chains = [StarknetInitializer, CitreaInitializer] as const; type SupportedChains = typeof chains; const Factory = new SwapperFactory(chains); const Tokens: TypedTokens = Factory.Tokens; const swapper: TypedSwapper = Factory.newSwapper({ chains: { STARKNET: { rpcUrl: starknetRpc // Can also pass Provider object }, CITREA: { rpcUrl: citreaRpc, // Can also pass JsonRpcProvider object } }, bitcoinNetwork: BitcoinNetwork.TESTNET4 }); await swapper.init(); ``` info Initialize the swapper with `await swapper.init();` shown above once when your app starts. You should create **only one swapper instance** for your entire app, and use that instance for all your swaps. This checks existing in-progress swaps and does initial LP discovery. ## Setting Up Signers Atomiq's [`AbstractSigner`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/AbstractSigner) object wraps around the wallet object of the respective chain and makes it easy to approve transactions via it by passing it to the swap methods. You can alternatively skip this step, obtain the transactions required to be signed and [sign & send them manually](https://docs.atomiq.exchange/sdk-guide/advanced/manual-transactions.md). ### Solana Using Solana wallet adapter in React Install the [Solana wallet adapter](https://github.com/anza-xyz/wallet-adapter): ``` npm install --save \ @solana/wallet-adapter-base \ @solana/wallet-adapter-react \ @solana/wallet-adapter-react-ui \ @solana/wallet-adapter-wallets ``` Wrap your app with the wallet adapter providers: ``` import { useMemo } from "react"; import { ConnectionProvider, WalletProvider, useAnchorWallet } from "@solana/wallet-adapter-react"; import { WalletModalProvider, WalletMultiButton } from "@solana/wallet-adapter-react-ui"; import "@solana/wallet-adapter-react-ui/styles.css"; import { SolanaSigner } from "@atomiqlabs/chain-solana"; // In your component, create the signer from the connected wallet: function YourApp() { const anchorWallet = useAnchorWallet(); //Create atomiq signer object out of the anchor wallet, which you can then use to execute swaps const wallet = useMemo(() => new SolanaSigner(anchorWallet), [anchorWallet]); return (

Your App

{/* Place a `WalletMultiButton` button in your app to allow the user to connect their wallet */}
); } function App() { return ( // Use the same solanaRpc that was used to create the swapper earlier ); } ``` ### Starknet Using get-starknet in the Browser Install [get-starknet](https://github.com/starknet-io/get-starknet): ``` npm install --save \ @starknet-io/get-starknet ``` Connect the wallet and create a signer: ``` import { connect, getStarknet } from "@starknet-io/get-starknet"; import { WalletAccount } from "starknet"; import { StarknetBrowserSigner } from "@atomiqlabs/chain-starknet"; const starknet = getStarknet(); async function connectStarknetWallet() { // Connect the starknet wallet const swo = await connect(); // Use the same starknetRpc that was used to create the swapper earlier const walletAccount = await WalletAccount.connect(starknetRpc, swo); //Create atomiq signer object out of the wallet account, which you can then use to execute swaps const wallet = new StarknetBrowserSigner(walletAccount); } ``` ### EVM (Citrea, etc.) Using Wagmi + RainbowKit in React Install [Wagmi](https://wagmi.sh/) and [RainbowKit](https://rainbowkit.com/): ``` npm install --save @rainbow-me/rainbowkit \ wagmi \ viem@2.x \ @tanstack/react-query ``` Set up Wagmi with your EVM chain and wrap your app: ``` import { useEffect, useState } from "react"; import { getDefaultConfig, RainbowKitProvider, ConnectButton } from '@rainbow-me/rainbowkit'; import { WagmiProvider, useAccount } from "wagmi"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { BrowserProvider } from "ethers"; import { EVMBrowserSigner } from "@atomiqlabs/chain-evm"; // Define your chain (example: Citrea Mainnet) const citreaChain = { id: 4114, name: "Citrea", nativeCurrency: { name: "cBTC", symbol: "cBTC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.citrea.xyz"] } }, }; const config = getDefaultConfig({ appName: 'My RainbowKit App', projectId: 'YOUR_PROJECT_ID', // Every dApp that relies on WalletConnect needs to obtain a projectId from https://dashboard.reown.com chains: [citreaChain], // Include your desired EVM chain (can have multiple) ssr: false, // If your dApp uses server side rendering (SSR) }); const queryClient = new QueryClient(); // In your component, create the signer from the connected wallet: function YourApp() { const { connector, isConnected } = useAccount(); //Create atomiq signer object out of browser provider, which you can then use to execute swaps const [wallet, setWallet] = useState(); useEffect(() => { if(!isConnected) { setWallet(undefined); return; } connector.getProvider().then(provider => { return new BrowserProvider(provider).getSigner(); }).then(signer => { setWallet(new EVMBrowserSigner(signer, signer.address)); }); }, [connector, isConnected]); return (

Your App

{/* Place a `ConnectButton` button in your app to allow the user to connect their wallet */}
); } function App() { return ( ); } ``` ## Your First Swap Here's a complete example of a Smart Chain to Bitcoin on-chain swap: ``` import {SwapAmountType} from "@atomiqlabs/sdk"; // Create a swap: SOL to Bitcoin on-chain const swap = await swapper.swap( Tokens.SOLANA.SOL, // From token Tokens.BITCOIN.BTC, // To Bitcoin on-chain "0.0001", // Amount of BTC to receive SwapAmountType.EXACT_OUT, // Specify amount in output token solanaSigner.getAddress(), // Source address "bc1q..." // Bitcoin destination address ); // Check quote details console.log("Input:", swap.getInput().toString()); console.log("Output:", swap.getOutput().toString()); console.log("Expires:", new Date(swap.getQuoteExpiry())); // Execute the swap const success = await swap.execute(solanaSigner, { onSourceTransactionSent: (txId) => console.log("Tx sent:", txId), onSwapSettled: (btcTxId) => console.log("Bitcoin transaction sent:", btcTxId) }); // Handle failure if (!success) { await swap.refund(solanaSigner); } ``` ## Next Steps ### Creating Quotes Guides you through creating swap quotes. **[Creating Quotes →](https://docs.atomiq.exchange/sdk-guide/quick-start/creating-quotes.md)** *** ### Configuration Explains swapper configuration parameters passed in the `Factory.newSwapper()` function and debug logging config. **[Swapper Configuration →](https://docs.atomiq.exchange/sdk-guide/advanced/configuration.md)** *** ### Utilities SDK-exposed utilities allow you to parse addresses (including LNURL links), fetch token balances, list supported tokens and swap types. **[Utilities →](https://docs.atomiq.exchange/sdk-guide/utilities/.md)** *** --- # Quick Start – Node.js This guide covers installing the Atomiq SDK in Node.js and its chain-specific connectors and walks you through setting up and initializing the Atomiq SDK. ## Core SDK Install the main SDK package, and for Node.js applications, install the SQLite storage adapter: ``` npm install @atomiqlabs/sdk@latest npm install @atomiqlabs/storage-sqlite@latest ``` ### Chain Connectors The SDK supports multiple chains. Install only the chain connectors your project needs, and mix and match them as required: ``` npm install @atomiqlabs/chain-solana@latest npm install @atomiqlabs/chain-starknet@latest npm install @atomiqlabs/chain-evm@latest ``` ## Setup tip See [setup.ts](https://github.com/atomiqlabs/atomiq-sdk-demo/blob/main/src/setup.ts) in the demo repo for a ready-to-use version of the setup below. * Mainnet * Testnet3 * Testnet4 Set your RPC URLs: ``` const solanaRpc = "https://api.mainnet-beta.solana.com"; const starknetRpc = "https://rpc.starknet.lava.build/"; //Alternatively: https://starknet.api.onfinality.io/public or https://api.zan.top/public/starknet-mainnet const citreaRpc = "https://rpc.mainnet.citrea.xyz"; ``` Create a swapper factory with your desired chain support. Use `as const` so TypeScript can properly infer the types: ``` import {SolanaInitializer} from "@atomiqlabs/chain-solana"; import {StarknetInitializer} from "@atomiqlabs/chain-starknet"; import {CitreaInitializer} from "@atomiqlabs/chain-evm"; import {BitcoinNetwork, TypedSwapper, SwapperFactory, TypedTokens} from "@atomiqlabs/sdk"; import {SqliteStorageManager, SqliteUnifiedStorage} from "@atomiqlabs/storage-sqlite"; // Define chains you want to support const chains = [SolanaInitializer, StarknetInitializer, CitreaInitializer] as const; type SupportedChains = typeof chains; // Create the swapper factory const Factory = new SwapperFactory(chains); // Get the tokens for the supported chains const Tokens: TypedTokens = Factory.Tokens; // Create one swapper instance for your entire app, and use that instance for all your swaps. const swapper: TypedSwapper = Factory.newSwapper({ chains: { SOLANA: { rpcUrl: solanaRpc // Can also pass Connection object }, STARKNET: { rpcUrl: starknetRpc // Can also pass Provider object }, CITREA: { rpcUrl: citreaRpc, // Can also pass JsonRpcProvider object } }, bitcoinNetwork: BitcoinNetwork.MAINNET, swapStorage: chainId => new SqliteUnifiedStorage("CHAIN_MAINNET_"+chainId+".sqlite3"), chainStorageCtor: name => new SqliteStorageManager("STORE_MAINNET_"+name+".sqlite3"), }); await swapper.init(); ``` info On testnets, only native tokens are generally supported (e.g. SOL, STRK, cBTC). Set your testnet RPC URLs: ``` const solanaRpc = "https://api.devnet.solana.com"; const starknetRpc = "https://rpc.starknet-testnet.lava.build/"; ``` Create a swapper factory with your desired chain support: ``` import {SolanaInitializer} from "@atomiqlabs/chain-solana"; import {StarknetInitializer} from "@atomiqlabs/chain-starknet"; import {BitcoinNetwork, TypedSwapper, SwapperFactory, TypedTokens} from "@atomiqlabs/sdk"; import {SqliteStorageManager, SqliteUnifiedStorage} from "@atomiqlabs/storage-sqlite"; const chains = [SolanaInitializer, StarknetInitializer] as const; type SupportedChains = typeof chains; const Factory = new SwapperFactory(chains); const Tokens: TypedTokens = Factory.Tokens; const swapper: TypedSwapper = Factory.newSwapper({ chains: { SOLANA: { rpcUrl: solanaRpc // Can also pass Connection object }, STARKNET: { rpcUrl: starknetRpc // Can also pass Provider object } }, bitcoinNetwork: BitcoinNetwork.TESTNET3, swapStorage: chainId => new SqliteUnifiedStorage("CHAIN_TESTNET3_"+chainId+".sqlite3"), chainStorageCtor: name => new SqliteStorageManager("STORE_TESTNET3_"+name+".sqlite3"), }); await swapper.init(); ``` info On testnets, only native tokens are generally supported (e.g. SOL, STRK, cBTC). Set your testnet RPC URLs: ``` const starknetRpc = "https://rpc.starknet-testnet.lava.build/"; const citreaRpc = "https://rpc.testnet.citrea.xyz"; ``` Create a swapper factory with your desired chain support: ``` import {StarknetInitializer} from "@atomiqlabs/chain-starknet"; import {CitreaInitializer} from "@atomiqlabs/chain-evm"; import {BitcoinNetwork, TypedSwapper, SwapperFactory, TypedTokens} from "@atomiqlabs/sdk"; import {SqliteStorageManager, SqliteUnifiedStorage} from "@atomiqlabs/storage-sqlite"; const chains = [StarknetInitializer, CitreaInitializer] as const; type SupportedChains = typeof chains; const Factory = new SwapperFactory(chains); const Tokens: TypedTokens = Factory.Tokens; const swapper: TypedSwapper = Factory.newSwapper({ chains: { STARKNET: { rpcUrl: starknetRpc // Can also pass Provider object }, CITREA: { rpcUrl: citreaRpc, // Can also pass JsonRpcProvider object } }, bitcoinNetwork: BitcoinNetwork.TESTNET4, swapStorage: chainId => new SqliteUnifiedStorage("CHAIN_TESTNET4_"+chainId+".sqlite3"), chainStorageCtor: name => new SqliteStorageManager("STORE_TESTNET4_"+name+".sqlite3"), }); await swapper.init(); ``` info Initialize the swapper with `await swapper.init();` once when your app starts. You should create only one swapper instance for your entire app, and use that instance for all your swaps. This checks existing in-progress swaps and does initial LP discovery. ## Setting Up Signers Atomiq's [`AbstractSigner`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/AbstractSigner) object wraps around the wallet object of the respective chain and makes it easy to approve transactions via it by passing it to the swap methods. You can alternatively skip this step, obtain the transactions required to be signed and [sign & send them manually](https://docs.atomiq.exchange/sdk-guide/advanced/manual-transactions.md). tip See [wallets.ts](https://github.com/atomiqlabs/atomiq-sdk-demo/blob/main/src/wallets.ts) in the demo repo for a ready-to-use version of the signers below. ### Solana ``` import {Keypair} from "@solana/web3.js"; import {SolanaKeypairWallet, SolanaSigner} from "@atomiqlabs/chain-solana"; // Generate random private key const solanaKey: Uint8Array = Keypair.generate().secretKey; // From private key const solanaSigner = new SolanaSigner( new SolanaKeypairWallet(Keypair.fromSecretKey(solanaKey)), Keypair.fromSecretKey(solanaKey) ); ``` ### Starknet ``` import {StarknetSigner, StarknetKeypairWallet} from "@atomiqlabs/chain-starknet"; import {RpcProvider} from "starknet"; // Generate random private key const starknetKey: string = StarknetKeypairWallet.generateRandomPrivateKey(); // From private key, this uses a simple openzeppelin account class with a single public key. const starknetProvider = new RpcProvider({nodeUrl: starknetRpc}); const starknetSigner = new StarknetSigner( new StarknetKeypairWallet(starknetProvider, starknetKey) ); ``` ### EVM (Citrea, etc.) ``` import {BaseWallet, SigningKey, JsonRpcProvider, Wallet} from "ethers"; import {EVMSigner} from "@atomiqlabs/chain-evm"; // Generate random private key const evmKey: string = Wallet.createRandom().privateKey; // From private key const evmProvider = new JsonRpcProvider(citreaRpc); const wallet = new BaseWallet(new SigningKey(evmKey), evmProvider); const evmWallet = new EVMSigner(wallet, wallet.address); ``` ## Your First Swap Here's a complete example of a Smart Chain to Bitcoin on-chain swap: ``` import {SwapAmountType} from "@atomiqlabs/sdk"; // Create a swap: SOL to Bitcoin on-chain const swap = await swapper.swap( Tokens.SOLANA.SOL, // From token Tokens.BITCOIN.BTC, // To Bitcoin on-chain "0.0001", // Amount of BTC to receive SwapAmountType.EXACT_OUT, // Specify amount in output token solanaSigner.getAddress(), // Source address "bc1q..." // Bitcoin destination address ); // Check quote details console.log("Input:", swap.getInput().toString()); console.log("Output:", swap.getOutput().toString()); console.log("Expires:", new Date(swap.getQuoteExpiry())); // Execute the swap const success = await swap.execute(solanaSigner, { onSourceTransactionSent: (txId) => console.log("Tx sent:", txId), onSwapSettled: (btcTxId) => console.log("Bitcoin transaction sent: ", btcTxId) }); // Handle failure if (!success) { await swap.refund(solanaSigner); } ``` ## Next Steps ### Creating Quotes Guides you through creating swap quotes. **[Creating Quotes →](https://docs.atomiq.exchange/sdk-guide/quick-start/creating-quotes.md)** *** ### Configuration Explains swapper configuration parameters passed in the `Factory.newSwapper()` function and debug logging config. **[Swapper Configuration →](https://docs.atomiq.exchange/sdk-guide/advanced/configuration.md)** *** ### Utilities SDK-exposed utilities allow you to parse addresses (including LNURL links), fetch token balances, list supported tokens and swap types. **[Utilities →](https://docs.atomiq.exchange/sdk-guide/utilities/.md)** *** --- # Swap Management This section covers what happens after a swap has already been created and persisted. These pages are for retrieving past swaps, recovering them after app restarts, and handling the cases where a swap needs follow-up outside the normal high-level flow. info This is still a core responsibility of an app integrating the SDK. The common swap path may complete automatically, but real integrations still need to detect when a saved swap requires explicit user action and guide the user through refund or claim recovery flows. ## Usage A typical integration uses this section in roughly this order: 1. Use [Historical Swaps](https://docs.atomiq.exchange/sdk-guide/swap-management/historical-swaps.md) to load previously created swaps from storage, either by ID or as a filtered list for a chain or signer. 2. Check whether any saved **Smart Chain → Bitcoin/Lightning** swaps have become refundable, then use [Refunds](https://docs.atomiq.exchange/sdk-guide/swap-management/refunds.md) to return those funds to the source wallet. 3. Check whether any saved **Bitcoin/Lightning → Smart Chain** swaps have become claimable, then use [Claiming](https://docs.atomiq.exchange/sdk-guide/swap-management/claiming.md) to settle those funds to the destination wallet. Apps usually run this recovery flow on startup and then periodically in long-running sessions, so saved swaps that still need attention are surfaced as clear refund or claim actions for the user. ## Topics ### Historical Swaps Retrieve persisted swaps by ID or query them in bulk so your app can resume previously created swaps after restart. **[Historical Swaps →](https://docs.atomiq.exchange/sdk-guide/swap-management/historical-swaps.md)** *** ### Refunds Handle failed **Smart Chain → Bitcoin/Lightning** swaps that can be refunded back to the source-chain wallet. **[Refunds →](https://docs.atomiq.exchange/sdk-guide/swap-management/refunds.md)** *** ### Claiming Handle **Bitcoin/Lightning → Smart Chain** swaps that need manual destination-side settlement because automatic settlement did not finish. **[Claiming →](https://docs.atomiq.exchange/sdk-guide/swap-management/claiming.md)** *** --- # Claiming For swaps in the **Bitcoin (on-chain or Lightning) → Smart Chain** direction, the SDK normally handles claiming automatically via watchtower services. Having to claim (or settle) the swap manually is an edge-case that only happens when the automatic settlement service (watchtower) didn't settle the swap. tip See the complete working example: [utils/pastSwaps.ts](https://github.com/atomiqlabs/atomiq-sdk-demo/blob/main/src/utils/pastSwaps.ts) ## Checking Claimable Status Check if a swap is claimable with the [`isClaimable()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/interfaces/IClaimableSwap#isclaimable) function exposed in [SpvFromBTCSwap](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SpvFromBTCSwap), [FromBTCSwap](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCSwap), [FromBTCLNAutoSwap](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCLNAutoSwap) and [FromBTCLNSwap](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCLNSwap) classes. You can additionally also wait for the automatic settlement before attempting to claim manually with the [`waitTillClaimed()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/interfaces/IClaimableSwap#waittillclaimed) function. ``` // Check if swap needs manual claiming if (swap.isClaimable()) { // Optionally wait for automatic settlement for 60 seconds await swap.waitTillClaimed(60); // Claim with a signer object await swap.claim(signer); } ``` ## Get All Claimable Swaps tip It's good practice to check for claimable swaps when your app starts and also periodically if your app is long-running. You can then either claim the swaps automatically or prompt the user to authorize the claim. To retrieve all the swaps that are currently claimable call the [`getClaimableSwaps()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/Swapper#getclaimableswaps) function on the swapper, which returns swaps implementing the [`IClaimableSwap`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/interfaces/IClaimableSwap) interface. You can also pass in the chain identifier and signer address to retrieve swaps only for that respective smart chain and/or signer. * All * Single chain * Specific signer ``` import {IClaimableSwap} from "@atomiqlabs/sdk"; // Returns all claimable swaps from storage if no argument is passed const allClaimableSwaps: IClaimableSwap[] = await swapper.getClaimableSwaps(); // You can check the respective swap's chain by reading its chainIdentifier property for (let swap of allClaimableSwaps) { const chainId = swap.chainIdentifier; // Retrieves smart chain type of the swap, e.g.: SOLANA, STARKNET, CITREA, ... // Retrieve the relevant destination chain signer for the chainId const signer = smartChainSigners[chainId]; // We can also additionally check if the swap is indeed created for the signer if (swap.getOutputAddress() === signer.getAddress()) { await swap.claim(signer); } } ``` ``` import {IClaimableSwap} from "@atomiqlabs/sdk"; import {StarknetChainType} from "@atomiqlabs/chain-starknet"; // Returns all claimable swaps for a given smart chain (swaps between BTC/Lightning and the given smart chain) const starknetClaimableSwaps: IClaimableSwap[] = await swapper.getClaimableSwaps("STARKNET"); for (let swap of starknetClaimableSwaps) { // We can also additionally check if the swap is indeed created for the signer if (swap.getOutputAddress() === starknetSigner.getAddress()) { await swap.claim(starknetSigner); } } ``` ``` import {IClaimableSwap} from "@atomiqlabs/sdk"; import {StarknetChainType} from "@atomiqlabs/chain-starknet"; // Returns claimable swaps for a specific signer address const signerClaimableSwaps: IClaimableSwap[] = await swapper.getClaimableSwaps("STARKNET", starknetSigner.getAddress()); for (let swap of signerClaimableSwaps) { await swap.claim(starknetSigner); } ``` info Type of the signer object passed to the [`claim()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/interfaces/IClaimableSwap#claim) function is dependent on the destination network: * For **Solana** swaps: [SolanaSigner](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-solana/src/classes/SolanaSigner), or native `@coral-xyz/anchor` [Wallet](https://solana-foundation.github.io/anchor/ts/classes/Wallet.html) * For **Starknet** swaps: [StarknetSigner](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-starknet/src/classes/StarknetSigner), [StarknetBrowserSigner](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-starknet/src/classes/StarknetBrowserSigner), or native `starknet` [Account](https://starknetjs.com/docs/API/classes/Account) * For **EVM** swaps: [EVMSigner](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-evm/src/classes/EVMSigner), or native `ethers` [Signer](https://docs.ethers.org/v6/api/providers/#Signer) ## Manually Signing Transactions You can alternatively use the [`txsClaim()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/interfaces/IClaimableSwap#txsclaim) function to retrieve the smart chain transactions that you can then sign and send manually: ``` const txsClaim = await swap.txsClaim(); ... // Sign and send claim transactions here // Important to wait till SDK processes the swap claim and updates the swap state await swap.waitTillClaimed(); ``` info The transactions returned by the `txsClaim()` function use the following types: * For **Solana**, uses the [SolanaTx](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-solana/src/type-aliases/SolanaTx) type * For **Starknet**, uses the [StarknetTx](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-starknet/src/type-aliases/StarknetTx) type * For **EVM**, uses the `ethers` [TransactionRequest](https://docs.ethers.org/v6/api/providers/#TransactionRequest) type For more information about how to sign and send these transactions manually refer to the [Manual Transactions](https://docs.atomiq.exchange/sdk-guide/advanced/manual-transactions.md) page. ## API Reference * [isClaimable](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/interfaces/IClaimableSwap#isclaimable) - Check claim status * [waitTillClaimed](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/interfaces/IClaimableSwap#waittillclaimed) - Wait for automatic settlement or manual claim * [claim](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/interfaces/IClaimableSwap#claim) - Execute claim * [txsClaim](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/interfaces/IClaimableSwap#txsclaim) - Get claim transactions * [getClaimableSwaps](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/Swapper#getclaimableswaps) - Get all claimable swaps --- # Historical Swaps The SDK persists all swaps locally, allowing you to retrieve them later by ID. tip See the complete working example: [utils/pastSwaps.ts](https://github.com/atomiqlabs/atomiq-sdk-demo/blob/main/src/utils/pastSwaps.ts) ## Retrieving Swap by ID Every swap has a unique ID accessible with the [`swap.getId()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ISwap#getid) getter that can be used for later retrieval: ``` const swap = await swapper.swap(/* ... */); // Get the swap ID const swapId = swap.getId(); console.log("Swap ID:", swapId); // Store this ID for later (e.g., in your database, returned data, URL, etc.) ``` info Swaps are by default only persisted after they are initiated (i.e. `commit()`, `execute()` or `waitForPayment()` being called), if you want to serve uninitialized quotes from e.g. a REST API endpoint, you need to set the `saveUninitializedSwaps` flag in the [Swapper Configuration](https://docs.atomiq.exchange/sdk-guide/advanced/configuration.md#runtime-flags), to make sure you can later retrieve these uninitialized quotes. Unexecuted expired quotes are automatically purged from the persistent storage. You can then retrieve the swap from storage based on this ID: * use the [`getTypedSwapById()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/Swapper#gettypedswapbyid) function to get the properly-typed swap (e.g. [`ToBTCSwap`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ToBTCSwap), [`ToBTCLNSwap`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ToBTCLNSwap), [`FromBTCLNAutoSwap`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCLNAutoSwap), etc.) if you know the type upfront. * use the [`getSwapById()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/Swapper#getswapbyid) function to get the abstract [`ISwap`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ISwap) swap type, which you can then narrow with the [`isSwapType()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/functions/isSwapType) type-guard. - getTypedSwapById() - getSwapById() If you know the expected swap type: ``` import {SwapType} from "@atomiqlabs/sdk"; // Retrieve with specific type - returns correctly typed swap or undefined if the type doesn't match const typedSwap = await swapper.getTypedSwapById( swapId, "SOLANA", // Chain ID SwapType.TO_BTC // Expected swap type ); if (typedSwap) { // typedSwap is properly typed as ToBTCSwap console.log("Output address:", typedSwap.getOutputAddress()); } ``` If you don't know the swap type: ``` import {isSwapType, SwapType} from "@atomiqlabs/sdk"; // Get base swap const swap = await swapper.getSwapById(swapId); if (swap) { // Use type guards to narrow the type if (isSwapType(swap, SwapType.TO_BTC)) { // Now typed as ToBTCSwap console.log("This is a TO_BTC swap"); } else if (isSwapType(swap, SwapType.FROM_BTCLN_AUTO)) { // Now typed as FromBTCLNAutoSwap console.log("This is a Lightning to smart chain swap"); } } ``` info Both variations for getting the swap return `undefined` in case the swap wasn't found in the persistent storage. ## Swap Type Guards To help narrowing types of the abstract `ISwap` class, the SDK provides [`SwapType`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/enumerations/SwapType)-based type-guards which help you properly narrow down the exact type of the swap. For the overview of swap types and their respective swap classes see the [Swap Types](https://docs.atomiq.exchange/sdk-guide/swaps/.md) page. ``` import { isSwapType, SwapType, ToBTCSwap, ToBTCLNSwap, FromBTCSwap, SpvFromBTCSwap, FromBTCLNSwap, FromBTCLNAutoSwap } from "@atomiqlabs/sdk"; const swap = await swapper.getSwapById(swapId); if (isSwapType(swap, SwapType.TO_BTC)) { const toBtcSwap: ToBTCSwap = swap; // Smart chain → Bitcoin swap } if (isSwapType(swap, SwapType.TO_BTCLN)) { const toBtcLnSwap: ToBTCLNSwap = swap; // Smart chain → Lightning swap } if (isSwapType(swap, SwapType.SPV_VAULT_FROM_BTC)) { const spvFromBtcSwap: SpvFromBTCSwap = swap; // Bitcoin → Smart chain swap } if (isSwapType(swap, SwapType.FROM_BTCLN_AUTO)) { const fromBtcLnAutoSwap: FromBTCLNAutoSwap = swap; // Lightning → Smart chain swap } if (isSwapType(swap, SwapType.FROM_BTC)) { const fromBtcSwap: FromBTCSwap = swap; // Legacy Bitcoin → Solana swap } if (isSwapType(swap, SwapType.FROM_BTCLN)) { const fromBtcLnSwap: FromBTCLNSwap = swap; // Legacy Lightning → Solana swap } ``` ## Retrieving All Swaps The [`getAllSwaps()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/Swapper#getallswaps) function allows you to retrieve all existing swaps from the storage, swaps only for a specific chain or swaps created by a specific smart chain signer. * All * Single chain * Specific signer ``` import {ISwap, isSwapType, SwapType, ToBTCSwap} from "@atomiqlabs/sdk"; // Returns all existing swaps from storage if no argument is passed const allSwaps: ISwap[] = await swapper.getAllSwaps(); // You can further narrow down the type of the swap with the type-guards and check the respective swap's chain for (let swap of allSwaps) { if (isSwapType(swap, SwapType.TO_BTC)) { const typedSwap: ToBTCSwap = swap; // Properly narrowed down type const chainId = swap.chainIdentifier; // Retrieves smart chain type of the swap, e.g.: SOLANA, STARKNET, CITREA, ... } } ``` ``` import {ISwap, isSwapType, SwapType, ToBTCSwap} from "@atomiqlabs/sdk"; import {StarknetChainType} from "@atomiqlabs/chain-starknet"; // Returns all swaps for a given smart chain (returns all swaps between BTC and the given smart chain) const starknetSwaps: ISwap[] = await swapper.getAllSwaps("STARKNET"); // You can further narrow down the type of the swap with the type-guards and check the respective swap's chain for (let swap of starknetSwaps) { if (isSwapType(swap, SwapType.TO_BTC)) { const typedSwap: ToBTCSwap = swap; // Properly narrowed down type } } ``` ``` import {ISwap, isSwapType, SwapType, ToBTCSwap} from "@atomiqlabs/sdk"; import {StarknetChainType} from "@atomiqlabs/chain-starknet"; // Returns all swaps created with a specific signer address const signerSwaps: ISwap[] = await swapper.getAllSwaps("STARKNET", starknetSigner.getAddress()); // You can further narrow down the type of the swap with the type-guards and check the respective swap's chain for (let swap of signerSwaps) { if (isSwapType(swap, SwapType.TO_BTC)) { const typedSwap: ToBTCSwap = swap; // Properly narrowed down type } } ``` ## API Reference * [getId](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ISwap#getid) - Get the persistent ID of a swap * [getSwapById](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/Swapper#getswapbyid) - Get swap by ID (base type) * [getTypedSwapById](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/Swapper#gettypedswapbyid) - Get swap by ID with type * [getAllSwaps](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/Swapper#getallswaps) - Get all swaps from storage * [isSwapType](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/functions/isSwapType) - Type guard for swap types ## Next Steps ### Refunding Swaps Retrieving & processing swaps that were not executed by the LPs and can be refunded, returning funds to the user. **[Refunds →](https://docs.atomiq.exchange/sdk-guide/swap-management/refunds.md)** *** ### Claiming Swaps Retrieving & processing swaps that didn't get automatically settled and need to be claimed manually. **[Claiming →](https://docs.atomiq.exchange/sdk-guide/swap-management/claiming.md)** *** --- # Refunds When a swap in the **Smart Chain → Bitcoin** direction fails, you can refund your tokens back to your wallet, this can happen when: * the LP tried to execute but failed (e.g. Lightning routing failed) and returned a cooperative refund authorization allowing the user to refund immediately. * the LP went offline and didn't execute the swap within the timelock period, in this case you have to wait for the timelock to expire this is **12 hours** for Bitcoin on-chain swaps and **4-5 days** for Lightning network swaps (swap then becomes refundable automatically). tip See the complete working example: [utils/pastSwaps.ts](https://github.com/atomiqlabs/atomiq-sdk-demo/blob/main/src/utils/pastSwaps.ts) ## Checking Refund Status Check if a swap is refundable with the [`isRefundable()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/interfaces/IRefundableSwap#isrefundable) function exposed in [ToBTCSwap](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ToBTCSwap) and [ToBTCLNSwap](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ToBTCLNSwap) classes. You can also use the [`getExpiry()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/IToBTCSwap#getexpiry) function to determine when the swap becomes refundable in case of LP going offline. ``` // Check if swap is refundable if (swap.isRefundable()) { // Refund with a signer object await swap.refund(signer); } else if (swap.isInProgress()) { // Check when the swap becomes refundable if the LP doesn't execute it const refundableAt: number = swap.getExpiry(); // Returns UNIX timestamp milliseconds console.log(`The swap will become refundable at ${new Date(refundableAt).toString()}`); } ``` ## Get All Refundable Swaps tip It's good practice to check for refundable swaps when your app starts and also periodically if your app is long-running. You can then either refund the swaps automatically or prompt the user to authorize a refund. To retrieve all the swaps that are currently refundable call the [`getRefundableSwaps()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/Swapper#getrefundableswaps) function on the swapper, which returns the swaps with [`IToBTCSwap`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/IToBTCSwap) abstract class. You can also pass in the chain identifier and signer address to retrieve swaps only for that respective smart chain and/or signer. * All * Single chain * Specific signer ``` import {IToBTCSwap} from "@atomiqlabs/sdk"; // Returns all refundable swaps from storage if no argument is passed const allRefundableSwaps: IToBTCSwap[] = await swapper.getRefundableSwaps(); // You can check the respective swap's chain by reading its chainIdentifier property for (let swap of allRefundableSwaps) { const chainId = swap.chainIdentifier; // Retrieves smart chain type of the swap, e.g.: SOLANA, STARKNET, CITREA, ... // Retrieve the relevant smart chain signer for the chainId const signer = smartChainSigners[chainId]; // We can also additionally check if the swap is indeed created by the signer if (swap.getInputAddress() === signer.getAddress()) { await swap.refund(signer); } } ``` ``` import {IToBTCSwap} from "@atomiqlabs/sdk"; import {StarknetChainType} from "@atomiqlabs/chain-starknet"; // Returns all refundable swaps for a given smart chain (swaps between BTC and the given smart chain) const starknetRefundableSwaps: IToBTCSwap[] = await swapper.getRefundableSwaps("STARKNET"); for (let swap of starknetRefundableSwaps) { // We can also additionally check if the swap is indeed created by the signer if (swap.getInputAddress() === starknetSigner.getAddress()) { await swap.refund(starknetSigner); } } ``` ``` import {IToBTCSwap} from "@atomiqlabs/sdk"; import {StarknetChainType} from "@atomiqlabs/chain-starknet"; // Returns refundable swaps for a specific signer address const signerRefundableSwaps: IToBTCSwap[] = await swapper.getRefundableSwaps("STARKNET", starknetSigner.getAddress()); for (let swap of signerRefundableSwaps) { await swap.refund(starknetSigner); } ``` info Type of the signer object passed to the [`refund()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/IToBTCSwap#refund) function is dependent on the source network: * For **Solana** swaps: [SolanaSigner](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-solana/src/classes/SolanaSigner), or native `@coral-xyz/anchor` [Wallet](https://solana-foundation.github.io/anchor/ts/classes/Wallet.html) * For **Starknet** swaps: [StarknetSigner](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-starknet/src/classes/StarknetSigner), [StarknetBrowserSigner](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-starknet/src/classes/StarknetBrowserSigner), or native `starknet` [Account](https://starknetjs.com/docs/API/classes/Account) * For **EVM** swaps: [EVMSigner](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-evm/src/classes/EVMSigner), or native `ethers` [Signer](https://docs.ethers.org/v6/api/providers/#Signer) ## Manually Signing Transactions You can alternatively use the [`txsRefund()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/IToBTCSwap#txsrefund) function to retrieve the smart chain transactions that you can then sign and send manually: ``` const txsRefund = await swap.txsRefund(); ... // Sign and send refund transactions here // Important to wait till SDK processes the swap refund and updates the swap state await swap.waitTillRefunded(); ``` info The transactions returned by the `txsRefund()` function use the following types: * For **Solana**, uses the [SolanaTx](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-solana/src/type-aliases/SolanaTx) type * For **Starknet**, uses the [StarknetTx](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-starknet/src/type-aliases/StarknetTx) type * For **EVM**, uses the `ethers` [TransactionRequest](https://docs.ethers.org/v6/api/providers/#TransactionRequest) type For more information about how to sign and send these transactions manually refer to the [Manual Transactions](https://docs.atomiq.exchange/sdk-guide/advanced/manual-transactions.md) page. ## API Reference * [isRefundable](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/interfaces/IRefundableSwap#isrefundable) - Check refund status * [getExpiry](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/IToBTCSwap#getexpiry) - Get the timelock expiry for unilateral refund * [refund](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/IToBTCSwap#refund) - Execute refund * [txsRefund](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/IToBTCSwap#txsrefund) - Get refund transactions * [getRefundableSwaps](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/Swapper#getrefundableswaps) - Get all refundable swaps --- # Swap Guides This section helps you choose the guide for the exact swap your app is executing. Each page covers one swap family in detail, including the protocol behind it, the required wallets or signers, the manual execution path, and the recovery actions that matter when the automatic flow does not fully finish. info Use this section together with [Executing Swaps](https://docs.atomiq.exchange/sdk-guide/quick-start/executing-swaps.md). The quick-start page explains the shared high-level `execute()` flow, while the swap guides explain what changes from one swap family to another. ## Usage The simplest way to choose a guide is by the Bitcoin layer involved in the swap: ### Bitcoin On-Chain (L1) | Source | Destination | `SwapType` | Swap Class | Guide | Primitive | | ----------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | BTC L1 | Smart Chain | [`SPV_VAULT_FROM_BTC`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/enumerations/SwapType#spv_vault_from_btc) | [`SpvFromBTCSwap`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SpvFromBTCSwap) | [Bitcoin → Smart Chain](https://docs.atomiq.exchange/sdk-guide/swaps/btc-to-smart-chain.md) | [UTXO-controlled vault](https://docs.atomiq.exchange/overview/core-primitives/utxo-controlled-vault.md) | | Smart Chain | BTC L1 | [`TO_BTC`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/enumerations/SwapType#to_btc) | [`ToBTCSwap`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ToBTCSwap) | [Smart Chain → BTC/Lightning](https://docs.atomiq.exchange/sdk-guide/swaps/smart-chain-to-btc.md) | [PrTLC](https://docs.atomiq.exchange/overview/core-primitives/prtlc.md) | ### Bitcoin Lightning (L2) | Source | Destination | `SwapType` | Swap Class | Guide | Primitive | | ----------- | ----------- | ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | Lightning | Smart Chain | [`FROM_BTCLN_AUTO`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/enumerations/SwapType#from_btcln_auto) | [`FromBTCLNAutoSwap`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCLNAutoSwap) | [Lightning → Smart Chain](https://docs.atomiq.exchange/sdk-guide/swaps/lightning-to-smart-chain.md) | [LP-initiated HTLC](https://docs.atomiq.exchange/overview/core-primitives/htlc.md) | | Smart Chain | Lightning | [`TO_BTCLN`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/enumerations/SwapType#to_btcln) | [`ToBTCLNSwap`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ToBTCLNSwap) | [Smart Chain → BTC/Lightning](https://docs.atomiq.exchange/sdk-guide/swaps/smart-chain-to-btc.md) | [HTLC](https://docs.atomiq.exchange/overview/core-primitives/htlc.md) | The Lightning guides also cover the LNURL-based variants of these swaps: [Smart Chain → BTC/Lightning](https://docs.atomiq.exchange/sdk-guide/swaps/smart-chain-to-btc.md) includes LNURL-pay, while [Lightning → Smart Chain](https://docs.atomiq.exchange/sdk-guide/swaps/lightning-to-smart-chain.md) and [Lightning → Solana](https://docs.atomiq.exchange/sdk-guide/swaps/solana/lightning-to-solana.md) include LNURL-withdraw flows. ### Legacy Solana Swaps info The Solana guides are separate because the Bitcoin/Lightning → Solana direction still uses older protocol flows with different requirements and recovery behavior than the newer Starknet and EVM swaps. The reverse Smart Chain → Bitcoin/Lightning direction is the same across Solana, Starknet, and EVM. | Source | Destination | `SwapType` | Swap Class | Guide | Primitive | | --------- | ----------- | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | | BTC L1 | Solana | [`FROM_BTC`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/enumerations/SwapType#from_btc) | [`FromBTCSwap`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCSwap) | [Bitcoin → Solana](https://docs.atomiq.exchange/sdk-guide/swaps/solana/btc-to-solana.md) | [PrTLC](https://docs.atomiq.exchange/overview/core-primitives/prtlc.md) | | Lightning | Solana | [`FROM_BTCLN`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/enumerations/SwapType#from_btcln) | [`FromBTCLNSwap`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCLNSwap) | [Lightning → Solana](https://docs.atomiq.exchange/sdk-guide/swaps/solana/lightning-to-solana.md) | [User-initiated HTLC](https://docs.atomiq.exchange/overview/core-primitives/htlc.md) | ## Topics ### Standard Swaps Use these guides for the standard swap flows used across Starknet and EVM, as well as for the common **Smart Chain → Bitcoin/Lightning** flow shared across Solana, Starknet, and EVM. **[Smart Chain → BTC/Lightning](https://docs.atomiq.exchange/sdk-guide/swaps/smart-chain-to-btc.md)** | **[Bitcoin → Smart Chain](https://docs.atomiq.exchange/sdk-guide/swaps/btc-to-smart-chain.md)** | **[Lightning → Smart Chain](https://docs.atomiq.exchange/sdk-guide/swaps/lightning-to-smart-chain.md)** *** ### Legacy Swaps Use these guides when the destination is Solana and the source is Bitcoin on-chain or Lightning, since those swaps still follow the legacy Solana-specific flows. **[Bitcoin → Solana](https://docs.atomiq.exchange/sdk-guide/swaps/solana/btc-to-solana.md)** | **[Lightning → Solana](https://docs.atomiq.exchange/sdk-guide/swaps/solana/lightning-to-solana.md)** *** --- # Bitcoin → Smart Chain Swap Bitcoin L1 (on-chain) to Starknet or EVM tokens. These swaps are based on the [UTXO-controlled vault](https://docs.atomiq.exchange/overview/core-primitives/utxo-controlled-vault.md) primitive and verified through the on-chain [Bitcoin light client](https://docs.atomiq.exchange/overview/core-primitives/bitcoin-light-client.md). The user and LP cooperatively sign a single Bitcoin transaction that atomically sends BTC to the LP and authorizes the smart-chain payout to the user. Users do not need destination-chain gas upfront, and watchtowers or liquidity fronters can settle the swap automatically once the Bitcoin transaction is confirmed. tip * [btc-to-smartchain/swapBasic.ts](https://github.com/atomiqlabs/atomiq-sdk-demo/blob/main/src/btc-to-smartchain/swapBasic.ts) * [btc-to-smartchain/swapAdvancedStarknet.ts](https://github.com/atomiqlabs/atomiq-sdk-demo/blob/main/src/btc-to-smartchain/swapAdvancedStarknet.ts) * [btc-to-smartchain/swapAdvancedEVM.ts](https://github.com/atomiqlabs/atomiq-sdk-demo/blob/main/src/btc-to-smartchain/swapAdvancedEVM.ts) info Solana uses a different (legacy) swap protocol. See [BTC to Solana](https://docs.atomiq.exchange/sdk-guide/swaps/solana/btc-to-solana.md). ## Executing the Swap Here is a full flow for creating and executing the swap in the Bitcoin → Smart chain direction via the `execute()` helper function, uses the [SpvFromBTCSwap](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SpvFromBTCSwap) swap class: ``` import {SwapAmountType} from "@atomiqlabs/sdk"; // Create a quote const swap = await swapper.swap( Tokens.BITCOIN.BTC, // Bitcoin on-chain input Tokens.STARKNET.STRK, // Destination smart-chain token, e.g. STRK on Starknet or cBTC on Citrea "0.00003", // Amount (3000 sats to send) SwapAmountType.EXACT_IN, undefined, // Source address is not used for BTC source swaps starknetSigner.getAddress(), // Destination smart-chain address { gasAmount: "0" // Optional: request native destination token as gas drop // For additional options checks the `Swap Options` section } ); // Type gets inferred as SpvFromBTCSwap // Execute the swap const automaticallySettled = await swap.execute( { address: "bc1q...", // User's Bitcoin address publicKey: "03...", // User's Bitcoin public key signPsbt: (psbt, signInputs) => { // Sign the PSBT with the Bitcoin wallet // Return the signed PSBT in hex or base64 format return ""; } }, { onSourceTransactionSent: (txId) => { console.log(`Bitcoin tx sent: ${txId}`); }, onSourceTransactionConfirmationStatus: (txId, confirmations, target, etaMs) => { console.log(`Confirmations: ${confirmations}/${target}, ETA: ${etaMs / 1000}s`); }, onSourceTransactionConfirmed: (txId) => { console.log(`Bitcoin tx confirmed: ${txId}`); }, onSwapSettled: (destinationTxId) => { console.log(`Swap settled on destination chain: ${destinationTxId}`); } } ); if (!automaticallySettled) { // Handle the edge-case when automatic settlement does not happen in time console.log("Automatic settlement timed out, claiming manually..."); await swap.claim(starknetSigner); console.log("Claimed!"); } else { console.log("Success! Output transaction ID: ", swap.getOutputTxId()); } ``` warning `execute()` requires a single-address (non-BIP32) Bitcoin wallet capable of signing PSBTs. If your signing flow is handled by an external wallet UI, or you use a BIP-32 HD wallet, use the manual **Funded PSBT** or **Raw PSBT** flow below. ## Manual Execution Flow Choose the Bitcoin signing flow that matches your wallet integration: * use **Bitcoin wallet** flow if you have a wallet which already implements [IBitcoinWallet](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/interfaces/IBitcoinWallet) (e.g. the built-in [SingleAddressBitcoinWallet](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SingleAddressBitcoinWallet)) or [MinimalBitcoinWalletInterfaceWithSigner](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/MinimalBitcoinWalletInterfaceWithSigner). * use **Funded PSBT** if you have a single address bitcoin wallet (non-HD) and want to get a PSBT already funded with wallet input UTXOs. * use **Raw PSBT** if you want full control of the PSBT construction, allowing you to add arbitrary inputs and outputs to the PSBT yourself. - Bitcoin wallet - Funded PSBT - Raw PSBT Pass the `bitcoinWallet` argument which follows the [IBitcoinWallet](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/interfaces/IBitcoinWallet) interface or [MinimalBitcoinWalletInterfaceWithSigner](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/MinimalBitcoinWalletInterfaceWithSigner) type to the [`sendBitcoinTransaction()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SpvFromBTCSwap#sendbitcointransaction) function. ``` import {SwapAmountType} from "@atomiqlabs/sdk"; // Create a quote const swap = await swapper.swap( Tokens.BITCOIN.BTC, Tokens.STARKNET.STRK, "0.00003", SwapAmountType.EXACT_IN, undefined, starknetSigner.getAddress(), { gasAmount: "0" } ); // 1. Let the SDK build, sign, and submit the Bitcoin transaction through your IBitcoinWallet implementation const bitcoinTxId = await swap.sendBitcoinTransaction(bitcoinWallet); // 2. Wait for the Bitcoin transaction to reach the required confirmation count await swap.waitForBitcoinTransaction((txId, confirmations, targetConfirmations, txEtaMs) => { console.log(`${confirmations}/${targetConfirmations} confirmations, ETA ${txEtaMs / 1000}s`); }); // 3. Wait for automatic settlement or fronting on the destination chain const automaticallySettled = await swap.waitTillClaimedOrFronted(60); // 4. If automatic settlement does not happen in time, claim manually if (!automaticallySettled) { await swap.claim(starknetSigner); } ``` Get the funded PSBT from the [`getFundedPsbt()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SpvFromBTCSwap#getfundedpsbt) function to sign with an external single-address non-BIP32 wallet (e.g.: Xverse, Unisat, Phantom, etc.). ``` import {SwapAmountType} from "@atomiqlabs/sdk"; // Create a quote const swap = await swapper.swap( Tokens.BITCOIN.BTC, Tokens.STARKNET.STRK, "0.00003", SwapAmountType.EXACT_IN, undefined, starknetSigner.getAddress(), { gasAmount: "0" } ); // 1. Ask the SDK for a funded PSBT for a non-HD single-address Bitcoin wallet const {psbtBase64, signInputs} = await swap.getFundedPsbt({ address: "bc1q...", publicKey: "03..." }); // 2. Pass psbtBase64 (or psbtHex) and signInputs to an external signer like Xverse, Unisat, Phantom, etc. const signedPsbt = await externalBitcoinWallet.signPsbt(psbtBase64, signInputs); // 3. Submit the signed PSBT back to the SDK - supports hexadecimal and base64 formats const bitcoinTxId = await swap.submitPsbt(signedPsbt); // 4. Wait for the Bitcoin transaction to reach the required confirmation count await swap.waitForBitcoinTransaction((txId, confirmations, targetConfirmations, txEtaMs) => { console.log(`${confirmations}/${targetConfirmations} confirmations, ETA ${txEtaMs / 1000}s`); }); // 5. Wait for automatic settlement or fronting on the destination chain const automaticallySettled = await swap.waitTillClaimedOrFronted(60); // 6. If automatic settlement does not happen in time, claim manually if (!automaticallySettled) { await swap.claim(starknetSigner); } ``` Get the raw PSBT from the [`getPsbt`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SpvFromBTCSwap#getpsbt) function to which you can add arbitrary inputs and outputs. ``` import {SwapAmountType} from "@atomiqlabs/sdk"; // Create a quote const swap = await swapper.swap( Tokens.BITCOIN.BTC, Tokens.STARKNET.STRK, "0.00003", SwapAmountType.EXACT_IN, undefined, starknetSigner.getAddress(), { gasAmount: "0" } ); // 1. Get the raw swap PSBT and fund it with your own wallet flow const {psbt, psbtHex, psbtBase64, in1sequence} = await swap.getPsbt(); ... // Add your own inputs and outputs to the PSBT // 2. IMPORTANT: Preserve the required nSequence on the second input (input index 1) // e.g.: psbt.updateInput(1, {sequence: in1sequence}); // 3. Sign every input except the first one, which belongs to the LP vault ... // 4. Submit the fully funded and signed PSBT back to the SDK - supports hexadecimal and base64 formats const bitcoinTxId = await swap.submitPsbt(psbt); // 5. Wait for the Bitcoin transaction to reach the required confirmation count await swap.waitForBitcoinTransaction((txId, confirmations, targetConfirmations, txEtaMs) => { console.log(`${confirmations}/${targetConfirmations} confirmations, ETA ${txEtaMs / 1000}s`); }); // 6. Wait for automatic settlement or fronting on the destination chain const automaticallySettled = await swap.waitTillClaimedOrFronted(60); // 7. If automatic settlement does not happen in time, claim manually if (!automaticallySettled) { await swap.claim(starknetSigner); } ``` warning Make sure that you set the second input's `nSequence` (input index `1`) to the returned `in1sequence` after funding the PSBT, then sign every input except the first one and submit the PSBT with [`submitPsbt()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SpvFromBTCSwap#submitpsbt). warning Legacy non-SegWit Bitcoin inputs are not allowed in the submitted PSBT. Use witness-type inputs such as P2WPKH, P2WSH or P2TR. ## Claiming Past Unsettled Swaps If the app was offline and the automatic settlement did not happen, the swap can be manually claimed on the destination chain. Checking if a single swap is claimable and claiming it: ``` if (swap.isClaimable()) { await swap.claim(starknetSigner); } ``` Getting all swaps that are claimable and claiming them: ``` const claimable = await swapper.getClaimableSwaps( "STARKNET", // Only get claimable swaps on STARKNET starknetSigner.getAddress() // Only get claimable swaps for this destination address ); // This returns all claimable swaps, which could be either Bitcoin → Smart chain or Lightning → Smart chain for (const swap of claimable) { // All the claimable swap types have the same `claim()` function signature await swap.claim(starknetSigner); } ``` info It is a good practice to query claimable swaps on your app's startup and either claim them automatically or prompt the user to claim them. Bitcoin → Smart chain swaps can be claimed at **any time** after the swap Bitcoin transaction gets enough confirmations. After the swap is settled on Bitcoin, the swap becomes claimable forever - there is no timelock. ## Gas Drop You can request native tokens on the destination chain along with the main swap output. This helps cover gas fees for the first transactions on the destination chain. ``` const swap = await swapper.swap( Tokens.BITCOIN.BTC, Tokens.STARKNET.WBTC, "0.0001", SwapAmountType.EXACT_IN, undefined, starknetSigner.getAddress(), { gasAmount: "1" // Request 1 STRK as gas drop } ); console.log("Gas drop:", swap.getGasDropOutput().toString()); ``` warning When already swapping to the native token of the respective destination chain (i.e. STRK on Starknet, cBTC on Citrea, etc.) don't specify the `gasAmount`, as LPs usually don't hold the necessary gas drop liquidity for those tokens! This will lead to errors during quoting and make it unable for you to request the quote. ## Swap Options The overview of possible options you can pass to the swap via the `options` argument of `swapper.swap(...)`. For Bitcoin → Smart Chain swaps these are useful when you want to request a gas drop, cap the accepted Bitcoin fee rate, or tune automatic-settlement incentives. | Name | Type | Description | | -------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `gasAmount` | `bigint \| string` | Optional extra native-token gas drop to receive on the destination chain. `bigint` is in base units, `string` is a human-readable decimal amount. Do not use it when swapping to the native token itself.
Default: no gas drop. | | `maxAllowedBitcoinFeeRate` | `number` | Upper bound, in sats/vB, on the LP-required minimum Bitcoin fee rate for the funding transaction.
Default: computed dynamically as `10 + currentBitcoinFeeRate * 1.5`. | | `unsafeZeroWatchtowerFee` | `boolean` | Attaches zero watchtower fee to the swap. This can make automatic settlement unattractive, so you may need to settle manually with `swap.claim(...)`.
Default: `false`. | | `feeSafetyFactor` | `number` | Multiplier used when estimating the watchtower fee. Higher values make automatic settlement more attractive.
Default: `1.25`. | ## Swap States Read the current state of the swap in its [SpvFromBTCSwapState](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/enumerations/SpvFromBTCSwapState) enum form with [`getState()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ISwap#getstate) or in human readable [SwapStateInfo](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/SwapStateInfo) form with description with [`getStateInfo()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ISwap#getstateinfo): ``` import {SpvFromBTCSwapState, SwapStateInfo} from "@atomiqlabs/sdk"; const state: SpvFromBTCSwapState = swap.getState(); console.log(`State (numeric): ${state}`); const richState: SwapStateInfo = swap.getStateInfo(); console.log(`State name: ${richState.name}`); console.log(`State description: ${richState.description}`); ``` Subscribe to swap state updates with: ``` swap.events.on("swapState", () => { const state: SpvFromBTCSwapState = swap.getState(); }); ``` ### Table of States | State | Value | Description | | -------------------- | ----- | ------------------------------------------------------------------------------------------------------ | | `CLOSED` | -5 | Catastrophic failure has occurred when processing the swap on the smart-chain side. | | `FAILED` | -4 | Some of the bitcoin swap transaction inputs were double-spent, so the swap failed and no BTC was sent. | | `DECLINED` | -3 | The intermediary (LP) declined to co-sign the submitted PSBT. | | `QUOTE_EXPIRED` | -2 | Swap has expired for good and there is no way it can be executed anymore. | | `QUOTE_SOFT_EXPIRED` | -1 | Swap is almost expired and should be presented as expired, though it still might be processed. | | `CREATED` | 0 | Swap was created, waiting for the user to sign and submit the Bitcoin swap PSBT. | | `SIGNED` | 1 | Swap Bitcoin PSBT was submitted by the client to the SDK. | | `POSTED` | 2 | Swap PSBT was sent to the intermediary (LP), waiting for co-sign and broadcast. | | `BROADCASTED` | 3 | The intermediary co-signed and broadcasted the Bitcoin transaction. | | `FRONTED` | 4 | Settlement on the destination smart chain was fronted and funds were received before final settlement. | | `BTC_TX_CONFIRMED` | 5 | Bitcoin transaction has the required confirmations, waiting for automatic settlement or manual claim. | | `CLAIMED` | 6 | Swap settled on the smart chain and funds were received. | ## API Reference * [SpvFromBTCSwap](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SpvFromBTCSwap) - Swap class for Bitcoin → Smart chain * [SwapAmountType](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/enumerations/SwapAmountType) - Amount type enum * [SpvFromBTCSwapState](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/enumerations/SpvFromBTCSwapState) - Swap states --- # Lightning → Smart Chain Swap Bitcoin Lightning to Starknet or EVM tokens. These swaps are based on an LP-initiated [HTLC](https://docs.atomiq.exchange/overview/core-primitives/htlc.md) on the destination chain. After the LP receives the Lightning payment, it creates the HTLC and the user broadcasts the payment secret over Nostr so [watchtowers](https://docs.atomiq.exchange/overview/actors.md) can claim on the user's behalf. The user can always self-claim as a fallback. tip * [btcln-to-smartchain/swapBasic.ts](https://github.com/atomiqlabs/atomiq-sdk-demo/blob/main/src/btcln-to-smartchain/swapBasic.ts) * [btcln-to-smartchain/swapBasicLNURL.ts](https://github.com/atomiqlabs/atomiq-sdk-demo/blob/main/src/btcln-to-smartchain/swapBasicLNURL.ts) * [btcln-to-smartchain/swapAdvancedStarknet.ts](https://github.com/atomiqlabs/atomiq-sdk-demo/blob/main/src/btcln-to-smartchain/swapAdvancedStarknet.ts) * [btcln-to-smartchain/swapAdvancedEVM.ts](https://github.com/atomiqlabs/atomiq-sdk-demo/blob/main/src/btcln-to-smartchain/swapAdvancedEVM.ts) info Solana uses a different (legacy) swap protocol. See [Lightning to Solana](https://docs.atomiq.exchange/sdk-guide/swaps/solana/lightning-to-solana.md). ## Executing the Swap Here is a full flow for creating and executing the swap in the Lightning → Smart chain direction via the `execute()` helper function. The flow is the same for all the cases, with the only difference being how the Lightning payment is sourced. Uses the [FromBTCLNAutoSwap](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCLNAutoSwap) swap class. * Lightning wallet * External payment * LNURL-withdraw Pass a [MinimalLightningNetworkWalletInterface](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/MinimalLightningNetworkWalletInterface) object to pay the invoice programmatically. ``` import {MinimalLightningNetworkWalletInterface, SwapAmountType} from "@atomiqlabs/sdk"; // Create a quote const swap = await swapper.swap( Tokens.BITCOIN.BTCLN, // Lightning BTC input Tokens.STARKNET.STRK, // Destination smart-chain token, e.g. STRK on Starknet or cBTC on Citrea 10_000n, // Amount in sats SwapAmountType.EXACT_IN, undefined, // Source address is not used for standard Lightning source swaps starknetSigner.getAddress(), // Destination smart-chain address { gasAmount: "0" // Optional: request native destination token as gas drop // For additional options checks the `Swap Options` section } ); // Type gets inferred as FromBTCLNAutoSwap // Execute the swap const automaticallySettled = await swap.execute( { payInvoice: async (bolt11) => { // Pay the BOLT11 invoice with WebLN, NWC, or any other Lightning wallet integration return ""; // Empty string is fine if the wallet does not expose the payment preimage } }, // Pass the MinimalLightningNetworkWalletInterface object { onSourceTransactionReceived: (paymentHash) => { console.log(`Lightning payment received by LP: ${paymentHash}`); }, onSwapSettled: (destinationTxId) => { console.log(`Swap settled on destination chain: ${destinationTxId}`); } } ); if (!automaticallySettled) { // Handle the edge-case when watchtowers do not settle the HTLC automatically console.log("Automatic settlement timed out, claiming manually..."); await swap.claim(starknetSigner); console.log("Claimed!"); } else { console.log("Success! Output transaction ID:", swap.getOutputTxId()); } ``` Pay the invoice or `lightning:` deeplink, externally then wait for the payment to be received by passing `null` or `undefined` to the `execute()` function. warning Make sure to call `execute()` on the swap before you display the lightning invoice to the user. This ensures the swap is properly initiated before receiving the lightning network payment. ``` import {SwapAmountType} from "@atomiqlabs/sdk"; // Create a quote const swap = await swapper.swap( Tokens.BITCOIN.BTCLN, Tokens.STARKNET.STRK, 10_000n, SwapAmountType.EXACT_IN, undefined, starknetSigner.getAddress(), { gasAmount: "0" // Optional: request native destination token as gas drop // For additional options checks the `Swap Options` section } ); // Show the Lightning invoice or `lightning:` deeplink to the user const lightningInvoice = swap.getAddress(); const lightningUri = swap.getHyperlink(); // Wait for an external payment to the invoice const automaticallySettled = await swap.execute( undefined, // You can also pass `null` here { onSourceTransactionReceived: (paymentHash) => { console.log(`Lightning payment received by LP: ${paymentHash}`); }, onSwapSettled: (destinationTxId) => { console.log(`Swap settled on destination chain: ${destinationTxId}`); } } ); if (!automaticallySettled) { // Handle the edge-case when watchtowers do not settle the HTLC automatically console.log("Automatic settlement timed out, claiming manually..."); await swap.claim(starknetSigner); console.log("Claimed!"); } else { console.log("Success! Output transaction ID:", swap.getOutputTxId()); } ``` Pass an LNURL-withdraw link `string` or [LNURLWithdraw](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/LNURLWithdraw) object to source the payment from LNURL-withdraw. ``` import {LNURLWithdraw, SwapAmountType} from "@atomiqlabs/sdk"; // Create a quote const swap = await swapper.swap( Tokens.BITCOIN.BTCLN, Tokens.STARKNET.STRK, 10_000n, SwapAmountType.EXACT_IN, undefined, starknetSigner.getAddress(), { gasAmount: "0" // Optional: request native destination token as gas drop // For additional options checks the `Swap Options` section } ); const automaticallySettled = await swap.execute( "lnurl1...", // Pass an LNURL-withdraw link or parsed LNURLWithdraw object to source the payment { onSourceTransactionReceived: (paymentHash) => { console.log(`Lightning payment received by LP: ${paymentHash}`); }, onSwapSettled: (destinationTxId) => { console.log(`Swap settled on destination chain: ${destinationTxId}`); } } ); if (!automaticallySettled) { // Handle the edge-case when watchtowers do not settle the HTLC automatically console.log("Automatic settlement timed out, claiming manually..."); await swap.claim(starknetSigner); console.log("Claimed!"); } else { console.log("Success! Output transaction ID:", swap.getOutputTxId()); } ``` info For more information about how to parse LNURLs, distinguish between LNURL-withdraw and LNURL-pay variants, check out the [LNURL-withdraw](#lnurl-withdraw) section. warning Ensure that the SDK stays online when the lightning network payment is sent, this is necessary since the SDK needs to actively broadcast the swap secret over Nostr upon receiving the swap HTLC. ## Manual Execution Flow ``` import {SwapAmountType} from "@atomiqlabs/sdk"; // Create a quote const swap = await swapper.swap( Tokens.BITCOIN.BTCLN, Tokens.STARKNET.STRK, 10_000n, SwapAmountType.EXACT_IN, undefined, starknetSigner.getAddress() ); // 1. Show the invoice to the user const lightningInvoice = swap.getAddress(); const lightningUri = swap.getHyperlink(); // 2. Wait for the Lightning payment and for the LP to create the destination HTLC // This is important to make sure the swap is properly initiated before the user attempts a lightning network payment const paymentReceived = await swap.waitForPayment(); if (!paymentReceived) { console.log("Lightning payment was not received before the quote expired."); return; } // 3. Wait for watchtower settlement on the destination chain const automaticallySettled = await swap.waitTillClaimed(60); // 4. If watchtowers do not settle in time, claim manually if (!automaticallySettled) { await swap.claim(starknetSigner); } ``` info If you need to sign the destination-chain claim manually, use [`txsClaim()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCLNAutoSwap#txsclaim) instead of [`claim()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCLNAutoSwap#claim). The returned transactions use the following types: * For Starknet, uses the [StarknetTx](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-starknet/src/type-aliases/StarknetTx) type * For EVM, uses the `ethers` [TransactionRequest](https://docs.ethers.org/v6/api/providers/#TransactionRequest) type For more information about how to sign and send these transactions manually refer to the [Manual Transactions](https://docs.atomiq.exchange/sdk-guide/advanced/manual-transactions.md) page. ## Claiming Past Unsettled Swaps If the app was offline when the LP created the destination HTLC and/or the watchtowers didn't settle the swap automatically, the swap can still be manually claimed as long as the HTLC has not expired yet. Checking if a single swap is claimable and claiming it: ``` if (swap.isClaimable()) { await swap.claim(starknetSigner); } ``` Getting all swaps that are claimable and claiming them: ``` const claimable = await swapper.getClaimableSwaps( "STARKNET", // Only get claimable swaps on STARKNET starknetSigner.getAddress() // Only get claimable swaps for this destination address ); // This returns all claimable swaps, which could be either Bitcoin → Smart chain or Lightning → Smart chain for (const swap of claimable) { // All the claimable swap types have the same `claim()` function signature await swap.claim(starknetSigner); } ``` info It is a good practice to query claimable swaps on your app's startup and either claim them automatically or prompt the user to claim them. Unlike Bitcoin → Smart chain swaps, Lightning → Smart chain swaps are only claimable until the destination HTLC expires, so this recovery check should run promptly. ## Gas Drop You can request native tokens on the destination chain along with the main swap output. This helps cover gas fees for the first transactions on the destination chain. ``` const swap = await swapper.swap( Tokens.BITCOIN.BTCLN, Tokens.STARKNET.WBTC, 20_000n, SwapAmountType.EXACT_IN, undefined, starknetSigner.getAddress(), { gasAmount: "1" // Request 1 STRK as gas drop } ); console.log("Main output:", swap.getOutput().toString()); console.log("Gas drop:", swap.getGasDropOutput().toString()); ``` ## LNURL-withdraw LNURL-withdraw links (`lnurl1...`) can be used as the source of a Lightning → Smart chain swap, letting the SDK generate a BOLT11 invoice for the quote and submit it to the LNURL-withdraw service automatically when you call [`waitForPayment()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCLNAutoSwap#waitforpayment) or [`execute()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCLNAutoSwap#execute). You can parse LNURLs either with the generic [`SwapperUtils.parseAddress()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils#parseaddress) function exposed by the swapper as `swapper.Utils.parseAddress()` or with the specific [`SwapperUtils.getLNURLTypeAndData()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils#getlnurltypeanddata) function, which return the [LNURLPay](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/LNURLPay) or [LNURLWithdraw](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/LNURLWithdraw) object allowing you to read the LNURL's details. ``` import {isLNURLWithdraw, LNURLPay, LNURLWithdraw, SwapAmountType} from "@atomiqlabs/sdk"; // LNURL-withdraw link const lnurlOrLightningAddress: string = "lnurl1..."; // Check if it has correct format if (!swapper.Utils.isValidLNURL(lnurlOrLightningAddress)) throw new Error("Invalid LNURL specified!"); // Fetch the details of the LNURL, it can be either LNURL-pay or LNURL-withdraw const lnurlDetails: LNURLPay | LNURLWithdraw = await swapper.Utils.getLNURLTypeAndData(lnurlOrLightningAddress); // Check if the LNURL is of the LNURL-withdraw type if (!isLNURLWithdraw(lnurlDetails)) throw new Error("LNURL isn't of the LNURL-withdraw type!"); // You can inspect the LNURL-withdraw details before quoting const swapMinimumSats: bigint = lnurlDetails.min; const swapMaximumSats: bigint = lnurlDetails.max; const defaultDescription: string = lnurlDetails.params.defaultDescription; // Create swap with LNURL-withdraw as the Lightning source const swap = await swapper.swap( Tokens.BITCOIN.BTCLN, Tokens.STARKNET.STRK, 10_000n, SwapAmountType.EXACT_IN, lnurlDetails, // Fetched details or raw LNURL-withdraw link string starknetSigner.getAddress() ); // No wallet is needed, funds come from the LNURL-withdraw source const automaticallySettled = await swap.execute( undefined, { onSourceTransactionReceived: (paymentHash) => { console.log(`Lightning payment received by LP: ${paymentHash}`); }, onSwapSettled: (destinationTxId) => { console.log(`Swap settled on destination chain: ${destinationTxId}`); } } ); if (!automaticallySettled) { await swap.claim(starknetSigner); } ``` tip If you created a normal Lightning quote first and later want to fund it from an LNURL-withdraw source instead, call [`settleWithLNURLWithdraw(lnurl)`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCLNSwap#settlewithlnurlwithdraw) before or during `waitForPayment()`/`execute()`. This is useful when you want to display a standard invoice QR code and also allow LNURL-withdraw or NFC-card settlement. ## Swap Options The overview of possible options you can pass to the swap via the `options` argument of `swapper.swap(...)`. For Lightning → Smart Chain swaps these are useful when you want to request a gas drop, attach Lightning invoice metadata, or adjust automatic-settlement and safety-check behavior. | Name | Type | Description | | ------------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `paymentHash` | `Buffer \| string` | Lets you supply your own payment hash instead of letting the SDK generate it. When set, the swap will not settle automatically until you later provide the preimage/secret manually during claim or waiting.
Default: generated automatically by the SDK. | | `description` | `string` | Optional Lightning invoice description. Keep it below 500 UTF-8 bytes.
Default: none. | | `descriptionHash` | `Buffer \| string` | Optional Lightning invoice description hash, useful when the invoice is exposed through LNURL-pay.
Default: none. | | `gasAmount` | `bigint \| string` | Optional extra native-token gas drop to receive on the destination chain. `bigint` is in base units, `string` is a human-readable decimal amount. Do not use it when swapping to the native token itself.
Default: no gas drop. | | `unsafeSkipLnNodeCheck` | `boolean` | Skips checking whether the LP's Lightning node has enough channel liquidity for the swap.
Default: `false`. | | `unsafeZeroWatchtowerFee` | `boolean` | Attaches zero watchtower fee to the swap. This can prevent automatic settlement and leave you with a manual `swap.claim(...)` flow.
Default: `false`. | | `feeSafetyFactor` | `number` | Multiplier used when estimating the watchtower fee. Higher values make automatic settlement more attractive.
Default: `1.25`. | ## Swap States Read the current state of the swap in its [FromBTCLNAutoSwapState](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/enumerations/FromBTCLNAutoSwapState) enum form with [`getState()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ISwap#getstate) or in human readable [SwapStateInfo](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/SwapStateInfo) form with description with [`getStateInfo()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ISwap#getstateinfo): ``` import {FromBTCLNAutoSwapState, SwapStateInfo} from "@atomiqlabs/sdk"; const state: FromBTCLNAutoSwapState = swap.getState(); console.log(`State (numeric): ${state}`); const richState: SwapStateInfo = swap.getStateInfo(); console.log(`State name: ${richState.name}`); console.log(`State description: ${richState.description}`); ``` Subscribe to swap state updates with: ``` swap.events.on("swapState", () => { const state: FromBTCLNAutoSwapState = swap.getState(); }); ``` ### Table of States | State | Value | Description | | -------------------- | ----- | ----------------------------------------------------------------------------------------------------------------------------- | | `FAILED` | -4 | The destination HTLC was not claimed before expiry, so the Lightning payment refunds. | | `QUOTE_EXPIRED` | -3 | Swap quote expired and can no longer be executed. | | `QUOTE_SOFT_EXPIRED` | -2 | Swap should be treated as expired, though it might still succeed if payment is already in flight. | | `EXPIRED` | -1 | The destination HTLC expired, so it is no longer safe to claim and the incoming Lightning payment will refund. | | `PR_CREATED` | 0 | Quote created. Pay the invoice from `getAddress()` or `getHyperlink()`, then wait with `waitForPayment()`. | | `PR_PAID` | 1 | The LP received the Lightning payment, but the destination HTLC is not created yet. Continue waiting with `waitForPayment()`. | | `CLAIM_COMMITED` | 2 | The destination HTLC exists. Wait for watchtowers with `waitTillClaimed()` or claim manually with `claim()` / `txsClaim()`. | | `CLAIM_CLAIMED` | 3 | Swap settled on the destination chain and the Lightning payment can complete. | ## API Reference * [FromBTCLNAutoSwap](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCLNAutoSwap) - Swap class for Lightning → Smart chain * [SwapAmountType](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/enumerations/SwapAmountType) - Amount type enum * [FromBTCLNAutoSwapState](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/enumerations/FromBTCLNAutoSwapState) - Swap states --- # Smart Chain → BTC/Lightning Swap smart chain tokens (Solana, Starknet & EVM tokens) to Bitcoin L1 (on-chain) or Lightning L2. These swaps are based on the [HTLC](https://docs.atomiq.exchange/overview/core-primitives/htlc.md) (for lightning) & [PrTLC](https://docs.atomiq.exchange/overview/core-primitives/prtlc.md) (for on-chain) primitives. tip For Smart chain → Bitcoin L1 (on-chain) swaps: * [smartchain-to-btc/swapBasic.ts](https://github.com/atomiqlabs/atomiq-sdk-demo/blob/main/src/smartchain-to-btc/swapBasic.ts) * [smartchain-to-btc/swapAdvancedSolana.ts](https://github.com/atomiqlabs/atomiq-sdk-demo/blob/main/src/smartchain-to-btc/swapAdvancedSolana.ts) * [smartchain-to-btc/swapAdvancedStarknet.ts](https://github.com/atomiqlabs/atomiq-sdk-demo/blob/main/src/smartchain-to-btc/swapAdvancedStarknet.ts) * [smartchain-to-btc/swapAdvancedEVM.ts](https://github.com/atomiqlabs/atomiq-sdk-demo/blob/main/src/smartchain-to-btc/swapAdvancedEVM.ts) For Smart chain → Lightning L2 swaps: * [smartchain-to-btcln/swapBasic.ts](https://github.com/atomiqlabs/atomiq-sdk-demo/blob/main/src/smartchain-to-btcln/swapBasic.ts) * [smartchain-to-btcln/swapAdvancedSolana.ts](https://github.com/atomiqlabs/atomiq-sdk-demo/blob/main/src/smartchain-to-btcln/swapAdvancedSolana.ts) * [smartchain-to-btcln/swapAdvancedStarknet.ts](https://github.com/atomiqlabs/atomiq-sdk-demo/blob/main/src/smartchain-to-btcln/swapAdvancedStarknet.ts) * [smartchain-to-btcln/swapAdvancedEVM.ts](https://github.com/atomiqlabs/atomiq-sdk-demo/blob/main/src/smartchain-to-btcln/swapAdvancedEVM.ts) ## Executing the Swap Here is a full flow for creating an executing the swap in the Smart Chain → BTC/Lightning direction via the `execute()` helper function. The flow is the same for all the cases, with the only differences being: * Smart chain → Lightning swaps require `EXACT_OUT` mode and a BOLT11 invoice with explicit amount in sats. * Smart chain → LNURL-pay swaps allow `EXACT_IN` mode, support passing additional comment & reading success actions after payment. - Smart chain → BTC - Smart chain → Lightning - Smart chain → LNURL-pay Uses the [ToBTCSwap](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ToBTCSwap) swap class. ``` import {SwapAmountType, ToBTCSwap} from "@atomiqlabs/sdk"; // Create a quote const swap: ToBTCSwap = await swapper.swap( Tokens.STARKNET.STRK, // Any smart chain asset, e.g. SOL on Solana, STRK on Starknet or cBTC on Citrea Tokens.BITCOIN.BTC, // Or `Tokens.BITCOIN.BTCLN` when swapping into Lightning "0.00003", // Amount (3000 sats to receive) SwapAmountType.EXACT_OUT, // NOTE: When swapping to BOLT11 lightning invoice, only EXACT_OUT mode is supported starknetSigner.getAddress(), // Source signer address "bc1q..." // Bitcoin on-chain address, BOLT11 lightning invoice or LNURL-pay link ); // Type gets infered as ToBTCSwap (for Bitcoin on-chain) // Execute the swap const swapSuccessful = await swap.execute( starknetSigner, // Pass in the signer on the source chain to sign the transactions { onSourceTransactionSent: (txId) => { console.log(`Source tx sent: ${txId}`); }, onSourceTransactionConfirmed: (txId) => { console.log(`Source tx confirmed: ${txId}`); }, onSwapSettled: (btcTxId) => { console.log(`Bitcoin tx sent: ${btcTxId}`); } } ); if (!swapSuccessful) { // Handle the edge-case when LP fails to process the swap console.log("Swap failed, refunding..."); // Refund funds back to the await swap.refund(starknetSigner); console.log("Refunded!"); } else { console.log("Success! Output transaction ID: ", swap.getOutputTxId()); // For lightning network swaps you can get the payment preimage with swap.getSecret() } ``` Uses the [ToBTCLNSwap](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ToBTCLNSwap) swap class. ``` import {SwapAmountType, ToBTCLNSwap} from "@atomiqlabs/sdk"; // Create a quote const swap: ToBTCLNSwap = await swapper.swap( Tokens.STARKNET.STRK, // Any smart chain asset, e.g. SOL on Solana, STRK on Starknet or cBTC on Citrea Tokens.BITCOIN.BTCLN, // Swap into Lightning BTC undefined, // Amount is determined by the passed BOLT11 lightning invoice SwapAmountType.EXACT_OUT, // When swapping to BOLT11 lightning invoice, only EXACT_OUT mode is supported starknetSigner.getAddress(), // Source signer address "lnbc10u1p...", // BOLT11 lightning invoice, needs to have a fixed amount of sats! { // For additional swap options checks the `Swap Options` section } ); // Type gets infered as ToBTCLNSwap (for Lightning) // Execute the swap const swapSuccessful = await swap.execute( starknetSigner, // Pass in the signer on the source chain to sign the transactions { onSourceTransactionSent: (txId) => { console.log(`Source tx sent: ${txId}`); }, onSourceTransactionConfirmed: (txId) => { console.log(`Source tx confirmed: ${txId}`); }, onSwapSettled: (btcTxId) => { console.log(`Bitcoin tx sent: ${btcTxId}`); } } ); // When swapping to lightning you can also additionally check: // The swap will likely fail due to lightning network routing error if (swap.willLikelyFail()) { console.log("Lightning network payment cannot be routed, ensure the recipient is online and has channels!"); } // Destination wallet is probably a non-custodial wallet, which needs to be online to accept the payment if (swap.isPayingToNonCustodialWallet()) { console.log("Destination lightning wallet is non-custodial, ensure it is online to accept the payment!"); } if (!swapSuccessful) { // Handle the edge-case when LP fails to process the swap console.log("Swap failed, refunding..."); // Refund funds back to the await swap.refund(starknetSigner); console.log("Refunded!"); } else { console.log("Success! Output transaction ID: ", swap.getOutputTxId()); // For lightning network swaps you can get the payment preimage with swap.getSecret() } ``` warning When swapping to Bitcoin Lightning and using BOLT11 lightning network invoices ("lnbc10u1p...") the invoice needs to have a fixed amount and therefore only `EXACT_OUT` swap mode is supported. For swapping variable amounts in `EXACT_IN` mode to Bitcoin Lightning check the **Smart chain → LNURL-pay** tab, or the [EXACT\_IN Lightning Swaps](#exact_in-lightning-swaps) section below if you have programatic API access to the destination lightning network wallet. Uses the [ToBTCLNSwap](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ToBTCLNSwap) swap class. ``` import {SwapAmountType, ToBTCLNSwap} from "@atomiqlabs/sdk"; // Create swap with LNURL-pay or Lightning address const swap: ToBTCLNSwap = await swapper.swap( Tokens.STARKNET.STRK, // Swap STRK Tokens.BITCOIN.BTCLN, // To Lightning BTC "100", // Now we can specify amount, and even an input amount! SwapAmountType.EXACT_IN, // With LNURL-pay we can use EXACT_IN mode on Lightning! starknetSigner.getAddress(), // Source signer address "lnurl1...", // LNURL-pay link or Lightning address ("user@example.com") { comment: "Payment for coffee" // Optional comment // For additional swap options checks the `Swap Options` section } ); // Type gets infered as ToBTCLNSwap (for Lightning) // When swapping to lightning you can also additionally check: // The swap will likely fail due to lightning network routing error if (swap.willLikelyFail()) { console.log("Lightning network payment cannot be routed, ensure the recipient is online and has channels!"); } // Destination wallet is probably a non-custodial wallet, which needs to be online to accept the payment if (swap.isPayingToNonCustodialWallet()) { console.log("Destination lightning wallet is non-custodial, ensure it is online to accept the payment!"); } // Execute the swap const swapSuccessful = await swap.execute( starknetSigner, // Pass in the signer on the source chain to sign the transactions { onSourceTransactionSent: (txId) => { console.log(`Source tx sent: ${txId}`); }, onSourceTransactionConfirmed: (txId) => { console.log(`Source tx confirmed: ${txId}`); }, onSwapSettled: (btcTxId) => { console.log(`Bitcoin tx sent: ${btcTxId}`); } } ); if (!swapSuccessful) { // Handle the edge-case when LP fails to process the swap console.log("Swap failed, refunding..."); // Refund funds back to the await swap.refund(starknetSigner); console.log("Refunded!"); return; } // LNURL-pay link might also contain a success action, this can be displayed to the user // upon successful payment if (swap.hasSuccessAction()) { const action = swap.getSuccessAction(); console.log("Description:", action.description); console.log("Text:", action.text); // May be null console.log("URL:", action.url); // May be null } ``` info For more information about how to parse LNURLs, distinguish between LNURL-withdraw and LNURL-pay variants, check out the [LNURL-pay](#lnurl-pay) section. ## Manual Execution Flow * Signer object * Manually sign transactions ``` import {SwapAmountType} from "@atomiqlabs/sdk"; // Create a quote const swap = await swapper.swap( Tokens.STARKNET.STRK, // Any smart chain asset, e.g. SOL on Solana, STRK on Starknet or cBTC on Citrea Tokens.BITCOIN.BTC, // Or `Tokens.BITCOIN.BTCLN` when swapping into Lightning "0.00003", // Amount (3000 sats to receive) SwapAmountType.EXACT_OUT, // NOTE: When swapping to BOLT11 lightning invoice, only EXACT_OUT mode is supported starknetSigner.getAddress(), // Source signer address "bc1q..." // Bitcoin on-chain address, BOLT11 lightning invoice or LNURL-pay link ); // Type gets infered as ToBTCSwap (for Bitcoin on-chain) or ToBTCLNSwap (for Lightning) // 1. Initiate the swap on the source chain await swap.commit(starknetSigner); // 2. Wait for the swap to execute and for the destination tx to be sent const swapSuccessful = await swap.waitForPayment(); // 3. Refund in case of failure if(!swapSuccessful) { await swap.refund(starknetSigner); } ``` info Type of the signer object passed to the [`commit()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/IToBTCSwap#commit) & [`refund()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/IToBTCSwap#refund) functions is dependent on the source network: * For **Solana** swaps: [SolanaSigner](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-solana/src/classes/SolanaSigner), or native `@coral-xyz/anchor` [Wallet](https://solana-foundation.github.io/anchor/ts/classes/Wallet.html) * For **Starknet** swaps: [StarknetSigner](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-starknet/src/classes/StarknetSigner), [StarknetBrowserSigner](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-starknet/src/classes/StarknetBrowserSigner), or native `starknet` [Account](https://starknetjs.com/docs/API/classes/Account) * For **EVM** swaps: [EVMSigner](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-evm/src/classes/EVMSigner), or native `ethers` [Signer](https://docs.ethers.org/v6/api/providers/#Signer) ``` import {SwapAmountType} from "@atomiqlabs/sdk"; // Create a quote const swap = await swapper.swap( Tokens.STARKNET.STRK, // Any smart chain asset, e.g. SOL on Solana, STRK on Starknet or cBTC on Citrea Tokens.BITCOIN.BTC, // Or `Tokens.BITCOIN.BTCLN` when swapping into Lightning "0.00003", // Amount (3000 sats to receive) SwapAmountType.EXACT_OUT, // NOTE: When swapping to BOLT11 lightning invoice, only EXACT_OUT mode is supported starknetSigner.getAddress(), // Source signer address "bc1q..." // Bitcoin on-chain address, BOLT11 lightning invoice or LNURL-pay link ); // Type gets infered as ToBTCSwap (for Bitcoin on-chain) or ToBTCLNSwap (for Lightning) // 1. Initiate the swap on the source chain const txsCommit = await swap.txsCommit(); ... // Sign and send transactions here // Important to wait till SDK processes the swap initialization await swap.waitTillCommited(); // 2. Wait for the swap to execute and for the destination tx to be sent const swapSuccessful = await swap.waitForPayment(); // 3. Refund in case of failure if(!swapSuccessful) { const txsRefund = await swap.txsRefund(); ... // Sign and send refund transactions here // Important to wait till SDK processes the swap refund and updates the swap state await swap.waitTillRefunded(); } ``` info The transactions returned by the `txs*` functions use the following types: * For **Solana**, uses the [SolanaTx](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-solana/src/type-aliases/SolanaTx) type * For **Starknet**, uses the [StarknetTx](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-starknet/src/type-aliases/StarknetTx) type * For **EVM**, uses the `ethers` [TransactionRequest](https://docs.ethers.org/v6/api/providers/#TransactionRequest) type For more information about how to sign and send these transactions manually refer to the [Manual Transactions](https://docs.atomiq.exchange/sdk-guide/advanced/manual-transactions.md) page. ## Refunding Past Failed Swaps The swaps might only transition into the refundable state after several hours (in case of LP non-cooperativeness combined with Lightning swaps even multiple days). Therefore there are a few helper functions to allow you to determine whether the swap is already refundable & to list all refundable swaps. Checking if a single swap is refundable and refunding it: ``` if (swap.isRefundable()) { await swap.refund(signer); } ``` Getting all swaps that are refundable and refunding them: ``` const refundable = await swapper.getRefundableSwaps( "STARKNET", // Allows you to specify to only get refundable swaps on `STARKNET` starknetSigner.getAddress() // Allows you to specify to only get refundable swpas for the signer address ); for (const swap of refundable) { await swap.refund(starknetSigner); } ``` info It is a good practice to get the refundable swaps on your app's startup and either refunding them automatically or prompting the user to refund. ## LNURL-pay LNURL-pay links ("lnurl1p...") and static lightning addresses ("") are reusable and allow payer to set an amount. Hence, they can be used to swap into Lightning with `EXACT_IN` mode. Additionally, they also support attaching an optional comment to the payment (which you can pass in the `swap()` function options), and also expose a success action, which can be displayed to the user after a successful payment. You can parse LNURLs either with the generic [`SwapperUtils.parseAddress()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils#parseaddress) function exposed by the swapper as `swapper.Utils.parseAddress()` or with the specific [`SwapperUtils.getLNURLTypeAndData()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils#getlnurltypeanddata) function, which return the [LNURLPay](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/LNURLPay) or [LNURLWithdraw](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/LNURLWithdraw) object allowing you to read the LNURL's details. ``` // LNURL-pay link or Lightning address ("user@example.com") const lnurlOrLightningAddress: string = "lnurl1..."; // Check if it has correct format if(!swapper.Utils.isValidLNURL(lnurlOrLightningAddress)) throw new Error("Invalid LNURL specified!"); // Fetch the details of the LNURL, it can be either LNURL-pay or LNURL-withdraw const lnurlDetails: LNURLPay | LNURLWithdraw = await swapper.Utils.getLNURLTypeAndData(lnurlOrLightningAddress); // Check if the LNURL is of the LNURL-pay type if(!isLNURLPay(lnurlDetails)) throw new Error("LNURL isn't of the LNURL-pay type!"); //You can check the details of the LNURL-pay link, for the full details refer to the `LNURLPay` typedoc page. const swapMinimumSats: bigint = lnurlDetails.min; const swapMaximumSats: bigint = lnurlDetails.max; const shortDescription: string = lnurlDetails.shortDescription; // Create swap with LNURL-pay or Lightning address const swap = await swapper.swap( Tokens.STARKNET.STRK, // Swap STRK Tokens.BITCOIN.BTCLN, // To Lightning BTC "100", // Now we can specify amount, and even an input amount! SwapAmountType.EXACT_IN, // With LNURL-pay we can use EXACT_IN mode on Lightning! starknetSigner.getAddress(), // Source signer address lnurlDetails, // Fetched details or raw LNURL-pay link or Lightning address string { comment: "Payment for coffee" // Optional comment } ); ... // Handle swap execution ``` ## Swap Options The overview of possible options you can pass to the swap via the `options` argument of `swapper.swap(...)`. For Bitcoin on-chain swaps there are currently no quote creation options, while Lightning swaps let you fine-tune Lightning routing and LNURL-pay behavior. * Smart chain → BTC * Smart chain → Lightning > No swap creation options are currently exposed for this swap type. | Name | Type | Description | | ------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `expirySeconds` | `number` | HTLC expiration timeout, in seconds, offered to the LP. Larger values allow more Lightning routes, but also keep funds locked for longer if the LP does not cooperate.
Default: 5 days. | | `maxRoutingFeePercentage` | `number` | Caps the percentage component of the maximum Lightning routing fee.
Default: `0.2` (0.2% of the swap value). | | `maxRoutingBaseFee` | `bigint` | Caps the base component of the maximum Lightning routing fee, in sats.
Default: `10` sats. | | `comment` | `string` | Optional LNURL-pay comment. Only applies when the destination is an LNURL-pay endpoint and must fit the service's `commentAllowed` limit.
Default: none. | ## Swap States Read the current state of the swap with either in its [ToBTCSwapState](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/enumerations/ToBTCSwapState) enum form with [`getState()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ISwap#getstate) or in human readable [SwapStateInfo](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/SwapStateInfo) form with description with [`getStateInfo()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ISwap#getstateinfo): ``` import {ToBTCSwapState, SwapStateInfo} from "@atomiqlabs/sdk"; const state: ToBTCSwapState = swap.getState(); console.log(`State state (numeric): ${state}`); const richState: SwapStateInfo = swap.getStateInfo(); console.log(`State name: ${richState.name}`); console.log(`State description: ${richState.description}`); ``` Subscribe to swap state updates with: ``` swap.events.on("swapState", () => { const state: ToBTCSwapState = swap.getState(); }); ``` ### Table of States | State | Value | Description | | -------------------- | ----- | --------------------------------------------------------------- | | `REFUNDED` | -3 | Swap failed and was refunded | | `QUOTE_EXPIRED` | -2 | Quote expired before execution | | `QUOTE_SOFT_EXPIRED` | -1 | Quote probably expired (may still succeed if init tx in flight) | | `CREATED` | 0 | Quote created, waiting for execution | | `COMMITED` | 1 | Init transaction sent | | `SOFT_CLAIMED` | 2 | LP processing (BTC tx sent, but not yet confirmed) | | `CLAIMED` | 3 | Swap complete, BTC sent | | `REFUNDABLE` | 4 | LP failed, can refund | ## EXACT\_IN Lightning Swaps info This is an advanced feature, you should only use this if you have programmatic API to fetch invoices from a lightning network wallet and want to support `EXACT_IN` swaps. The main limitation of regular lightning network swaps (Smart chains → Lightning), is the fact that exactIn swaps are not possible (as invoices need to have a fixed amount). LNURL-pay links solve this issue, but are not supported by all the wallets. Therefore, the SDK exposes a hook/callback that can be implemented by lightning wallets directly, which request fixed amount invoices on-demand. This then makes exact input amount swaps possible. The way it works: 1. SDK sends a request to the LP saying it wants to swap `x` USDC to BTC, with a dummy invoice (either 1 sat or as specified in the `minMsats` parameter - this is requested from the `getInvoice()` function) - this dummy invoice is used to estimate the routing fees by the LP (extra care must be taken for both invoices, dummy and the real one to have the same destination node public key & routing hints). 2. LP responds with the output amount of `y` BTC 3. SDK calls the provided `getInvoice()` callback to request the real invoice for the `y` amount of BTC (in satoshis) 4. SDK forwards the returned fixed amount (`y` BTC) lightning network invoice back to the LP to finish creating the quote To get the `EXACT_IN` quote for Smart Chain → Lightning swap from the SDK: ``` const swap = await swapper.swap( Tokens.STARKNET.STRK, Tokens.BITCOIN.BTCLN, "100", // 100 STRK input SwapAmountType.EXACT_IN, starknetSigner.getAddress(), { getInvoice: async (amountSats, abortSignal?) => { // Generate invoice for the calculated output amount const invoice = await myLnWallet.createInvoice(amountSats); return invoice; }, minMsats: 1_000_000n, // Optional: 1000 sats minimum maxMsats: 1_000_000_000n // Optional: 1M sats maximum } ); ``` ## API Reference * [ToBTCSwap](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ToBTCSwap) - Swap class for Smart Chain to BTC * [ToBTCLNSwap](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ToBTCLNSwap) - Swap class for Smart Chain to Lightning * [SwapAmountType](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/enumerations/SwapAmountType) - Amount type enum * [ToBTCSwapState](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/enumerations/ToBTCSwapState) - Swap states --- # Bitcoin → Solana Swap Bitcoin L1 (on-chain) to Solana tokens. Solana still uses the legacy [PrTLC](https://docs.atomiq.exchange/overview/core-primitives/prtlc.md) based Bitcoin → smart chain flow, verified through the on-chain [Bitcoin light client](https://docs.atomiq.exchange/overview/core-primitives/bitcoin-light-client.md). Unlike the newer [UTXO-controlled vault](https://docs.atomiq.exchange/overview/core-primitives/utxo-controlled-vault.md) protocol used on Starknet and EVM, the user must first initialize a destination-side swap escrow on Solana and lock a SOL deposit before sending BTC. The user first pre-locks LP funds in the Solana-side PrTLC, sends the exact BTC amount to a dedicated swap address, and then a watchtower or the user proves the confirmed Bitcoin payment on Solana to claim the output. Because the user must post a security deposit and watchtower bounty in SOL on swap initation, this legacy flow has a cold-start requirement that the newer protocol avoids. info Starknet and EVM use the newer UTXO-controlled vault protocol. See [Bitcoin → Smart Chain](https://docs.atomiq.exchange/sdk-guide/swaps/btc-to-smart-chain.md). ## Executing the Swap Here is a full flow for creating and executing the swap in the Bitcoin → Solana direction via the `execute()` helper function, using the [FromBTCSwap](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCSwap) swap class: ``` import {SwapAmountType} from "@atomiqlabs/sdk"; // Create a quote const swap = await swapper.swap( Tokens.BITCOIN.BTC, // Bitcoin on-chain input Tokens.SOLANA.SOL, // Destination Solana token "0.0001", // Amount (10,000 sats to send) SwapAmountType.EXACT_IN, undefined, // Source address is not used for BTC source swaps solanaSigner.getAddress(), // Destination Solana address { // For additional swap options checks the `Swap Options` section } ); // Type gets inferred as FromBTCSwap // Inspect the native-SOL amounts locked during initialization console.log("Security deposit:", swap.getSecurityDeposit().toString()); console.log("Claimer bounty:", swap.getClaimerBounty().toString()); // Execute the swap const automaticallySettled = await swap.execute( solanaSigner, // Destination signer used to open the PrTLC escrow on Solana { address: "bc1q...", // User's Bitcoin address publicKey: "03...", // User's Bitcoin public key signPsbt: (psbt, signInputs) => { // Sign the funded PSBT with the Bitcoin wallet // Return the signed PSBT in hex or base64 format return ""; } }, { onDestinationCommitSent: (txId) => { console.log(`Swap escrow opened on Solana: ${txId}`); }, onSourceTransactionSent: (txId) => { console.log(`Bitcoin tx sent: ${txId}`); }, onSourceTransactionConfirmationStatus: (txId, confirmations, target, etaMs) => { console.log(`Confirmations: ${confirmations}/${target}, ETA: ${etaMs / 1000}s`); }, onSourceTransactionConfirmed: (txId) => { console.log(`Bitcoin tx confirmed: ${txId}`); }, onSwapSettled: (destinationTxId) => { console.log(`Swap settled on Solana: ${destinationTxId}`); } } ); if (!automaticallySettled) { // Handle the edge-case when watchtowers do not settle the PrTLC in time console.log("Automatic settlement timed out, claiming manually..."); await swap.claim(solanaSigner); console.log("Claimed!"); } else { console.log("Success! Output transaction ID:", swap.getOutputTxId()); } ``` warning `execute()` requires a Solana signer because the legacy Solana flow must open the destination-side PrTLC before a usable Bitcoin swap address exists, use [SolanaSigner](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-solana/src/classes/SolanaSigner), or native `@coral-xyz/anchor` [Wallet](https://solana-foundation.github.io/anchor/ts/classes/Wallet.html). This flow assumes a single-address (non-HD) Bitcoin wallet capable of signing PSBTs. If you want to handle the Bitcoin payment manually from an external wallet instead, use the manual flow below. ## Manual Execution Flow Choose the Bitcoin payment flow that matches your wallet integration: * use **Bitcoin wallet** if you already implement [IBitcoinWallet](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/interfaces/IBitcoinWallet) or [MinimalBitcoinWalletInterfaceWithSigner](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/MinimalBitcoinWalletInterfaceWithSigner) and want the SDK to send the BTC transaction after the Solana commit step. * use **Funded PSBT** if you want a signable PSBT for a single-address non-HD Bitcoin wallet. * use **External wallet** if you want to pay the exact amount from an external wallet via [`getAddress()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCSwap#getaddress) or [`getHyperlink()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCSwap#gethyperlink). - Bitcoin wallet - Funded PSBT - External wallet Pass the `bitcoinWallet` argument which follows the [IBitcoinWallet](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/interfaces/IBitcoinWallet) interface or [MinimalBitcoinWalletInterfaceWithSigner](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/MinimalBitcoinWalletInterfaceWithSigner) type to the [`sendBitcoinTransaction()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCSwap#sendbitcointransaction) function. ``` import {SwapAmountType} from "@atomiqlabs/sdk"; // Create a quote const swap = await swapper.swap( Tokens.BITCOIN.BTC, Tokens.SOLANA.SOL, "0.0001", SwapAmountType.EXACT_IN, undefined, solanaSigner.getAddress() ); // 1. Commit the swap on Solana to open the Bitcoin swap address await swap.commit(solanaSigner); // 2. Let the SDK build, sign, and send the Bitcoin transaction through your wallet integration const bitcoinTxId = await swap.sendBitcoinTransaction(bitcoinWallet); // 3. Wait for the Bitcoin transaction to reach the required confirmation count await swap.waitForBitcoinTransaction((txId, confirmations, targetConfirmations, txEtaMs) => { console.log(`${confirmations}/${targetConfirmations} confirmations, ETA ${txEtaMs / 1000}s`); }); // 4. Wait for watchtower settlement on Solana const automaticallySettled = await swap.waitTillClaimed(30); // 5. If watchtowers do not settle in time, claim manually if (!automaticallySettled) { await swap.claim(solanaSigner); } ``` Get the funded PSBT from the [`getFundedPsbt()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCSwap#getfundedpsbt) function to sign with an external single-address non-BIP32 wallet (e.g. Xverse, Unisat, Phantom, etc.). ``` import {SwapAmountType} from "@atomiqlabs/sdk"; // Create a quote const swap = await swapper.swap( Tokens.BITCOIN.BTC, Tokens.SOLANA.SOL, "0.0001", SwapAmountType.EXACT_IN, undefined, solanaSigner.getAddress() ); // 1. Commit the swap on Solana to open the Bitcoin swap address await swap.commit(solanaSigner); // 2. Ask the SDK for a funded PSBT for a non-HD single-address Bitcoin wallet const {psbtBase64, signInputs} = await swap.getFundedPsbt({ address: "bc1q...", publicKey: "03..." }); // 3. Pass psbtBase64 (or psbtHex) and signInputs to an external signer like Xverse, Unisat, Phantom, etc. const signedPsbt = await externalBitcoinWallet.signPsbt(psbtBase64, signInputs); // 4. Submit the signed PSBT back to the SDK - supports hexadecimal and base64 formats const bitcoinTxId = await swap.submitPsbt(signedPsbt); // 5. Wait for the Bitcoin transaction to reach the required confirmation count await swap.waitForBitcoinTransaction((txId, confirmations, targetConfirmations, txEtaMs) => { console.log(`${confirmations}/${targetConfirmations} confirmations, ETA ${txEtaMs / 1000}s`); }); // 6. Wait for watchtower settlement on Solana const automaticallySettled = await swap.waitTillClaimed(30); // 7. If watchtowers do not settle in time, claim manually if (!automaticallySettled) { await swap.claim(solanaSigner); } ``` Use the committed Bitcoin swap address directly from an external wallet. This is the legacy Solana-specific flow that does not require PSBT signing, but the paid amount must match [`getInput()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ISwap#getinput) exactly. ``` import {SwapAmountType} from "@atomiqlabs/sdk"; // Create a quote const swap = await swapper.swap( Tokens.BITCOIN.BTC, Tokens.SOLANA.SOL, "0.0001", SwapAmountType.EXACT_IN, undefined, solanaSigner.getAddress() ); // 1. Commit the swap on Solana to open the Bitcoin swap address await swap.commit(solanaSigner); // 2. Show the exact-address payment request to the external Bitcoin wallet const btcSwapAddress = swap.getAddress(); const btcDeepLink = swap.getHyperlink(); const exactInputAmount = swap.getInput().toString(); // 3. Wait for the Bitcoin transaction to reach the required confirmation count await swap.waitForBitcoinTransaction((txId, confirmations, targetConfirmations, txEtaMs) => { console.log(`${confirmations}/${targetConfirmations} confirmations, ETA ${txEtaMs / 1000}s`); }); // 4. Wait for watchtower settlement on Solana const automaticallySettled = await swap.waitTillClaimed(30); // 5. If watchtowers do not settle in time, claim manually if (!automaticallySettled) { await swap.claim(solanaSigner); } ``` warning `getAddress()` and `getHyperlink()` only work after the swap has been committed. When paying from an external wallet, the amount must match [`getInput()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ISwap#getinput) exactly. Sending a different amount can lead to loss of funds. info If you need to sign the Solana transactions manually, use [`txsCommit()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCSwap#txscommit) instead of [`commit()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCSwap#commit) and [`txsClaim()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCSwap#txsclaim) instead of [`claim()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCSwap#claim). The returned transactions use the [SolanaTx](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-solana/src/type-aliases/SolanaTx) type. For more information about how to sign and send these transactions manually refer to the [Manual Transactions](https://docs.atomiq.exchange/sdk-guide/advanced/manual-transactions.md) page. ## Claiming Past Unsettled Swaps If the app was offline and the watchtower did not settle the PrTLC automatically, the swap can still be manually claimed on Solana once the Bitcoin transaction reaches the required confirmations. Checking if a single swap is claimable and claiming it: ``` if (swap.isClaimable()) { await swap.claim(solanaSigner); } ``` Getting all swaps that are claimable and claiming them: ``` const claimable = await swapper.getClaimableSwaps( "SOLANA", // Only get claimable swaps on SOLANA solanaSigner.getAddress() // Only get claimable swaps for this destination address ); // This returns claimable Bitcoin → Solana and Lightning → Solana swaps for (const swap of claimable) { // All the claimable swap types have the same `claim()` function signature await swap.claim(solanaSigner); } ``` info It is a good practice to query claimable swaps on your app's startup and either claim them automatically or prompt the user to claim them. This legacy flow is more time-sensitive than the newer Bitcoin → Smart chain protocol, because the LP can refund the PrTLC after expiry if no successful claim happens in time. ## Native SOL Requirements Legacy Bitcoin → Solana swaps require native SOL for both the pre-funded destination-side security deposit, claimer bounty and the Solana transaction fees needed to open the PrTLC. * `getSecurityDeposit()`: the slashable SOL deposit that the LP can keep if the user never sends the Bitcoin payment after initiating the swap on Solana. * `getClaimerBounty()`: the SOL reward reserved for watchtowers that claim the swap automatically after Bitcoin confirmation. * `getTotalDeposit()`: the total SOL amount locked during initialization, combining the security deposit and claimer bounty (a maximum of claimer bounty and security deposit, not a sum). You can inspect the total native-token requirements before calling `commit()` or `execute()`: ``` const feeCheck = await swap.hasEnoughForTxFees(); console.log("Enough SOL:", feeCheck.enoughBalance); console.log("Wallet balance:", feeCheck.balance.toString()); console.log("Required SOL:", feeCheck.required.toString()); console.log("Security deposit:", swap.getSecurityDeposit().toString()); console.log("Claimer bounty:", swap.getClaimerBounty().toString()); console.log("Total locked deposit:", swap.getTotalDeposit().toString()); ``` warning You need enough SOL to cover the legacy deposit requirement and the destination-chain initialization fee before calling `commit()` or `execute()`. This cold-start requirement is specific to the legacy Solana flow and does not exist in the newer Bitcoin → Smart chain protocol used on Starknet and EVM. ## Swap Options The overview of possible options you can pass to the swap via the `options` argument of `swapper.swap(...)`. These let you adjust watchtower incentives. | Name | Type | Description | | ------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `unsafeZeroWatchtowerFee` | `boolean` | Attaches zero watchtower fee to the swap. This can make automatic settlement unattractive, so you may need to settle manually with `swap.claim(...)`.
Default: `false`. | | `feeSafetyFactor` | `number \| bigint` | Multiplier used when estimating the watchtower fee. Higher values make automatic settlement more attractive. Also accepts `bigint` for legacy compatibility.
Default: `1.5`. | ## Swap States Read the current state of the swap in its [FromBTCSwapState](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/enumerations/FromBTCSwapState) enum form with [`getState()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ISwap#getstate) or in human readable [SwapStateInfo](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/SwapStateInfo) form with description with [`getStateInfo()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ISwap#getstateinfo): ``` import {FromBTCSwapState, SwapStateInfo} from "@atomiqlabs/sdk"; const state: FromBTCSwapState = swap.getState(); console.log(`State (numeric): ${state}`); const richState: SwapStateInfo = swap.getStateInfo(); console.log(`State name: ${richState.name}`); console.log(`State description: ${richState.description}`); ``` Subscribe to swap state updates with: ``` swap.events.on("swapState", () => { const state: FromBTCSwapState = swap.getState(); }); ``` ### Table of States | State | Value | Description | | -------------------- | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `FAILED` | -4 | The Bitcoin swap address expired and the LP already refunded its Solana-side liquidity. No BTC should be sent anymore. | | `EXPIRED` | -3 | The Bitcoin swap address expired and should no longer be used, though a Bitcoin transaction already in flight might still let the swap succeed. | | `QUOTE_EXPIRED` | -2 | Swap quote expired and can no longer be executed. | | `QUOTE_SOFT_EXPIRED` | -1 | Swap should be treated as expired, though it might still succeed if the commit transaction is already in flight. | | `PR_CREATED` | 0 | Quote created. Use `commit()` or `txsCommit()` to open the destination-side PrTLC escrow and activate the Bitcoin swap address. | | `CLAIM_COMMITED` | 1 | The Solana escrow is open and the user can send BTC with `getFundedPsbt()`, `getAddress()`, or `getHyperlink()`. | | `BTC_TX_CONFIRMED` | 2 | The Bitcoin payment has enough confirmations. Wait for watchtowers with `waitTillClaimed()` or claim manually with `claim()` / `txsClaim()`. | | `CLAIM_CLAIMED` | 3 | Swap settled on Solana and funds were received. | ## API Reference * [FromBTCSwap](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCSwap) - Swap class for Bitcoin → Solana * [SwapAmountType](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/enumerations/SwapAmountType) - Amount type enum * [FromBTCSwapState](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/enumerations/FromBTCSwapState) - Swap states --- # Lightning → Solana Swap Bitcoin Lightning to Solana tokens. Solana still uses the legacy [HTLC](https://docs.atomiq.exchange/overview/core-primitives/htlc.md) based Lightning → smart chain flow. The user pays a Lightning invoice first, then manually initializes and claims the destination-side HTLC on Solana, revealing the secret that lets the LP settle the Lightning payment. Unlike the newer LP-initiated Lightning → smart chain protocol used on Starknet and EVM, this legacy Solana flow requires native SOL upfront for the destination transactions and a security deposit. There is no watchtower auto-claim path here: after the Lightning payment is received by the LP, the user must initialize and settle the swap on Solana themselves. info Starknet and EVM use the newer LP-initiated HTLC protocol. See [Lightning → Smart Chain](https://docs.atomiq.exchange/sdk-guide/swaps/lightning-to-smart-chain.md). ## Executing the Swap Here is a full flow for creating and executing the swap in the Lightning → Solana direction via the `execute()` helper function. The flow is the same for all the cases, with the only difference being how the Lightning payment is sourced. Uses the [FromBTCLNSwap](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCLNSwap) swap class. * Lightning wallet * External payment * LNURL-withdraw Pass a [MinimalLightningNetworkWalletInterface](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/MinimalLightningNetworkWalletInterface) object to pay the invoice programmatically. ``` import {MinimalLightningNetworkWalletInterface, SwapAmountType} from "@atomiqlabs/sdk"; // Create a quote const swap = await swapper.swap( Tokens.BITCOIN.BTCLN, // Lightning BTC input Tokens.SOLANA.SOL, // Destination Solana token 10_000n, // Amount in sats SwapAmountType.EXACT_IN, undefined, // Source address is not used for standard Lightning source swaps solanaSigner.getAddress(), // Destination Solana address { // For additional swap options checks the `Swap Options` section } ); // Type gets inferred as FromBTCLNSwap // Inspect the Lightning invoice and native-SOL requirements console.log("Invoice:", swap.getAddress()); console.log("Deep link:", swap.getHyperlink()); console.log("Security deposit:", swap.getSecurityDeposit().toString()); // Execute the swap await swap.execute( solanaSigner, // Destination signer used to commit and claim the HTLC on Solana { payInvoice: async (bolt11) => { // Pay the BOLT11 invoice with WebLN, NWC, or any other Lightning wallet integration return ""; // Empty string is fine if the wallet does not expose the payment preimage } }, // Pass the MinimalLightningNetworkWalletInterface object { onSourceTransactionReceived: (paymentHash) => { console.log(`Lightning payment received by LP: ${paymentHash}`); }, onDestinationCommitSent: (txId) => { console.log(`HTLC committed on Solana: ${txId}`); }, onDestinationClaimSent: (txId) => { console.log(`HTLC claimed on Solana: ${txId}`); }, onSwapSettled: (destinationTxId) => { console.log(`Swap settled on Solana: ${destinationTxId}`); } } ); console.log("Success! Output transaction ID:", swap.getOutputTxId()); ``` Pay the invoice or `lightning:` deeplink externally, then wait for the payment to be received by passing `null` or `undefined` to the `execute()` function. warning Make sure to call `execute()` on the swap before you display the lightning invoice to the user. This ensures the swap is properly initiated before receiving the lightning network payment. ``` import {SwapAmountType} from "@atomiqlabs/sdk"; // Create a quote const swap = await swapper.swap( Tokens.BITCOIN.BTCLN, Tokens.SOLANA.SOL, 10_000n, SwapAmountType.EXACT_IN, undefined, solanaSigner.getAddress(), { // For additional swap options checks the `Swap Options` section } ); // Show the Lightning invoice or `lightning:` deeplink to the user const lightningInvoice = swap.getAddress(); const lightningUri = swap.getHyperlink(); // Execute the swap and wait for the external payment await swap.execute( solanaSigner, undefined, // You can also pass `null` here { onSourceTransactionReceived: (paymentHash) => { console.log(`Lightning payment received by LP: ${paymentHash}`); }, onDestinationCommitSent: (txId) => { console.log(`HTLC committed on Solana: ${txId}`); }, onDestinationClaimSent: (txId) => { console.log(`HTLC claimed on Solana: ${txId}`); }, onSwapSettled: (destinationTxId) => { console.log(`Swap settled on Solana: ${destinationTxId}`); } } ); console.log("Success! Output transaction ID:", swap.getOutputTxId()); ``` Pass an LNURL-withdraw link `string` or [LNURLWithdraw](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/LNURLWithdraw) object to source the payment from LNURL-withdraw. ``` import {LNURLWithdraw, SwapAmountType} from "@atomiqlabs/sdk"; // Create a quote const swap = await swapper.swap( Tokens.BITCOIN.BTCLN, Tokens.SOLANA.SOL, 10_000n, SwapAmountType.EXACT_IN, undefined, solanaSigner.getAddress(), { // For additional swap options checks the `Swap Options` section } ); await swap.execute( solanaSigner, "lnurl1...", // Pass an LNURL-withdraw link or parsed LNURLWithdraw object to source the payment { onSourceTransactionReceived: (paymentHash) => { console.log(`Lightning payment received by LP: ${paymentHash}`); }, onDestinationCommitSent: (txId) => { console.log(`HTLC committed on Solana: ${txId}`); }, onDestinationClaimSent: (txId) => { console.log(`HTLC claimed on Solana: ${txId}`); }, onSwapSettled: (destinationTxId) => { console.log(`Swap settled on Solana: ${destinationTxId}`); } } ); console.log("Success! Output transaction ID:", swap.getOutputTxId()); ``` info For more information about how to parse LNURLs, distinguish between LNURL-withdraw and LNURL-pay variants, check out the [LNURL-withdraw](#lnurl-withdraw) section. warning `execute()` requires a destination-chain signer and enough native SOL, because the legacy Solana flow must actively commit and claim the HTLC on Solana, use [SolanaSigner](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-solana/src/classes/SolanaSigner), or native `@coral-xyz/anchor` [Wallet](https://solana-foundation.github.io/anchor/ts/classes/Wallet.html). ## Manual Execution Flow * Signer object * Manually sign transactions ``` import {SwapAmountType} from "@atomiqlabs/sdk"; // Create a quote const swap = await swapper.swap( Tokens.BITCOIN.BTCLN, Tokens.SOLANA.SOL, 10_000n, SwapAmountType.EXACT_IN, undefined, solanaSigner.getAddress() ); // 1. Show the Lightning invoice to the user const lightningInvoice = swap.getAddress(); const lightningUri = swap.getHyperlink(); // 2. Wait for the Lightning payment to be received by the LP // This is important to make sure the swap is properly initiated before the user attempts a lightning network payment const paymentReceived = await swap.waitForPayment(); if (!paymentReceived) { console.log("Lightning payment was not received before the quote expired."); return; } // 3. Commit and claim the HTLC on Solana await swap.commitAndClaim(solanaSigner); ``` info Type of the signer object passed to [`commitAndClaim()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCLNSwap#commitandclaim) is [SolanaSigner](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-solana/src/classes/SolanaSigner), or native `@coral-xyz/anchor` [Wallet](https://solana-foundation.github.io/anchor/ts/classes/Wallet.html). ``` import {SwapAmountType} from "@atomiqlabs/sdk"; // Create a quote const swap = await swapper.swap( Tokens.BITCOIN.BTCLN, Tokens.SOLANA.SOL, 10_000n, SwapAmountType.EXACT_IN, undefined, solanaSigner.getAddress() ); // 1. Show the Lightning invoice to the user const lightningInvoice = swap.getAddress(); const lightningUri = swap.getHyperlink(); // 2. Wait for the Lightning payment to be received by the LP // This is important to make sure the swap is properly initiated before the user attempts a lightning network payment const paymentReceived = await swap.waitForPayment(); if (!paymentReceived) { console.log("Lightning payment was not received before the quote expired."); return; } // 3. Get the Solana commit+claim transactions const txsCommitAndClaim = await swap.txsCommitAndClaim(); // Sign and send these transactions sequentially here, always wait for the prior confirmation first ... // 4. Important: wait till the SDK processes the successful claim and updates the swap state await swap.waitTillClaimed(); ``` info The transactions returned by [`txsCommitAndClaim()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCLNSwap#txscommitandclaim) use the [SolanaTx](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-solana/src/type-aliases/SolanaTx) type. For more information about how to sign and send these transactions manually refer to the [Manual Transactions](https://docs.atomiq.exchange/sdk-guide/advanced/manual-transactions.md) page. warning When sending `txsCommitAndClaim()` manually, do not broadcast the claim transaction before the commit transaction is confirmed. Revealing the HTLC preimage too early can let the LP settle the Lightning payment before your Solana claim is safely in place. ## Claiming Past Unsettled Swaps If the app was offline after the Lightning payment was received and the HTLC was already committed (but not claimed/settled) on Solana, the swap can still be manually claimed as long as the HTLC has not expired yet. Checking if a single swap is claimable and claiming it: ``` if (swap.isClaimable()) { await swap.claim(solanaSigner); } ``` Getting all swaps that are claimable and claiming them: ``` const claimable = await swapper.getClaimableSwaps( "SOLANA", // Only get claimable swaps on SOLANA solanaSigner.getAddress() // Only get claimable swaps for this destination address ); // This returns claimable Bitcoin → Solana and Lightning → Solana swaps for (const swap of claimable) { // All the claimable swap types have the same `claim()` function signature await swap.claim(solanaSigner); } ``` info It is a good practice to query claimable swaps on your app's startup and either claim them automatically or prompt the user to claim them. Unlike the newer Lightning → Smart chain flow, there is no watchtower fallback here. Settlement remains time-limited by the HTLC expiry, so this recovery check should run promptly. ## Native SOL Requirements Legacy Lightning → Solana swaps require native SOL for both the slashable security deposit and the destination-chain transaction fees. There is no watchtower claimer bounty in this legacy flow, because settlement is user-driven. * `getSecurityDeposit()`: the SOL bond the LP can keep if the user pays the Lightning invoice but never completes the Solana-side HTLC flow. * `getCommitAndClaimNetworkFee()`: the estimated Solana network fee for the commit and claim transactions combined. You can inspect the total native-token requirements before executing the swap: ``` const feeCheck = await swap.hasEnoughForTxFees(); console.log("Enough SOL:", feeCheck.enoughBalance); console.log("Wallet balance:", feeCheck.balance.toString()); console.log("Required SOL:", feeCheck.required.toString()); console.log("Security deposit:", swap.getSecurityDeposit().toString()); console.log("Commit + claim fee:", (await swap.getCommitAndClaimNetworkFee()).toString()); ``` warning You need enough SOL to cover the legacy deposit requirement and both destination transactions before calling `execute()` or `commitAndClaim()`. This cold-start requirement is specific to the legacy Solana flow and does not exist in the newer Lightning → Smart chain protocol used on Starknet and EVM. ## LNURL-withdraw LNURL-withdraw links (`lnurl1...`) can be used as the source of a Lightning → Smart chain swap, letting the SDK generate a BOLT11 invoice for the quote and submit it to the LNURL-withdraw service automatically when you call [`waitForPayment()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCLNAutoSwap#waitforpayment) or [`execute()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCLNAutoSwap#execute). You can parse LNURLs either with the generic [`SwapperUtils.parseAddress()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils#parseaddress) function exposed by the swapper as `swapper.Utils.parseAddress()` or with the specific [`SwapperUtils.getLNURLTypeAndData()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils#getlnurltypeanddata) function, which return the [LNURLPay](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/LNURLPay) or [LNURLWithdraw](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/LNURLWithdraw) object allowing you to read the LNURL's details. ``` const swap = await swapper.swap( Tokens.BITCOIN.BTCLN, Tokens.SOLANA.SOL, 10_000n, SwapAmountType.EXACT_IN, "lnurl1...", // LNURL-withdraw link as the source solanaSigner.getAddress() ); await swap.execute( solanaSigner, undefined, // No wallet is needed, funds come from the LNURL-withdraw source { onSourceTransactionReceived: (paymentHash) => { console.log(`Lightning payment received by LP: ${paymentHash}`); }, onDestinationCommitSent: (txId) => { console.log(`HTLC committed on Solana: ${txId}`); }, onDestinationClaimSent: (txId) => { console.log(`HTLC claimed on Solana: ${txId}`); }, onSwapSettled: (destinationTxId) => { console.log(`Swap settled on Solana: ${destinationTxId}`); } } ); ``` tip If you created a normal Lightning quote first and later want to fund it from an LNURL-withdraw source instead, call [`settleWithLNURLWithdraw(lnurl)`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCLNSwap#settlewithlnurlwithdraw) before or during `waitForPayment()`/`execute()`. This is useful when you want to display a standard invoice QR code and also allow LNURL-withdraw or NFC-card settlement. ## Swap Options The overview of possible options you can pass to the swap via the `options` argument of `swapper.swap(...)`. These let you control Lightning invoice metadata and the LP node liquidity check during quote creation. | Name | Type | Description | | ----------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `paymentHash` | `Buffer \| string` | Lets you supply your own payment hash instead of letting the SDK generate it. If you do this, you must later reveal the preimage manually during claim.
Default: generated automatically by the SDK. | | `description` | `string` | Optional Lightning invoice description. Keep it below 500 UTF-8 bytes.
Default: none. | | `descriptionHash` | `Buffer \| string` | Optional Lightning invoice description hash, useful when the invoice is exposed through LNURL-pay.
Default: none. | | `unsafeSkipLnNodeCheck` | `boolean` | Skips checking whether the LP's Lightning node has enough channel liquidity for the swap.
Default: `false`. | ## Swap States Read the current state of the swap in its [FromBTCLNSwapState](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/enumerations/FromBTCLNSwapState) enum form with [`getState()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ISwap#getstate) or in human readable [SwapStateInfo](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/SwapStateInfo) form with description with [`getStateInfo()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ISwap#getstateinfo): ``` import {FromBTCLNSwapState, SwapStateInfo} from "@atomiqlabs/sdk"; const state: FromBTCLNSwapState = swap.getState(); console.log(`State (numeric): ${state}`); const richState: SwapStateInfo = swap.getStateInfo(); console.log(`State name: ${richState.name}`); console.log(`State description: ${richState.description}`); ``` Subscribe to swap state updates with: ``` swap.events.on("swapState", () => { const state: FromBTCLNSwapState = swap.getState(); }); ``` ### Table of States | State | Value | Description | | -------------------- | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `FAILED` | -4 | The user did not settle the destination HTLC before expiry, so the Lightning payment refunds. | | `QUOTE_EXPIRED` | -3 | Swap quote expired and can no longer be executed. | | `QUOTE_SOFT_EXPIRED` | -2 | Swap should be treated as expired, though it might still succeed if the destination initialization is already in flight. | | `EXPIRED` | -1 | The destination HTLC expired, so it is no longer safe to claim on Solana. | | `PR_CREATED` | 0 | Quote created. Pay the invoice from `getAddress()` or `getHyperlink()`, then continue with `waitForPayment()`. | | `PR_PAID` | 1 | The LP received the Lightning payment. Continue by settling on Solana with `commitAndClaim()`, or with `commit()` and `claim()` separately if needed. | | `CLAIM_COMMITED` | 2 | The destination HTLC exists on Solana. Continue by claiming it with `claim()` or `txsClaim()`. | | `CLAIM_CLAIMED` | 3 | Swap settled on Solana and the revealed secret lets the LP settle the Lightning payment. | ## API Reference * [FromBTCLNSwap](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCLNSwap) - Swap class for Lightning → Solana * [SwapAmountType](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/enumerations/SwapAmountType) - Amount type enum * [FromBTCLNSwapState](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/enumerations/FromBTCLNSwapState) - Swap states --- # Utilities The Utilities section is for the helper APIs you use while assembling a swap form or route selection flow. Rather than walking through a single swap end to end, these pages focus on the smaller decisions that happen around it: parsing user input, choosing valid token pairs, understanding the route, checking limits, and estimating spendable balance. In UI terms, this includes parsing Bitcoin addresses, BOLT11 invoices, LNURLs, Lightning addresses, and smart-chain addresses, building route-aware token selectors, and adapting the form based on the selected swap protocol. ## Usage A typical integration uses these utilities in roughly this order: 1. Normalize any user-entered destination or source address with [Address Parser](https://docs.atomiq.exchange/sdk-guide/utilities/address-parser.md). This lets the UI accept Bitcoin addresses, BOLT11 invoices, LNURLs, Lightning addresses, and smart-chain addresses in a single input field, while also automatically updating the selected tokens and swap type based on the provided address. 2. Use [Supported Tokens](https://docs.atomiq.exchange/sdk-guide/utilities/supported-tokens.md) to populate the token selectors in the UI and constrain them to routes that are actually available for the current LP and chain setup. 3. Use [Swap Types](https://docs.atomiq.exchange/sdk-guide/utilities/swap-types.md) to understand which protocol the chosen pair maps to, whether the route requires a connected source or destination wallet, and whether optional capabilities such as gas drop are supported. 4. Before the user requests a quote, combine [Wallet Balance](https://docs.atomiq.exchange/sdk-guide/utilities/wallet-balance.md) and [Swap Limits](https://docs.atomiq.exchange/sdk-guide/utilities/swap-limits.md) to guide amount entry, enforce min and max bounds for the selected route, and support "Send Max" actions where appropriate. ## Topics ### Address Parser Parse destination and source inputs such as Bitcoin addresses, BOLT11 invoices, LNURLs, Lightning addresses, and smart-chain addresses before creating a quote. **[Address Parser →](https://docs.atomiq.exchange/sdk-guide/utilities/address-parser.md)** *** ### Supported Tokens Discover which source and destination tokens are currently swappable and constrain token selectors to valid routes. **[Supported Tokens →](https://docs.atomiq.exchange/sdk-guide/utilities/supported-tokens.md)** *** ### Swap Types Inspect which swap protocol a token pair uses and whether that route supports capabilities such as gas drop or requires a wallet on a specific side. **[Swap Types →](https://docs.atomiq.exchange/sdk-guide/utilities/swap-types.md)** *** ### Swap Limits Read route-specific minimum and maximum bounds so your UI can validate amounts and react to changing LP limits. **[Swap Limits →](https://docs.atomiq.exchange/sdk-guide/utilities/swap-limits.md)** *** ### Wallet Balance Estimate how much can actually be swapped after fees, both for smart-chain wallets and for Bitcoin on-chain balances. **[Wallet Balance →](https://docs.atomiq.exchange/sdk-guide/utilities/wallet-balance.md)** *** --- # Address Parser `swapper.Utils.parseAddress()` & `swapper.Utils.parseAddressSync()` are the SDK's unified parsers for user-entered destination and source fields. It is useful anywhere you accept free-form input before calling [`swapper.swap(...)`](https://docs.atomiq.exchange/sdk-guide/quick-start/creating-quotes.md), because the same field may contain a Bitcoin address, a BOLT11 invoice, an LNURL, a Lightning address, or a smart chain address. The parsers recognize: * Bitcoin on-chain addresses and [`bitcoin:` BIP-21](https://en.bitcoin.it/wiki/BIP_0021) URIs * BOLT11 Lightning invoices with an embedded amount, including `lightning:`-prefixed invoice strings * LNURL-pay links ([LUD-6](https://github.com/lnurl/luds/blob/luds/06.md)), in both: bech32 `lnurl1...` form & raw `lnurlp://...` form ([LUD-17](https://github.com/lnurl/luds/blob/luds/17.md)) * Lightning addresses ([LUD-16](https://github.com/lnurl/luds/blob/luds/16.md)) * LNURL-withdraw link ([LUD-3](https://github.com/lnurl/luds/blob/luds/03.md)), in both: bech32 `lnurl1...` form & raw `lnurlw://...` form ([LUD-17](https://github.com/lnurl/luds/blob/luds/17.md)) * Smart chain addresses for the chains configured in your swapper instance ## Parsing User Input The SDK exposes two parser variants: * an async [`parseAddress()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils#parseaddress) which parses the address and in case of LNURLs also fetches the LNURL metadata so it can resolve LNURL-pay vs LNURL-withdraw and return amount limits. * a synchronous [`parseAddressSync()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils#parseaddresssync) which parses the address data, but it does not fetch LNURL metadata and therefore cannot fully resolve LNURL inputs on its own. tip You can use synchronous `parseAddressSync()` in case you don't want support for LNURL links to prevent any hassle for handling an async address parser. In case you want to support LNURL links the common pattern is to use the sync parser while the user is typing then run the async `parseAddress()` parser on blur, paste or submit before you create a quote. * parseAddress() * parseAddressSync() Use the async [`parseAddress()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils#parseaddress) helper when you need the final normalized result for quoting or execution. This is the variant you should use when the input may be an LNURL or Lightning address, because it fetches LNURL metadata and resolves whether the destination is LNURL-pay or LNURL-withdraw. ``` const parsed = await swapper.Utils.parseAddress(userInput); if (parsed == null) { throw new Error("Unsupported address format"); } switch (parsed.type) { case "BITCOIN": console.log("Bitcoin destination:", parsed.address); console.log("Optional amount from BIP-21:", parsed.amount?.toString()); break; case "LIGHTNING": console.log("Lightning invoice amount:", parsed.amount?.toString()); break; case "LNURL": if (parsed.lnurl.type === "pay") { console.log("LNURL-pay / Lightning address"); console.log("Min:", parsed.min?.toString()); console.log("Max:", parsed.max?.toString()); console.log("Fixed amount:", parsed.amount?.toString()); } if (parsed.lnurl.type === "withdraw") { console.log("LNURL-withdraw"); console.log("Min:", parsed.min?.toString()); console.log("Max:", parsed.max?.toString()); console.log("Fixed amount:", parsed.amount?.toString()); } break; default: console.log("Smart chain address on:", parsed.type); // e.g. SOLANA, STARKNET, CITREA } ``` Use [`parseAddressSync()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils#parseaddresssync) for fast local validation while the user is typing. This version does not fetch LNURL metadata, so it can detect that an input looks like LNURL, but it cannot tell you whether it is LNURL-pay or LNURL-withdraw. ``` const parsed = swapper.Utils.parseAddressSync(userInput); if (parsed == null) { console.log("Unsupported address format"); } else { switch (parsed.type) { case "BITCOIN": console.log("Looks like a Bitcoin destination"); break; case "LIGHTNING": console.log("Looks like a Lightning invoice with amount"); break; case "LNURL": console.log("Looks like LNURL, resolve it asynchronously before quoting"); break; default: console.log("Looks like a smart chain address on:", parsed.type); } } ``` info When `parseAddressSync()` returns `type === "LNURL"`, call the async [`parseAddress()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils#parseaddress) before you rely on LNURL limits, comment support or pay/withdraw routing. ## Parsed Result Both parsers return a normalized object with the following fields: | Parameter | Type | Description | | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `address` | `string` | Normalized address or identifier recognized by the parser. For `bitcoin:` or `lightning:` inputs this is returned without the scheme prefix. | | `type` | `"BITCOIN" \| "LIGHTNING" \| "LNURL" \| "SOLANA" \| "STARKNET" ...` | The detected address family, or the smart-chain identifier for supported smart-chain addresses. | | `swapType` | [`SwapType`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/enumerations/SwapType) or `null` | The swap direction inferred from the input format when possible, for example Bitcoin on-chain for `TO_BTC`, Lightning payout for `TO_BTCLN`, or LNURL-withdraw source for `FROM_BTCLN`. | | `amount?` | [`TokenAmount`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/TokenAmount) | Fixed amount encoded in a BIP-21 URI, BOLT11 invoice or fixed-amount LNURL. | | `min?` | [`TokenAmount`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/TokenAmount) | Minimum amount allowed by a variable-amount LNURL. *This is only populated by the async parser when the input is LNURL-based.* | | `max?` | [`TokenAmount`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/TokenAmount) | Maximum amount allowed by a variable-amount LNURL. *This is only populated by the async parser when the input is LNURL-based.* | | `lnurl?` | [`LNURLPay`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/LNURLPay) or [`LNURLWithdraw`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/LNURLWithdraw) | Resolved LNURL metadata. *This is only populated by the async parser when the input is LNURL-based.* | ## Address Types ### Bitcoin and BIP-21 Bitcoin addresses and `bitcoin:` payment URIs are parsed as `type === "BITCOIN"`. This is the format you typically use as the destination in [Smart Chain → BTC/Lightning](https://docs.atomiq.exchange/sdk-guide/swaps/smart-chain-to-btc.md) swaps. If the user pastes a BIP-21 URI with an amount, that amount is returned as `parsed.amount`, which is useful for pre-filling an `EXACT_OUT` quote. ``` const parsed = await swapper.Utils.parseAddress( "bitcoin:bc1q...?amount=0.0001" ); if (parsed?.type === "BITCOIN") { console.log("Destination address:", parsed.address); console.log("Requested BTC amount:", parsed.amount?.toString()); } ``` info If you only need to check whether a given string is a bitcoin address use [`isValidBitcoinAddress()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils#isvalidbitcoinaddress) instead of the general parser. ### Lightning Invoices warning Lightning invoices without an amount are not accepted by `parseAddress()`, as the **Smart Chain → Lightning** swap requires the use of fixed amount invoices. BOLT11 invoices (also the ones prefixed with `lightning:` deeplink) are parsed as `type === "LIGHTNING"`. With the invoice amount returned in `parsed.amount`. ``` const parsed = await swapper.Utils.parseAddress("lnbc1000n1..."); if (parsed?.type === "LIGHTNING") { console.log("Invoice amount:", parsed.amount.toString()); } ``` tip If your UI needs to distinguish between "syntactically valid invoice" and "invoice valid for swap creation", use both helpers: * [`isLightningInvoice()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils#islightninginvoice) checks whether the string is a valid BOLT11 invoice at all. * [`isValidLightningInvoice()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils#isvalidlightninginvoice) checks whether the invoice also contains an amount and is therefore usable for creating **Smart Chain → Lightning** swaps. ### LNURLs and Lightning Addresses LNURL links and Lightning addresses (also with `lightning:` deeplink prefix) are parsed as `type === "LNURL"`. The parser accepts both, the bech32 & raw url encoding format for LNURLs. To fetch the type and amounts of the LNURL you have to use the asynchronous `parseAddress()` parser, as it has to fetch the LNURL metadata from the LNURL-service. The amounts are returned as: * `parsed.amount` when the LNURL has a fixed amount * `parsed.min` and `parsed.max` when the amount is user-selectable And the LNURL types (withdraw or pay): * `parsed.lnurl.type === "pay"` for **LNURL-pay** / Lightning address inputs, can be used as a destination for [Smart Chain → BTC/Lightning](https://docs.atomiq.exchange/sdk-guide/swaps/smart-chain-to-btc.md) swaps. * `parsed.lnurl.type === "withdraw"` for **LNURL-withdraw** inputs, can be used as source for [Lightning → Smart Chain](https://docs.atomiq.exchange/sdk-guide/swaps/lightning-to-smart-chain.md) or [Lightning → Solana](https://docs.atomiq.exchange/sdk-guide/swaps/solana/lightning-to-solana.md) swaps. - LNURL-pay - LNURL-withdraw ``` // Also supports lightning addresses "user@example.com" and raw "lnurlp://" links. const parsed = await swapper.Utils.parseAddress("lnurl1..."); if (parsed?.type === "LNURL" && parsed.lnurl.type === "pay") { console.log("Min payable:", parsed.min?.toString()); console.log("Max payable:", parsed.max?.toString()); console.log("Fixed amount:", parsed.amount?.toString()); console.log("Comment limit:", parsed.lnurl.commentMaxLength); console.log("Short description:", parsed.lnurl.shortDescription); // Create a quote that swaps to the LNURL-pay destination const swap = await swapper.swap( Tokens.STARKNET.STRK, Tokens.BITCOIN.BTCLN, // Swapping to Lightning parsed.min?.amount, // Swap the minimum LNURL-pay amount SwapAmountType.EXACT_OUT, // Use EXACT_OUT mode (though EXACT_IN is also supported for LNURL-pay links!!!) starknetSigner.getAddress(), // Source signer address parsed.lnurl // Use parsed LNURL-pay object as destination ); } ``` ``` // Also supports raw "lnurlw://" links. const parsed = await swapper.Utils.parseAddress("lnurl1..."); if (parsed?.type === "LNURL" && parsed.lnurl.type === "withdraw") { console.log("Min withdrawable:", parsed.min?.toString()); console.log("Max withdrawable:", parsed.max?.toString()); console.log("Fixed amount:", parsed.amount?.toString()); console.log("Description:", parsed.lnurl.params.defaultDescription); // Create a quote that swaps to the LNURL-pay destination const swap = await swapper.swap( Tokens.BITCOIN.BTCLN, // Swapping from Lightning Tokens.STARKNET.STRK, parsed.max?.amount, // Swap the maximum LNURL-withdraw amount SwapAmountType.EXACT_IN, // Use EXACT_IN mode, since the LNURL maximum is defined in sats parsed.lnurl, // Use parsed LNURL-withdraw object as source starknetSigner.getAddress() // Starknet signer's address as destination ); ... // Swap gets automatically paid by the LNURL-withdraw link when `waitForPayment()` or `execute()` is called } ``` info If you only need to check whether a string looks like LNURL before fetching metadata, use [`isValidLNURL()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils#isvalidlnurl). To fetch the metadata after that, call [`getLNURLTypeAndData()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils#getlnurltypeanddata). ### Smart Chain Addresses If the input is a supported smart chain address, the parser returns the configured chain identifier as the `type`, for example `SOLANA`, `STARKNET` or `CITREA`. This is the format you typically use as the destination in [Bitcoin → Smart Chain](https://docs.atomiq.exchange/sdk-guide/swaps/btc-to-smart-chain.md) and [Lightning → Smart Chain](https://docs.atomiq.exchange/sdk-guide/swaps/lightning-to-smart-chain.md) swaps. ``` const parsed = await swapper.Utils.parseAddress(starknetSigner.getAddress()); if (parsed?.type === "STARKNET") { console.log("Destination is a Starknet address:", parsed.address); } ``` info If you only want to know whether a given string is a an address of a specific smart chain (Solana, Starknet, EVM, tc.) you can use the [`isValidSmartChainAddress()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils#isvalidsmartchainaddress) function. ## Usage in Quote Forms Parsed address data is most useful to automatically adjust your form before you create a quote: * use `parsed.type` to automatically pick source or destination tokens / chains * use `parsed.amount` from BIP-21, BOLT11 or LNURLs to pre-fill the quote amount * use `parsed.min` and `parsed.max` from LNURLs to clamp user-entered amounts * use `parsed.lnurl.type === "pay"` to show comment input for LNURL-pay if `commentMaxLength > 0` ``` const parsed = await swapper.Utils.parseAddress(destinationInput); if (parsed == null) throw new Error("Unsupported destination"); // The address requests a fixed amount if (parsed.amount != null) { console.log("Pre-fill quoted amount:", parsed.amount.toString()); } // Can add comment to the quote for LNURL-pay -links if (parsed.type === "LNURL" && parsed.lnurl.type === "pay") { console.log("User may add a comment up to", parsed.lnurl.commentMaxLength, "chars"); } const quote = await swapper.swap( fromToken, toToken, parsed.amount?.amount ?? userSelectedAmount, amountType, sourceAddress, parsed.address, { comment: ... // In case the type is LNURL-pay and `commentMaxLength` is larger than 0 } ); ``` ## API Reference * [SwapperUtils](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils) - Utility class exposed as `swapper.Utils` * [parseAddress](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils#parseaddress) - Parse any supported address format with LNURL metadata fetching * [parseAddressSync](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils#parseaddresssync) - Parse locally without LNURL network requests * [getLNURLTypeAndData](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils#getlnurltypeanddata) - Fetch LNURL-pay or LNURL-withdraw details directly * [isValidBitcoinAddress](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils#isvalidbitcoinaddress) - Validate Bitcoin addresses * [isLightningInvoice](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils#islightninginvoice) - Validate generic BOLT11 invoices * [isValidLightningInvoice](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils#isvalidlightninginvoice) - Validate BOLT11 invoices with amount * [isValidLNURL](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils#isvalidlnurl) - Validate LNURL format * [isValidSmartChainAddress](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils#isvalidsmartchainaddress) - Validate smart chain addresses, optionally for a specific chain * [TokenAmount](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/TokenAmount) - Amount object returned by the parser * [LNURLPay](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/LNURLPay) - Parsed LNURL-pay data * [LNURLWithdraw](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/LNURLWithdraw) - Parsed LNURL-withdraw data --- # Supported Tokens The SDK exposes route discovery helpers on the `swapper` instance so you can build token selectors, validate route availability before quoting, and resolve stored token identifiers back into typed token objects. These helpers reflect two things: * the chains enabled in your `SwapperFactory` * the LPs discovered during `await swapper.init()` That means `Factory.Tokens` gives you the static typed token catalog with a list of supported tokens for the configured chains, while the methods below give you the runtime view of what is currently swappable. info Call these helpers after `await swapper.init()` so LP discovery has already completed. The returned set depends on the currently discovered LPs and the chains you initialized. tip See the complete working example: [utils/supportedTokens.ts](https://github.com/atomiqlabs/atomiq-sdk-demo/blob/main/src/utils/supportedTokens.ts) ## Token Object All token helpers and `Factory.Tokens.*` return the same [`Token`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/Token) object, this contains all the information about the token and comes with helpers for checking token equality and serializing the token to its string representation. | Field | Type | Description | | ----------------- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `chainId` | `"BITCOIN"` \| `"LIGHTNING"` \| `"STARKNET"` \| ... | Canonical chain identifier, i.e. `SOLANA`, `STARKNET`, `CITREA`, etc. for Smart chain tokens and `BITCOIN` or `LIGHTNING` for Bitcoin tokens | | `ticker` | `string` | Token symbol used in quotes and UI. | | `name` | `string` | Full token name. | | `decimals` | `number` | Raw token precision used for amount parsing. | | `displayDecimals` | `number` | Optional preferred display precision for UI formatting. | | `address` | `string` | Smart-chain contract address or mint. For BTC and BTC-LN this is an empty string. | | `equals(other)` | `boolean` | Checks whether the token equals some other token. | | `toString()` | `string` | Stable token identifier string that can be passed back into `swapper.getToken(...)`. Smart-chain tokens use `-` format. | [`SCToken`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/SCToken) and [`BtcToken`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/BtcToken) are narrowed down types for smart-chain and bitcoin based tokens, respectively. This distinction is used for automatic swap type inference in `swap()` function and also in other places. Use [`isSCToken()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/functions/isSCToken) and [`isBtcToken()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/functions/isBtcToken) typeguards to narrow down the type of `Token` to the respective `BtcToken` and `SCToken` types. tip If you need to persist a token in your database, URL, or form state, prefer `token.toString()` over manually constructing an identifier string. The returned value is compatible with `swapper.getToken(...)`. ## Listing Supported Tokens Use [`getSupportedTokens()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/Swapper#getsupportedtokens) to get a list of available source and destination tokens. This takes into account the current set of active and discovered LPs and returns tokens for which the LPs advertise support. ``` import {SwapSide} from "@atomiqlabs/sdk"; const sourceTokens = swapper.getSupportedTokens(SwapSide.INPUT); // Usable as the swap input const destinationTokens = swapper.getSupportedTokens(SwapSide.OUTPUT); // Usable as the swap output console.log("Sources:", sourceTokens.map((token) => token.toString())); console.log("Destinations:", destinationTokens.map((token) => token.toString())); ``` info `Factory.Tokens` may contain more tokens than `swapper.getSupportedTokens(...)`, because a token can exist in the configured chain catalog even when no currently discovered LP offers a route for it. ## Getting Valid Counter Tokens Use [`getSwapCounterTokens()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/Swapper#getswapcountertokens) when the token one side of the swap is already known and you want to constrain the other side. If the user already selected the input token, pass `SwapSide.INPUT` to get the valid output tokens: ``` import {SwapSide} from "@atomiqlabs/sdk"; const outputsFromStrk = swapper.getSwapCounterTokens( Tokens.STARKNET.STRK, SwapSide.INPUT ); console.log(outputsFromStrk.map((token) => token.toString())); // Returns BTC and/or BTC-LN if STRK can currently be swapped out to them ``` If the user already selected the output token, pass `SwapSide.OUTPUT` to get the valid input tokens: ``` import {SwapSide} from "@atomiqlabs/sdk"; const inputsForBtc = swapper.getSwapCounterTokens( Tokens.BITCOIN.BTC, SwapSide.OUTPUT ); console.log(inputsForBtc.map((token) => token.toString())); // Returns various smart-chain tokens that can currently be swapped to BTC ``` ## Resolving Tokens from Identifiers Use [`getToken()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/Swapper#gettoken) to resolve `Token` object from strings returned from `Token.toString()`, raw contract addresses or tickers. ``` // Bitcoin tokens const btc = swapper.getToken("BTC"); const btcln = swapper.getToken("BTC-LN"); // Chain-prefixed smart chain tickers const strk = swapper.getToken("STARKNET-STRK"); const solUsdc = swapper.getToken("SOLANA-USDC"); // Parse based on token contract address const starknetEth = swapper.getToken( "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7" ); ``` You can round-trip a token through its string representation: ``` const token = swapper.getToken("STARKNET-STRK"); const serialized = token.toString(); // "STARKNET-STRK" const sameToken = swapper.getToken(serialized); console.log(serialized); // "STARKNET-STRK" console.log(token.equals(sameToken)); // true ``` warning Bare tickers are convenient, but they can be ambiguous in a multichain context: ``` const token = swapper.getToken("WBTC"); // Can throw if WBTC exists on multiple configured chains const starknetWbtc = swapper.getToken("STARKNET-WBTC"); // Unambiguous ``` tip For compile-time setup prefer the use of a statically typed `Factory.Tokens`, instead of relying on `swapper.getToken()`. Using the `swapper.getToken()` method is useful for resolving tokens during runtime. ## Usage in a Token Selector UI You can use the exposed helper functions to build a route-aware token selector form, which automatically detects which tokens are supported and also which routes are available given an input token is chosen. ``` import {SwapSide} from "@atomiqlabs/sdk"; // Get tokens supported as a swap input const supportedSourceTokens = swapper.getSupportedTokens(SwapSide.INPUT); // You can also filter the tokens based on the chain const starknetSupportedSourceTokens = supportedSourceTokens .filter(token => token.chainId === "STARKNET"); // Check which swap routes are available from a selected source token function onSourceTokenChange(sourceTokenId: string) { // Can also pass `sourceToken: Token` directly as a function argument const sourceToken = swapper.getToken(sourceTokenId); const supportedDestinationTokens = swapper.getSwapCounterTokens(sourceToken, SwapSide.INPUT); ... } ``` ## API Reference * [Swapper](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/Swapper) - Main SDK client * [getSupportedTokens](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/Swapper#getsupportedtokens) - Get currently supported inputs or outputs * [getSwapCounterTokens](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/Swapper#getswapcountertokens) - Get valid counterpart tokens for one side of a route * [getToken](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/Swapper#gettoken) - Resolve a token by ticker or address * [SwapSide](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/enumerations/SwapSide) - Input/output side enum for token helper functions * [Token](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/Token) - Shared token object shape returned by all token helpers * [BtcToken](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/BtcToken) - Bitcoin token specialization * [SCToken](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/SCToken) - Smart-chain token specialization * [isBtcToken](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/functions/isBtcToken) - Bitcoin token type-guard * [isSCToken](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/functions/isSCToken) - Smart-chain token type-guard * [SwapperFactory](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperFactory) - Factory for building typed swappers * [TypedTokens](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/TypedTokens) - Typed token catalog derived from the configured chains ## Next Steps ### Creating Quotes Once you know the source and destination tokens, the next step is requesting a route quote with `swapper.swap(...)`. **[Creating Quotes →](https://docs.atomiq.exchange/sdk-guide/quick-start/creating-quotes.md)** *** ### Swap Types Inspect the swap protocol used for a given token pair and what capabilities it supports. **[Swap Types →](https://docs.atomiq.exchange/sdk-guide/utilities/swap-types.md)** *** ### Wallet Balance For source-token selectors and "Max" buttons, pair route discovery with the SDK's fee-aware balance helpers. **[Wallet Balance →](https://docs.atomiq.exchange/sdk-guide/utilities/wallet-balance.md)** *** --- # Swap Limits `swapper.getSwapLimits()` returns the currently known minimum and maximum bounds for a given token pair. This is useful for amount validation and min/max hints in quote forms. Like the other route utilities, swap limits are pair-specific and LP-dependent. Use [Supported Tokens](https://docs.atomiq.exchange/sdk-guide/utilities/supported-tokens.md) first to choose a valid route, then use `getSwapLimits()` to understand how large or small the input / output amounts can be. info Call `getSwapLimits()` after `await swapper.init()`, as the returned data depends on the currently discovered LPs. ## Getting Swap Limits [`getSwapLimits()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/Swapper#getswaplimits) accepts the input and output token and determines the swap minimum and maximum amounts for a given route. It returns both, limits for input (`EXACT_IN` mode) and output (`EXACT_OUT` mode). warning When discovering LPs, only the BTC-denominated minimums / maximums are part of the handshake. Limits denominated in other tokens are populated only after a quote fails because the requested amount was too low or too high. For automatically refreshing swap limits after they are populated see the next [Listening for Limit Changes](#listening-for-limit-changes) section. That means a pair can have partially known bounds at startup, then become more complete as the SDK interacts with LPs. ``` const limits = swapper.getSwapLimits( Tokens.BITCOIN.BTC, Tokens.STARKNET.WBTC ); // Swap limits for EXACT_IN mode, correspond to the source token passed (i.e. here BTC) console.log("Input min:", limits.input.min.toString()); console.log("Input max:", limits.input.max?.toString()); // Swap limits for EXACT_OUT mode, correspond to the destination token passed (i.e. here WBTC) console.log("Output min:", limits.output.min.toString()); console.log("Output max:", limits.output.max?.toString()); // May initially be undefined as it is a non-BTC asset ``` All returned amounts use the [`TokenAmount`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/TokenAmount) type: | Field | Description | | ------------- | ------------------------------------------------------------ | | `input.min` | Lowest currently known amount on the source-token side | | `input.max?` | Highest currently known amount on the source-token side | | `output.min` | Lowest currently known amount on the destination-token side | | `output.max?` | Highest currently known amount on the destination-token side | ## Listening for Limit Changes The swapper emits a `swapLimitsChanged` event whenever the swap bounds are updated, this can be due to: * new LP is discovered, or existing LP is removed. * swap with an LP failed due to the amount being too high or too low and the LP returned the new minimums / maximums. tip This is the right hook for keeping UI state in sync. ``` import {Token} from "@atomiqlabs/sdk"; function subscribeToLimits(srcToken: Token, dstToken: Token) { const handler = () => { const limits = swapper.getSwapLimits(srcToken, dstToken); updateUI(limits); }; swapper.on("swapLimitsChanged", handler); return () => swapper.off("swapLimitsChanged", handler); } ``` ## Using Limits in UI The most common use of `getSwapLimits()` is validating the user-entered amount before or after attempting to create a quote. Or passing the minimums / maximums to the HTML input fields for input / output amounts. ``` import {SwapAmountType, Token} from "@atomiqlabs/sdk"; function validateAmount( amount: bigint, amountType: SwapAmountType, srcToken: Token, dstToken: Token ): string | null { // Get the swap limits for the pair const limits = swapper.getSwapLimits(srcToken, dstToken); // Select the correct limits based on EXACT_IN or EXACT_OUT mode const side = amountType === SwapAmountType.EXACT_IN ? limits.input : limits.output; if (amount < side.min.rawAmount) return `Minimum amount is ${side.min.toString()}`; // If maximum is populated also check if the amount isn't higher than the maximum if (side.max && amount > side.max.rawAmount) return `Maximum amount is ${side.max.toString()}`; return null; } ``` ## Handling `OutOfBoundsError` If `swapper.swap(...)` fails with [`OutOfBoundsError`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/OutOfBoundsError), the SDK is telling you that the requested amount is outside the route's currently allowed bounds. ``` import {OutOfBoundsError, SwapAmountType} from "@atomiqlabs/sdk"; try { await swapper.swap( srcToken, dstToken, amount, amountType, sourceAddress, destinationAddress ); } catch (error) { if (error instanceof OutOfBoundsError) { const limits = swapper.getSwapLimits(srcToken, dstToken); const side = amountType === SwapAmountType.EXACT_IN ? limits.input : limits.output; console.log("Allowed minimum:", side.min.toString()); console.log("Allowed maximum:", side.max?.toString()); } else { throw error; } } ``` `OutOfBoundsError.min` and `OutOfBoundsError.max` are raw `bigint` values in base units of the side in which the quote amount was requested. In UI code it is usually more convenient to re-read `getSwapLimits()` and use the `TokenAmount` values for display. ## API Reference * [Swapper](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/Swapper) - Main SDK client exposing `getSwapLimits()` * [getSwapLimits](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/Swapper#getswaplimits) - Get input and output bounds for a token pair * [TokenAmount](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/TokenAmount) - Amount object returned inside the limits structure * [SwapAmountType](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/enumerations/SwapAmountType) - Distinguishes `EXACT_IN` vs `EXACT_OUT` * [OutOfBoundsError](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/OutOfBoundsError) - Error returned when a quote amount is outside the allowed range ## Next Steps ### Supported Tokens Choose a valid token pair first, then query the limits for that route. **[Supported Tokens →](https://docs.atomiq.exchange/sdk-guide/utilities/supported-tokens.md)** *** ### Creating Quotes Once you know the allowed range, the next step is requesting a quote with `swapper.swap(...)`. **[Creating Quotes →](https://docs.atomiq.exchange/sdk-guide/quick-start/creating-quotes.md)** *** --- # Swap Types `swapper.getSwapType()` is the SDK's protocol classifier for a token pair. Given a source [`Token`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/Token) and destination `Token`, it tells you which swap protocol the SDK will use. This is useful when you want to branch UI, show capability flags such as gas drop support, or understand which swap flow a later `swapper.swap(...)` quote will follow. For an overview and background on various swap types, see [Swap Types](https://docs.atomiq.exchange/sdk-guide/swaps/.md). tip See the complete working example: [utils/swapTypes.ts](https://github.com/atomiqlabs/atomiq-sdk-demo/blob/main/src/utils/swapTypes.ts) info Use [Supported Tokens](https://docs.atomiq.exchange/sdk-guide/utilities/supported-tokens.md) when you need LP-aware token lists or valid counter tokens. `getSwapType()` classifies a token pair, but it does not replace runtime route discovery. ## Getting Swap Type [`getSwapType()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/Swapper#getswaptype) accepts the input and output token and infers the swap type that will be used to execute the swap between these tokens. It returns the [`SwapType`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/enumerations/SwapType) enum. Check the [Swap Types](https://docs.atomiq.exchange/sdk-guide/swaps/.md) page to understand the differences between the various swap types. ``` import {SwapType} from "@atomiqlabs/sdk"; const swapType = swapper.getSwapType( Tokens.BITCOIN.BTC, Tokens.STARKNET.STRK ); console.log(SwapType[swapType]); // "SPV_VAULT_FROM_BTC" ``` warning Currently atomiq only supports swaps between Smart Chains and Bitcoin in both directions. Swaps between different Smart Chains (i.e. Starknet <> Solana) or between the Bitcoin layers (i.e. Lightning <> Bitcoin) are not currently supported. If you build your token selector by following [Supported Tokens](https://docs.atomiq.exchange/sdk-guide/utilities/supported-tokens.md), you usually avoid these invalid combinations before `getSwapType()` is called. info The main pattern is that Bitcoin/Lightning → Solana swaps use the legacy `FROM_BTC` and `FROM_BTCLN` swap types, non-Solana Bitcoin/Lightning → smart-chain swaps use the newer `SPV_VAULT_FROM_BTC` and `FROM_BTCLN_AUTO` swap types, and all smart-chain → Bitcoin/Lightning swaps use the same `TO_BTC` and `TO_BTCLN` swap types. ## Swap Type Capabilities Once you have the `SwapType`, you can inspect its capability flags through the statically typed [`SwapProtocolInfo`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/variables/SwapProtocolInfo) dictionary. ``` import {SwapProtocolInfo, SwapType} from "@atomiqlabs/sdk"; // Get swap type const swapType = swapper.getSwapType( Tokens.BITCOIN.BTCLN, Tokens.STARKNET.WBTC ); // Retrieve capabilites of the given swap type const capabilities = SwapProtocolInfo[swapType]; console.log(SwapType[swapType]); // "FROM_BTCLN_AUTO" console.log(capabilities.requiresInputWallet); // false console.log(capabilities.requiresOutputWallet); // false console.log(capabilities.supportsGasDrop); // true ``` ### Capabilities | Field | Description | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `requiresInputWallet` | Whether the normal flow requires a connected wallet on the input side that can sign or pay. | | `requiresOutputWallet` | Whether the normal flow requires a connected wallet on the output smart-chain side that can sign transactions. | | `supportsGasDrop` | Whether the swap type supports receiving a small amount of native smart-chain token together with the output - a "gas drop" feature. | ### Capability Matrix | Direction | SwapType | `requiresInputWallet` | `requiresOutputWallet` | `supportsGasDrop` | | --------------------------- | -------------------- | --------------------- | ---------------------- | ----------------- | | Smart chain → Bitcoin | `TO_BTC` | `true` | `false` | `false` | | Smart chain → Lightning | `TO_BTCLN` | `true` | `false` | `false` | | Bitcoin → Smart chain | `SPV_VAULT_FROM_BTC` | `true` | `false` | `true` | | Lightning → Smart chain | `FROM_BTCLN_AUTO` | `false` | `false` | `true` | | *Legacy Bitcoin → Solana* | `FROM_BTC` | `false` | `true` | `false` | | *Legacy Lightning → Solana* | `FROM_BTCLN` | `false` | `true` | `false` | ## Using Swap Type in UI Swap type and swap type capabilities are most useful when you want to enforce the need to have either a source or destination wallets connected, toggle optional features such as gas drop, or explain the route to the user. ``` import {SwapProtocolInfo, SwapType, Token} from "@atomiqlabs/sdk"; function describeRoute(from: Token, to: Token) { const swapType = swapper.getSwapType(from, to); const info = SwapProtocolInfo[swapType]; return { swapType: swapType, from: from.toString(), to: to.toString(), supportsGasDrop: info.supportsGasDrop, requiresInputWallet: info.requiresInputWallet, requiresOutputWallet: info.requiresOutputWallet }; } const route = describeRoute(Tokens.BITCOIN.BTC, Tokens.STARKNET.WBTC); if (route.supportsGasDrop) { showGasDropToggle(); } ``` This is also a clean place to branch user-facing text, for example showing that: * `FROM_BTC` and `FROM_BTCLN` are legacy Bitcoin/Lightning → Solana routes, which also require the destination wallet connected and **don't** support gas drop * `SPV_VAULT_FROM_BTC` and `FROM_BTCLN_AUTO` are the newer Bitcoin/Lightning → Smart chain routes which don't require the destination wallet to be connected and support a gas drop * `TO_BTC` and `TO_BTCLN` are Smart chain → Bitcoin/Lightning routes ## API Reference * [Swapper](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/Swapper) - Main SDK client exposing `getSwapType()` and `SwapTypeInfo` * [getSwapType](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/Swapper#getswaptype) - Get the swap protocol classification for a token pair * [SwapType](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/enumerations/SwapType) - Swap type enum * [SwapProtocolInfo](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/variables/SwapProtocolInfo) - Static capability metadata for swap types * [SwapTypeMapping](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/SwapTypeMapping) - Type-level mapping from swap type to swap class * [Token](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/Token) - Token type accepted by `getSwapType()` ## Next Steps ### Supported Tokens Use the token discovery helpers to build valid route selectors before you classify the selected pair with `getSwapType()`. **[Supported Tokens →](https://docs.atomiq.exchange/sdk-guide/utilities/supported-tokens.md)** *** ### Creating Quotes Once you know the token pair and swap family, the next step is requesting a quote with `swapper.swap(...)`. **[Creating Quotes →](https://docs.atomiq.exchange/sdk-guide/quick-start/creating-quotes.md)** *** ### Swaps Overview For the protocol-level background behind the swap types, see the overview docs covering legacy vs newer swap designs. **[Swaps Overview →](https://docs.atomiq.exchange/overview/swaps/.md)** *** --- # Wallet Balance `swapper.Utils` exposes fee-aware balance helpers for both smart chain wallets and Bitcoin wallets. These helpers are meant for swap UIs and "Max" buttons. Unlike a raw RPC balance, they estimate how much can actually be used in a swap without leaving too little for network fees. This is especially important for the exact-input flows described in [Creating Quotes](https://docs.atomiq.exchange/sdk-guide/quick-start/creating-quotes.md) and [Executing Swaps](https://docs.atomiq.exchange/sdk-guide/quick-start/executing-swaps.md). * use [`getSpendableBalance()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils#getspendablebalance) for smart chain wallet balances. * use [`getBitcoinSpendableBalance()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils#getbitcoinspendablebalance) for Bitcoin on-chain wallet balances. The returned balances use the [`TokenAmount`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/TokenAmount) object type which makes it easy to show the balance in human-readable format, get the raw token amount or estimate the USD value of the amount. * Smart chain * Bitcoin Use [`SwapperUtils.getSpendableBalance()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils#getspendablebalance) to get the spendable balance of a smart chain (i.e. Solana, Starknet, etc.) wallet, as is the case in the [Smart Chain → BTC/Lightning](https://docs.atomiq.exchange/sdk-guide/swaps/smart-chain-to-btc.md) flow. For native tokens such as `SOL`, `STRK` or the native token of an EVM chain, the SDK automatically deducts the estimated fee required to initiate the swap. The returned amount is therefore safe to use as a "Max" amount for exact-input swaps. ``` const strkBalance = await swapper.Utils.getSpendableBalance( starknetSigner, Tokens.STARKNET.STRK, { feeMultiplier: 1.1 // Optionally keep an extra 10% fee buffer } ); console.log("Spendable balance:", strkBalance.toString()); ``` For non-native tokens such as `WBTC`, transaction fees are paid in the native token of the chain, so the token balance itself is usually fully spendable. You can use the [`hasEnoughForTxFees()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/IToBTCSwap#hasenoughfortxfees) function to check if the source wallet has enough native token balance to cover the transaction fees for initiating the swap. ``` // Check if a ToBTCSwap or ToBTCLNSwap can be initiated const {enoughBalance, balance, required} = await swap.hasEnoughForTxFees(); if (!enoughBalance) { console.log(`The source wallet has insufficient balance to execute the swap, required: ${required.toString()}, balance: ${balance.toString()}`); } ``` warning A large non-native token balance does not guarantee that the swap can be initiated. The wallet still needs enough native gas token balance. This matters even more in the legacy Solana flows, where even the destination side requires SOL for deposits or transaction fees. See [Bitcoin → Solana](https://docs.atomiq.exchange/sdk-guide/swaps/solana/btc-to-solana.md) and [Lightning → Solana](https://docs.atomiq.exchange/sdk-guide/swaps/solana/lightning-to-solana.md). Use [`SwapperUtils.getBitcoinSpendableBalance()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils#getbitcoinspendablebalance) for Bitcoin on-chain source balances. This helper estimates the maximum sendable BTC amount after miner fees and takes the destination smart chain into account. Swap destination chain matters for spendable balance estimation because [Bitcoin → Smart Chain](https://docs.atomiq.exchange/sdk-guide/swaps/btc-to-smart-chain.md) on Starknet/EVM uses the newer UTXO-controlled vault flow, while [Bitcoin → Solana](https://docs.atomiq.exchange/sdk-guide/swaps/solana/btc-to-solana.md) uses the legacy Solana flow with a different Bitcoin transaction footprint. ``` const {balance, feeRate} = await swapper.Utils.getBitcoinSpendableBalance( bitcoinWalletAddress, "STARKNET", // Swap destination chain { gasDrop: true, // When swapping with the "gas drop" feature, the Bitcoin transaction gets slightly larger minFeeRate: 2 // Optionally enforce a minimum sats/vB floor } ); console.log("Spendable balance for BTC → STARKNET swaps:", balance.toString()); console.log("Using bitcoin fee rate:", feeRate, "sats/vB"); ``` info The `wallet` argument can be either a plain Bitcoin address string or a wallet object implementing [IBitcoinWallet](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/interfaces/IBitcoinWallet) or [MinimalBitcoinWalletInterface](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/MinimalBitcoinWalletInterface). tip If the user requests a gas drop in the [Bitcoin → Smart Chain](https://docs.atomiq.exchange/sdk-guide/swaps/btc-to-smart-chain.md#gas-drop) flow, be sure to pass `gasDrop: true` to `getBitcoinSpendableBalance()`, since requesting gas drop slightly increases the size of the Bicoin transaction. ## Swaps Using Max Balance Spendable balance helpers are most useful for estimating maximum amount of a swap source token that can be used for `EXACT_IN`-mode swaps. To create a swap spending the full balance of a wallet you can use the following: * Smart chain * Bitcoin ``` import {SwapAmountType} from "@atomiqlabs/sdk"; // Get the spendable balance of the signer const maxSpendable = await swapper.Utils.getSpendableBalance( starknetSigner, Tokens.STARKNET.STRK ); // Create an EXACT_IN swap spending the full wallet balance const swap = await swapper.swap( Tokens.STARKNET.STRK, Tokens.BITCOIN.BTC, maxSpendable.amount, SwapAmountType.EXACT_IN, starknetSigner.getAddress(), btcAddress ); ``` ``` import {SwapAmountType} from "@atomiqlabs/sdk"; // Get the spendable balance of the signer const {balance, feeRate} = await swapper.Utils.getBitcoinSpendableBalance( bitcoinWalletAddress, "STARKNET", // Use the proper swap destination chain { gasDrop: true // We also want to request a gas drop so set this to true } ); // Create an EXACT_IN swap spending the full wallet balance const swap = await swapper.swap( Tokens.BITCOIN.BTC, Tokens.STARKNET.WBTC, balance.amount, SwapAmountType.EXACT_IN, undefined, starknetSigner.getAddress(), { gasAmount: 1_000_000_000_000_000_000n // Also request a gas drop } ); ``` In the case of non-legacy **Bitcoin → Smart chain** swaps (*not* applicable to legacy **Bitcoin → Solana**) the LPs require a certain minimum fee rate to be used for the bitcoin transaction, this fee rate can be higher than the fee that was initially used to estimate balance, hence you might need to re-fetch the bitcoin balance using this new minimal fee rate: ``` if (swap.minimumBtcFeeRate > feeRate) { // Re-compute the maximum spendable spendable balance with the fee minimum const {balance, feeRate} = await swapper.Utils.getBitcoinSpendableBalance( bitcoinWalletAddress, "STARKNET", // Use the proper swap destination chain { gasDrop: true, // We also want to request a gas drop so set this to true minFeeRate: swap.minimumBtcFeeRate // Pass in the minimum fee rate from the created swap quote } ); ... // Now get the quote using this adjusted amount } ``` info In your UI you can hook the `getBitcoinSpendableBalance()` and `getSpendableBalance` functions to recompute on state changes, i.e. if the source/destination chain types change or gas drop request is toggled. ## API Reference * [SwapperUtils](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils) - Utility class exposed as `swapper.Utils` * [getSpendableBalance](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils#getspendablebalance) - Get spendable smart chain balance * [getBitcoinSpendableBalance](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils#getbitcoinspendablebalance) - Get spendable Bitcoin balance for a route * [getNativeToken](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils#getnativetoken) - Get the native token object for a chain * [IBitcoinWallet](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/interfaces/IBitcoinWallet) - Bitcoin wallet interface usable by the balance helper ---