XSWD Wallet Connect Tutorial
Complete guide to implementing XSWD wallet connection in your TELA site.
Having "Connection Closed" Issues? This tutorial addresses the exact problem. Jump to Troubleshooting Section for immediate solutions.
Verified Against Official Demo: This tutorial's code patterns match the exact working implementation from tela_tests/app1 - the official TELA demo application. All examples here have been proven to work.
Table of Contents
- Understanding XSWD Connection
- Prerequisites
- Step 1: Enable XSWD in Your Wallet
- Step 2: Basic Connection Implementation
- Step 3: Using XSWD Templates
- Step 4: Complete Working Example
- Troubleshooting Connection Closed Errors
- Common Pitfalls
Understanding XSWD Connection
XSWD connects your TELA application to DERO wallets through a WebSocket connection. The connection process has three critical phases:
1. WebSocket Opens → ws://localhost:44326/xswd
2. Send Application Data → Wallet shows approval prompt
3. Receive "accepted" → Connection authorized, ready for API callsMost "connection closed" errors happen during phase 2 - the handshake fails or the wallet rejects the connection.
Prerequisites
Before starting, ensure you have:
- ✅ DERO wallet running (Engram or CLI wallet)
- ✅ XSWD enabled and configured
- ✅ Basic HTML/JavaScript knowledge
- ✅ Your TELA site structure ready
- ✅ Page served via HTTP (for local testing:
python3 -m http.server 8080)
Step 1: Enable XSWD in Your Wallet
Engram Wallet
- Open Engram wallet
- Go to Settings → XSWD (or Advanced → XSWD)
- Enable "Enable XSWD Server"
- Note the port (default: 44326)
- Restart wallet if required
Default Ports (use localhost to match app1):
- Engram:
ws://localhost:44326/xswd✅ Matches official app1 demo - DERO Wallet CLI (mainnet):
ws://localhost:10103/xswd - DERO Wallet CLI (testnet):
ws://localhost:40403/xswd
DERO CLI Wallet
If using the CLI wallet, XSWD must be enabled via command line:
# Start wallet with XSWD enabled
dero-wallet-cli --rpc-server --rpc-bind=127.0.0.1:10103 --rpc-login=user:pass
# Or in wallet prompt after opening wallet:
[xswd]
enableVerify XSWD is running:
# Test if port is accessible
curl http://localhost:44326/xswd
# Should return WebSocket upgrade response (or connection refused if not running)
# Or check if process is listening
lsof -i :44326 # macOS/Linux
netstat -an | findstr 44326 # WindowsImportant: The official app1 demo uses localhost, not 127.0.0.1. Both work, but localhost matches the exact working pattern.
Step 2: Basic Connection Implementation
Here's the minimum working implementation based on proven patterns:
// XSWD Basic Connection - EXACT pattern from official app1 demo
// This matches the proven working implementation from tela_tests/app1
// Global WebSocket variable (matches app1 pattern)
let socket;
// Generate and store 64-character hex ID (required by wallet)
// This matches the app1 pattern of using a static ID for proper permission management
function getOrCreateAppId() {
let appId = localStorage.getItem('xswd_app_id');
if (!appId) {
// Generate 64-character hex string (matches xswd-core.js pattern)
appId = Array.from({length: 64}, () =>
Math.floor(Math.random() * 16).toString(16)
).join('');
localStorage.setItem('xswd_app_id', appId);
}
return appId;
}
// XSWD application data (matches app1 format)
const applicationData = {
"id": getOrCreateAppId(), // 64-character hex string (static - maintains permission continuity)
"name": "My TELA Application",
"description": "Connecting to wallet for DERO operations",
"url": "http://localhost:" + location.port // IMPORTANT: Must match origin URL
};
// Function to send data (matches app1 pattern)
function sendData(d) {
if (socket && socket.readyState === WebSocket.OPEN) {
try {
socket.send(JSON.stringify(d));
if (d.method) {
console.log(d.method, "request sent to the server");
} else {
console.log("Connection request sent to the server");
}
} catch (error) {
console.error("Failed to send data:", error);
}
} else {
console.log("Web socket is not open. State:", socket ? socket.readyState : "N/A");
}
}
// Connect to XSWD (EXACT pattern from app1)
function connectWebSocket() {
// Connect to WebSocket - note: app1 uses localhost, not 127.0.0.1
socket = new WebSocket("ws://localhost:44326/xswd");
// Listen for open event
socket.addEventListener("open", function(event) {
console.log("Web socket connection established:", event);
// CRITICAL: Send application data immediately after connection opens
sendData(applicationData);
});
let connecting = true;
// Listen for messages from wallet
socket.addEventListener("message", function(event) {
const response = JSON.parse(event.data);
console.log("Response received:", response);
// CRITICAL: Check for acceptance first (matches app1 pattern)
if (response.accepted) {
console.log("Connected message received:", response.message);
connecting = false;
// Now safe to make API calls - request wallet address as first call
sendData({
jsonrpc: "2.0",
id: "1",
method: "GetAddress"
});
} else if (response.result) {
// Handle API response results
const res = response.result;
if (res.address) {
console.log("Connected address:", res.address);
// Connection fully established
} else if (res.unlocked_balance !== undefined) {
const balance = res.unlocked_balance / 100000;
console.log("Balance:", balance.toFixed(5), "DERO");
} else if (res.height) {
console.log("Height:", res.height);
}
// Add other response handlers as needed
} else if (response.error) {
console.error("Error:", response.error.message);
if (connecting) {
// Connection failed during handshake
console.error("Connection failed:", response.error.message);
}
}
});
// Listen for errors
socket.addEventListener("error", function(event) {
console.error("Web socket error:", event);
});
// Listen for close
socket.addEventListener("close", function(event) {
console.log("Web socket connection closed:", event.code, event.reason);
socket = null;
});
}
// Initialize when page loads
document.addEventListener('DOMContentLoaded', function() {
connectWebSocket();
});
// Cleanup on page unload
window.addEventListener('beforeunload', function() {
if (socket) {
socket.close();
}
});Critical Points (from official app1 demo):
- ID must be exactly 64 hexadecimal characters - The wallet requires a 64-character hex string (e.g.,
"71605a32e3b0c44298fc1c549afbf4c8496fb92427ae41e4649b934ca495991b") - Use a static ID (store and reuse) - Generate once and store in localStorage. This maintains permission continuity across connections and matches the app1 pattern. The wallet tracks permissions by ID, so using the same ID ensures permissions persist across sessions.
- Use
localhostnot127.0.0.1- The official demo usesws://localhost:44326/xswd - Send application data immediately after
onopen- don't wait - Wait for
response.acceptedbefore making API calls - Check
response.acceptedFIRST - Only proceed after wallet approves - Handle all events:
open,message,error,close - Check socket state before sending:
socket.readyState === WebSocket.OPEN - URL must match origin - Use
"http://localhost:" + location.portfor app data
Step 3: Using XSWD Templates
For production applications, use the proven XSWD templates:
Option 1: XSWD Basic (Under 18KB)
Best for simple apps with size constraints:
<!DOCTYPE html>
<html>
<head>
<title>My TELA App</title>
</head>
<body>
<!-- Load XSWD Basic -->
<script src="xswd-basic.js"></script>
<script>
// Initialize XSWD Basic
window.xswd.initialize({
name: 'My TELA App',
description: 'App description',
allowDevelopmentMode: true // Allows testing without wallet
}).then(connected => {
if (connected) {
console.log('✅ Connected!');
loadData();
} else {
console.log('⚠️ Not connected - check wallet');
}
});
async function loadData() {
const address = await window.xswd.getAddress();
const balance = await window.xswd.getBalance();
console.log('Address:', address);
console.log('Balance:', balance);
}
</script>
</body>
</html>See: XSWD Basic Template
Option 2: XSWD Advanced (Production Grade)
For full-featured applications:
<script src="xswd-advanced.js"></script>
<script>
window.xswd.initialize({
allowDevelopmentMode: true,
blockExternalConnections: true
}).then(connected => {
if (connected) {
// Use high-level methods
window.xswd.getNetworkInfo().then(info => {
console.log('Network height:', info.height);
});
}
});
</script>Step 4: Complete Working Example
Here's a complete, tested implementation you can copy directly:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>XSWD Wallet Connect Example</title>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
background: #0a0e27;
color: #fff;
}
.status {
padding: 15px;
border-radius: 8px;
margin: 20px 0;
}
.status.connecting { background: #1a4d80; }
.status.connected { background: #0d4a26; }
.status.error { background: #6b1a1a; }
button {
padding: 10px 20px;
background: #2563eb;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
margin: 5px;
}
button:hover { background: #1d4ed8; }
button:disabled { background: #4b5563; cursor: not-allowed; }
.data { background: #1a1a2e; padding: 15px; border-radius: 8px; margin: 10px 0; }
</style>
</head>
<body>
<h1>🔗 XSWD Wallet Connect</h1>
<div id="status" class="status connecting">
Connecting to wallet...
</div>
<button onclick="connect()">Connect Wallet</button>
<button onclick="getAddress()" id="btn-address" disabled>Get Address</button>
<button onclick="getBalance()" id="btn-balance" disabled>Get Balance</button>
<button onclick="getNetworkInfo()" id="btn-network" disabled>Get Network Info</button>
<div id="data" class="data" style="display:none;">
<h3>Wallet Data:</h3>
<pre id="output"></pre>
</div>
<script>
let socket = null;
let isConnected = false;
let requestId = 0;
const pendingRequests = new Map();
function updateStatus(message, type = 'connecting') {
const statusEl = document.getElementById('status');
statusEl.textContent = message;
statusEl.className = `status ${type}`;
}
function enableButtons() {
document.getElementById('btn-address').disabled = false;
document.getElementById('btn-balance').disabled = false;
document.getElementById('btn-network').disabled = false;
}
function connect() {
if (socket && socket.readyState === WebSocket.OPEN) {
console.log('Already connected');
return;
}
updateStatus('Connecting to ws://localhost:44326/xswd...', 'connecting');
// Use localhost to match official app1 demo pattern
socket = new WebSocket("ws://localhost:44326/xswd");
socket.onopen = function(event) {
console.log('✅ WebSocket opened');
updateStatus('Sending connection request...', 'connecting');
// CRITICAL: Send application data immediately
// Match exact app1 format - ID must be 64-character hex string (static)
const appData = {
id: "71605a32e3b0c44298fc1c549afbf4c8496fb92427ae41e4649b934ca495991b", // Static 64-char hex (from app1)
name: "XSWD Connect Example",
description: "Example TELA app connecting to DERO wallet",
url: "http://localhost:" + location.port // Must include port!
};
socket.send(JSON.stringify(appData));
console.log('📤 Sent application data');
};
socket.onmessage = function(event) {
try {
const response = JSON.parse(event.data);
console.log('📨 Received:', response);
if (response.accepted) {
console.log('✅ Connection accepted!');
isConnected = true;
updateStatus('✅ Connected to wallet!', 'connected');
enableButtons();
} else if (response.rejected) {
console.error('❌ Connection rejected:', response.message);
updateStatus('❌ Connection rejected: ' + (response.message || 'Unknown'), 'error');
isConnected = false;
} else if (response.jsonrpc && response.id) {
// Handle RPC response
handleRPCResponse(response);
} else {
console.log('Unknown response:', response);
}
} catch (error) {
console.error('Parse error:', error);
}
};
socket.onerror = function(error) {
console.error('🚨 WebSocket error:', error);
updateStatus('❌ Connection error - Is wallet running?', 'error');
};
socket.onclose = function(event) {
console.log('🔌 Connection closed:', event.code, event.reason);
isConnected = false;
updateStatus('❌ Disconnected (Code: ' + event.code + ')', 'error');
// Disable buttons
document.getElementById('btn-address').disabled = true;
document.getElementById('btn-balance').disabled = true;
document.getElementById('btn-network').disabled = true;
if (event.code === 1006) {
updateStatus('❌ Connection refused - Wallet not running or XSWD disabled', 'error');
}
};
}
function sendRPC(method, params = {}) {
return new Promise((resolve, reject) => {
if (!isConnected || !socket || socket.readyState !== WebSocket.OPEN) {
reject(new Error('Not connected'));
return;
}
const id = (++requestId).toString();
const request = {
jsonrpc: "2.0",
id: id,
method: method
};
if (Object.keys(params).length > 0) {
request.params = params;
}
pendingRequests.set(id, { resolve, reject, method });
socket.send(JSON.stringify(request));
// Timeout after 10 seconds
setTimeout(() => {
if (pendingRequests.has(id)) {
pendingRequests.delete(id);
reject(new Error(`Timeout: ${method}`));
}
}, 10000);
});
}
function handleRPCResponse(response) {
if (response.id && pendingRequests.has(response.id)) {
const { resolve, reject } = pendingRequests.get(response.id);
pendingRequests.delete(response.id);
if (response.error) {
reject(new Error(response.error.message || 'RPC error'));
} else {
resolve(response.result);
}
}
}
function showData(data) {
document.getElementById('data').style.display = 'block';
document.getElementById('output').textContent = JSON.stringify(data, null, 2);
}
async function getAddress() {
try {
const result = await sendRPC('GetAddress');
showData({ address: result.address });
console.log('Address:', result.address);
} catch (error) {
alert('Error: ' + error.message);
console.error(error);
}
}
async function getBalance() {
try {
const result = await sendRPC('GetBalance');
const balance = result.unlocked_balance / 100000;
showData({
balance: balance + ' DERO',
unlocked: result.unlocked_balance,
locked: result.balance - result.unlocked_balance
});
console.log('Balance:', balance, 'DERO');
} catch (error) {
alert('Error: ' + error.message);
console.error(error);
}
}
async function getNetworkInfo() {
try {
const result = await sendRPC('DERO.GetInfo');
showData({
height: result.height,
difficulty: result.difficulty,
peer_count: result.peer_count,
version: result.version
});
console.log('Network info:', result);
} catch (error) {
alert('Error: ' + error.message);
console.error(error);
}
}
// Auto-connect on page load
window.addEventListener('load', function() {
connect();
});
// Cleanup on page unload
window.addEventListener('beforeunload', function() {
if (socket) {
socket.close();
}
});
</script>
</body>
</html>Save this as index.html and open it in a browser with your wallet running!
Troubleshooting Connection Closed Errors
Error: "Connection closed" (Code 1006)
Symptoms:
🔌 Connection closed: 1006
❌ Connection refused - Wallet not running or XSWD disabledSolutions:
1. 💼 Check Wallet is Running
Ensure Engram or CLI wallet is running and unlocked.
# Check if Engram is running (macOS/Linux)
ps aux | grep -i engram
# Check if wallet process exists
lsof -i :443262. ⚙️ Verify XSWD is Enabled
In Engram: Settings → XSWD → Enable XSWD Server
For CLI wallet, ensure XSWD is enabled in wallet prompt.
3. 🔌 Check Correct Port
Try standard ports (use localhost to match app1 pattern):
ws://localhost:44326/xswd(Engram default - matches app1)ws://127.0.0.1:44326/xswd(Alternative)ws://localhost:10103/xswd(CLI mainnet)ws://localhost:40403/xswd(CLI testnet)
4. 🧪 Test Connection Manually
Test WebSocket connection:
// Match app1 pattern - use localhost
const ws = new WebSocket("ws://localhost:44326/xswd");
ws.onopen = () => console.log("✅ Port accessible");
ws.onerror = () => console.error("❌ Port not accessible");5. 🔥 Check Firewall
Ensure firewall isn't blocking localhost connections:
# macOS
sudo lsof -i :44326
# Linux
sudo ufw status
sudo ufw allow 44326
# Windows: Check Windows Defender Firewall6. 🔄 Restart Wallet
Sometimes wallet needs restart after enabling XSWD. Close wallet completely and restart.
Error: Connection Closes Immediately After Open
Cause: Application data not sent or sent incorrectly.
Solution:
// ✅ CORRECT: Send immediately in onopen (matches app1)
socket.addEventListener("open", function(event) {
sendData(applicationData); // Do this immediately - no delay!
});
// ❌ WRONG: Delaying or not sending
socket.addEventListener("open", function(event) {
setTimeout(() => {
sendData(applicationData); // Too late - connection may close
}, 1000);
};Error: Connection Rejected
Symptoms:
{
"rejected": true,
"message": "Connection rejected by user"
}Solutions:
-
Check application data format:
// ✅ REQUIRED fields const applicationData = { id: "71605a32e3b0c44298fc1c549afbf4c8496fb92427ae41e4649b934ca495991b", // 64-char hex (REQUIRED) name: "App Name", // REQUIRED description: "App desc", // REQUIRED url: "http://localhost:" + location.port // REQUIRED (must include port) }; -
User clicked "Deny" in wallet - Ask user to approve connection in wallet popup
-
Wallet security settings - Check if wallet has connection restrictions enabled
Error: "Invalid ID size"
Symptoms:
{
"accepted": false,
"message": "Could not connect the application: Invalid ID size"
}Cause: The id field must be exactly 64 hexadecimal characters. Variable-length IDs or IDs with incorrect format will cause this error.
Solution:
// ✅ CORRECT: Generate and store static ID (recommended approach)
function getOrCreateAppId() {
let appId = localStorage.getItem('xswd_app_id');
if (!appId) {
// Generate exactly 64 hexadecimal characters
appId = Array.from({length: 64}, () =>
Math.floor(Math.random() * 16).toString(16)
).join('');
localStorage.setItem('xswd_app_id', appId);
}
return appId;
}
const applicationData = {
id: getOrCreateAppId(), // Static ID - maintains permission continuity
name: "My TELA Application",
description: "App description",
url: "http://localhost:" + location.port
};
// ❌ WRONG: Variable-length ID (causes "Invalid ID size" error)
const applicationData = {
id: "my-app-" + Date.now().toString(16), // Wrong size!
// ...
};Important: The ID must be static (same across all connections) for proper permission management. Generate once and store in localStorage using getOrCreateAppId().
Error: "Invalid URL compared to origin"
Symptoms:
{
"accepted": false,
"message": "Invalid URL compared to origin"
}Cause: The url field in application data must exactly match the page origin. This happens when:
- Opening HTML file as
file://URL (no port available) - URL in application data doesn't match the actual page URL
location.portis empty or incorrect
Solution:
// ✅ CORRECT: Use location.port for HTTP pages
const applicationData = {
id: getOrCreateAppId(),
name: "My App",
description: "App description",
url: "http://localhost:" + location.port // Matches origin
};
// ❌ WRONG: file:// URLs don't have a port
// If opened as file://, location.port is empty string
url: "http://localhost:" + location.port // Becomes "http://localhost:" (invalid)
// ❌ WRONG: URL doesn't match origin
url: "http://example.com:8080" // Doesn't match actual page originFor local testing: Always serve your page via HTTP server:
# Start local HTTP server
python3 -m http.server 8080
# Then open: http://localhost:8080/your-page.htmlNote: TELA apps are deployed via HTTP, so this matches real-world usage. The file:// limitation only affects local development/testing.
Common Pitfalls
Pitfall 1: Not Waiting for response.accepted
// ❌ WRONG: Making API calls too early
socket.onopen = function() {
socket.send(JSON.stringify(applicationData));
makeRPCCall("GetAddress"); // Too soon! Not accepted yet
};
// ✅ CORRECT: Wait for acceptance
if (response.accepted) {
isConnected = true;
makeRPCCall("GetAddress"); // Now safe to call
}Pitfall 2: Wrong WebSocket URL Format
// ❌ WRONG
new WebSocket("http://localhost:44326/xswd") // HTTP not WS
new WebSocket("ws://localhost:44326") // Missing /xswd path
// ✅ CORRECT (matches app1 pattern)
new WebSocket("ws://localhost:44326/xswd")Pitfall 3: Invalid Application ID Format
// ❌ WRONG: Variable-length ID (causes "Invalid ID size" error)
const applicationData = {
id: "my-app-" + Date.now().toString(16), // ~20-25 chars, wrong!
// ...
};
// ❌ NOT RECOMMENDED: Generating new ID each time (breaks permission continuity)
// const applicationData = {
// id: Array.from({length: 64}, () => Math.floor(Math.random() * 16).toString(16)).join(''),
// // ... This works functionally but breaks permission management
// };
// ✅ CORRECT: Generate and store static 64-character hex ID (recommended)
function getOrCreateAppId() {
let id = localStorage.getItem('xswd_app_id');
if (!id) {
id = Array.from({length: 64}, () =>
Math.floor(Math.random() * 16).toString(16)
).join('');
localStorage.setItem('xswd_app_id', id);
}
return id;
}
const applicationData = {
id: getOrCreateAppId(), // Static ID - maintains permission continuity
// ...
};Pitfall 4: Not Handling Multiple Endpoints
// ✅ BETTER: Try multiple endpoints (use localhost to match app1)
const endpoints = [
"ws://localhost:44326/xswd", // Engram default (matches app1)
"ws://localhost:10103/xswd", // CLI mainnet
"ws://localhost:40403/xswd" // CLI testnet
];
async function connectWithFallback() {
for (const endpoint of endpoints) {
try {
const connected = await connectToEndpoint(endpoint);
if (connected) return true;
} catch (e) {
console.log(`Failed: ${endpoint}`);
}
}
return false;
}Pitfall 4: Sending Requests Too Fast
// ❌ WRONG: Overwhelming connection
for (let i = 0; i < 10; i++) {
makeRPCCall("GetInfo"); // Too many at once!
}
// ✅ CORRECT: Rate limit requests
let lastCall = 0;
async function rateLimitedCall(method) {
const now = Date.now();
if (now - lastCall < 500) {
await new Promise(r => setTimeout(r, 500 - (now - lastCall)));
}
lastCall = Date.now();
return makeRPCCall(method);
}Quick Reference
Connection States
| State | Description | Action |
|---|---|---|
WebSocket.CONNECTING (0) | Establishing connection | Wait |
WebSocket.OPEN (1) | Connected, ready | Can send data |
WebSocket.CLOSING (2) | Closing | Cannot send |
WebSocket.CLOSED (3) | Closed | Reconnect needed |
Close Codes
| Code | Meaning | Solution |
|---|---|---|
1000 | Normal closure | User closed wallet |
1006 | Abnormal closure | Wallet not running / XSWD disabled |
1001 | Going away | Wallet closed |
1002 | Protocol error | Check application data format |
Essential RPC Methods
// Wallet methods
GetAddress() // Get wallet address
GetBalance() // Get wallet balance
GetHeight() // Get wallet sync height
// Network methods
DERO.GetInfo() // Network status
DERO.GetBlock() // Get block data
DERO.GetTransaction() // Get transaction dataNext Steps
- Learn more: XSWD Protocol Overview
- Use templates: XSWD Templates
- Troubleshooting: Error Troubleshooting Guide
- API reference: Complete API Guide
Verified Demo Reference:
- Official Demo (app1) - Complete source code this tutorial is based on
- XSWD Connection Pattern - Detailed connection code analysis
Still Having Issues? Check that:
- Wallet is running and unlocked
- XSWD is enabled in wallet settings
- Port 44326 (or your wallet's port) is accessible
- Application data is sent immediately after
onopen - You're waiting for
response.acceptedbefore making API calls
This tutorial covers the exact implementation patterns proven to work with Engram and CLI wallets. If you follow these steps, your XSWD connection should work!