Account type things
Since we’ve just gone through the smart account setup, let’s begin there. We’ll create the functions for signup, login, and the “profile menu” that drops down when a user is logged in (with buttons for viewing the wallet on a block explorer, sending a donation, requesting more Testnet funds, etc.).We’re using Svelte state to keep track of the user’s smart account address, in the
wallet object we built on the previous page. Your implementation may differ depending on your frontend, state management, and project design. Draw inspiration from the pattern rather than the exact code.Connect Buttons setup
TheConnectButtons.svelte component shows either the Signup and Login buttons (when logged out) or the Settings popover (when logged in). On mount, it also tries to reconnect a returning user:
src/lib/components/ConnectButtons/ConnectButtons.svelte
connectWallet() is called here with no arguments. That’s deliberate: without prompt: true, the kit will restore an existing session if it finds one and otherwise do nothing at all. No passkey prompt fires on page load, which is exactly what you want. We never assign to wallet.contractAddress in this component either, because the kit emits walletConnected and our Wallet class is already listening.
Let’s dig into each interaction.
User signup
For signup, we callaccount.createWallet(). Under the hood, Smart Account Kit:
- Runs the WebAuthn ceremony to create a new passkey on the user’s device,
- Builds a deploy transaction for a fresh OpenZeppelin Smart Account using the passkey’s public key as the initial signer,
- Submits that deploy transaction through our
/api/sendroute (because we passedautoSubmit: true), and - Tops the new account up with some Testnet XLM (because we passed
autoFund: true).
src/lib/components/ConnectButtons/Signup.svelte
createWallet reporting two outcomes which don’t deserve equal treatment: a failed deploy leaves the user with nothing, while a failed top-up leaves them with a working (if empty) smart account. Treating those the same way would mean failing signup over play money.
userDismissedPasskey earns its keep for a similar reason. A user who opens the passkey prompt and thinks better of it has not encountered an error, and telling them “something went wrong” is a small lie that makes your app feel broken. The signup and login flows check for the dismissal first.
User login
For returning users,account.connectWallet({ prompt: true }) asks the browser’s passkey picker to let the user choose a credential. Smart Account Kit takes the selected credential and resolves the smart account contract it belongs to.
src/lib/components/ConnectButtons/Login.svelte
catch block runs the error past describeConnectionError from the setup page before falling back to a generic message. “Something went wrong, please try again later” is actively misleading for a passkey that is never going to work on this account.
User logout
Logging out is a single call.account.disconnect() clears the kit’s stored session and emits walletDisconnected, which our Wallet class picks up to null out the address, which flips ConnectButtons back to showing Signup and Login. No manual state juggling and no page reload.
src/lib/components/ConnectButtons/Settings.svelte
The “profile menu”
When a user is logged in, the Settings popover shows their balance, their contract address, and buttons for funding, donating, and logging out. Three of those are worth a look: reading a balance (no signing at all), topping the wallet up, and sending a donation (the first user-signed transaction in the tutorial).Reading the balance
This one’s already done. We wrotegetNativeBalance back on the setup page, and it simulates a balance call against the native Stellar Asset Contract. Simulation costs nothing and needs no signature, so reading a balance is about as cheap as an interaction gets.
src/lib/components/ConnectButtons/Settings.svelte
/ 1e7 in the markup that renders it as XLM.
Funding the wallet
Signup already funded the account once, but Testnet play money has a way of running out.account.fundWallet() is the same machinery behind the autoFund option, exposed as a button.
src/lib/components/ConnectButtons/Settings.svelte
{ success, error } result rather than by throwing, which is generally very pleasant. But toaster.promise wants a rejected promise to show its error state. So we translate between the two conventions, and this little if (!result.success) throw result.error shim shows up in a few places in the codebase for exactly that reason.
This is worth internalizing before you go hunting for a bug that isn’t there: with the kit’s submission methods, a
try/catch alone will not catch a failed transaction. You have to check result.success. The catch block is for the unexpected stuff (a dismissed passkey, a network blip, a programming error).Sending a donation
The hubris of soliciting donations on a guestbook is a matter for the maintainer’s conscience. Mechanically, though, it’s a lovely demonstration, because a donation is the first thing in this tutorial that moves real value and needs the user’s passkey. It’s also a one-liner:src/lib/components/ConnectButtons/DonateButton.svelte
account.transfer() takes the token contract, the recipient, and an amount in whole tokens (the kit handles the conversion to stroops for you), then builds the transfer, prompts the user’s authenticator, signs the authorization entry, re-simulates with the real signature in place, and submits through our relayer route. That last re-simulation matters more than you’d think: a WebAuthn signature is substantially larger than the placeholder used in the first simulation, so the resource fees have to be recalculated before submission. The kit does it; you just need to know it’s happening if you ever go building transactions by hand.
The recipient here is networks.testnet.contractId, which is the guestbook contract itself, pulled straight from our generated bindings. The from address is implicit: it’s the connected smart account, and the SAC enforces that with the passkey-signed authorization entry.
Sign the guestbook
Now the main event: writing a guestbook entry. The page itself is an ordinary form, with inputs bound tomessageTitle and messageText and a Sign button wired to the function below, so we’ll skip the markup and go straight to where the contract call happens:
src/routes/sign/+page.svelte
write_message function comes from our generated contract bindings, so invoking a Soroban function looks like any other typed TypeScript call. account.signAndSubmit() then does the passkey ceremony, the re-simulation, and the submission.
With the ID in hand, we redirect the user to the page for their shiny new entry.
Read guestbook entries
Read a single entry
Reading doesn’t involve passkeys, relayers, or signatures at all, which makes it a nice palate cleanser. We use a server-sideload function so the query runs on the server (and could benefit from caching if we were using a paid RPC). The [id] in the filename is a path parameter.
src/routes/read/[id]/+page.server.ts
read_message is a read-only function, so the bindings client simulates it and we can use the result directly. No signing and no submission needed. The result.unwrap() call is there because our contract function returns a Rust Result, which the bindings faithfully carry across into TypeScript.
The page component then hands that message straight to a shared GuestbookMessage component for display, which is where we’re headed next.
Read all entries
For the “list all entries” page, we keep the query logic server-side insrc/lib/server/getLedgerEntries.ts. Rather than invoking the contract once per message, it reads the ledger directly: one rpc.getLedgerEntries() call against the contract’s instance storage to find the message count, then batched getLedgerEntries calls of at most 200 keys (the Stellar RPC cap).
src/lib/server/getLedgerEntries.ts
Message(u32) variant of the DataKey enum, remember), we can construct those ledger keys ourselves and ask RPC for them in batches of at most 200. See the full implementation for the key-building helper and the message-count read.
Edit a guestbook entry
Inside theGuestbookMessage component, the logged-in author of a message can edit it. When they submit the edit, we call edit_message the same way we called write_message: build, sign, submit, done.
src/lib/components/GuestbookMessage.svelte
author argument here, and there’s no pollTransaction either. We don’t need the author because the contract’s edit_message function reads it from its own storage and requires authentication from that specific smart account, which means the original author is the only one who can modify their entry. Not even the guestbook’s host can change it. And we don’t need to poll because edit_message doesn’t return anything we care about, so the hash is a perfectly good receipt.
That’s the full flow. With account.signAndSubmit(at) standing in for “sign this with my passkey and get it on-chain without charging my user,” wiring a Soroban dapp up to a smart wallet gets about as light as it’s ever been.
Way to go! Now go put something in the guestbook.