diff --git a/docs/build/apps/guestbook/bindings.mdx b/docs/build/apps/guestbook/bindings.mdx index fa5f54889..998872515 100644 --- a/docs/build/apps/guestbook/bindings.mdx +++ b/docs/build/apps/guestbook/bindings.mdx @@ -19,37 +19,37 @@ We'll be generating our contract bindings, and keeping them in the same reposito - Your deploy process might include a step that builds/deploys/binds a contract package at deploy-time. - You could even generate and publish a bindings package all by itself. Then `pnpm install ` can be done in any dapp that you (or somebody else) might need to interact with that contract. -### The manual method +## The manual method Before you skip ahead! Take a look at this (brief) section. It's _really_ useful to have a full understanding of what steps we're going through in the automated section. This will help you adapt and/or troubleshoot this tutorial for your specific purposes. -#### Install the compiled contract +### Install the compiled contract The smart contract code needs to be installed to the network first. This uploads the compiled, binary Wasm file to the blockchain to be instantiated into a contract later on. From inside your project directory: -```shell +```sh stellar contract upload \ --source-account \ --network testnet \ --wasm ./target/wasm32v1-none/release/ye_olde_guestbook.wasm ``` -#### Deploy a contract instance +### Deploy a contract instance -This will return a hexadecimal hash corresponding to the uploaded Wasm executable. This hash can then be used in the deploy command to create a new contract instance: +This will return a hexadecimal hash corresponding to the uploaded Wasm executable (it's just the Sha256 hash of the executable file, fyi). This hash can then be used in the deploy command to create a new contract instance: -```shell +```sh stellar contract deploy \ --source-account \ --network testnet \ --wasm-hash ``` -#### Generate bindings for the deployed contract +### Generate bindings for the deployed contract Now we can (again) use the Stellar CLI to generate bindings from the contract we've just deployed. You can also generate these bindings from your local Wasm file using the `--wasm-hash` parameter. The `--overwrite` parameter is used to tell the CLI that it should output the generated bindings package, even if it finds the directory is not empty (i.e., we're re-binding a contract because we've modified the code and redeployed it). -```shell +```sh stellar contract bindings typescript \ --network testnet \ --id \ @@ -59,68 +59,76 @@ stellar contract bindings typescript \ We'll need to build the bindings package, since (in its initial state) the package is mostly TypeScript types and stubs for the various contract functions. -```shell +```sh cd packages/ye_olde_guestbook pnpm install pnpm run build cd ../.. ``` -#### Import the bindings package as a project dependency +:::tip[Customize your bindings] + +You could take this opportunity to customize your generated bindings _before_ you build them. By default, generated bindings will re-export the entirety of `@stellar/stellar-sdk` for your frontend application. If this behavior isn't desired, you can remove it. These packages are your own to modify as you see fit. + +::: + +### Import the bindings package as a project dependency With our bindings generated, we can add it to our frontend project. Run this from the root of your project: -```shell +```sh pnpm add file:./packages/ye_olde_guestbook ``` -#### Import the bindings client into the SvelteKit project +### Import the bindings client into the SvelteKit project :::info -We're straying just a _bit_ into the Svelte-ish side of things here. The main goal of this step is to get the contract client (which is the "bindings package" we've just generated) into our frontend in a way that makes it usable anywhere we need it. In SvelteKit, we put it into `src/lib/contracts` because that means we can easily access the client by importing from `$lib/contracts/ye_olde_guestbook` whenever and wherever we need it. +We're straying just a _bit_ into the Svelte-ish side of things here. The main goal of this step is to get the contract client (which is the "bindings package" we've just generated) into our frontend in a way that makes it usable anywhere we need it. In SvelteKit, we're putting it into `src/lib/contracts` because that means we can easily access the client by importing from `$lib/contracts/ye_olde_guestbook` whenever and wherever we need it. ::: 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 { Client, networks } from "ye_olde_guestbook"; // import from 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 +export default new 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 }); ``` -### The automated way +## The automated way + +That was a lot of steps and a lot of work wasn't it!? -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: +The good news is that our starter template (remember [that](./overview.mdx#start-from-the-stellar-template-repository)?) 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: - 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. Check out the [source code here](https://github.com/ElliotFriend/stellar-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. Run the initialization script like so: -```shell +```sh node initialize.js ``` :::info -For a more comprehensive overview of the process of creating, customizing, and using initialization scripts like this, check out the [frontend template guide](https://developers.stellar.org/docs/build/guides/dapps/soroban-contract-init-template). +For a more comprehensive overview of the process of creating, customizing, and using initialization scripts like this, check out the [frontend template guide](../../guides/dapps/soroban-contract-init-template.mdx). ::: We've also added a command to the `package.json` scripts, so you can run this initialize script simply by running (from your project's root directory): -```shell +```sh pnpm run setup ``` diff --git a/docs/build/apps/guestbook/frontend.mdx b/docs/build/apps/guestbook/frontend.mdx index 978c2ff44..e1c489859 100644 --- a/docs/build/apps/guestbook/frontend.mdx +++ b/docs/build/apps/guestbook/frontend.mdx @@ -11,21 +11,25 @@ Since we've just gone through all the passkeys setup, let's begin there. We'll c :::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 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 ID 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. ::: ### 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. +We have a few components in the `$lib/components/ConnectButtons` directory. In that directory, the sub-components house all the signup, login, and logout functionality (as well as some "goodies" for logged in users). This gets put into the header component, and is available throughout the entirety of the dapp. The basic premise of these components are that we have a collection of buttons, as well as the corresponding functions that should take place when the button is clicked. -The buttons themselves are simple enough: +The stripped-down basics of the component is simple enough: -```html title="src/lib/components/connectButtons.svelte" +```html title="src/lib/components/ConnectButtons/ConnectButtons.svelte"
- - - + + +
``` @@ -39,10 +43,9 @@ In order to signup our user, we'll make use of the `account` instance of the `Pa 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. -```js title="src/lib/components/connectButtons.svelte" +```js title="src/lib/components/ConnectButtons/Signup.svelte" import { account, send, fundContract } from "$lib/passkeyClient"; -import { keyId } from "$lib/stores/keyId"; -import { contractId } from "$lib/stores/contractId"; +import { user } from "$lib/state/UserState.svelte"; async function signup() { console.log("signing up"); @@ -51,31 +54,31 @@ async function signup() { // 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"); + const { keyIdBase64, contractId, signedTx } = 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); + // Store the passkey ID and contract address in our localStorage state + user.set({ + keyId: keyIdBase64, + contractAddress: contractId, + }); - if (!built) { + if (!signedTx) { error(500, { message: "built transaction missing", }); } - // Send the transaction, fund the smart wallet, refresh the balance - await send(built); - await fundContract($contractId); - getBalance(); + // Send the transaction, and then fund the smart wallet + await send(signedTx); + await fundContract(user.contractAddress); } catch (err) { - console.log(err); - toastStore.trigger({ - message: "Something went wrong signing up. Please try again later.", - background: "variant-filled-error", + console.error(err); + toaster.error({ + title: "Error", + description: "Something went wrong signing up. Please try again later.", }); } } @@ -85,29 +88,28 @@ async function signup() { 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"; - +```js title="src/lib/components/ConnectButtons/Login.svelte" 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, - }); + const { keyIdBase64, contractId } = await account.connectWallet(); // Store the key ID and contract address in our localStorage stores - keyId.set(keyId_base64); - console.log($keyId); - contractId.set(cid); - console.log($contractId); + user.set({ + keyId: keyIdBase64, + contractAddress: contractId, + }); + + console.log("keyId", user.keyId); + console.log("contractAddress", user.contractAddress); } catch (err) { - console.log(err); - toastStore.trigger({ - message: "Something went wrong logging in. Please try again later.", - background: "variant-filled-error", + console.error(err); + toaster.error({ + title: "Error", + description: "Something went wrong logging in. Please try again later.", }); } } @@ -116,7 +118,7 @@ async function login() { Similar, yet simpler, when compared with our `signup` function. We're using the `account.connectWallet` function. This function will: 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 +2. Determine the deployed contract address given it was salted using the passkey's ID, and finally 3. Return the passkey ID and smart wallet address to our dapp. Great! Let's get the user logged out when they need to. @@ -125,23 +127,21 @@ Great! Let's get the user logged out when they need to. 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"); +:::info - // Set the contract address store to an empty string - contractId.set(""); +This functionality is actually nested _within_ the context of our `Settings.svelte` component. However, we're pulling out this logout function in isolation for now. + +::: - // Refresh the page, just for good measure - window.location.reload(); +```js title="src/lib/components/ConnectButtons/Settings.svelte" +async function logout() { + try { + user.reset(); } catch (err) { - console.log(err); - toastStore.trigger({ - message: "Something went wrong logging out. Please try again later.", - background: "variant-filled-error", + console.error(err); + toaster.error({ + title: "Error", + description: "Something went wrong logging out. Please try again later.", }); } } @@ -151,22 +151,28 @@ With those three functions, our dapp is ready for users to authenticate with the ### 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. +Still in our `ConnectButtons` directory, we also have a collection of buttons and functions that represent a "profile menu" of sorts (`Settings.svelte`). 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/Settings.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" +:::info + +This functionality is actually _even further_ nested in the `DonateButton.svelte` component, to keep the logic more isolated. This component is then placed into the `Settings` component, which will then be placed into the parent `ConnectButtons` component. + +::: + +```html title="src/lib/components/ConnectButtons/DonateButton.svelte" ``` @@ -288,36 +294,36 @@ The `signGuestbook` function (which is executed when the button is clicked), is ```js title="src/routes/sign/+page.svelte" import ye_olde_guestbook from '$lib/contracts/ye_olde_guestbook'; import { contractId } from '$lib/stores/contractId'; -import { keyId } from '$lib/stores/keyId'; +import { user } from '$lib/state/UserState.svelte'; import { account, send } from '$lib/passkeyClient'; async function signGuestbook() { - try { - isLoading = true; - const at = await ye_olde_guestbook.write_message({ - author: $contractId, - title: messageTitle, - text: messageText, - }); - - let txn = await account.sign(at.built!, { keyId: $keyId }); - const { returnValue } = await send(txn.built!); - const messageId = xdr.ScVal.fromXDR(returnValue, 'base64').u32(); - - toastStore.trigger({ - message: 'Huzzah!! You signed my guestbook! Thanks.', - background: 'variant-filled-success', - }); - goto(`/read/${messageId}`); - } catch (err) { - console.log(err); - toastStore.trigger({ - message: 'Something went wrong signing the guestbook. Please try again later.', - background: 'variant-filled-error', - }); - } finally { - isLoading = false; - } + try { + isLoading = true; + const at = await ye_olde_guestbook.write_message({ + author: user.contractAddress, + title: messageTitle, + text: messageText, + }); + + let txn = await account.sign(at.built!, { keyId: user.keyId }); + const { returnValue } = await send(txn.built!); + const messageId = xdr.ScVal.fromXDR(returnValue, 'base64').u32(); + + toaster.success({ + title: 'Success', + description: 'Huzzah!! You signed my guestbook! Thanks.', + }); + goto(`/read/${messageId}`); + } catch (err) { + console.error(err); + toaster.error({ + title: 'Error', + description: 'Something went wrong signing the guestbook. Please try again later.', + }); + } finally { + isLoading = false; + } } ``` @@ -411,68 +417,67 @@ export const load: PageServerLoad = async () => { We're making use of two functions that we've defined elsewhere. The `welcomeMessage` will **always** have ID 1, and we want to _always_ display it at the top of the page. The two functions are defined like this: ```js title="src/lib/server/getLedgerEntries.ts" +import { Address, Contract, xdr, scValToNative } from '@stellar/stellar-sdk'; +import { networks, type Message } from 'ye_olde_guestbook'; import { rpc } from '$lib/passkeyClient'; -// notice our bindings re-exports the Stellar SDK, so we don't even really need -// to import any Stellar-related classes or functions from elsewhere. -import { Address, networks, Contract, type Message, xdr, scValToNative } from 'ye_olde_guestbook'; // First, we need a function to build these LedgerKeys so we can query the network function buildMessageLedgerKey(messageId: number) { - const ledgerKey = xdr.LedgerKey.contractData( - new xdr.LedgerKeyContractData({ - contract: new Address(networks.testnet.contractId).toScAddress(), - key: xdr.ScVal.scvVec([xdr.ScVal.scvSymbol('Message'), xdr.ScVal.scvU32(messageId)]), - durability: xdr.ContractDataDurability.persistent(), - }), - ); - - return ledgerKey; + const ledgerKey = xdr.LedgerKey.contractData( + new xdr.LedgerKeyContractData({ + contract: new Address(networks.testnet.contractId).toScAddress(), + key: xdr.ScVal.scvVec([xdr.ScVal.scvSymbol('Message'), xdr.ScVal.scvU32(messageId)]), + durability: xdr.ContractDataDurability.persistent(), + }), + ); + + return ledgerKey; } // To get our welcome message, we use the `getLedgerEntries` function // from the RPC instance. export async function getWelcomeMessage(): Promise { - const result = await rpc.getLedgerEntries(buildMessageLedgerKey(1)); - return scValToNative(result.entries[0].val.contractData().val()); + const result = await rpc.getLedgerEntries(buildMessageLedgerKey(1)); + return scValToNative(result.entries[0].val.contractData().val()); } // Our contract stores the number of guestbook messages in its instance // storage. So, we have a function to query exactly how many messages we // need to retrieve. export async function getMessageCount() { - const result = await rpc.getLedgerEntries( - new Contract(networks.testnet.contractId).getFootprint(), - ); - - const messageCount = result.entries[0].val - .contractData() - .val() - .instance() - .storage() - ?.filter((item) => item.val().switch().name === 'scvU32'); - - return messageCount![0].val().value() as number; + const result = await rpc.getLedgerEntries( + new Contract(networks.testnet.contractId).getFootprint(), + ); + + const messageCount = result.entries[0].val + .contractData() + .val() + .instance() + .storage() + ?.filter((item) => item.val().switch().name === 'scvU32'); + + return messageCount![0].val().value() as number; } // Now we can iterate and make ledger key for each relevant message, // and add that to our getLedgerEntries query. The maximum number of ledger entries // to query is 200. export async function getAllMessages(): Promise { - const totalCount = await getMessageCount(); - const ledgerKeysArray = []; - for (let messageId = 2; messageId <= totalCount; messageId++) { - ledgerKeysArray.push(buildMessageLedgerKey(messageId)); - } + const totalCount = await getMessageCount(); + const ledgerKeysArray = []; + for (let messageId = 2; messageId <= totalCount; messageId++) { + ledgerKeysArray.push(buildMessageLedgerKey(messageId)); + } - const result = await rpc.getLedgerEntries(...ledgerKeysArray); - const messages = result.entries.map((message) => { - return { - ...scValToNative(message.val.contractData().val()), - }; - }); + const result = await rpc.getLedgerEntries(...ledgerKeysArray); + const messages = result.entries.map((message) => { + return { + ...scValToNative(message.val.contractData().val()), + }; + }); - return messages; + return messages; } ``` @@ -480,20 +485,26 @@ Did you catch all that?! Well done! That's the querying part of reading all mess ```html title="src/routes/read/+page.svelte"
Take a gander at all these messages!

- Showing + + (sortNewestFirst = e.checked)} >Showing {sortNewestFirst ? 'Newest' : 'Oldest'} - First
-
+
-{#each messages as message, i (message.ledger)} - +{#each messages as message (message.id)} + {/each} ``` @@ -542,14 +549,19 @@ The benefit of including this functionality within the message-displaying compon ```js title="src/lib/components/GuestbookMessage.svelte" import { account, send } from '$lib/passkeyClient'; -import { keyId } from '$lib/stores/keyId'; +import { user } from '$lib/state/UserState.svelte'; // This is how we receive the "props" from the pages that instantiate this component -export let message: Message; -export let messageId: number; +interface Props { + message: Message; + messageId: number; +} +let { message, messageId }: Props = $props(); -let editing: boolean; -let isLoading: boolean; +let isEditing: boolean = $state(false); +let isLoading: boolean = $state(false); +let messageTitle = $state(message.title); +let messageText = $state(message.text); // Store the original values from the contract's storage. The form will be "bound" // to these values later on, when the user is modifying the entry. @@ -567,32 +579,32 @@ const cancelEdit = () => { }; const submitEdit = async () => { - console.log('submitting message edit'); - isLoading = true; - try { - const at = await ye_olde_guestbook.edit_message({ - message_id: messageId, - title: messageTitle, - text: messageText, - }); - - const txn = await account.sign(at.built!, { keyId: $keyId }); - await send(txn.built!); - - toastStore.trigger({ - message: 'Message edited successfully.', - background: 'variant-filled-success', - }); - } catch (err) { - console.log(err); - toastStore.trigger({ - message: 'Something went wrong editing your message. Please try again later.', - background: 'variant-filled-error', - }); - } finally { - editing = false; - isLoading.set(false); - } + console.log('submitting message edit'); + isLoading = true; + try { + const at = await ye_olde_guestbook.edit_message({ + message_id: messageId, + title: messageTitle, + text: messageText, + }); + + const txn = await account.sign(at.built!, { keyId: user.keyId }); + await send(txn.built!); + + toaster.success({ + title: 'Success', + description: 'Message edited successfully.', + }); + } catch (err) { + console.log(err); + toaster.error({ + title: 'Error', + description: 'Something went wrong editing your message. Please try again later.', + }); + } finally { + isEditing = false; + isLoading = false; + } }; ``` diff --git a/docs/build/apps/guestbook/overview.mdx b/docs/build/apps/guestbook/overview.mdx index 25ce26580..202c970e3 100644 --- a/docs/build/apps/guestbook/overview.mdx +++ b/docs/build/apps/guestbook/overview.mdx @@ -61,16 +61,16 @@ This tutorial is probably best viewed as "_nearly_ comprehensive." We aren't goi Here are the steps we've taken to start building Ye Olde Guestbook. Feel free to be inspired and customize these directions as you see fit. The entire [Ye Olde Guestbook codebase](https://github.com/elliotfriend/ye-olde-guestbook) is freely open and available on GitHub for reference. -### Start from the soroban-template repository +### Start from the stellar-template repository -With the move to smart contract development, a newly emerging utility in the Stellar ecosystem is the "[Soroban template](https://developers.stellar.org/docs/build/guides/dapps/soroban-contract-init-template)." These templates can help alleviate the burden of writing boilerplate code, and can help adapt typical Stellar development workflows into framework-specific reference templates. We've created [just such a template](https://github.com/ElliotFriend/soroban-template-sveltekit-passkeys) that can help you get started developing with SvelteKit and passkeys from the very beginning. You can either use the template on the GitHub website: +With the move to smart contract development, a newly emerging utility in the Stellar ecosystem is the "[Stellar template](../../guides/dapps/soroban-contract-init-template.mdx)." These templates can help alleviate the burden of writing boilerplate code, and can help adapt typical Stellar development workflows into framework-specific reference templates. We've created [just such a template](https://github.com/ElliotFriend/stellar-template-sveltekit-passkeys) that can help you get started developing with SvelteKit and passkeys from the very beginning. You can either use the template on the GitHub website: ![GitHub Template Project](/assets/guestbook/github_template.png) Or, you can (fork and) clone the template repository locally, and start working that way: -```shell -git clone https://github.com/ElliotFriend/soroban-template-sveltekit-passkeys ye-olde-guestbook +```sh +git clone https://github.com/ElliotFriend/stellar-template-sveltekit-passkeys ye-olde-guestbook ``` This frontend template will give you some scaffolding and some (opinionated) defaults. What you do from there is up to you! @@ -89,7 +89,7 @@ What more could you want!? The template comes with a `.env.example` file, that you will need to modify. First, copy or move this file to `.env`: -```shell +```sh cp .env.example .env ``` @@ -97,8 +97,8 @@ Then, open up the `.env` file, and begin customizing any of the entries you need Some variables you will want to change include: -```shell -PUBLIC_STELLAR_ACCOUNT=stroopy # you're welcome to use stroopy, but if you have an name you'd prefer, put that here +```sh +PUBLIC_STELLAR_ACCOUNT=stroopy # you're welcome to use stroopy, but if you have another name you'd prefer, put that here PRIVATE_FUNDER_SECRET_KEY=S...ECRET # fund an account on Testnet and put the secret key here PUBLIC_FUNDER_PUBLIC_KEY=G...ADDRESS # put the public key from the funded account here ``` @@ -107,13 +107,13 @@ PUBLIC_FUNDER_PUBLIC_KEY=G...ADDRESS # put the public key from the funded accoun With our pre-existing template, everything you need should be pulled in from the `package.json` and `Cargo.toml` files. All you've got to do is open up a terminal and install the dependencies: -```bash +```sh pnpm install ``` :::note -We're not aiming to dictate which package manager you should use. When building a full-stack SvelteKit app using Stellar and passkeys, we've recently seen a lot of success and reliability using `pnpm`. Who's to say _why_ that's the case, and it certainly could be a fluke and limited to our own experience. In any case, we'll be using `pnpm` for the remainder of this tutorial. +We're not aiming to dictate which package manager you should use. When building a full-stack SvelteKit app using Stellar and passkeys, we've recently seen a lot of success and reliability using `pnpm`. Who's to say _why_ that's the case? It certainly could be a fluke and limited to our own experience, as well. In any case, we'll be using `pnpm` for the remainder of this tutorial. ::: diff --git a/docs/build/apps/guestbook/passkeys-prerequisites.mdx b/docs/build/apps/guestbook/passkeys-prerequisites.mdx index 848bb4eb2..17eb6f531 100644 --- a/docs/build/apps/guestbook/passkeys-prerequisites.mdx +++ b/docs/build/apps/guestbook/passkeys-prerequisites.mdx @@ -7,11 +7,15 @@ Passkeys are an amazing way to help dapp developers (like yourself) connect user We have been hard at work pioneering some tools to increase the adoption and ease-of-use for passkeys on Stellar. For this tutorial we'll be using the **incredible** [`passkey-kit` package](https://github.com/kalepail/passkey-kit), which takes SO MUCH of the headache and hassle out of the equation. -Before we get into the nitty gritty on passkeys, we have some chores to do. First, we'll set up Launchtube, a service that will help get our transactions on-chain without worrying about gas fees, sequence numbers, or source accounts. Really useful. Then, we'll create a Mercury indexing program, which will be used to keep track of the public key half of a user-generated passkey and then do a reverse lookup to see which smart wallet address the passkey has been added to. +Before we get into the nitty gritty on passkeys, we have some chores to do. First, we'll set up Launchtube, a service that will help get our transactions on-chain without worrying about gas fees, sequence numbers, or source accounts. Really useful. + +In a more complex application, we could make use of an indexer, as well. If a smart wallet will have multiple signers added to it, an indexer will help capture and track which smart wallet those signers are connected to. For example, if a single smart wallet is intended to be used with a distinct passkey from multiple applications, an indexer can act as a "reverse lookup" of sorts to retrieve the smart wallet's address when given a passkey public key. + +In our example, however, we won't get that complicated. The deployed smart wallet address gets seeded with the user's passkey public key at deploy-time. So, having the public key, which the user will "present" to our application when they attempt to login, will be enough to derive the contract address. ## Launchtube -Let's start with [Launchtube](https://launchtube.xyz). As we mentioned earlier, Launchtube is similar to a "paymaster" service, if you're familiar with account abstraction in EVM networks. We won't actually need to interact with Launchtube _directly_. All that will be handled by the `passkey-kit` package. However, we'll need to get a JWT token that will allow us to authenticate our dapp with Launchtube. +Let's start in on [Launchtube](https://launchtube.xyz). As we mentioned earlier, Launchtube is similar to a "paymaster" service, if you're familiar with account abstraction in EVM networks. We won't actually need to interact with Launchtube _directly_. All that will be handled by the `passkey-kit` package. However, we'll need to get a JWT token that will allow us to authenticate our dapp with Launchtube. For Testnet Launchtube tokens, we can generate one any time we like. All you have to do is visit `https://testnet.launchtube.xyz/gen` to receive a JWT token that will be valid for three months, and will have 100 XLM in credits (these credits will be consumed when you submit network transactions through Launchtube). Go ahead, [give it a try](https://testnet.launchtube.xyz/gen)! @@ -23,7 +27,7 @@ We do have Mainnet Launchtube tokens available! You can request a token in the [ Once you have your Launchtube token, copy/paste it into the `.env` file, as the `PRIVATE_LAUNCHTUBE_JWT` variable: -```shell +```sh PRIVATE_LAUNCHTUBE_JWT= ``` @@ -33,63 +37,11 @@ The `PRIVATE_` and `PUBLIC_` environment variables are a SvelteKit convention, a ::: -## Mercury - -Now, on to [Mercury](https://www.mercurydata.app/). This is a data indexer, running on both Testnet and Mainnet. The team is developing some bleeding-edge data tools that are beginning to redefine what's possible with network data. One such development is the [ZephyrVM](https://docs.mercurydata.app/zephyr-full-customization/introduction): Mercury's cloud execution environment. In short, Zephyr allows you to write (Rust) programs that will run at the close of _every_ ledger on the Stellar network. Inside that program, you can access any kind of current or past data, interact with external web services, create serverless functions, and populate databases. Similar to Launchtube, we won't be _directly_ interacting with Mercury inside the Ye Olde Guestbook dapp. Those interactions will be handled by the `passkey-kit` package. Also similar to Launchtube, this one takes some setting up. - -The `passkey-kit` package doesn't "ship" with a Zephyr program in the published package, but it _does_ have all the Zephyr goodness you'll need in the source repository. Here's how you get that Zephyr program running on Mercury so you can access the indexed smart wallet events. By the way, these commands are probably best run _outside_ the directory where you're building your guestbook dapp. - -1. Clone the `passkey-kit` repository from GitHub and enter the `zephyr` directory within it: - - ```shell - git clone https://github.com/kalepail/passkey-kit - cd passkey-kit/zephyr - ``` - -2. Get an authentication token from the Mercury website. You can login to the [Testnet dashboard](https://test.mercurydata.app/) here. Click on the **Get access token** button under the "Active subscriptions" section. You'll be given a JWT which will be valid for the next seven days. - - ![Mercury Data JWT Token](/assets/guestbook/mercury_token.png) - - Copy/paste this token into the `.env` file: - - ```shell - PRIVATE_MERCURY_JWT= - ``` - -3. (Optionally) You can get a [long-lasting authentication token](https://docs.mercurydata.app/get-started-with-mercury/authentication) for your account using this token, and making a request to Mercury's API: - - ```shell - curl -X POST https://api.mercurydata.app/v2/key \ - -H "authorization: Bearer - ``` - - This will give you an API key that can also be added to your `.env` file. The benefit of this API key is that it will not expire until you generate another API key. - - :::info - - For this tutorial, you'll only need one of these. You can specify the JWT **or** the API key, and get things working exactly the same. In the `PasskeyServer`, though, make sure you specify the corresponding value. - - ::: - -4. Compile and deploy the event indexer Zephyr program to the Testnet network. - - ```shell - cargo install mercury-cli - export MERCURY_JWT="" - # Make sure you're using Rust version 1.79.0 or newer - mercury-cli --jwt $MERCURY_JWT --local false --mainnet false deploy - ``` - -If everything succeeds, you're ready to go! Well done! - -You're now ready to dive into the actual passkey implementation and get your users authenticated with the guestbook dapp! Let's get to it! - ## Troubleshooting -It's possible something has gone wrong during your execution of the processes above. Here are some general suggestions of fixes or things you can try if something goes wrong with your use of Launchtube or Mercury: +It's possible something has gone wrong during your execution of the processes above. Here are some general suggestions of fixes or things you can try if something goes wrong with your use of Launchtube: 1. **Generate a new Launchtube token.** It's possible the Launchtube token you're using has run out of credits. Since we're using Testnet for this tutorial, there's no harm in generating a brand new token any time by visiting `https://testnet.launchtube.xyz/gen` in your browser. -2. **Make sure your Zephyr program successfully deployed.** I've been stuck more than once with a not-working Mercury request because the Zephyr program hadn't actually deployed successfully. Make sure the `mercury-cli deploy` command's output doesn't have any errors in it. -3. **Check the [Mercury documentation](https://docs.mercurydata.app/).** It's quite good and can help you get past a lot of the hurdles you might face. +2. **Set your Launchtube request headers.** Launchtube will fee- and rate-limit transactions interacting with a certain highly active project on the network (such as [KALE](https://kalefarm.xyz)). Setting your own Launchtube headers can identify your transactions as non-KALE, so you won't be subject to the same limit. (On Testnet, this shouldn't be an issue you come across, however.) In any case, feel free to ask questions or drop a chat in the [`#passkeys`](https://discord.com/channels/897514728459468821/1250851135561142423) and [`#launchtube`](https://discord.com/channels/897514728459468821/1293204627361108141) channels in the Stellar Developer Discord server. There's usually somebody around who's ready and willing to help out! diff --git a/docs/build/apps/guestbook/setup-passkeys.mdx b/docs/build/apps/guestbook/setup-passkeys.mdx index 1e56bf4a0..bd666cf80 100644 --- a/docs/build/apps/guestbook/setup-passkeys.mdx +++ b/docs/build/apps/guestbook/setup-passkeys.mdx @@ -26,11 +26,11 @@ export const account = new PasskeyKit({ }); ``` -The `PUBLIC_WALLET_WASM_HASH` variable is the Wasm hash of the smart wallet's contract code. This Wasm hash identifies the executable code that will be deployed for new smart wallets and is simply the Sha256 hash of the compiled contract executable file. This hash is returned during when a compiled contract is installed on the network. +The `PUBLIC_WALLET_WASM_HASH` variable is the Wasm hash of the smart wallet's contract code. This Wasm hash identifies the executable code that will be deployed for new smart wallets and is simply the Sha256 hash of the compiled contract executable file. This hash is returned when a compiled contract is installed on the network. That's all there is to it! This `account` will be fully ready to authenticate users and sign transactions! (It's even easier than all the prerequisites isn't it!) -Now, we've also added some useful "helpers" into the `$lib/passkeyClient.ts` file in our template. The [source code file](https://github.com/ElliotFriend/soroban-template-sveltekit-passkeys/blob/main/src/lib/passkeyClient.ts) is commented to reflect what these helpers are, and how they work. These are strictly for convenience, though. You could stop right here and come away with perfectly valid signed passkey transactions. These helpers are: +Now, we've also added some useful "helpers" into the `$lib/passkeyClient.ts` file in our template. The [source code file](https://github.com/ElliotFriend/stellar-template-sveltekit-passkeys/blob/main/src/lib/passkeyClient.ts) is commented to reflect what these helpers are, and how they work. These are strictly for convenience, though. You could stop right here and come away with perfectly valid signed passkey transactions. These helpers are: - A configured instance of the `rpc.Server` class so we can make RPC requests without having to know/import the RPC's URL all the time. @@ -61,64 +61,64 @@ Now, we've also added some useful "helpers" into the `$lib/passkeyClient.ts` fil /** * A SAC client for the native XLM asset. */ - export const native = sac.getSACClient(PUBLIC_NATIVE_CONTRACT_ADDRESS); + export const native = sac.getSACClient( + Asset.native().contractId(PUBLIC_STELLAR_NETWORK_PASSPHRASE), + ); ``` ## Passkey server So, that's the client-facing passkey code (and some helpers) taken care of. What about the server-side, where we want to be cautious about leaking secrets and tokens?! -We're setting this up in `src/lib/server/passkeyServer.ts`, for similar reasons we listed above. This gives us an importable `server` instance that can be accessed and used in other server-side logic. Svelte gives us the added benefit of [keeping the code in this directory safe](https://svelte.dev/docs/kit/server-only-modules#Your-modules). When we want to safeguard credentials and secrets, we can put any sensitive code in the `$lib/server` directory. +We're setting this up in `src/lib/server/passkeyServer.ts`, for similar reasons we listed above. This gives us an importable `server` instance that can be accessed and used in other server-side logic. SvelteKit gives us the added benefit of [keeping the code in this directory safe](https://svelte.dev/docs/kit/server-only-modules#Your-modules). When we want to safeguard credentials and secrets, we can put any sensitive code in the `$lib/server` directory. ```js title="src/lib/server/passkeyServer.ts" import { PasskeyServer } from "passkey-kit"; import { PUBLIC_LAUNCHTUBE_URL, - PUBLIC_MERCURY_URL, PUBLIC_STELLAR_RPC_URL, } from "$env/static/public"; -import { - PRIVATE_LAUNCHTUBE_JWT, - PRIVATE_MERCURY_JWT, -} from "$env/static/private"; +import { PRIVATE_LAUNCHTUBE_JWT } from "$env/static/private"; export const server = new PasskeyServer({ rpcUrl: PUBLIC_STELLAR_RPC_URL, launchtubeUrl: PUBLIC_LAUNCHTUBE_URL, launchtubeJwt: PRIVATE_LAUNCHTUBE_JWT, - mercuryUrl: PUBLIC_MERCURY_URL, - mercuryJwt: PRIVATE_MERCURY_JWT, - // mercuryKey: PRIVATE_MERCURY_KEY, // optionally + launchtubeHeaders: { + "X-Client-Name": "ye-olde-guestbook", + "X-Client-Version": version, + }, }); ``` And you're done with the `PasskeyServer`! Well done! -This `server` instance will be used for sending transactions (via Launchtube) and reverse-looking-up contract addresses from a known passkey ID (via Mercury). +This `server` instance will be used in our application for sending transactions via Launchtube. ### API routes -Now, we'll need a way to utilize some of the functionality of this `server` from the client without exposing any of the sensitive information. For that, we'll set up a collection of (SvelteKit) routes to act as a backend, and _those routes_ (not the client-side code) will make use of the `server` instance. These files live in `src/routes/api/*` in the project repo. +Now, we'll need a way to utilize some of the functionality of this `server` from the client without exposing any of the sensitive information. For that, we'll set up a simple (SvelteKit) route to act as a backend, and _this route_ (not the client-side code) will make use of the `server` instance. These files live in `src/routes/api/*` in the project repo. Some of the structure here is a bit Svelte-specific, but it should pretty easily make sense enough to non-Svelte developers regardless. The _one_ SvelteKit-specific thing to note is any file named `*server.{ts,svelte}` will **only** run [on the server](https://svelte.dev/docs/kit/routing#server). Your secrets, tokens, credentials, etc. are considered safe to use within these files. #### `/api/send` -This API endpoint will send a transaction to the network, via Launchtube. It receives a `POST` request, whose `body` object contains a base64-encoded transaction. +This API endpoint will send a transaction to the network, via Launchtube. It receives a `POST` request, whose `body` object contains a base64-encoded transaction, and returns a JSON object with Launchtube's response. :::warning -If you're creating a `yourdomain.com/api/send` method, you will probably need to do "something" to ensure that only the right "kinds" of transactions are actually sent to the network. I.e., make sure it's coming from your dapp, your users, etc. Otherwise, it would be possible for a bad actor to discover they could use this to send their own transactions, while you pick up the tab for the fees! +If you're creating a `yourdomain.com/api/send` endpoint, you will probably need to do "something" to ensure that only the right "kinds" of transactions are actually sent to the network. I.e., make sure it's coming from your dapp, your users, etc. Otherwise, it would be possible for a bad actor to discover and use this endpoint to send their own transactions, while you pick up the tab for the fees! The implementation of this is outside the scope of this tutorial, but be sure to consider these kinds of risks as you prepare for a more production-level deployment. ::: ```js title="src/routes/api/send/+server.ts" -import { server } from "$lib/server/passkeyServer"; -import { json } from "@sveltejs/kit"; -import type { RequestHandler } from "./$types"; +import type { RequestHandler } from './$types'; +import { json } from '@sveltejs/kit'; + +import { server } from '$lib/server/passkeyServer'; export const POST: RequestHandler = async ({ request }) => { const { xdr } = await request.json(); @@ -127,71 +127,63 @@ export const POST: RequestHandler = async ({ request }) => { }; ``` -#### `/api/contract/[signer]` - -This endpoint will reverse-lookup (via Mercury) a contract address given a passkey ID. The path parameter `[signer]` is how we'll give the passkey ID to the API `GET` request. - -```js title="src/routes/api/contract/[signer]/+server.ts" - -import { server } from '$lib/server/passkeyServer'; -import type { RequestHandler } from './$types'; - -export const GET: RequestHandler = async ({ params }) => { - const contractId = await server.getContractId(params.signer!); - - return new Response(String(contractId)); -}; -``` - #### `/api/fund/[address]` -This is another helper, but on the API side of things! [Friendbot](../../../networks/README.mdx#friendbot) doesn't support `C...` addresses for Testnet funding. So, we're setting up an endpoint so we can add some funds to the dapp users' wallets. This gives them some tokens to play around with, and allows _us_ to receive those guestbook donations! +This is another helper, but on the API side of things! [Friendbot](../../../networks/README.mdx#friendbot) doesn't support `C...` addresses for Testnet funding. So, we're setting up an endpoint so we can add some funds to the dapp users' wallets. This gives them some tokens to play around with, and allows _us_ to receive those lucrative guestbook donations! This API endpoint is not strictly necessary. But, it is a useful way to see how these kinds of interactions can occur between a "regular" `G...` address and a soroban contract `C...` address. ```js title="src/routes/api/fund/[address]/+server.ts" - -import { error, json } from '@sveltejs/kit'; -import { PRIVATE_FUNDER_SECRET_KEY } from '$env/static/private'; -import { native } from '$lib/passkeyClient'; import type { RequestHandler } from './$types'; +import { error, json } from '@sveltejs/kit'; + import { Keypair } from '@stellar/stellar-sdk'; import { basicNodeSigner } from '@stellar/stellar-sdk/contract'; +import { native } from '$lib/passkeyClient'; + import { PUBLIC_STELLAR_NETWORK_PASSPHRASE } from '$env/static/public'; +import { PRIVATE_FUNDER_SECRET_KEY } from '$env/static/private'; export const GET: RequestHandler = async ({ params, fetch }) => { - const fundKeypair = Keypair.fromSecret(PRIVATE_FUNDER_SECRET_KEY); - const fundSigner = basicNodeSigner(fundKeypair, PUBLIC_STELLAR_NETWORK_PASSPHRASE); - - try { - const { built, ...transfer } = await native.transfer({ - from: fundKeypair.publicKey(), - to: params.address, - amount: BigInt(25 * 10_000_000), - }); - - await transfer.signAuthEntries({ - publicKey: fundKeypair.publicKey(), - signAuthEntry: (auth) => fundSigner.signAuthEntry(auth), - }); - - await fetch('/api/send', { - method: 'POST', - body: JSON.stringify({ - xdr: built!.toXDR(), - }), - }); - - return json({ - status: 200, - message: 'Smart wallet successfully funded', - }); - } catch (err) { - console.error(err); - error(500, { - message: 'Error when funding smart wallet', - }); - } + const fundKeypair = Keypair.fromSecret(PRIVATE_FUNDER_SECRET_KEY); + const fundSigner = basicNodeSigner(fundKeypair, PUBLIC_STELLAR_NETWORK_PASSPHRASE); + + try { + // build a transfer invocation, sending 25 XLM to the address provided + const { built, ...transfer } = await native.transfer({ + from: fundKeypair.publicKey(), + to: params.address, + amount: BigInt(25 * 10_000_000), + }); + + // sign the auth entry in the operation, so we aren't depending on the + // transaction source for authorization to send XLM. this lets us... + await transfer.signAuthEntries({ + address: fundKeypair.publicKey(), + signAuthEntry: (auth) => fundSigner.signAuthEntry(auth), + }); + + // send the transaction via Launchtube. see, even our server-side + // transactions can benefit from this! + await fetch('/api/send', { + method: 'POST', + body: JSON.stringify({ + xdr: built!.toXDR(), + }), + }); + + // return a success message + return json({ + status: 200, + message: 'Smart wallet successfully funded', + }); + } catch (err) { + // throw an error + console.error(err); + error(500, { + message: 'Error when funding smart wallet', + }); + } }; ``` @@ -202,19 +194,21 @@ Each of those API endpoints receives a corresponding function in the `$lib/passk This allows us to write the `fetch` code once, and use it consistently everywhere else. They're pretty straightforward and don't really need much explanation. We'll add them to the end of the file: ```js title="src/lib/passkeyClient.ts" +import type { Tx } from '@stellar/stellar-sdk/contract'; + /** * A wrapper function so it's easier for our client-side code to access the * `/api/send` endpoint we have created. * - * @param xdr - The base64-encoded, signed transaction. This transaction + * @param tx - The base64-encoded, signed transaction. This transaction * **must** contain a Soroban operation * @returns JSON object containing the RPC's response */ -export async function send(xdr: string) { - return fetch("/api/send", { - method: "POST", +export async function send(tx: Tx) { + return fetch('/api/send', { + method: 'POST', body: JSON.stringify({ - xdr, + xdr: tx.toXDR(), }), }).then(async (res) => { if (res.ok) return res.json(); @@ -222,20 +216,6 @@ export async function send(xdr: string) { }); } -/** - * A wrapper function so it's easier for our client-side code to access the - * `/api/contract/[signer]` endpoint we have created. - * - * @param signer - The passkey ID we want to find an associated smart wallet for - * @returns The contract address to which the specified signer has been added - */ -export async function getContractId(signer: string) { - return fetch(`/api/contract/${signer}`).then(async (res) => { - if (res.ok) return res.text(); - else throw await res.text(); - }); -} - /** * A wrapper function so it's easier for our client-side code to access the * `/api/fund/[address]` endpoint we have created. diff --git a/docs/build/apps/guestbook/smart-contract.mdx b/docs/build/apps/guestbook/smart-contract.mdx index 4bf9d8ac8..9ce92b7fd 100644 --- a/docs/build/apps/guestbook/smart-contract.mdx +++ b/docs/build/apps/guestbook/smart-contract.mdx @@ -9,15 +9,15 @@ The heart of this project starts with our smart contract. This smart contract wi All the following "business logic" will be handled by our smart contract: -- **A means of writing messages.** Users can invoke this function to leave a message for the site maintainer. They will have to authenticate this function, and it must contain a `title` and `text` field (both strings). The function will return the ID number of the message, which increments sequentially. +- **A means of writing messages.** Users can invoke this function to leave a message for the site maintainer. They will have to authenticate this function, and it must contain a `title` and `text` field (both `String`s). The function will return the `u32` ID number of the message, which increments sequentially. - **A means of reading messages.** This function will allow a user to "query" the contract for a guestbook message, by providing the message ID. Non-existing IDs will result in an error. - **A means of editing messages (with authentication).** If a user needs to modify their previously written message, they can use this function to do so. They must provide proper authorization to do so, and they must provide either a `title` or `text` field (both cannot be empty, but one of them could). -- **A means of retrieving donations and transferring them to the "admin" address.** The hubris associated with requesting donations on a site like this speaks volumes of the maintainer's sense of self. However, providing this functionality is an excellent exercise in asset interactions within the smart contract. +- **A means of retrieving donations and transferring them to the `Admin` address.** The hubris associated with requesting donations on a site like this speaks volumes of the maintainer's sense of self. However, providing this functionality is an excellent exercise in asset interactions within the smart contract. - We'll also need some utility functions that the contract will use internally, as well as a `__constructor` and an `upgrade` function, in case we need to upgrade our smart contract in the future. :::note -We'll be diving into each of the main functions below, but if you want to see the whole smart contract uninterrupted, it can be found here: [https://github.com/ElliotFriend/ye-olde-guestbook/blob/main/contracts/ye_olde_guestbook/src/lib.rs](https://github.com/ElliotFriend/ye-olde-guestbook/blob/main/contracts/ye_olde_guestbook/src/lib.rs) +We'll be diving into each of the main functions below, but if you want to see the whole smart contract uninterrupted, it can be found [on Github](https://github.com/ElliotFriend/ye-olde-guestbook/blob/main/contracts/ye_olde_guestbook/src/lib.rs) ::: @@ -197,7 +197,9 @@ pub fn read_message(env: Env, message_id: u32) -> Result { #### `read_latest` -But, what if someone just wants to read the _latest_ message, and doesn't know what its ID number is? Well, we're providing a function for exactly that. No arguments to pass in. No authentication. Just pull the message from the contract's persistent storage, and return the struct (or panic, if the contract doesn't have any messages yet). Easy peasy. +But, what if someone just wants to read the _latest_ message, and doesn't know what its ID number is? Well, we're providing a function for exactly that. No arguments to pass in. No authentication. Just pull the message from the contract's persistent storage, and return the struct. Thanks to the `__constructor` function, any contract instantiated from this executable will **always** have ate least the first message. So, we don't even need to panic if there's no message to retrieve, because that won't happen. + +Easy peasy. ```rust /// Read the latest message to be sent to the guestbook. @@ -222,7 +224,9 @@ We'll set aside whether or not the maintainer of the guestbook _should_ be solic The `claim_donations` function will allow the invoker of the function to send a balance of any token to the admin of the guestbook contract. We'll direct your attention to two aspects of this function, in particular. -First, we're requiring an `Address` for the token that should be claimed. It may be your first instinct to hard-code and default to native XLM for these donations. This can certainly be done, but the address for that contract will be different on Mainnet, Testnet, or Futurenet, and the contract would have to be modified and re-compiled for each network you want to deploy to. A more "universally" applicable approach is to take the token address as an argument to this function, and allow the donors and admin to use whichever token they deem suitable for the situation. +First, we're requiring an `Address` for the token that should be claimed. It may be your first instinct to hard-code and default to native XLM for these donations. This can certainly be done, but the address for that contract will be different on Mainnet, Testnet, or Futurenet, and the contract would have to be modified and re-compiled for each network you want to deploy to. Or, we could pass in a contract address in the `__constructor` function, and pull donations for that asset later on. But, what if someone wants to donate with some other token? Say, `USDC` or `KALE`. + +A more "universally" applicable approach is to take the token address as an argument to this function, and allow the donors and admin to use whichever token they deem suitable for the situation. Second, we're not requiring any authentication for this function. It's not really necessary to add that logic into the mix, because no real harm will come if a non-admin invokes the function: @@ -300,7 +304,7 @@ fn get_message(env: &Env, message_id: u32) -> Message { #### `save_message` -We're abstracting away the method we're using to write a message to the contract storage because it's used in two places: the `initialize` and `write_message` functions. We want both to store messages in the same manner, so we're enforcing that by using this utility function. +We're abstracting away the method we're using to write a message to the contract storage because it's used in two places: the `__constructor` and `write_message` functions. We want both to store messages in the same manner, so we're enforcing that by using this utility function. We're storing a `MessageCount` in the contract's instance storage, to assist us in message saves, reads, edits, etc. This could certainly be done differently, but it will be convenient for us when it comes to saving new messages, reading messages from the contract, querying for contract state in the frontend, etc. @@ -346,21 +350,21 @@ pub struct Message { #### `DataKey` -This is a struct that's used elsewhere in the contract to define the keys for the various storage entries the contract will hold. Nothing groundbreaking or remarkable here, to be honest, but it's still worth showing. The `Message(ID_NUMBER)` will be used as the key to store a `Message` struct on-chain as the corresponding value. +This is a struct that's used elsewhere in the contract to define the keys for the various storage entries the contract will hold. Nothing groundbreaking or remarkable here, to be honest, but it's still worth showing. The `Message(u32)` variant will be used as the key to store a `Message` struct on-chain as the corresponding value. ```rust #[contracttype] #[derive(Clone)] pub enum DataKey { - Admin, - MessageCount, - Message(u32), + Admin, // : address + MessageCount, // : u32 + Message(u32), // : Message } ``` #### `Error` -We're holding to the typical [contract conventions](https://developers.stellar.org/docs/build/guides/conventions/error-enum), and creating an `enum` to keep track of our errors. +We're holding to the typical [contract conventions](../../guides/conventions/error-enum.mdx), and creating an `enum` to keep track of our errors. ```rust #[contracterror] diff --git a/static/assets/guestbook/github_template.png b/static/assets/guestbook/github_template.png index 659da5188..94503b55f 100644 Binary files a/static/assets/guestbook/github_template.png and b/static/assets/guestbook/github_template.png differ