toncorp-wallet-connect-sdk
v0.1.26
Published
Wallet Connect SDK — connect TonPass, OneDollar and more wallets from any DApp
Maintainers
Readme
toncorp-wallet-connect-sdk
Connect TonPass, OneDollar and more wallets from any DApp or website.
Install
npm install toncorp-wallet-connect-sdkOr via CDN:
<script src="https://cdn.jsdelivr.net/npm/toncorp-wallet-connect-sdk@latest/dist/index.umd.js"></script>Usage
Specific wallet
import { TonPassConnect } from 'toncorp-wallet-connect-sdk';
const wallet = new TonPassConnect({
appId: 'your_app_id',
siteTmaUrl: 't.me/your_bot/your_app', // your DApp's TMA — the wallet reopens it after approval
});
// On page load: wait for the SDK to restore + validate the stored session first
await wallet.ready();
if (wallet.isConnected()) {
console.log(wallet.getWallet()); // restored & still-valid session
}
// First-time connect (opens the modal, waits for approval)
const result = await wallet.connect();
console.log(result.wallet); // { address: 'UQ...', chain: 'TON' }
console.log(result.user); // { username: 'john', avatar: '...' }
// Access via window global
window.tonpass.isConnected() // true
window.tonpass.getWallet() // same resultThe SDK auto-validates the stored session on init and clears it (with a "Session expired" toast) when it's no longer live. See Session lifecycle & auto-expiry.
import { OneDollarConnect } from 'toncorp-wallet-connect-sdk';
const wallet = new OneDollarConnect({
appId: 'your_app_id',
siteTmaUrl: 't.me/your_bot/your_app',
});
const result = await wallet.connect();
// Access via window global
window.onedollar.isConnected()
window.onedollar.getWallet()All wallets (show picker)
import { BaseWalletConnect } from 'toncorp-wallet-connect-sdk';
const wallet = new BaseWalletConnect({
appId: 'your_app_id',
siteTmaUrl: 't.me/your_bot/your_app', // required for TMA DApps (wallet reopens you)
// walletId: ['tonpass', 'onedollar'], // optional filter
});
const result = await wallet.connect();
siteTmaUrlmatters for Telegram Mini App DApps. After the user approves in the wallet, the wallet reopens your TMA via this URL (on desktop it redirects back; on mobile it returns to your app). Omit it only for plain web widgets that stay open the whole time.
Script tag (UMD)
<script src="https://cdn.jsdelivr.net/npm/toncorp-wallet-connect-sdk@latest/dist/index.umd.js"></script>
<script>
const wallet = new WalletConnectSDK.OneDollarConnect({
appId: 'your_app_id',
siteTmaUrl: 't.me/your_bot/your_app',
});
document.getElementById('connect-btn').onclick = async () => {
const result = await wallet.connect();
console.log(result);
};
// Available globally after instantiation
// window.onedollar.isConnected()
// window.onedollar.getWallet()
// window.onedollar.disconnect()
</script>API
Constructor options
| Option | Type | Required | Description |
|--------|------|----------|-------------|
| appId | string | Yes | Your registered app ID |
| registryUrl | string | No | Override wallet registry URL (defaults to CDN) |
| walletId | string \| string[] | No | Filter wallets (only for BaseWalletConnect) |
| theme | 'light' \| 'dark' | No | Modal theme (default: dark) |
| siteTmaUrl | string | No | This DApp's own TMA URL (t.me/<bot>/<app>). The wallet reopens it after the user confirms/rejects the connection. |
Methods
| Method | Returns | Description |
|--------|---------|-------------|
| ready() | Promise<void> | Await SDK init (resume + session-expiry check). Call before reading isConnected() / getWallet(). |
| connect() | Promise<ConnectResult> | Open modal and wait for user to approve |
| disconnect() | void | Disconnect, clear session and call API |
| sendTransaction(params) | Promise<{ txId: string }> | Send a transaction through the connected wallet |
| waitForResult(txId, options?) | Promise<{ status, msgHash?, action? }> | Poll until a transaction resolves |
| resume() | Promise<{ type: 'tx'; status; result } \| null> | Resolve a pending tx after the app was closed/reopened (DApp shows its own result UI) |
| isConnected() | boolean | Check connection status |
| getWallet() | ConnectResult \| null | Get current connection info |
| on(event, cb) | void | Listen to events |
| off(event, cb) | void | Remove listener |
ConnectResult
{
wallet: {
address: string; // friendly TON address
chain: string; // 'TON'
} | null;
user: {
username?: string;
avatar?: string;
};
session?: {
sessionToken: string;
walletId: string;
apiUrl: string;
};
}sendTransaction
Send a transaction through the connected wallet. Requires an active connection.
const { txId } = await wallet.sendTransaction({
msgBoc: 'te6cck...', // base64-encoded BOC
toAddress: 'UQ...', // destination address
amount: '1000000000', // nano units
wallet_address: 'UQ...', // connected wallet address
network: 'ton-mainnet',
action: 'transfer',
});
console.log(txId);Internally POSTs { ...params, source: 'widget' } to {apiUrl}/dex/sendTx.
Errors:
| Cause | Thrown message |
|-------|----------------|
| Not connected / 401 | Wallet not connected |
| 429 | Too many pending transactions |
| Network failure | Connection failed |
waitForResult
Poll {apiUrl}/dex/status/{txId} until the transaction leaves the PENDING
state, then resolve with its final status.
const { txId } = await wallet.sendTransaction({ /* ... */ });
const result = await wallet.waitForResult(txId, {
interval: 2000, // poll every 2s (default)
timeout: 120000, // give up after 2min (default), throws 'Timeout'
});
console.log(result); // { status: 'CONFIRMED', msgHash: '...', action: 'transfer' }| Option | Type | Default | Description |
|--------|------|---------|-------------|
| interval | number | 2000 | Delay between polls in ms |
| timeout | number | 120000 | Max wait before throwing Timeout |
Emits pending on every poll while the tx is PENDING, and resolved once it
settles:
wallet.on('pending', (r) => console.log('still pending', r));
wallet.on('resolved', (r) => console.log('done', r.status));Events
| Event | Data | Description |
|-------|------|-------------|
| connect | ConnectResult | Connected successfully |
| disconnect | - | Disconnected — manual disconnect() or an expired session cleared on init |
| error | string | Transport error |
| session_expired | - | Connect handshake timed out (during connect()) |
Session lifecycle & auto-expiry
A connection stays valid for a limited idle window (configured on the backend). The SDK enforces this automatically on every init — you don't call anything:
- On construction, after restoring the stored session, the SDK asks the backend
GET /connect/status. - If the connection is no longer live (expired / disconnected server-side), the SDK:
- clears the stored session for that wallet from
localStorage(scoped — other wallets untouched), - emits
disconnect, - shows a lightweight "Session expired" toast (no page reload).
- clears the stored session for that wallet from
- Your app then reads
isConnected()/getWallet()→null→ render the Connect button.
To read connection state deterministically right after construction, await ready():
const wallet = new TonPassConnect({ appId: 'your_app_id' });
await wallet.ready(); // init: resume + expiry check (clears + toasts if expired)
if (wallet.isConnected()) {
renderConnected(wallet.getWallet());
} else {
renderConnectButton(); // stored session was empty or just expired
}
// …or react live:
wallet.on('disconnect', renderConnectButton);ready() also resolves after a redirect-back connect flow (wallet reopens your DApp after
approval), so it's safe to gate your first render on it.
Window globals
Each wallet instance auto-registers on window:
| Class | Window global |
|-------|--------------|
| TonPassConnect | window.tonpass |
| OneDollarConnect | window.onedollar |
| BaseWalletConnect | window.walletconnect |
Integrate into TMA Wallet App (Telegram Bot)
This section is for wallet app developers who want their Telegram Mini App (TMA) to support connect requests from DApps using this SDK.
How it works
DApp (SDK) Your Backend TMA (Telegram)
──────────────────────────────────────────────────────────────────────
1. SDK calls POST /connect/init
← { sessionToken, deeplink }
2. User clicks deeplink / scans QR
────────────────────────────────────────────────→ TMA opens
parse startapp param
3. ← POST /connect/verify TMA sends initData
verify Telegram HMAC
→ { appId, origin }
4. TMA shows confirm UI
5. User taps Approve
← POST /connect/confirm TMA sends JWT + approved
get user wallet
emit WS event ──────→ SDK receives result
modal shows successStep 1: Parse startapp param in your TMA
When a DApp initiates a connect request, the user is redirected to your TMA via deeplink. The startapp parameter contains the session token.
// In your TMA (React / vanilla JS)
const tg = window.Telegram.WebApp;
const startParam = tg.initDataUnsafe?.start_param; // "sessionToken_abc123..."
if (startParam && startParam.startsWith('sessionToken_')) {
const sessionToken = startParam.replace('sessionToken_', '');
// → proceed to Step 2
}Step 2: Verify session with your backend
Call your backend to verify the session and get DApp info to show the user.
const response = await fetch('https://your-api.com/connect/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sessionToken,
initData: tg.initData, // Telegram initData for HMAC verification
botId: 'your_bot_id',
}),
});
const { appId, appName, origin, walletId } = await response.json();
// → Show confirm UI to userStep 3: Show confirm popup
Display the DApp info and let the user approve or reject.
// React example
function ConnectConfirm({ appName, origin, onApprove, onReject }) {
return (
<div className="confirm-popup">
<h2>{appName} wants to connect</h2>
<p>Origin: {origin}</p>
<button onClick={onApprove}>Approve</button>
<button onClick={onReject}>Reject</button>
</div>
);
}Step 4: Confirm or reject
When the user taps Approve/Reject, call the confirm endpoint with their JWT token.
const token = 'user_jwt_token'; // from your TMA auth flow
const result = await fetch('https://your-api.com/connect/confirm', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({
sessionToken,
approved: true, // or false to reject
}),
});
const data = await result.json();
// {
// success: true,
// status: 'CONFIRMED',
// wallet: { address: 'UQ...', chain: 'TON' },
// user: { username: 'john', avatar: '...' }
// }
// The SDK on the DApp side receives this automatically via WebSocket/polling
// → DApp modal shows "Connection Successful"Step 5: In-app QR scanner (optional)
Allow users already in your TMA to scan a QR code from a DApp to connect.
// User scans QR → gets deeplink URL
const scannedUrl = 'https://t.me/YourBot/app?startapp=sessionToken_abc123...';
// Parse sessionToken from URL
const url = new URL(scannedUrl);
const startapp = url.searchParams.get('startapp') || '';
const sessionToken = startapp.replace('sessionToken_', '');
// → Same flow: verify → confirm popup → approve/rejectStep 6: Manage connected DApps
Let users view and disconnect DApps from your TMA.
// List connected DApps
const dapps = await fetch('https://your-api.com/connect/dapps', {
headers: { 'Authorization': `Bearer ${token}` },
}).then(r => r.json());
// Disconnect a specific DApp
await fetch('https://your-api.com/connect/disconnect', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({
appId: 'the_app_id',
walletId: 'onedollar',
}),
});Backend API endpoints
Your backend needs these endpoints:
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | /connect/wallets | No | List supported wallets (SDK fetches this) |
| POST | /connect/init | No | Create connect session (SDK calls this) |
| GET | /connect/status?token= | No | Poll session status (SDK fallback) |
| POST | /connect/verify | initData | Verify session + Telegram user (TMA calls) |
| POST | /connect/confirm | JWT | Approve/reject session (TMA calls) |
| POST | /connect/disconnect | JWT | Disconnect a DApp (TMA calls) |
| POST | /connect/disconnect-by-token | No | Disconnect by session token (SDK calls) |
| POST | /connect/resolve | No* | Resolve a session token → user + TON wallet (server-side verify) |
| GET | /connect/dapps | JWT | List connected DApps (TMA calls) |
| POST | /dex/sendTx | No | Submit a tx from the widget (SDK sendTransaction) |
| GET | /dex/status/:txId | No | Poll tx status (SDK waitForResult) |
| POST | /dex/confirm | JWT | Confirm/cancel a tx (TMA calls) |
* /connect/resolve is public but requires the caller to know both the sessionToken
and the owning appId — call it only from your backend.
Verify a user server-side
After a user connects, your backend can verify — server-to-server — who the session belongs
to instead of trusting an address sent from the client. Send the sessionToken
(wallet.getWallet().session.sessionToken) from the client to your backend, then:
// on YOUR backend
const res = await fetch(`${walletApiUrl}/connect/resolve`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionToken, appId: 'your_app_id' }),
});
// res.ok → authoritative identity:
// { valid: true, telegramId, userId, user: { username, avatar }, wallet: { address, chain: 'TON' } }The returned wallet.address is the address shared at connect time (stable even if the
user later switches their selected wallet) — compare it against the address you received
client-side to detect spoofing.
| HTTP | Meaning |
|------|---------|
| 404 Session not found | wrong token or appId mismatch |
| 400 Session not confirmed | user hasn't approved yet |
| 400 Session expired | connection past its idle TTL / disconnected |
| 404 no_wallet… | user has no TON root wallet |
Register your wallet in the registry
To make your wallet available to all DApps using the SDK, add it to registry.json:
{
"walletId": "yourwallet",
"name": "Your Wallet",
"icon": "https://your-cdn.com/icon.png",
"botUsername": "YourBot",
"appName": "app",
"apiUrl": "https://your-api.com",
"description": "Your wallet description",
"order": 0
}License
MIT
