Basic Usage

Learn the fundamentals of using the Portal TypeScript SDK.

Quick Example

Here's a complete example showing the basic workflow:

import { PortalSDK } from 'portal-sdk';

async function main() {
  // 1. Create client
  const client = new PortalSDK({
    serverUrl: 'ws://localhost:3000/ws'
  });

  // 2. Connect
  await client.connect();
  console.log('✅ Connected');

  // 3. Authenticate
  await client.authenticate('your-auth-token');
  console.log('✅ Authenticated');

  // 4. Generate auth URL for user
  const authUrl = await client.newKeyHandshakeUrl((mainKey) => {
    console.log('✅ User authenticated:', mainKey);
  });
  
  console.log('Share this URL:', authUrl);

  // Keep running...
  await new Promise(() => {});
}

main().catch(console.error);

Core Concepts

1. Client Initialization

Create a Portal client instance:

import { PortalSDK } from 'portal-sdk';

const client = new PortalSDK({
  serverUrl: 'ws://localhost:3000/ws',
  connectTimeout: 10000  // Optional: timeout in milliseconds
});

Configuration Options:

  • serverUrl (required): WebSocket URL of your Portal daemon
  • connectTimeout (optional): Connection timeout in ms (default: 10000)

2. Connection Management

Connect

try {
  await client.connect();
  console.log('Connected successfully');
} catch (error) {
  console.error('Connection failed:', error);
}

Disconnect

client.disconnect();
console.log('Disconnected');

Connection Events

client.on({
  onConnected: () => {
    console.log('Connection established');
  },
  onDisconnected: () => {
    console.log('Connection closed');
  },
  onError: (error) => {
    console.error('Connection error:', error);
  }
});

3. Authentication

Authenticate with your Portal daemon:

try {
  await client.authenticate('your-auth-token');
  console.log('Authenticated with Portal');
} catch (error) {
  console.error('Authentication failed:', error);
}

Important: You must authenticate before calling any other API methods.

Basic Workflows

User Authentication Flow

import { PortalSDK } from 'portal-sdk';

const client = new PortalSDK({
  serverUrl: 'ws://localhost:3000/ws'
});

await client.connect();
await client.authenticate(process.env.AUTH_TOKEN);

// Generate authentication URL
const authUrl = await client.newKeyHandshakeUrl(async (userPubkey, preferredRelays) => {
  console.log('User pubkey:', userPubkey);
  console.log('User relays:', preferredRelays);
  
  // Authenticate the user's key
  const authResponse = await client.authenticateKey(userPubkey);
  
  if (authResponse.status.status === 'approved') {
    console.log('User approved authentication!');
    // Create session, store user info, etc.
  } else {
    console.log('User declined authentication');
  }
});

console.log('Send this to user:', authUrl);

Request a Single Payment

import { PortalSDK, Currency } from 'portal-sdk';

const client = new PortalSDK({
  serverUrl: 'ws://localhost:3000/ws'
});

await client.connect();
await client.authenticate(process.env.AUTH_TOKEN);

const userPubkey = 'user-public-key-hex';

await client.requestSinglePayment(
  userPubkey,
  [], // subkeys (optional)
  {
    amount: 5000, // 5 sats (in millisats)
    currency: Currency.Millisats,
    description: 'Premium subscription'
  },
  (status) => {
    console.log('Payment status:', status.status);
    
    if (status.status === 'paid') {
      console.log('Payment received! Preimage:', status.preimage);
      // Grant access to premium features
    } else if (status.status === 'user_rejected') {
      console.log('User rejected payment');
    } else if (status.status === 'timeout') {
      console.log('Payment timed out');
    }
  }
);

Fetch User Profile

const userPubkey = 'user-public-key-hex';

const profile = await client.fetchProfile(userPubkey);

if (profile) {
  console.log('Name:', profile.name);
  console.log('Display name:', profile.display_name);
  console.log('Picture:', profile.picture);
  console.log('About:', profile.about);
  console.log('NIP-05:', profile.nip05);
} else {
  console.log('No profile found');
}

Working with Types

Timestamps

import { Timestamp } from 'portal-sdk';

// Create from specific date
const specificTime = Timestamp.fromDate(new Date('2024-12-31'));

// Create from now + seconds
const oneHourFromNow = Timestamp.fromNow(3600); // 1 hour
const oneDayFromNow = Timestamp.fromNow(86400); // 24 hours

// Use in payment requests
const paymentRequest = {
  amount: 1000,
  currency: Currency.Millisats,
  expires_at: Timestamp.fromNow(3600)
};

Currency

import { Currency } from 'portal-sdk';

// Currently only Millisats is supported
const amount = 1000; // 1 sat = 1000 millisats
const currency = Currency.Millisats;

Profiles

import { Profile } from 'portal-sdk';

const profile: Profile = {
  id: 'unique-id',
  pubkey: 'user-public-key',
  name: 'johndoe',
  display_name: 'John Doe',
  picture: 'https://example.com/avatar.jpg',
  about: 'Software developer',
  nip05: 'john@example.com'
};

