This guide covers wallet connection and transaction signing for web applications using @proton/web-sdk.
npm install @proton/web-sdk @proton/link
# or
yarn add @proton/web-sdk @proton/linkImportant: The @proton/link package is required for mobile wallet support. See Mobile Wallet Support for details.
import ProtonWebSDK from '@proton/web-sdk';
import '@proton/link'; // Required for mobile wallet support
// Login
const { link, session } = await ProtonWebSDK({
linkOptions: {
chainId: '384da888112027f0321850a169f737c33e53b388aad48b5adace4bab97f437e0',
endpoints: ['https://proton.eosusa.io']
},
selectorOptions: { appName: 'My dApp' }
});
// session.auth = { actor: 'username', permission: 'active' }
console.log('Logged in as:', session.auth.actor);
// Send transaction
const result = await session.transact({
actions: [{
account: 'eosio.token',
name: 'transfer',
authorization: [session.auth],
data: {
from: session.auth.actor,
to: 'recipient',
quantity: '1.0000 XPR',
memo: 'Hello!'
}
}]
}, { broadcast: true });| Key | Type | Required | Description |
|---|---|---|---|
endpoints |
string[] |
Yes | Array of RPC endpoints (multiple for fault tolerance) |
chainId |
string |
No | Chain ID (defaults to mainnet) |
storage |
LinkStorage |
No | Custom storage adapter |
storagePrefix |
string |
No | Prefix for storage keys (default: proton-storage) |
restoreSession |
boolean |
No | Restore previous session without wallet selector |
| Key | Type | Description |
|---|---|---|
requestAccount |
string |
Required for mobile. Your dApp's account name - used for deep link callbacks so mobile app knows where to return after signing |
backButton |
boolean |
Show back button in modal (default: true) |
Important: Without
requestAccount, the WebAuth mobile app will sign transactions but won't return to your browser. Always set this to your contract or dApp account name.
| Key | Type | Description |
|---|---|---|
appName |
string |
Your app name (shown in wallet selector) |
appLogo |
string |
URL to your app logo |
enabledWalletTypes |
string[] |
Wallet types to show: proton, webauth, anchor |
customStyleOptions |
object |
Custom styling for modal |
import ProtonWebSDK from '@proton/web-sdk';
const CHAIN_ID = '384da888112027f0321850a169f737c33e53b388aad48b5adace4bab97f437e0';
const ENDPOINTS = ['https://proton.eosusa.io', 'https://proton.protonuk.io'];
class ProtonService {
private link: any = null;
private session: any = null;
get isLoggedIn(): boolean {
return this.session !== null;
}
get actor(): string {
return this.session?.auth?.actor ?? '';
}
get permission(): string {
return this.session?.auth?.permission ?? 'active';
}
async login(): Promise<{ actor: string; permission: string } | null> {
try {
const { link, session } = await ProtonWebSDK({
linkOptions: {
chainId: CHAIN_ID,
endpoints: ENDPOINTS
},
transportOptions: {
requestAccount: 'myapp'
},
selectorOptions: {
appName: 'My dApp',
appLogo: 'https://myapp.com/logo.png',
enabledWalletTypes: ['proton', 'webauth', 'anchor']
}
});
this.link = link;
this.session = session;
return session.auth;
} catch (error) {
console.error('Login failed:', error);
return null;
}
}
async restoreSession(): Promise<boolean> {
try {
const { link, session } = await ProtonWebSDK({
linkOptions: {
chainId: CHAIN_ID,
endpoints: ENDPOINTS,
restoreSession: true
},
transportOptions: {
requestAccount: 'myapp'
},
selectorOptions: {
appName: 'My dApp'
}
});
if (session) {
this.link = link;
this.session = session;
return true;
}
return false;
} catch (error) {
console.error('Session restore failed:', error);
return false;
}
}
async logout(): Promise<void> {
if (this.link && this.session) {
await this.link.removeSession('myapp', this.session.auth);
}
this.link = null;
this.session = null;
}
async transact(actions: any[]): Promise<any> {
if (!this.session) {
throw new Error('Not logged in');
}
return this.session.transact(
{ actions },
{ broadcast: true }
);
}
}
export const protonService = new ProtonService();async function transferTokens(to: string, amount: string, memo: string = '') {
const result = await session.transact({
actions: [{
account: 'eosio.token',
name: 'transfer',
authorization: [session.auth],
data: {
from: session.auth.actor,
to: to,
quantity: amount, // e.g., '10.0000 XPR'
memo: memo
}
}]
}, { broadcast: true });
return result;
}| Token | Contract | Decimals | Example |
|---|---|---|---|
| XPR | eosio.token |
4 | 1.0000 XPR |
| XUSDT | xtokens |
6 | 1.000000 XUSDT |
| FOOBAR | xtokens |
6 | 1.000000 FOOBAR |
| LOAN | loan.token |
4 | 1.0000 LOAN |
async function createBattle(amount: string, direction: number, duration: number) {
const result = await session.transact({
actions: [{
account: 'pricebattle',
name: 'create',
authorization: [session.auth],
data: {
creator: session.auth.actor,
amount: amount,
direction: direction, // 1=UP, 2=DOWN
oracle_index: 4, // BTC/USD
duration: duration // seconds
}
}]
}, { broadcast: true });
return result;
}async function depositAndStake(amount: string) {
const result = await session.transact({
actions: [
// First action: transfer
{
account: 'eosio.token',
name: 'transfer',
authorization: [session.auth],
data: {
from: session.auth.actor,
to: 'stakingcontract',
quantity: amount,
memo: 'deposit'
}
},
// Second action: stake
{
account: 'stakingcontract',
name: 'stake',
authorization: [session.auth],
data: {
account: session.auth.actor,
amount: amount
}
}
]
}, { broadcast: true });
return result;
}const { link, session } = await ProtonWebSDK({
linkOptions: { /* ... */ },
selectorOptions: {
appName: 'My dApp',
customStyleOptions: {
modalBackgroundColor: '#1a1a2e',
logoBackgroundColor: '#16213e',
isLogoRound: true,
optionBackgroundColor: '#0f3460',
optionFontColor: '#e94560',
primaryFontColor: '#ffffff',
secondaryFontColor: '#a0a0a0',
linkColor: '#e94560'
}
}
});By default, the SDK uses localStorage. For custom storage (e.g., encrypted storage, server-side):
interface LinkStorage {
write(key: string, data: string): Promise<void>;
read(key: string): Promise<string | null>;
remove(key: string): Promise<void>;
}
class SecureStorage implements LinkStorage {
async write(key: string, data: string): Promise<void> {
// Your secure storage logic
localStorage.setItem(key, encrypt(data));
}
async read(key: string): Promise<string | null> {
const data = localStorage.getItem(key);
return data ? decrypt(data) : null;
}
async remove(key: string): Promise<void> {
localStorage.removeItem(key);
}
}
const { link, session } = await ProtonWebSDK({
linkOptions: {
endpoints: ENDPOINTS,
storage: new SecureStorage()
},
// ...
});import React, { createContext, useContext, useState, useEffect } from 'react';
import ProtonWebSDK from '@proton/web-sdk';
interface ProtonContextType {
session: any;
login: () => Promise<void>;
logout: () => Promise<void>;
transact: (actions: any[]) => Promise<any>;
}
const ProtonContext = createContext<ProtonContextType | null>(null);
export function ProtonProvider({ children }: { children: React.ReactNode }) {
const [link, setLink] = useState<any>(null);
const [session, setSession] = useState<any>(null);
useEffect(() => {
// Restore session on mount
restoreSession();
}, []);
async function restoreSession() {
try {
const { link, session } = await ProtonWebSDK({
linkOptions: {
chainId: CHAIN_ID,
endpoints: ENDPOINTS,
restoreSession: true
},
selectorOptions: { appName: 'My dApp' }
});
if (session) {
setLink(link);
setSession(session);
}
} catch (e) {
console.error('Restore failed:', e);
}
}
async function login() {
const { link, session } = await ProtonWebSDK({
linkOptions: {
chainId: CHAIN_ID,
endpoints: ENDPOINTS
},
selectorOptions: { appName: 'My dApp' }
});
setLink(link);
setSession(session);
}
async function logout() {
if (link && session) {
await link.removeSession('myapp', session.auth);
}
setLink(null);
setSession(null);
}
async function transact(actions: any[]) {
if (!session) throw new Error('Not logged in');
return session.transact({ actions }, { broadcast: true });
}
return (
<ProtonContext.Provider value={{ session, login, logout, transact }}>
{children}
</ProtonContext.Provider>
);
}
export function useProton() {
const context = useContext(ProtonContext);
if (!context) throw new Error('useProton must be used within ProtonProvider');
return context;
}function WalletButton() {
const { session, login, logout } = useProton();
if (session) {
return (
<div>
<span>Connected: {session.auth.actor}</span>
<button onClick={logout}>Disconnect</button>
</div>
);
}
return <button onClick={login}>Connect Wallet</button>;
}async function safeTransact(actions: any[]) {
try {
const result = await session.transact({ actions }, { broadcast: true });
return { success: true, result };
} catch (error: any) {
// User cancelled
if (error.message?.includes('User cancelled')) {
return { success: false, error: 'Transaction cancelled by user' };
}
// Insufficient resources
if (error.message?.includes('insufficient')) {
return { success: false, error: 'Insufficient resources (RAM/CPU/NET)' };
}
// Contract assertion failed
if (error.message?.includes('assertion failure')) {
const match = error.message.match(/assertion failure with message: (.+)/);
return { success: false, error: match?.[1] ?? 'Transaction failed' };
}
return { success: false, error: error.message ?? 'Unknown error' };
}
}For mobile wallet signing to work (WebAuth iOS/Android app), you must install and import @proton/link.
The @proton/link package provides the transport layer for mobile deep linking. Without it, the SDK cannot communicate with the WebAuth mobile app to request transaction signatures.
npm install @proton/web-sdk @proton/linkimport ProtonWebSDK from '@proton/web-sdk';
import '@proton/link'; // Required - enables mobile deep linking transportNote: The @proton/link import doesn't expose any API you need to call directly. Simply importing it registers the transport handlers needed for mobile wallet communication.
Important: Static imports often fail for mobile wallet support. Use dynamic imports with Promise.all to ensure @proton/link is fully loaded before any wallet operations:
let ConnectWallet: any;
let sdkReady: Promise<void> | null = null;
if (typeof window !== 'undefined') {
sdkReady = Promise.all([
import('@proton/web-sdk').then((mod) => {
ConnectWallet = mod.default;
}),
import('@proton/link') // Critical for mobile deep linking
]).then(() => {});
}
// Helper to ensure SDK is loaded before use
const waitForSdk = async () => {
if (sdkReady) await sdkReady;
};
// Always await before using ConnectWallet
async function login() {
await waitForSdk();
const { link, session } = await ConnectWallet({
// ... options
});
}This pattern ensures both packages are fully loaded and transport handlers are registered before any wallet connection attempts.
For mobile wallet to work correctly, ensure ALL of these:
-
Install both packages:
npm install @proton/web-sdk @proton/link -
Use dynamic imports with Promise.all (not static imports):
sdkReady = Promise.all([ import('@proton/web-sdk').then((mod) => { ConnectWallet = mod.default; }), import('@proton/link') ]).then(() => {});
-
Set
requestAccountto your dApp/contract name:transportOptions: { requestAccount: 'mycontract', // Required for mobile callback }
-
Enable wallet types explicitly:
selectorOptions: { appName: 'My dApp', enabledWalletTypes: ['webauth', 'proton'], }
| Symptom | Cause | Fix |
|---|---|---|
| Mobile stuck on "Processing..." | Missing @proton/link or static import |
Use dynamic import pattern |
| App signs but doesn't return to browser | requestAccount empty or missing |
Set to your contract name |
| Only browser wallet shown | enabledWalletTypes missing proton |
Add ['webauth', 'proton'] |
| "Unknown requestor" error | Missing @proton/link |
Install and import the package |
Safari iOS blocks popups by default, which prevents the WebAuth browser wallet from opening. Users on Safari iOS should either:
- Disable popup blocker: Settings > Safari > Block Pop-ups OFF
- Use the WebAuth mobile app instead of the browser wallet (recommended)
- Use a different browser (Chrome, Firefox)
Developer tip: Show a help message after several seconds of "processing" to guide users to check their popup blocker settings or switch to the WebAuth mobile app.
@proton/web-sdk@^4.4.1or later@proton/link@^3.2.3-27or later
Status: Unresolved - issue is in WebAuth iOS app
Symptom: After logout + login with different account via WebAuth iOS, the UI shows new account but transactions fail with wrong signature.
Workaround:
- Force quit WebAuth iOS app before switching accounts
- Or use webauth.com web wallet (works correctly)
If restoreSession: true doesn't restore the session:
- Clear localStorage keys starting with
proton-storageor your custom prefix - Ensure you're on the same domain where the session was created
- Check if storage is being blocked (private browsing, etc.)
For SSR frameworks, the SDK must only run client-side:
// Only import client-side
let ProtonWebSDK: any;
if (typeof window !== 'undefined') {
ProtonWebSDK = require('@proton/web-sdk').default;
}
// Check before using
async function login() {
if (!ProtonWebSDK) {
console.error('ProtonWebSDK not available');
return;
}
// ...
}Or use dynamic imports:
async function login() {
const { default: ProtonWebSDK } = await import('@proton/web-sdk');
// ...
}| Type | Description | Use Case |
|---|---|---|
proton |
WebAuth mobile app | Primary mobile wallet |
webauth |
webauth.com browser wallet | Web-based, no app needed |
anchor |
Anchor desktop wallet | Desktop users, power users |
// Show only specific wallets
selectorOptions: {
enabledWalletTypes: ['proton', 'webauth'] // Hide Anchor
}