Build on Liquid Connect

One protocol connects Liquid wallets to sites and services. Keys stay in the wallet, every approval happens there, and SideSwap runs the server in between.

Make your wallet a Liquid Connect wallet

One integration gives your users every Liquid Connect site, from trading to payments and contract flows, with keys that never leave your wallet and approvals that always happen on your screens.

  1. 1

    An identity, not an account

    Your wallet logs in with a challenge signed by a key derived from material it already holds. The public key is the wallet's identity, with nothing to register and nothing new to back up.

  2. 2

    Activation is the consent

    Joining Liquid Connect shares the wallet's descriptor with the Connect server, never with sites, and pairs the wallet with the payment service, so payment requests work from the first minute. There is no separate switch for payment requests. Activation turns them on, and every request is still approved in the wallet. Contact the server only once the user activates, and show them their Liquid Connect ID then.

  3. 3

    Catch the link

    Register the liquidconnect scheme and scan QR codes. A link carries a single use request ID. Your wallet claims it and shows the site that asks. A link that arrives before activation leads into activation first and then on to the pairing.

  4. 4

    Verify, then sign

    Requests to sign arrive as PSETs. Your wallet is the only verifier. It decodes, shows and signs with its own keys. The SDK turns a PSET into something to show, and says plainly what it cannot read.

use lc_wallet_core::key::{Network, WalletKey};
use lc_wallet_core::transport::{WalletConnect, WalletConnectConfig, WalletEvent};
use lc_wallet_core::{approval, wire};

// The wallet's Connect identity, from key material it already holds.
let key = WalletKey::new(master_blinding_key, Network::Liquid);

let (wallet, mut events) = WalletConnect::spawn(WalletConnectConfig {
    url: wire::MAINNET_URL.to_owned(),
    descriptor: wallet_descriptor, // shared with the Connect server, never with sites
    key,
    install_id,                    // InstallId::random(), persisted once
});

// A scanned QR or a tapped link claims the waiting request.
wallet.open_link(&scanned_text)?;

while let Some(event) = events.recv().await {
    match event {
        WalletEvent::LoginRequested(req) => {
            // Show req.domain, and on approval
            wallet.accept_login(&req.request_id);
        }
        WalletEvent::SignRequested(req) => {
            let summary = approval::summarize_pset(&req.pset, Network::Liquid)?;
            // Show the summary, check it with your own decode, sign with
            // your own signer, then hand back the result
            wallet.accept_sign(&req.request_id, &signed_pset);
        }
        _ => {}
    }
}

Reconnecting, keepalive, the challenge login and the life of each request are handled for you. The state machine does no networking of its own, so a wallet with its own networking can drive it directly.

A Rust core with Kotlin and Swift bindings. A headless example wallet ships with it. Point it at the testnet address under Networks and connect it to testnet.sideswap.io to watch the whole flow.

Optional, a reachable identity for your users

The identity module lets a user attach a verified email address or phone number to their wallet, so people who already know them can find and pay them. Matching is on hashes and discoverability is opt in. No contact detail is stored in plain text.

Connect

Free

Sessions, approvals and signing

Email identity

Free

A verified email address that people who have it can pay

Phone identity

0.5 USDt

A verified number, paid from the connected wallet itself. A wallet holding only USDt can pay, as the network fee goes through SideSwap's payjoin.

What the SDK will never do

Hold keys, sign transactions or approve anything. Signing belongs to your wallet alone. The Connect server relays requests and cannot spend.

Wallet connectivity without custody

Your site or app gets a live view of the wallets that approve it, limited to its own session, and can ask them to sign. That is all it gets. Keys stay in the wallet and the user can end the connection at any time.

One WebSocket and JSON frames, no SDK needed. sideswap.io is itself an integration.

  1. 1

    Connect as your domain

    Open a WebSocket to the Connect server and log in as your domain. It is what the user's wallet shows when you ask for anything. On both networks you prove you own it.

  2. 2

    Ask to connect

    StartLogin returns a single use, short lived request ID. Show it as a QR code or a link. The user approves in their wallet. On a web page the connect button does this part for you.

  3. 3

    Receive the session

    Approval creates a session and the server streams snapshots of the wallet's coins, blinding data included, so you can build valid confidential PSETs without looking anything up. You never see the descriptor.

  4. 4

    Ask for a signature

    Build the PSET and send StartSign with the session. The wallet checks it, the user approves, the signed PSET comes back and you broadcast it.

→ {"Req":{"id":1,"req":{"StartLogin":{"client_data":null}}}}
← {"Resp":{"id":1,"resp":{"StartLogin":{"login_request":{"request_id":"…","expires_at":…}}}}}
   … the user approves in their wallet …
← {"Notif":{"notif":{"SessionCreated":{"session":{…}}}}}
← {"Notif":{"notif":{"WalletUpdated":{"wallet":{"wallet_id":"…","utxos":[…]}}}}}
→ {"Req":{"id":2,"req":{"StartSign":{"session_id":"…","pset":"…","ttl":300000}}}}
← {"Notif":{"notif":{"SignRequestUpdated":{"sign_request":{"status":{"Succeed":{"pset":"…"}}}}}}}