// Update your service profile
await client.setProfile(profile);

Error Handling

Try-Catch Pattern

try {
  await client.connect();
  await client.authenticate('token');
  
  const url = await client.newKeyHandshakeUrl((key) => {
    console.log('User key:', key);
  });
  
} catch (error) {
  if (error.message.includes('timeout')) {
    console.error('Connection timed out');
  } else if (error.message.includes('Authentication failed')) {
    console.error('Invalid auth token');
  } else {
    console.error('Unknown error:', error);
  }
}

Graceful Degradation

async function connectWithRetry(maxAttempts = 3) {
  for (let i = 0; i < maxAttempts; i++) {
    try {
      await client.connect();
      return true;
    } catch (error) {
      console.log(`Attempt ${i + 1} failed, retrying...`);
      await new Promise(resolve => setTimeout(resolve, 1000));
    }
  }
  return false;
}

const connected = await connectWithRetry();
if (!connected) {
  console.error('Failed to connect after retries');
}

Best Practices

1. Reuse Client Instance

// ✅ Good - Single instance
const client = new PortalSDK({ serverUrl: 'ws://localhost:3000/ws' });
await client.connect();
await client.authenticate('token');

// Use client for multiple operations
const url1 = await client.newKeyHandshakeUrl(handler1);
const url2 = await client.newKeyHandshakeUrl(handler2);
// ❌ Bad - Multiple instances
const client1 = new PortalSDK({ serverUrl: 'ws://localhost:3000/ws' });
await client1.connect();

const client2 = new PortalSDK({ serverUrl: 'ws://localhost:3000/ws' });
await client2.connect();

2. Handle Cleanup

// Server shutdown
process.on('SIGTERM', () => {
  client.disconnect();
  process.exit(0);
});

// Unhandled errors
process.on('unhandledRejection', (error) => {
  console.error('Unhandled rejection:', error);
  client.disconnect();
  process.exit(1);
});

3. Use Environment Variables

// ✅ Good
const client = new PortalSDK({
  serverUrl: process.env.PORTAL_WS_URL || 'ws://localhost:3000/ws'
});
await client.authenticate(process.env.PORTAL_AUTH_TOKEN);
// ❌ Bad - Hardcoded secrets
const client = new PortalSDK({
  serverUrl: 'ws://localhost:3000/ws'
});
await client.authenticate('my-secret-token');

4. Validate User Input

async function authenticateUser(pubkey: string) {
  // Validate pubkey format
  if (!/^[0-9a-f]{64}$/i.test(pubkey)) {
    throw new Error('Invalid pubkey format');
  }
  
  return await client.authenticateKey(pubkey);
}

5. Log Important Events

const client = new PortalSDK({
  serverUrl: process.env.PORTAL_WS_URL
});

client.on({
  onConnected: () => {
    console.log('[Portal] Connected');
  },
  onDisconnected: () => {
    console.log('[Portal] Disconnected');
  },
  onError: (error) => {
    console.error('[Portal] Error:', error);
  }
});

Complete Example

Here's a complete example integrating authentication and payments:

import { PortalSDK, Currency, Timestamp } from 'portal-sdk';

class PortalService {
  private client: PortalSDK;

  constructor(wsUrl: string, authToken: string) {
    this.client = new PortalSDK({ serverUrl: wsUrl });
    this.init(authToken);
  }

  private async init(authToken: string) {
    await this.client.connect();
    await this.client.authenticate(authToken);
    
    this.client.on({
      onDisconnected: () => {
        console.log('Portal disconnected, attempting reconnect...');
        this.init(authToken);
      },
      onError: (error) => {
        console.error('Portal error:', error);
      }
    });
  }

  async createAuthUrl(onAuth: (pubkey: string) => void): Promise<string> {
    return await this.client.newKeyHandshakeUrl(async (pubkey) => {
      const authResult = await this.client.authenticateKey(pubkey);
      if (authResult.status.status === 'approved') {
        onAuth(pubkey);
      }
    });
  }

  async requestPayment(userPubkey: string, amount: number, description: string): Promise<boolean> {
    return new Promise((resolve) => {
      this.client.requestSinglePayment(
        userPubkey,
        [],
        {
          amount,
          currency: Currency.Millisats,
          description
        },
        (status) => {
          if (status.status === 'paid') {
            resolve(true);
          } else if (status.status === 'user_rejected' || status.status === 'timeout') {
            resolve(false);
          }
        }
      );
    });
  }

  async getUserProfile(pubkey: string) {
    return await this.client.fetchProfile(pubkey);
  }

  disconnect() {
    this.client.disconnect();
  }
}

// Usage
const portal = new PortalService(
  process.env.PORTAL_WS_URL!,
  process.env.PORTAL_AUTH_TOKEN!
);

const authUrl = await portal.createAuthUrl((pubkey) => {
  console.log('User authenticated:', pubkey);
});

console.log('Auth URL:', authUrl);

Next Steps: