Skip to content

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

bash
npm create vite@latest my-dock-app -- --template react-ts
cd my-dock-app

2. Install the SDK

bash
npm install @stratos/wallet-sdk

3. Connect to the Wallet

tsx
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

bash
npm run build
npx wrangler pages deploy dist

5. 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 config

Key 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:

typescript
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:

typescript
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

typescript
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

typescript
// 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.

typescript
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 value

EVM Transactions

typescript
// 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

typescript
// 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

typescript
// 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:

typescript
sdk.on('assetsChanged', (assets) => {
  setAssets(assets);
});

sdk.on('userChanged', (user) => {
  setUser(user);
});

sdk.on('transactionsChanged', (txs) => {
  setTransactions(txs);
});
EventPayloadWhen
connectConnectionStateConnection established
disconnectvoidDisconnected
userChangedAuthUserUser login/logout
assetsChangedAsset[]Balances changed
transactionsChangedTransaction[]New transactions
addressesChangedChainAddress[]Addresses changed

Lifecycle

typescript
// 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:

tsx
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

typescript
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);
  }
}
ErrorCause
Not in iframeApp opened outside the wallet
Request timeoutWallet didn't respond in time
User rejectedUser cancelled the operation
Insufficient fundsNot enough balance
Template not foundDAR package not installed

Deployment Checklist

  1. Build your app: npm run build
  2. Deploy to Cloudflare Pages: npx wrangler pages deploy dist
  3. Implement /api/package endpoint if you use Canton contracts
  4. 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

typescript
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

Enterprise-grade multi-chain wallet infrastructure.