Account Demolisher
Protocol

Wallet integration

For wallet developers. The signing surface the app uses, how a wallet gets listed, how the signed envelope is checked, and what Ledger and WalletConnect do.

The app never sees a wallet's secret key. It builds an unsigned transaction, hands it to the connected wallet, checks the signed envelope that comes back, and only then submits it.

That handoff goes through Stellar Wallets Kit (@creit.tech/stellar-wallets-kit, pinned at 2.7.1), wired in src/lib/wallet/kit.ts.

What the app calls

The app's Connector interface (src/lib/wallet/connector.ts) signs whole transactions only:

Connector methodKit call
connectauthModal, the wallet picker
getPublicKeygetAddress, the address the kit stored
signTransactionsignTransaction with the network passphrase and the address
disconnectThe app's own teardown (see below)

The kit is also initialized, themed and switched between networks (init, setTheme, setNetwork). The app never calls signAuthEntry or signMessage. Every Soroban call the app builds is authorized by the source account, so the envelope signature covers it.

The requirement on a wallet is the SEP-43 transaction-signing surface: return the address, and sign a transaction XDR for a given network passphrase. Nothing app-specific is needed.

The signed envelope is checked

Every guard the app runs before signing (the contract allow-list, the authorization-tree checks, the revoke and transfer shapes, the fee caps) runs on the transaction the app built. The bytes that reach the network are whatever the wallet returns, so verifySignedEnvelope (src/lib/wallet/signed-envelope.ts) refuses to pass them on unless:

  • the wallet returned a non-empty string that parses as an envelope for the same network,
  • it is the same kind of envelope (a fee bump only when a fee bump was asked for),
  • its transaction hash equals the hash of the transaction the app built, and
  • it carries a signature from the connected key, under that key's hint, that verifies over that hash.

The signer reported onward is the key whose signature verified, never an address the wallet echoes back. The secret-key connector runs the same check on its own signature.

How a wallet gets listed

The picker's module list is built in buildModules:

const modules = [...defaultModules(), new LedgerModule()];
// plus WalletConnect when NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID is set

defaultModules() in kit 2.7.1 returns fifteen wallets, in this order:

#WalletProduct id
1Albedoalbedo
2Freighterfreighter
3Fordefifordefi
4Rabetrabet
5xBullxbull
6LOBSTRlobstr
7Hana Wallethana
8Klever Walletklever
9OneKey Walletonekey
10Bitget WalletBitgetWallet
11Cactus Linkcactuslink
12D'CENT Walletdcent
13Scopulyscopuly
14MetaMaskmetamask
15GHOSTSIGghostsig

A wallet that ships a module in the kit's defaults appears once the app moves to a kit version that includes it. There is nothing to register on the app's side.

Two modules are added on top:

  • Ledger (LEDGER), over WebUSB, always. It needs a browser Buffer global, which the app adds only when none exists.
  • WalletConnect (wallet_connect), only when NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID is set.

The picker shows 16 entries without WalletConnect and 17 with it. The kit's Trezor and HOT Wallet modules are not in defaultModules() and are not added, so they do not appear.

Wallets that are not installed stay in the list with an install link, so the list is predictable. The picker's colors follow the page's light or dark theme.

Ledger

The app signs on a Ledger with nonBlindTx: true, so the device parses and shows the operations and the merge destination instead of signing a 32-byte hash. There is no fallback to hash signing. Device refusals become messages a user can act on (src/lib/wallet/ledger-errors.ts):

Device answerMessage
Rejected on the deviceYou rejected the transaction on your Ledger
Hash signing not enabledUpdate the Stellar app on your Ledger and try again
Transaction too large for the deviceClose this account with another wallet
Transaction not parseableUpdate the Stellar app on the device, or close this account with another wallet

A transaction too large for the device to show cannot be signed on a Ledger.

WalletConnect

The picker lists WalletConnect through a lightweight stand-in that needs no network. The real module, which opens the relay connection and starts Reown AppKit, is built only when WalletConnect is used: when you pick it, when you sign with a restored WalletConnect session, or when you disconnect or close a restored session, which has to go through the relay. Restoring a saved session after a reload reads the stored address and does not start it.

realWalletConnect in src/lib/wallet/kit.ts passes appKitOptions: { features: { analytics: false } } and signClientOptions: { telemetryEnabled: false }, so AppKit reports no optional analytics event and the sign client keeps no telemetry log. AppKit still queues its mandatory events (the initialization and a successful connection, with the page URL) and sends them to pulse.walletconnect.org with navigator.sendBeacon. The Content Security Policy leaves the pulse.walletconnect.* hosts out of connect-src, so the browser blocks those requests. The security model lists every host the browser contacts.

Picking WalletConnect waits up to 15 seconds for the relay handshake and then fails with WalletConnect could not reach its relay. The chain is the public or test network to match the app; switching the app's network updates the allowed chain.

Signing for someone else on /sign

A co-signer on /sign signs through withScopedKitSession: the wallet picked for that one signature never becomes the app's connected wallet. The session that existed before is put back afterwards, or the kit is disconnected when there was none, whether the signature succeeded or not. A teardown failure is shown to the co-signer and never replaces the signing outcome.

Session lifetime

The active connector lives in module scope, not in the persisted store, because a pasted-seed connector holds the seed and must never be serialized.

That state survives client-side navigation, so a connection made on /demolish stays usable on /allowances. A hard reload loses it, and only a wallet-kit connection can be restored afterwards, from the kit's own stored selection and address.

Disconnecting clears the connector and the display store first, then tears down the kit: the selected wallet is disconnected and every WalletConnect session is closed through the sign client. A store-only disconnect would leave the extension authorizing the site, and a later connect could silently reuse that session. A wallet that refuses the teardown is reported to you, and the app is disconnected either way.

Adding a wallet in your own deployment

When you run your own copy, you can add a custom module to buildModules, so a wallet that is not yet in the kit's defaults can be listed by your deployment.

SEP support for the full list of proposals the app implements.

On this page