Requests are single use and time limited, so a request the user never approves simply expires. The Connect server only relays. It never inspects your PSET and cannot sign anything.

What you get, and what you never get

  • Live snapshots of the coins of each wallet that approved you, for its session
  • A signing channel that ends at the user's own device
  • QR and app link hand off to phone and desktop wallets
  • The wallet descriptor, keys, or history beyond your session
  • A signature without the user approving your request, by name
  • Anything after the user ends the session

More than payments

Connections and PSET signing are the base. On testnet your service can also use richer requests, which reach production as the network rolls forward.

Clear signed messages

Ask the wallet to sign a digest with a readable description of what it allows. The wallet rebuilds the digest from the typed fields before anyone approves, so what the user reads is what gets signed.

Payment requests

Send the recipient, asset, amount and a memo, and the wallet builds, approves, signs and broadcasts the payment from its own coins.

Venue account links

Bind a wallet to an account at your service in one scan, with guards against a link being steered elsewhere.

The connect button

One script tag gives your site the whole flow of connecting a SideSwap wallet, the same on every Liquid Connect site. A phone opens the wallet directly and comes back by itself, a desktop chooses between a QR for the phone and a wallet on the same computer, and the wait survives the trip to the app. Your backend keeps talking to Liquid Connect as it does, and the button only owns the part people see.

<script src="https://sideswap.io/connect/lc-connect.js"></script>
<div id="lc"></div>
<script>
  LiquidConnect.mount("#lc", {
    start: "/api/connect/start",   // POST
    status: "/api/connect/status", // GET ?request_id=
    onConnected(answer) {
      // answer is what your status route returned
    },
  });
</script>

What your backend answers

RouteYour backend answersWhat the button does
POST start{ request_id, deep_link, expires_at }Shows the QR or opens the wallet for this single use request
GET status{ status: "pending" } or { status: "WaitUser" }Keeps waiting. The wallet has the request, or soon will
GET status{ status: "approved", … }Closes the modal and calls onConnected with this answer
GET statusanything elseShows a failure with a retry, unless your onStatus says to keep waiting

Options

OptionWhat it does
start, statusYour two routes. Either may be a function in place of an address, for a login that runs over your own websocket. The start function resolves to the same answer as the route, and the status function takes the request ID.
walletsUrl, installUrlWhere Need a wallet sends people. By default the wallets page next to the script and the SideSwap downloads page.
label, stringsThe button's text, and your own words for any line of the dialog.
themeDark is the default. Set it to light for a light page. The colours are CSS variables on the element you mount on, if you want your own.
chipShows the connected wallet with a menu to disconnect. Off by default, for sites that have a chip of their own.
disconnectA route the chip calls with POST when the person disconnects, or a function.
buttonSet it to false to draw no button and open the dialog from your own control.
storageKeyWhere the waiting request is remembered, so the flow survives the trip to the app and a reloaded tab.
onConnected, onStatus, onFailed, onCancelled, onDisconnectedYour callbacks, all optional. onStatus may answer stop to take over or continue to keep waiting. onFailed may answer handled to show your own fallback.

What it returns

Mounting returns an object with these.

MemberWhat it is for
connect(), cancel(), resume()Open the dialog, close it, or pick up a request that was waiting before a reload.
disconnect(), setDisconnected()End the connection from your own control, or tell the button that it has ended.
setConnected({ wallet })After a reload, when your page already holds a session.
setLive(bool)When the wallet's own Liquid Connect session comes or goes.
needWallet()Open the Need a wallet screen from your own link.
walletsUrl, isPhone, versionWhat the button worked out, for your own use.

Rules it keeps

  • On a phone the wallet is opened directly and told to hand the person back to your page once they approve.
  • The QR always carries the plain link, because the phone that scans it is a different device.
  • It keeps asking for 90 seconds past the request's expiry, so an approval given in time is still picked up.
  • Every screen offers a way out for someone without a wallet. Need a wallet is the third choice next to Desktop and Mobile, and it is on the retry screens too.
  • Nothing in it holds a secret. Your backend issues whatever session it likes in its answer.

Networks and domain proof

One protocol on two networks. Start on testnet. On both networks a site is identified by its domain, and both ask for proof that you own it before a login is accepted.

NetworkWallets connect toSites connect to
Testnetwss://connect-testnet.sideswap.io/wallet-connectwss://connect-testnet.sideswap.io/server-connect
Productionwss://connect.sideswap.io/wallet-connectwss://connect.sideswap.io/server-connect

Proving your domain

The proof is one record for the domain your site logs in as. Publish one of the two below, then log in. The user's wallet shows your domain on every request, and the proof is what makes that name worth trusting.

A DNS TXT record at _liquidconnect.<your-domain> or a file at https://<your-domain>/.well-known/liquid-connect/rp.json

NetworkThe TXT record's valueThe file's contents
Testnetv=lc1; server=connect-testnet.sideswap.io{"v":"lc1","domain":"<your-domain>","servers":["connect-testnet.sideswap.io"]}
Productionv=lc1; server=connect.sideswap.io{"v":"lc1","domain":"<your-domain>","servers":["connect.sideswap.io"]}

The verdict is kept for an hour. The first login after you publish may answer that domain verification is in progress. Reconnect and it goes through.

Talk to us about integrating at [email protected].