App Development
Build dock apps that run inside Stratos Vault with full access to wallet signing, asset balances, and Canton smart contracts.
How Dock Apps Work
Dock apps are web applications that run inside an iframe within the Stratos Vault interface. They communicate with the parent wallet via postMessage through the Stratos Wallet SDK.
┌─────────────────────────────────────────────────────────────┐
│ Stratos Vault (Parent) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Vault │ │ Swap │ │ RWA │ │ Your App │ │
│ └──────────┘ └──────────┘ └──────────┘ └────┬─────┘ │
│ │ │
│ ┌────────────────────────────────────────────▼─────────────┐ │
│ │ iframe │ │
│ │ │ │
│ │ @stratos/wallet-sdk ←—postMessage—→ Parent Wallet │ │
│ │ │ │
│ │ • connect() → User info, party ID │ │
│ │ • getAssets() → Balances │ │
│ │ • sendEVMTransaction → Client-side signing │ │
│ │ • cantonQuery() → Smart contract reads │ │
│ │ • cantonCreate() → Smart contract writes │ │
│ │ • cantonExercise() → Choice execution │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘Your app never handles private keys directly — all signing goes through the parent wallet's WebAuthn PRF infrastructure.
Quick Start
1. Scaffold the Project
npm create vite@latest my-dock-app -- --template react-ts
cd my-dock-app2. Install the SDK
npm install @stratos/wallet-sdk3. Connect to the Wallet
import { useEffect, useState } from 'react';
import { getSDK } from '@stratos/wallet-sdk';
function App() {
const [sdk] = useState(() => getSDK());
const [user, setUser] = useState(null);
const [assets, setAssets] = useState([]);
useEffect(() => {
async function init() {
const { connected, user } = await sdk.connect();
if (connected && user) {
setUser(user);
const assets = await sdk.getAssets();
setAssets(assets);
}
}
init();
return () => sdk.destroy();
}, []);
if (!user) return <div>Connecting to wallet...</div>;
return (
<div>
<h1>Welcome, {user.username}</h1>
<p>Party ID: {user.partyId}</p>
<h2>Assets</h2>
<ul>
{assets.map(a => (
<li key={a.symbol}>{a.symbol}: {a.balance}</li>
))}
</ul>
</div>
);
}4. Deploy
npm run build
npx wrangler pages deploy dist5. Register in Wallet
Ask the instance admin to add your app URL in the Dock Apps section of the superadmin panel.
Project Anatomy
A complete dock app with Canton integration follows this structure:
my-dock-app/
├── src/
│ ├── App.tsx # Main component — SDK connection + UI
│ ├── App.css # Styles
│ └── main.tsx # Entry point
├── functions/ # Cloudflare Pages Functions (optional)
│ └── api/
│ └── package/
│ ├── index.ts # GET /api/package — package metadata
│ └── download.ts # GET /api/package/download — DAR file
├── public/
│ └── packages/
│ └── my-protocol.dar # Compiled Daml package
├── daml/ # Daml source (optional, compiled separately)
│ └── Main.daml
├── package.json
├── vite.config.ts
└── wrangler.toml # Cloudflare Pages configKey files explained
src/App.tsx — Your main application. Initialize the SDK, connect to the wallet, and build your UI.
functions/api/package/index.ts — Exposes your Daml package metadata so wallet admins can install it with one click:
export const onRequestGet = async (context) => {
return Response.json({
name: 'My Protocol',
packageId: context.env.PACKAGE_ID,
darUrl: `${new URL(context.request.url).origin}/api/package/download`,
templates: [
'MyModule:Position',
'MyModule:Order',
],
version: '1.0.0',
});
};functions/api/package/download.ts — Serves the DAR file for automatic installation:
export const onRequestGet = async (context) => {
const dar = await context.env.ASSETS.fetch(
new URL('/packages/my-protocol.dar', context.request.url)
);
return new Response(dar.body, {
headers: {
'Content-Type': 'application/octet-stream',
'Content-Disposition': 'attachment; filename="my-protocol.dar"',
},
});
};When an admin adds your app to the dock, the wallet checks /api/package and shows an Install DAR button if the package isn't already on the Canton participant.
SDK API Overview
Connection
import { getSDK } from '@stratos/wallet-sdk';
const sdk = getSDK();
// Connect and get user info
const { connected, user, addresses } = await sdk.connect();
// user.partyId — Canton party ID (needed for contract operations)
// addresses — array of { chain, chainType, address }Assets and Balances
// All assets with balances
const assets = await sdk.getAssets();
// → [{ symbol: 'ETH', name: 'Ethereum', balance: 1.5, chain: 'Ethereum', ... }]
// Specific asset
const ethBalance = await sdk.getBalance('ETH');
// All chain addresses
const addresses = await sdk.getAddresses();
// → [{ chain: 'Ethereum', chainType: 'evm', address: '0x...' }]Canton Smart Contracts
The SDK wraps the Canton JSON API for contract operations. You need the template ID in the format packageId#Module:Template.
const PACKAGE_ID = 'abc123...'; // From your DAR upload
const POSITION = `${PACKAGE_ID}#DeFi:Position`;
// Query contracts visible to the user
const positions = await sdk.cantonQuery({
templateId: POSITION,
filter: { owner: user.partyId },
});
// Create a new contract
const result = await sdk.cantonCreate({
templateId: POSITION,
payload: {
owner: user.partyId,
poolId: 'pool-1',
shares: '100.0',
},
});
// result.contractId — the new contract's ID
// Exercise a choice on a contract
const exerciseResult = await sdk.cantonExercise({
contractId: positions[0].contractId,
templateId: POSITION,
choice: 'Withdraw',
argument: { amount: '50.0' },
});
// exerciseResult.exerciseResult — choice return valueEVM Transactions
// Send ETH
await sdk.sendEVMTransaction({
transaction: {
to: '0xRecipient...',
value: '0xDE0B6B3A7640000', // 1 ETH in wei (hex)
chainId: 1,
},
});
// Call a smart contract
await sdk.sendEVMTransaction({
transaction: {
to: '0xContractAddress...',
data: '0x...', // ABI-encoded call data
chainId: 8453, // Base
},
});
// Sign without broadcasting
const { signedTransaction } = await sdk.signEVMTransaction({
transaction: { to: '0x...', value: '0x0', chainId: 1 },
});
// Sign EIP-712 typed data
const signature = await sdk.signTypedData({
typedData: { types: {}, primaryType: 'Permit', domain: {}, message: {} },
});Other Chain Transactions
// Solana
const { signature } = await sdk.signRawSolanaTransaction({
transaction: base64Tx,
network: 'mainnet',
});
// TON
const { boc } = await sdk.signRawTonMessage({
to: 'EQContract...',
value: '1000000000', // nanotons
network: 'mainnet',
});
// TRON
const { txID } = await sdk.triggerTronSmartContract({
contractAddress: 'TContract...',
functionSelector: 'transfer(address,uint256)',
parameter: abiParams,
feeLimit: 100000000,
network: 'mainnet',
});Canton Transfers
// Send Canton token
await sdk.transfer({
to: 'receiverPartyId',
amount: '100.0',
symbol: 'CC',
chain: 'canton',
memo: 'Payment for services',
});
// Get pending transfer offers
const offers = await sdk.getTransferOffers();
// Accept a transfer
await sdk.acceptTransferOffer(offers[0].contractId);Events
React to real-time changes:
sdk.on('assetsChanged', (assets) => {
setAssets(assets);
});
sdk.on('userChanged', (user) => {
setUser(user);
});
sdk.on('transactionsChanged', (txs) => {
setTransactions(txs);
});| Event | Payload | When |
|---|---|---|
connect | ConnectionState | Connection established |
disconnect | void | Disconnected |
userChanged | AuthUser | User login/logout |
assetsChanged | Asset[] | Balances changed |
transactionsChanged | Transaction[] | New transactions |
addressesChanged | ChainAddress[] | Addresses changed |
Lifecycle
// Refresh all data from wallet
await sdk.refresh();
// Clean up on unmount
useEffect(() => {
return () => sdk.destroy();
}, []);Complete Example: Position Manager
A dock app that queries and manages DeFi positions via Canton:
import { useEffect, useState } from 'react';
import { getSDK } from '@stratos/wallet-sdk';
const PACKAGE_ID = import.meta.env.VITE_PACKAGE_ID;
const POSITION = `${PACKAGE_ID}#DeFi:Position`;
interface Position {
owner: string;
poolId: string;
shares: string;
}
function App() {
const [sdk] = useState(() => getSDK());
const [user, setUser] = useState(null);
const [positions, setPositions] = useState([]);
useEffect(() => {
sdk.connect().then(async ({ user }) => {
setUser(user);
await loadPositions(user.partyId);
});
sdk.on('assetsChanged', () => loadPositions(user?.partyId));
return () => sdk.destroy();
}, []);
async function loadPositions(partyId) {
if (!partyId) return;
const contracts = await sdk.cantonQuery<Position>({
templateId: POSITION,
filter: { owner: partyId },
});
setPositions(contracts);
}
async function withdraw(contractId: string) {
await sdk.cantonExercise({
contractId,
templateId: POSITION,
choice: 'Withdraw',
argument: { amount: '50.0' },
});
await sdk.refresh();
await loadPositions(user.partyId);
}
async function addPosition(poolId: string) {
await sdk.cantonCreate({
templateId: POSITION,
payload: {
owner: user.partyId,
poolId,
shares: '100.0',
},
});
await sdk.refresh();
await loadPositions(user.partyId);
}
if (!user) return <p>Connecting...</p>;
return (
<div>
<h1>My Positions</h1>
{positions.map((p) => (
<div key={p.contractId}>
<span>Pool: {p.payload.poolId} — {p.payload.shares} shares</span>
<button onClick={() => withdraw(p.contractId)}>Withdraw</button>
</div>
))}
<button onClick={() => addPosition('pool-1')}>Add Position</button>
</div>
);
}
export default App;Error Handling
try {
await sdk.cantonCreate({ ... });
} catch (error) {
if (error.message.includes('User rejected')) {
// User cancelled the operation
} else if (error.message.includes('Template not found')) {
// DAR package not installed — tell admin
} else if (error.message.includes('authorization')) {
// Party not authorized for this operation
} else if (error.message.includes('Insufficient funds')) {
// Not enough balance
} else {
console.error('Error:', error.message);
}
}| Error | Cause |
|---|---|
Not in iframe | App opened outside the wallet |
Request timeout | Wallet didn't respond in time |
User rejected | User cancelled the operation |
Insufficient funds | Not enough balance |
Template not found | DAR package not installed |
Deployment Checklist
- Build your app:
npm run build - Deploy to Cloudflare Pages:
npx wrangler pages deploy dist - Implement
/api/packageendpoint if you use Canton contracts - Ask the instance admin to:
- Add your app URL in Dock Apps
- Click Install DAR if your package isn't installed
- Set app access controls if needed
Type Reference
type ChainType = 'evm' | 'svm' | 'btc' | 'tron' | 'ton' | 'canton';
interface AuthUser {
id: string;
username: string;
displayName: string | null;
role: 'user' | 'admin';
partyId?: string;
}
interface Asset {
symbol: string;
name: string;
balance: number;
icon: string | null;
chain?: string;
chainType?: ChainType;
}
interface ChainAddress {
chain: string;
chainType: ChainType;
address: string;
}
interface CantonContract<T> {
contractId: string;
templateId: string;
payload: T;
createdAt?: string;
signatories?: string[];
observers?: string[];
}Next Steps
- SDK Reference — Complete method signatures and types
- Canton Contracts — Daml template ID format, type mapping, error handling
- Transactions — Multi-chain signing details
- Proxy API — Server-side Canton access for backends and bots
