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
Comprehensive documentation for this project is available in the docs/
directory:
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.
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:
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:
Paymaster Service:
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 build
Deploy contract:
stellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/chat_demo.wasm \
--source alice \
--network testnet
Get your Contract ID:
š https://stellar.expert/explorer/testnet/contract/CBK6E4G3DCE3OR44ZYMKV36O35LMUIGH7LRV4GIUUMA5UDNWS57MAJN3
ā
Deployed!
CBK6E4G3DCE3OR44ZYMKV36O35LMUIGH7LRV4GIUUMA5UDNWS57MAJN3
send()
Function:Review line 10 in the contract, contracts/chat-demo/src/lib.rs
:
soroban_sdk::address::require_auth
addr.require_auth();
Address
has authorized the current invocation(including all the invocation arguments)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-test2
Example 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:
Poll for events using a cursor
parameter:
stellar events \
--network testnet \
--cursor 0002533961985163263-4294967295 \
--id CBUMOJAEAPLQUCWVIM6HJH5XKXW5OP7CRVOOYMJYSTZ6GFDNA72O2QW6 \
--output pretty
Using a start-ledger
parameter:
stellar events \
--network testnet \
--start-ledger 589386 \
--id CBUMOJAEAPLQUCWVIM6HJH5XKXW5OP7CRVOOYMJYSTZ6GFDNA72O2QW6 \
--output pretty
HTTP 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
getEvents
RPC ResponseJSON 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:
Api.GetEventsResponse
s into structured ChatEvent
objects for the front-endFetch Contract Events:
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:
startLedger
or cursor
and limit
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:
Address
from first entry in event topic arrayEd25519
for scAddressTypeAccount
typescAddressTypeContract
typeevents.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
ChatEvent
interface defined in src/env.d.ts
ChatEvent
:string
string
Date
string
string
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).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 eventsLet's walk through how ChatEvent
s 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:
getEvents
and rpc
from utils/rpc.ts
getEvents()
and set ChatEvent in array msgs: ChatEvent[]
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:
{#each}
svelte expression language to interate through msgs[]
arrayChatEvent
fields 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}
chat-demo
Client
from chat-demo-sdk
contract bindingschat-demo-sdk bindings
were generated with Stellar CLIchat-demo-sdk/README.md
for more infochat.ts
with rpcUrl
, contractId
and networkPassphrase
from .env
paramssend()
function with bindings passing in Address
and msg
stringAssembledTransaction
with PasskeyKit
Signer passing in keyId
and transaction to sign()
PasskeyServer
configured with rpcUrl
, launchtubeUrl
and launchtubeJwt
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.