Setup Passkeys
Now, we've got the requisite accounts, tokens, etc. created, and we're ready to start putting the passkey-kit to work, getting our users connected!
Passkey client
We'll start by creating an instance of the PasskeyKit class. We'll call it account, and this account will be the primary point of interaction for the dapp and the user's passkey. Every transaction will be signed using account.sign(), users will signup with account.createWallet(), users will login with account.connectWallet(). This account is a pretty tough workhorse! Let's make it happen.
We're creating this in src/lib/passkeyClient.ts so it's available to us in the rest of our frontend codebase. The $lib import alias is a SvelteKit thing, but the important thing is we want this file (and its exports) to be available throughout all of our frontend files. How you make that happen for other frameworks is an exercise left to the reader.
import { PasskeyKit } from "passkey-kit";
import {
PUBLIC_STELLAR_RPC_URL,
PUBLIC_STELLAR_NETWORK_PASSPHRASE,
PUBLIC_WALLET_WASM_HASH,
} from "$env/static/public";
export const account = new PasskeyKit({
rpcUrl: PUBLIC_STELLAR_RPC_URL,
networkPassphrase: PUBLIC_STELLAR_NETWORK_PASSPHRASE,
walletWasmHash: PUBLIC_WALLET_WASM_HASH,
});
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 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.Serverclass so we can make RPC requests without having to know/import the RPC's URL all the time.src/lib/passkeyClient.tsimport { Server } from "@stellar/stellar-sdk/rpc";
/**
* A configured Stellar RPC server instance used to interact with the network
*/
export const rpc = new Server(PUBLIC_STELLAR_RPC_URL); -
A SAC client to interact with the native XLM asset contract. We're making an assumption that native lumens is a "good enough" asset interaction to get the tutorial working, and for playing on Testnet. You could easily export another SAC client to interact with USDC, for example. The native contract address can be obtained from Stellar-CLI with the command
stellar contract id asset --asset native.src/lib/passkeyClient.tsimport { SACClient } from "passkey-kit";
import { PUBLIC_NATIVE_CONTRACT_ADDRESS } from "$env/static/public";
/**
* A client allowing us to easily create SAC clients for any asset on the
* network.
*/
const sac = new SACClient({
rpcUrl: PUBLIC_STELLAR_RPC_URL,
networkPassphrase: PUBLIC_STELLAR_NETWORK_PASSPHRASE,
});
/**
* A SAC client for the native XLM asset.
*/
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. SvelteKit gives us the added benefit of keeping the code in this directory safe. When we want to safeguard credentials and secrets, we can put any sensitive code in the $lib/server directory.
import { PasskeyServer } from "passkey-kit";
import {
PUBLIC_LAUNCHTUBE_URL,
PUBLIC_STELLAR_RPC_URL,
} from "$env/static/public";
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,
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 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 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. 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, and returns a JSON object with Launchtube's response.
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.
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();
const res = await server.send(xdr);
return json(res);
};
/api/fund/[address]
This is another helper, but on the API side of things! 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.
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 {
// 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',
});
}
};
Passkey client helpers
Each of those API endpoints receives a corresponding function in the $lib/passkeyClient.ts file, just to make it a little easier on the client-side to make use of the API routes we just made.
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:
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 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(tx: Tx) {
return fetch('/api/send', {
method: 'POST',
body: JSON.stringify({
xdr: tx.toXDR(),
}),
}).then(async (res) => {
if (res.ok) return res.json();
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.
*
* @param address - The contract address to fund on the Testnet
*/
export async function fundContract(address: string) {
return fetch(`/api/fund/${address}`).then(async (res) => {
if (res.ok) return res.json();
else throw await res.text();
});
}
Still with us?! Incredible! You're a rock star! And, you're ready to get into the interactions with the smart contract! See you on the next page!