diff --git a/docs/build/apps/guestbook/README.mdx b/docs/build/apps/guestbook/README.mdx
index dc7f6f204f..902b7da868 100644
--- a/docs/build/apps/guestbook/README.mdx
+++ b/docs/build/apps/guestbook/README.mdx
@@ -5,6 +5,6 @@ sidebar_position: 57
import DocCardList from "@theme/DocCardList";
-This section walks you through designing and building a decentralized application (dapp) that interacts with a smart contract guestbook, allowing users to read and write public messages. This tutorial also implements a passkey-powered smart wallet for user authentication.
+This section walks you through designing and building a decentralized application (dapp) that interacts with a smart contract guestbook, allowing users to read and write public messages. The tutorial also implements a passkey-powered smart wallet for user authentication, using [Smart Account Kit](https://github.com/stellar/smart-account-kit), the [OpenZeppelin Smart Account](https://docs.openzeppelin.com/stellar-contracts/accounts/smart-account), and the [OpenZeppelin Relayer (Stellar Channels plugin)](https://docs.openzeppelin.com/relayer/1.4.x/guides/stellar-channels-guide).
diff --git a/docs/build/apps/guestbook/bindings.mdx b/docs/build/apps/guestbook/bindings.mdx
index 13ba71b1b6..11854719df 100644
--- a/docs/build/apps/guestbook/bindings.mdx
+++ b/docs/build/apps/guestbook/bindings.mdx
@@ -84,27 +84,28 @@ We're straying just a _bit_ into the Svelte-ish side of things here. The main go
Now, we'll define the contract client in a way we can easily access it through the rest of our app.
-```js title="src/lib/contracts/ye_olde_guestbook.ts"
-import * as Client from "ye_olde_guestbook"; // import the package we just added as a dependency
-import { PUBLIC_STELLAR_RPC_URL } from "$env/static/public"; // import the RPC url from the .env file
-
-// instantiate and export the Client class from the bindings package
-export default new Client.Client({
- ...Client.networks.testnet, // this includes the contract address and network passphrase
- rpcUrl: PUBLIC_STELLAR_RPC_URL, // this is required to invoke the contract through RPC calls
+```ts title="src/lib/contracts/ye_olde_guestbook.ts"
+import { Client, networks } from "ye_olde_guestbook";
+import { PUBLIC_STELLAR_RPC_URL } from "$env/static/public";
+
+// `networks.testnet` contains the contract address and network passphrase
+// baked in at bindings-generation time.
+export default new Client({
+ ...networks.testnet,
+ rpcUrl: PUBLIC_STELLAR_RPC_URL,
});
```
### The automated way
-That was a lot of steps and a lot of work wasn't it!? The good news is that our starter template (remember that?) comes with an `initialize.js` script that will perform all of those actions for you! This script will go through all the following steps for you:
+That was a lot of steps and a lot of work wasn't it!? The good news is that the guestbook repo ships with an `initialize.js` script that performs all of those actions for you. This script will:
- Create and fund a keypair in the CLI
- Install and deploy **all contracts** in the `/contracts` directory
- Generate bindings from the deployed contracts
- Create a `$lib/contracts/.ts` file for easy import into your frontend code
-You can always customize this script to suit your needs. Check out the [source code here](https://github.com/ElliotFriend/soroban-template-sveltekit-passkeys/blob/main/initialize.js) (which has been documented with comments). Or, you can see the [officially maintained script](https://github.com/stellar/soroban-template-astro/blob/main/initialize.js) in the [`soroban-template-astro` repository](https://github.com/stellar/soroban-template-astro), as well.
+You can always customize this script to suit your needs. See the [source code in the guestbook repo](https://github.com/AshFrancis/ye-olde-guestbook/blob/main/initialize.js) (which is documented with comments), or the [officially maintained version](https://github.com/stellar/soroban-template-astro/blob/main/initialize.js) in the [`soroban-template-astro` repository](https://github.com/stellar/soroban-template-astro).
Run the initialization script like so:
@@ -124,6 +125,6 @@ We've also added a command to the `package.json` scripts, so you can run this in
pnpm run setup
```
-Right, so we've now created a starter project, written a guestbook smart contract, and generated an NPM package that will help us interact with that contract on the network. Amazing!
+Right, so we've now cloned the starter project, written a guestbook smart contract, and generated an NPM package that will help us interact with that contract on the network. Amazing!
-Next up, let's take a look at how our users will connect with and interact with our dapp. It's time for passkeys! (insert air horn noises)📢
+Next up, let's set up the prerequisites for our passkey-powered smart wallets — the OpenZeppelin Relayer API key and a funder account.
diff --git a/docs/build/apps/guestbook/frontend.mdx b/docs/build/apps/guestbook/frontend.mdx
index 978c2ff44e..7297437eb3 100644
--- a/docs/build/apps/guestbook/frontend.mdx
+++ b/docs/build/apps/guestbook/frontend.mdx
@@ -7,348 +7,320 @@ So, we now have all the pieces in place, and we're ready to connect the dots.
## Account type things
-Since we've just gone through all the passkeys setup, let's begin there. We'll create the functions that will be used to create the user's smart wallet, login with their smart wallet, and the logout functionality. We'll also add a "profile menu" that can drop down when a user is logged in and give them options for viewing their smart wallet on a block explorer, sending one of those all-important donations to our guestbook, requesting more (Testnet) funds, etc.
+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.).
:::info
-We're using some pieces of [Svelte state](https://svelte.dev/docs/svelte/$state) to keep the value of the user's smart wallet contract address as well as the public key of their passkey. Your implementation of keeping this state may differ depending on your chosen frontend, state management, and project design. Hopefully, in any situation, you can draw inspiration from the way we've done it for this tutorial.
+We're using [Svelte state](https://svelte.dev/docs/svelte/$state) to keep the user's smart-account contract address and credential ID in a little store. 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
+### Connect Buttons setup
-We have a component in `$lib/components/connectButtons.svelte` that houses all the signup, login, and logout functionality. This gets put into the header component, and is available throughout the entirety of the dapp. The basic premise of this component is that we have a collection of buttons, as well as the corresponding functions that should take place when the button is clicked.
+The `ConnectButtons.svelte` component shows either the Signup/Login buttons (when logged out) or a Settings popover (when logged in). It also silently reconnects on page load if we have a saved credential ID:
-The buttons themselves are simple enough:
+```svelte title="src/lib/components/ConnectButtons/ConnectButtons.svelte"
+
-```html title="src/lib/components/connectButtons.svelte"
```
-If you look at the [source code](https://github.com/ElliotFriend/ye-olde-guestbook/blob/main/src/lib/components/ConnectButtons.svelte) of this component, you will see that we do quite a bit more state-checking surrounding the display of the buttons. This makes it so a "login" button doesn't display when a user is already _logged in_, for example. For the purpose of this tutorial, though, we'll focus on the functions themselves, rather than the HTML of the buttons.
-
-Let's begin with the Signup function.
+Let's dig into each interaction.
#### User signup
-In order to signup our user, we'll make use of the `account` instance of the `PasskeyKit` class from our `$lib/passkeyClient.ts` file. The `account` instance has a function called `createWallet` that will do most of the heavy lifting for us, we only need to make sure we call the function properly.
+For signup, we call `account.createWallet(appName, userName, { autoSubmit: true })`. Under the hood, Smart Account Kit:
-We do a little bit of error checking here, but not much. In practical applications, you would probably want to dive into the cause of any errors here, and ensure they are mitigated before telling a user to try again.
+1. Runs the WebAuthn ceremony to create a new passkey on the user's device,
+2. Builds a deploy transaction for a fresh [OpenZeppelin Smart Account](https://docs.openzeppelin.com/stellar-contracts/accounts/smart-account) using the passkey's public key as the initial signer, and
+3. Because `autoSubmit: true`, submits the deploy transaction through our `/api/relay` proxy.
-```js title="src/lib/components/connectButtons.svelte"
-import { account, send, fundContract } from "$lib/passkeyClient";
-import { keyId } from "$lib/stores/keyId";
-import { contractId } from "$lib/stores/contractId";
+Once the wallet is deployed we call `fundContract(contractId)` to airdrop 25 Testnet XLM into it.
-async function signup() {
- console.log("signing up");
- try {
- // The createWallet function takes two strings, an app name and a user name.
- // It returns the public key of the passkey, a contract address which will
- // be the user's wallet, and a built transaction (ready to submit) to create
- // the smart wallet on-chain.
- const {
- keyId_base64,
- contractId: cid,
- built,
- } = await account.createWallet("Ye Olde Guestbook", "User Name Goes Here");
-
- // Store the key ID and contract address in our localStorage stores
- keyId.set(keyId_base64);
- contractId.set(cid);
-
- if (!built) {
- error(500, {
- message: "built transaction missing",
- });
+```svelte title="src/lib/components/ConnectButtons/Signup.svelte"
+
```
#### User login
-Awesome! The user signs up and gets some (Testnet) lumens all in one go. Let's give them a way to login now with the passkey they've already associated with the smart wallet.
-
-```js title="src/lib/components/connectButtons.svelte"
-import { getContractId } from "$lib/passkeyClient";
-
-async function login() {
- console.log("logging in");
- try {
- // The connectWallet function requires us to pass a function that can
- // be used to reverse-lookup the smart wallet address, provided we know
- // the passkey's ID (the user supplies that during the function's execution)
- const { keyId_base64, contractId: cid } = await account.connectWallet({
- getContractId,
- });
+For returning users, `account.connectWallet({ prompt: true })` prompts the browser's passkey picker, and Smart Account Kit uses the selected credential to look up the corresponding smart-account contract in IndexedDB.
- // Store the key ID and contract address in our localStorage stores
- keyId.set(keyId_base64);
- console.log($keyId);
- contractId.set(cid);
- console.log($contractId);
- } catch (err) {
- console.log(err);
- toastStore.trigger({
- message: "Something went wrong logging in. Please try again later.",
- background: "variant-filled-error",
- });
- }
-}
+```svelte title="src/lib/components/ConnectButtons/Login.svelte"
+
```
-Similar, yet simpler, when compared with our `signup` function. We're using the `account.connectWallet` function. This function will:
+:::info
-1. Trigger the user to authenticate, providing the passkey's ID along the way,
-2. Use Mercury to reverse-lookup the contract ID given that passkey ID, and finally
-3. Return the passkey ID and smart wallet address to our dapp.
+No reverse-lookup indexer is needed for login. Earlier iterations of this tutorial used Mercury/Zephyr to resolve a passkey credential to a contract address; Smart Account Kit instead keeps that mapping in the browser's IndexedDB, which is written whenever a wallet is created or connected.
-Great! Let's get the user logged out when they need to.
+:::
#### User logout
-This is quite a bit easier than either signup or login functions. We don't really need to communicate with the Stellar network or Mercury here. All we'll do is clear out the user state, essentially.
-
-```js title="src/lib/components/connectButtons.svelte"
-async function logout() {
- try {
- // Reset the localStorage entry for the keyId
- keyId.reset();
- localStorage.removeItem("yog:keyId");
-
- // Set the contract address store to an empty string
- contractId.set("");
+Logout is just clearing local state:
- // Refresh the page, just for good measure
- window.location.reload();
- } catch (err) {
- console.log(err);
- toastStore.trigger({
- message: "Something went wrong logging out. Please try again later.",
- background: "variant-filled-error",
- });
- }
-}
+```ts title="src/lib/state/UserState.svelte.ts"
+reset = () => {
+ this.keyId = null;
+ this.contractAddress = null;
+ window.localStorage.clear();
+ window.location.reload();
+};
```
-With those three functions, our dapp is ready for users to authenticate with the dapp! Much easier than you probably expected it to be, right!?
+With those three flows our dapp is ready to onboard users.
### The "profile menu"
-Still in our `connectButtons.svelte` component, we also have a collection of buttons and functions that represent a "profile menu" of sorts. The user can use these buttons to view their smart wallet balance, see it on [Stellar Expert](https://stellar.expert), send a donation to our (humble) guestbook maintainer, request more (Testnet) funding, etc. Much of this is unnecessary to dive into here in this tutorial, though I highly recommend taking a look at the [source code](https://github.com/ElliotFriend/ye-olde-guestbook/blob/main/src/lib/components/ConnectButtons.svelte) to get a better understanding of this functionality.
-
-However, we will look into the `donate` function here. This is a really useful example of how a dapp can enable their smart wallet users to interact with any asset on the Stellar network. (Here, we are using Testnet XLM for our asset, but the flow would be identical for _any_ asset you may want to use.)
-
-The button is still pretty simple, just like the authentication buttons. We are adding some "loading" logic for when the transaction is taking place, though. So, it's got a _few_ more bells and whistles.
-
-```html title="src/lib/components/connectButtons.svelte"
-
-
-
-```
-
-The `donate` function takes advantage of the `native` SAC client we made in the `$lib/passkeyClient.ts` file. This allows us to call the transfer function of the contract just like any other JavaScript function.
-
-```js title="src/lib/components/connectButtons.svelte"
-import { account, send, native } from '$lib/passkeyClient';
-import { keyId } from '$lib/stores/keyId';
-import { contractId } from '$lib/stores/contractId';
-
-async function donate() {
- console.log('starting donation process');
- isDonating = true;
- try {
- const user = prompt("Give this passkey a name")
- const at = await native.transfer({
- to: networks.testnet.contractId,
- from: $contractId,
- amount: BigInt(donation * 10_000_000),
- });
-
- await account.sign(at, { keyId: $keyId });
- const res = await send(at.built!);
- console.log(res);
-
- toastStore.trigger({
- message: 'Donation received! You really ARE the goat.',
- background: 'variant-filled-success',
- });
- getBalance();
- } catch (err) {
- console.log(err);
- toastStore.trigger({
- message: 'Something went wrong donating. Please try again later.',
- background: 'variant-filled-error',
- });
- } finally {
- isDonating = false;
- }
+When a user is logged in, the Settings popover shows their balance, their contract address, and buttons for funding, donating, and logging out. Two flows here are worth a look: reading the balance (no signing), and sending a donation (the first user-signed transaction in the tutorial).
+
+#### Reading the balance
+
+The SAC stores each holder's balance in a `ContractData` ledger entry keyed by `['Balance', ownerAddress]`. We read it directly with `rpc.getLedgerEntries` — no simulation, no signing, and importantly no `contract.Client.from()` (whose SAC-spec round-trip trips the browser bundle).
+
+```ts title="src/lib/components/ConnectButtons/Settings.svelte"
+import { rpc } from "$lib/passkeyClient";
+import { Address, Asset, xdr, scValToNative } from "@stellar/stellar-sdk";
+import { PUBLIC_STELLAR_NETWORK_PASSPHRASE } from "$env/static/public";
+
+async function getBalance() {
+ const nativeContractId = Asset.native().contractId(
+ PUBLIC_STELLAR_NETWORK_PASSPHRASE,
+ );
+ const ownerScVal = new Address(user.contractAddress!).toScVal();
+ const balanceKey = xdr.LedgerKey.contractData(
+ new xdr.LedgerKeyContractData({
+ contract: new Address(nativeContractId).toScAddress(),
+ key: xdr.ScVal.scvVec([xdr.ScVal.scvSymbol("Balance"), ownerScVal]),
+ durability: xdr.ContractDataDurability.persistent(),
+ }),
+ );
+
+ const { entries } = await rpc.getLedgerEntries(balanceKey);
+ if (entries.length === 0) return "0";
+ const data = entries[0].val.contractData().val();
+ const record = scValToNative(data) as { amount?: bigint };
+ return (record.amount ?? 0n).toString();
}
```
-:::info
+A freshly-deployed wallet with no transfers yet has no `Balance(...)` entry at all, which is why we handle `entries.length === 0` as "balance is zero."
-We're simplifying this function _just a bit_ for this tutorial. In the [real dapp](https://github.com/ElliotFriend/ye-olde-guestbook/blob/1a55a5238a71b624b789dfd82a6d7fd996407bd7/src/lib/components/ConnectButtons.svelte#L138), we're using a modal to retrieve the user's input. That ends up looking a bit too cluttered for here, though.
+#### Sending a donation
-:::
+The donate button is a simple dialog with a number input. The interesting piece is `sendDonation`: we build the SAC `transfer(from, to, amount)` call directly with `contract.AssembledTransaction.build()`, passing explicit `ScVal` args so we don't need the SAC spec. Then `send(at)` hands the transaction to Smart Account Kit's `signAndSubmit`, which runs the passkey ceremony and submits through `/api/relay`.
-All in, that's a pretty easy invocation of the SAC's `transfer` function. We just pass the `from`, `to`, and `amount` fields. Then, we sign the transaction with our `account` instance, providing our passkey ID in the arguments. Finally, we send the transaction using our helper function, which will fire off the request to Launchtube, and we'll be good to go. In this case, we're not really stressed about the return value. We'll just catch any errors, and notify the user with a toast message.
+```ts title="src/lib/components/ConnectButtons/DonateButton.svelte"
+import { SAK_DEPLOYER_PUBLIC_KEY, send } from "$lib/passkeyClient";
+import { networks } from "ye_olde_guestbook";
+import { user } from "$lib/state/UserState.svelte";
+import { Address, Asset, contract, xdr } from "@stellar/stellar-sdk";
+import {
+ PUBLIC_STELLAR_NETWORK_PASSPHRASE,
+ PUBLIC_STELLAR_RPC_URL,
+} from "$env/static/public";
+
+let donation: number | undefined = $state();
+
+async function sendDonation() {
+ if (!donation) throw new Error("undefined donation amount");
+
+ const nativeContractId = Asset.native().contractId(
+ PUBLIC_STELLAR_NETWORK_PASSPHRASE,
+ );
+ const amountI128 = xdr.ScVal.scvI128(
+ new xdr.Int128Parts({
+ hi: xdr.Int64.fromString("0"),
+ lo: xdr.Uint64.fromString(
+ BigInt(Math.round(donation * 10_000_000)).toString(),
+ ),
+ }),
+ );
+
+ const at = await contract.AssembledTransaction.build({
+ contractId: nativeContractId,
+ method: "transfer",
+ args: [
+ new Address(user.contractAddress!).toScVal(), // from: smart account
+ new Address(networks.testnet.contractId).toScVal(), // to: guestbook
+ amountI128,
+ ],
+ rpcUrl: PUBLIC_STELLAR_RPC_URL,
+ networkPassphrase: PUBLIC_STELLAR_NETWORK_PASSPHRASE,
+ // G-address stand-in for the source — stellar-sdk's Account rejects
+ // C-addresses. signAndSubmit re-signs with its own deployer before
+ // submitting, so this only affects the initial simulation.
+ publicKey: SAK_DEPLOYER_PUBLIC_KEY,
+ timeoutInSeconds: 60,
+ parseResultXdr: (result: xdr.ScVal) => result,
+ });
+
+ await send(at);
+}
+```
-Enough of the account and asset things, let's get to the guestbook entries!
+The `from` argument is the user's smart account `C...` address — that's what the SAC checks against when enforcing authorization, via the smart account's passkey-signed auth entry. `send()` hands the assembled transaction to Smart Account Kit's `signAndSubmit`, which signs the auth entry, re-simulates, and submits via `/api/relay` → OZ Channels.
## Sign the guestbook
-First, we'll need a page that allows us to actually _sign_ the guestbook. We'll have a form that takes a `title` and `message` field, and then we'll submit the transaction with the `send` helper function, just like we did with the XLM transfer previously.
-
-The form is pretty simple, and it's barely worth mentioning. We have a text input, a textarea input, and a button. Some checks are performed to see if the button should be enabled (if a user is not logged in, for example). Otherwise, it's pretty unremarkable:
-
-```html title="src/routes/sign/+page.svelte"
-
+Now the main event: writing a guestbook entry. The page has a simple form with title + message inputs:
+```svelte title="src/routes/sign/+page.svelte"
-