Get started building on the Stellar Network with smart wallets powered by Stellar dev tools like the Stellar CLI, the Stellar Javascript SDK, Passkey Kit and Launchtube.
With Astro and Svelte on the front-end.
π Highlighted Tools
β¨ Features
- Passkey Kit for seamless biometric authentication
- Launchtube for transaction lifecycle management and paymaster functionality
- Zettablock for indexing event data
- TypeScript bindings generated with the Stellar CLI
Comprehensive documentation for this project is available in the docs/ directory:
- Project Overview - High-level explanation of the project
- Architecture - Detailed architecture with diagrams
- Smart Contract - Smart contract design and implementation
- Frontend - Frontend implementation details
- Authentication - Authentication with PasskeyKit
- Transaction Flow - How transactions work with Launchtube
- Event Handling - Event handling and processing
- Development Guide - Getting started for developers
This project is a decentralized chat application built on the Stellar blockchain using Soroban smart contracts. It demonstrates how to create a secure, user-friendly web3 application with modern authentication methods.
graph TD
A[User Browser] -->|Passkey Authentication| B[Frontend App]
B -->|Send Message| C[Smart Contract]
C -->|Emit Event| D[Blockchain]
D -->|Poll Events| B
B -->|Display Messages| A
E[Launchtube] -->|Transaction Management| C
F[Passkey Kit] -->|Authentication| B
G[Zettablock] -->|Event Indexing| B
style A fill:#f9f,stroke:#333,stroke-width:2px
style C fill:#bbf,stroke:#333,stroke-width:2px
style D fill:#dfd,stroke:#333,stroke-width:2px
The application follows a modern blockchain architecture pattern:
flowchart LR
subgraph Frontend ["Frontend (Astro + Svelte)"]
UI[User Interface]
Store[State Management]
end
subgraph Backend ["Blockchain (Stellar)"]
SC[Smart Contract]
Events[Event Storage]
end
subgraph Services ["Services"]
PK[Passkey Kit]
LT[Launchtube]
ZB[Zettablock]
end
UI -->|User Input| Store
Store -->|Contract Call| SC
SC -->|Emit Events| Events
Events -->|Poll| Store
Store -->|Update| UI
PK -->|Authentication| Store
LT -->|Transaction Processing| SC
ZB -->|Indexing| Events
classDef frontend fill:#f9f9ff,stroke:#333,stroke-width:1px
classDef backend fill:#f0f0ff,stroke:#333,stroke-width:1px
classDef services fill:#fffff0,stroke:#333,stroke-width:1px
class Frontend frontend
class Backend backend
class Services services
The sequence of operations when sending and receiving messages:
sequenceDiagram
participant User
participant Frontend
participant PasskeyKit
participant Launchtube
participant Contract
participant Blockchain
User->>Frontend: Type message
Frontend->>PasskeyKit: Request signature
PasskeyKit->>User: Biometric prompt
User->>PasskeyKit: Authenticate
PasskeyKit->>Frontend: Return signed transaction
Frontend->>Launchtube: Submit signed transaction
Launchtube->>Contract: Execute transaction
Contract->>Blockchain: Emit event with message
loop Every 12 seconds
Frontend->>Blockchain: Poll for new events
Blockchain->>Frontend: Return events
Frontend->>User: Display new messages
end
Self-custody can be complicated for users.
Passkey Kit streamlines user experience (UX) in Web3 by leveraging biometric authentication for signing and fine-grained authorization of Stellar transactions with Policy Signers.
Implementing WebAuthn standards, Passkey Kit removes the complexity of Web3 on-boarding.
- Passwordless Login: Streamlined onboarding. No more magic link emails, OTPs, or registration forms
- Biometric Signing: Users can use their device's biometric authentication or password managers
- Fine-Grained Authorization: Configured fine-grained transaction signing credentials with modular access
- Seamless UX: Provide familiar login flow
graph LR
A[User] -->|Biometric Authentication| B[PasskeyKit]
B -->|Generate Keypair| C[Secure Key Storage]
B -->|Sign Transactions| D[Blockchain]
style A fill:#f9f,stroke:#333,stroke-width:2px
style B fill:#bbf,stroke:#333,stroke-width:2px
style D fill:#dfd,stroke:#333,stroke-width:2px
Launchtube is a super cool service that abstracts away the complexity of submitting transactions.
Smart Contract Development is Complex:
- Determining the footprint of an operation
- Storage durability
- TTLs
- Managing XDR binary data
- Considering resource fees
- Transaction building, simulation, assembly and validation
Let Launchtube handle getting your operations onchain!
flowchart TD
A[Application] -->|Submit Transaction| B[Launchtube]
B -->|Fee Calculation| C{Processing}
C -->|Simulation| D[Resource Estimation]
C -->|Validation| E[Error Checking]
C -->|Retry Logic| F[Reliability]
B -->|Submit to Network| G[Blockchain]
style A fill:#f9f,stroke:#333,stroke-width:2px
style B fill:#bbf,stroke:#333,stroke-width:2px
style G fill:#dfd,stroke:#333,stroke-width:2px
-
Transaction Lifecycle Management:
- Transaction Submission
- Retries
- Working around rate limits
-
Paymaster Service:
- Subsidizes transaction fees
This project uses a simple but powerful smart contract to handle message broadcasting and storage through blockchain events.
classDiagram
class Contract {
+send(env: Env, addr: Address, msg: String)
}
class Environment {
+events()
+publish()
}
class Address {
+require_auth()
}
Contract --> Environment : uses
Contract --> Address : requires authentication
Secure, passkey-powered, chat message broadcasting.
Message content is persisted in emitted Soroban events upon invocation of the send() function.
Path: contracts/chat-demo
Getting your local environment setup is the first step.
Check out Getting Started guide here.
Visit our Discord server for more support.
Building the contract with the Stellar CLI:
stellar contract buildDeploy contract:
stellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/chat_demo.wasm \
--source alice \
--network testnetGet your Contract ID:
π https://stellar.expert/explorer/testnet/contract/CBK6E4G3DCE3OR44ZYMKV36O35LMUIGH7LRV4GIUUMA5UDNWS57MAJN3
β
Deployed!
CBK6E4G3DCE3OR44ZYMKV36O35LMUIGH7LRV4GIUUMA5UDNWS57MAJN3Review line 10 in the contract, contracts/chat-demo/src/lib.rs:
soroban_sdk::address::require_auth
addr.require_auth();- Ensures the
Addresshas authorized the current invocation(including all the invocation arguments) - Provided by the Soroban Rust SDK(Specifically the Soroban Env Common crate)
- Sensible built-in security
Auth on Stellar is powerful and gives you sensible security by default.
Invoke your deployed contract send() function:
stellar contract invoke \
--id CBUMOJAEAPLQUCWVIM6HJH5XKXW5OP7CRVOOYMJYSTZ6GFDNA72O2QW6 \
--source alice2 \
-- \
send \
--addr GDCJMCMYNDZ2FV6UMSEYRMUSCX53KCG2AWPBFQ24EA2FFYBCEDMFCBCV \
--msg new-mesg-test2Example Diagnostic Event:
When the send() function is invoked, it emits an event onto the Stellar network:
env.events().publish((addr.clone(), ), msg.clone());The Stellar CLI picks this up for you and displays it. βΉοΈ Why does it look like a cat walked across my Keyboard after hitting the CAPS LOCK key?
contract_event: soroban_cli::log::event: 1:
AAAAAQAAAAAAAAABaMckBAPXCgrVQzx0n7dV7dc/4o1c7DE4lPPjFG0H9O0AAAABAAAAAAAAAAEAAAASAAAAAAAAAADElgmYaPOi19RkiYiykhX7tQjaBZ4Sw1wgNFLgIiDYUQAAAA4AAAAQdGVzdC1tc2ctdG8tc2VuZA==
This is XDR a binary data format.
Help Decoding XDR:
XDR Decoded into JSON format:
{
"in_successful_contract_call": true,
"event": {
"ext": "v0",
"contract_id": "68c7240403d70a0ad5433c749fb755edd73fe28d5cec313894f3e3146d07f4ed",
"type_": "contract",
"body": {
"v0": {
"topics": [
{
"address": "GDCJMCMYNDZ2FV6UMSEYRMUSCX53KCG2AWPBFQ24EA2FFYBCEDMFCBCV"
}
],
"data": {
"string": "test-msg-to-send"
}
}
}
}
}componentDiagram
component Frontend {
component Components {
[Welcome.svelte]
}
component Utils {
[chat.ts]
[passkey-kit.ts]
[rpc.ts]
[zettablocks.ts]
}
component Store {
[contractId.ts]
[keyId.ts]
}
}
[Welcome.svelte] --> [chat.ts]
[Welcome.svelte] --> [passkey-kit.ts]
[Welcome.svelte] --> [rpc.ts]
[Welcome.svelte] --> [contractId.ts]
[Welcome.svelte] --> [keyId.ts]
[Welcome.svelte] --> [zettablocks.ts]
Make a Remote Procedure Call(RPC) with:
- Stellar CLI
- HTTP request (Axios or cURL)
- The Javascript SDK
- Using Stellar Lab
Poll for events using a cursor parameter:
stellar events \
--network testnet \
--cursor 0002533961985163263-4294967295 \
--id CBUMOJAEAPLQUCWVIM6HJH5XKXW5OP7CRVOOYMJYSTZ6GFDNA72O2QW6 \
--output prettyUsing a start-ledger parameter:
stellar events \
--network testnet \
--start-ledger 589386 \
--id CBUMOJAEAPLQUCWVIM6HJH5XKXW5OP7CRVOOYMJYSTZ6GFDNA72O2QW6 \
--output prettyHTTP cURL making a getEvents RPC call on testnet:
curl 'https://testnet.rpciege.com/' \
-H 'accept: */*' \
-H 'accept-language: en-US,en;q=0.9' \
-H 'content-type: application/json' \
-H 'origin: https://lab.stellar.org' \
--data-raw '{"jsonrpc":"2.0","id":8675309,"method":"getEvents","params":{"xdrFormat":"base64","startLedger":589386,"pagination":{"limit":10},"filters":[{"type":"contract","contractIds":["CBUMOJAEAPLQUCWVIM6HJH5XKXW5OP7CRVOOYMJYSTZ6GFDNA72O2QW6"],"topics":[]}]}}'Using Stellar Lab Stellar lab getEvents request
JSON response for Get Events RPC Call:
{
"jsonrpc": "2.0",
"id": 8675309,
"result": {
"events": [
{
"type": "contract",
"ledger": 589387,
"ledgerClosedAt": "2025-04-22T20:52:41Z",
"contractId": "CBUMOJAEAPLQUCWVIM6HJH5XKXW5OP7CRVOOYMJYSTZ6GFDNA72O2QW6",
"id": "0002531397889695744-0000000001",
"pagingToken": "0002531397889695744-0000000001",
"inSuccessfulContractCall": true,
"txHash": "86ad86ba26466e50b764cb7c0dab1082a5e1eec4e1cc82ae2bade7fbeb5d143f",
"topic": [
"AAAAEgAAAAAAAAAAxJYJmGjzotfUZImIspIV+7UI2gWeEsNcIDRS4CIg2FE="
],
"value": "AAAADgAAABB0ZXN0LW1zZy10by1zZW5k"
}
],
"latestLedger": 589890,
"cursor": "0002533562553204735-4294967295"
}
}Topic Field: ScVal representing the Address:
Path: result.events.topic
AAAAEgAAAAAAAAAAxJYJmGjzotfUZImIspIV+7UI2gWeEsNcIDRS4CIg2FE=
Decoded Event Topic: ScVal JSON representing an Address:
{
"address": "GDCJMCMYNDZ2FV6UMSEYRMUSCX53KCG2AWPBFQ24EA2FFYBCEDMFCBCV"
}XDR Value Field: ScVal representing the message payload:
Path: result.events.value
AAAADgAAABB0ZXN0LW1zZy10by1zZW5k
Decoded JSON:
{
"string": "test-msg-to-send"
}Path: src/utils/rpc.ts
rpc.ts provides an interface for calling a Stellar RPC server.
We will use it to retrieve and process emitted contract events.
It uses the Stellar Javascript SDK
Contract Event Retrieval:
- Fetches contract events
- Filters events by contract ID, topic and validates data integrity
- Converts
Api.GetEventsResponses into structuredChatEventobjects for the front-end
Fetch Contract Events:
- Instantiate RPC Server
- Call Get Events RPC call
export const rpc = new Server(import.meta.env.PUBLIC_RPC_URL, import.meta.env.PUBLIC_NETWORK_PASSPHRASE);
await rpc.getEvents()Filter Events by Contract ID:
- Pass in contract filter
- Import deployed contract ID from env
- Set
startLedgerorcursorandlimit
await rpc.getEvents({
filters: [
{
type: "contract",
contractIds: [import.meta.env.PUBLIC_CHAT_CONTRACT_ID],
},
],
startLedger: typeof limit === "number" ? limit : undefined,
limit: 10_000,
cursor: typeof limit === "string" ? limit : undefined,
})Convert from GetEvent API Response to Chat Event Object:
- Validate event type
- Get
Addressfrom first entry in event topic array - Output as publicKey type
Ed25519forscAddressTypeAccounttype - Or contractId for
scAddressTypeContracttype
events.forEach((event) => {
if (event.type !== "contract" || !event.contractId) return;
if (msgs.findIndex(({id}) => id === event.id) === -1) {
let addr: string | undefined;
let topic0 = event.topic[0].address();
switch (topic0.switch().name) {
case "scAddressTypeAccount": {
addr = Address.account(
topic0.accountId().ed25519(),
).toString();
break;
}
case "scAddressTypeContract": {
addr = Address.contract(
topic0.contractId(),
).toString();
break;
}
}
}
});Create ChatEvent from Event data
ChatEventinterface defined insrc/env.d.ts- Set fields in
ChatEvent:- id as
string - addr as
string - timestamp as
Date - txHash as
string - msg as
string
- id as
msgs.push({
id: event.id,
addr,
timestamp: new Date(event.ledgerClosedAt),
txHash: event.txHash,
msg: scValToNative(event.value),
});| Parameter | Type | Description |
|---|---|---|
contractId |
string |
The Stellar contract ID to filter events |
startLedger |
number |
The ledger number to start retrieving events from |
_limit: Maximum number of events to retrieve per request (default: 1,000)- Environment variables from
.env:PUBLIC_RPC_URL: The URL of the Stellar RPC serverPUBLIC_NETWORK_PASSPHRASE: The network passphrase for the target Stellar networkPUBLIC_CHAT_CONTRACT_ID: The default contract ID to filter events
Let's walk through how ChatEvents are displayed in the UI.
graph TD
A[Project Root] --> B[src/]
A --> C[contracts/]
A --> D[public/]
B --> E[components/]
B --> F[utils/]
B --> G[pages/]
B --> H[layouts/]
B --> I[store/]
C --> J[chat-demo/]
E --> K[Welcome.svelte]
F --> L[rpc.ts]
F --> M[chat.ts]
F --> N[passkey-kit.ts]
F --> O[zettablocks.ts]
F --> P[base.ts]
I --> Q[contractId.ts]
I --> R[keyId.ts]
style A fill:#f9f,stroke:#333,stroke-width:2px
style J fill:#bbf,stroke:#333,stroke-width:2px
style K fill:#dfd,stroke:#333,stroke-width:2px
style L fill:#dfd,stroke:#333,stroke-width:2px
Run from the root directory of the project:
| Command | Action |
|---|---|
pnpm install |
Installs dependencies |
pnpm run dev |
Starts local dev server at localhost:4321 |
pnpm run build |
Build your production site to ./dist/ |
pnpm run preview |
Preview your build locally, before deploying |
pnpm run astro ... |
Run Astro CLI commands like astro add |
pnpm run astro -- --help |
Get help using the Astro CLI |
Review the following file:
src/components/Welcome.svelte
This component prints out the chat messages from emitted events:
- Import
getEventsandrpcfromutils/rpc.ts - Call
getEvents()and set ChatEvent in arraymsgs: ChatEvent[] - Sort by Timestamp
async function callGetEvents(
limit: number | string,
found: boolean = false,
) {
msgs = await getEvents(msgs, limit, found);
msgs = msgs.sort(
(a, b) => a.timestamp.getTime() - b.timestamp.getTime(),
);
}Updating the UI
Updating the UI in responses to changes in the state.
Loop through msgs array and display ChatEvent in UI:
- Use
{#each}svelte expression language to interate throughmsgs[]array - https://svelte.dev/docs/svelte/each
- Print out
ChatEventfields embedded in styled HTML
{#each msgs as event}
<li class="mb-2"><span class="text-mono text-sm bg-black rounded-t-lg text-white px-3 py-1">
<a class="underline"
target="_blank"
href="https://stellar.expert/explorer/public/tx/{event.txHash}">
{truncate(event.addr, 4)}
</a>
<time class="text-xs text-gray-400"
datetime={event.timestamp.toUTCString()}>
{event.timestamp.toLocaleTimeString()}
</time>
{event.msg}
{/each}- Import
chat-demoClientfromchat-demo-sdkcontract bindings chat-demo-sdk bindingswere generated with Stellar CLI- Review
chat-demo-sdk/README.mdfor more info
- Review
- Client configured in
chat.tswithrpcUrl,contractIdandnetworkPassphrasefrom.envparams - Invoke deployed contract
send()function with bindings passing inAddressandmsgstring - Sign
AssembledTransactionwithPasskeyKitSigner passing inkeyIdand transaction tosign() - This will then prompt your browser to request your fingerprint
- Use the Launchtube
PasskeyServerconfigured withrpcUrl,launchtubeUrlandlaunchtubeJwt - Await JSON response from Launchtube server
async function send() {
let at = await chat.send({
addr: $contractId,
msg,
});
at = await account.sign(at, {keyId: $keyId});
await server.send(at);
}Feel free to check our documentation or jump into our Discord server.