# toncorp-wallet-connect-sdk

Connect TonPass, OneDollar and more wallets from any DApp or website.

## Install

```bash
npm install toncorp-wallet-connect-sdk
```

Or via CDN:

```html
<script src="https://cdn.jsdelivr.net/npm/toncorp-wallet-connect-sdk@latest/dist/index.umd.js"></script>
```

## Usage

### Specific wallet

```typescript
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 result
```

> The 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](#session-lifecycle--auto-expiry)**.

```typescript
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)

```typescript
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();
```

> **`siteTmaUrl` matters 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)

```html
<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

```typescript
{
  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.

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

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

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

1. On construction, after restoring the stored session, the SDK asks the backend
   `GET /connect/status`.
2. 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).
3. Your app then reads `isConnected()` / `getWallet()` → `null` → render the **Connect** button.

To read connection state deterministically right after construction, await `ready()`:

```typescript
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 success
```

### Step 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.

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

```typescript
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 user
```

### Step 3: Show confirm popup

Display the DApp info and let the user approve or reject.

```tsx
// 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.

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

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

### Step 6: Manage connected DApps

Let users view and disconnect DApps from your TMA.

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

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

```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
