Documentation: spare-wallets-kit

Documentation

spare-wallets-kit

Add Spare to an app that signs with Stellar Wallets Kit: before each signature, the user sees “Add US$0.50 for [foundation]?” and decides. Nothing is added without an explicit yes. Spare never holds funds.

Install

pnpm add spare-wallets-kit spare-sdk @creit.tech/stellar-wallets-kit @stellar/stellar-sdk

Quickstart

import { StellarWalletsKit } from "@creit.tech/stellar-wallets-kit";
import { HorizonAccountPort } from "spare-sdk";
import { withSpare } from "spare-wallets-kit";

const network = "testnet"; // "mainnet" in production

const spare = withSpare(StellarWalletsKit, {
  foundation: { account: "G...", domain: "foundation.org" }, // listed in its stellar.toml
  foundationName: "Tellus Cooperative",
  mode: { amount: "0.50" }, // or { roundUp: true } for USDC purchases
  appId: "my-app", // <= 22 ASCII bytes
  maxAmount: "5",
  accounts: new HorizonAccountPort({ network }),
  network,
  locale: "en",
});

// Same signature as the kit: use it wherever you called kit.signTransaction.
const { signedTxXdr } = await spare.signTransaction(xdr, { address });

Need to know whether the hash changed? Use signWithDonation: it returns { signedTxXdr, donation, changed, outcome }.

Soroban follow-up

A Soroban transaction holds one operation, so the donation is a separate payment after your call succeeds. It needs a second signature, and the modal says so.

// A Soroban tx holds one operation: the donation is a separate payment after success.
const follow = await spare.offerFollowUp({ successful: true, sourceAccount: address, hash });
if (follow.status === "signed") await submit(follow.signedTxXdr); // submitting is your job

API

ExportWhat it does
withSpare(kit, options)Returns { signTransaction, signWithDonation, offerFollowUp, lastDonation }.
createConfirmModal({ locale?, theme? })Default confirmation UI (vanilla DOM, shadow root). Resolves true only on the primary button.

Options

OptionRequiredDescription
foundationyes{ account, domain }. Domain-verified through SEP-1 stellar.toml.
foundationNamenoName shown to the user. Defaults to the domain.
modeyes{ amount: "0.50" } or { roundUp: true } (USDC purchases only).
appIdyesTag for the spare:<appId> memo.
maxAmountyesYour cap per donation, in USDC.
accountsyesAccountPort from spare-sdk (for example HorizonAccountPort).
fetchnoCustom fetch for stellar.toml lookups.
networkno"mainnet" (default) or "testnet". Selects the Circle USDC issuer; a transaction from another network is signed unchanged.
networkPassphrasenoUsed to parse XDR when the call does not pass one. Defaults to the passphrase of network. With neither, the tx is signed unchanged.
confirmno(prompt: DonationPrompt) => Promise<boolean>. Replaces the built-in modal.
locale / themeno"en" | "es"; "light" | "dark" | "auto" for the built-in modal.
walletnoWallet id (or getter). Defaults to kit.selectedModule.productId. LEDGER shows the “check your Ledger” note.
followUpAmountnoSoroban follow-up amount. Defaults to the fixed mode.amount; required with round-up.

Results

signWithDonation returns { signedTxXdr, donation, changed, outcome, reason? }. outcome is one of donated, declined, not-offered, passthrough or error. donation is null when the original transaction was signed, and changed is true when the signed transaction is not the one you passed in.

offerFollowUp resolves to { status: "signed", signedTxXdr, donation } or { status: "skipped", reason }. Submitting is your job.

DonationPrompt and custom confirm

DonationPrompt is { amount, foundationName, foundationDomain, mode: "fixed" | "round-up", wallet: "classic" | "soroban" | "ledger", locale, secondSignature, purchaseTotal?, roundUpTotal? }. purchaseTotal and roundUpTotal come straight from spare-sdk's donation.purchase / donation.roundedTotal. Pass your own confirm to replace the modal; return true only on an explicit yes.

Pass-through rules

  • Only unsigned, non-fee-bump, classic, single-source-signer transactions are modified. Adding an operation changes the hash and the fee, so anything else (partly signed, multisig, extra signers, fee-bump, Soroban) is signed unchanged and no donation is offered.
  • A failed check in spare-sdk (unverified foundation, no trustline, low balance, over the cap) means no prompt and the original is signed.
  • The user declines: the original XDR is signed, byte for byte.
  • Any error inside Spare (bad XDR, lookup failure, confirm crashing) fails closed: the original is signed. Errors from the wallet itself, for example the user rejecting in the wallet, are not swallowed.

role="dialog", aria-modal, labelled title, focus trap, Esc skips, focus returns to the opener, prefers-reduced-motion respected. It shows “Domain-verified: <domain>” under the title: the foundation's stellar.toml lists its account, nothing more. Focus starts on the dialog, not on a button, the primary button ignores activation for the first 400 ms, and the rest of the page is inert while it is open. Styles live in a shadow root. Fonts (Instrument Sans, Newsreader) are not bundled: load them yourself or the system fallbacks apply.