[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/bourgeoa/JavaScriptSolidServer/patchAppend/src/webledger.js [Back]  [Original]

/**
 * Web Ledger  spec-compliant balance tracking
 *
 * Implements the Web Ledgers specification (https://webledgers.org/)
 * for mapping URIs to numerical balances. Default unit: satoshi.
 *
 * JSON-LD context: https://w3id.org/webledgers
 *
 * @module webledger
 */

import * as storage from './storage/filesystem.js';

const DEFAULT_PATH = '/.well-known/webledgers/webledgers.json';
const CONTEXT = 'https://w3id.org/webledgers';

/**
 * Create an empty spec-compliant ledger
 * @param {object} options
 * @param {string} options.name - Ledger name
 * @param {string} options.description - Ledger description
 * @param {string} options.id - Ledger URI identifier
 * @param {string} options.defaultCurrency - Default currency (default 'satoshi')
 * @returns {object} Empty WebLedger object
 */
export function createLedger(options = {}) {
  const now = Math.floor(Date.now() / 1000);
  return {
    '@context': CONTEXT,
    type: 'WebLedger',
    ...(options.id && { id: options.id }),
    name: options.name ?? 'Pod Credits',
    description: options.description ?? 'Paid API balance ledger',
    defaultCurrency: options.defaultCurrency ?? 'satoshi',
    created: now,
    updated: now,
    entries: []
  };
}

/**
 * Read a webledger from storage
 * @param {string} ledgerPath - URL path to ledger file
 * @returns {Promise} WebLedger object
 */
export async function readLedger(ledgerPath = DEFAULT_PATH) {
  const buf = await storage.read(ledgerPath);
  if (!buf) {
    return createLedger();
  }
  try {
    const ledger = JSON.parse(buf.toString('utf8'));
    // Migrate legacy format: add missing spec fields
    if (!ledger['@context']) ledger['@context'] = CONTEXT;
    if (!ledger.type) ledger.type = 'WebLedger';
    if (!ledger.defaultCurrency) ledger.defaultCurrency = 'satoshi';
    if (!ledger.created) ledger.created = Math.floor(Date.now() / 1000);
    if (!ledger.entries) ledger.entries = [];
    return ledger;
  } catch {
    return createLedger();
  }
}

/**
 * Write a webledger to storage (updates the `updated` timestamp)
 * @param {object} ledger - WebLedger object
 * @param {string} ledgerPath - URL path to ledger file
 * @returns {Promise}
 */
export async function writeLedger(ledger, ledgerPath = DEFAULT_PATH) {
  ledger.updated = Math.floor(Date.now() / 1000);
  return storage.write(ledgerPath, JSON.stringify(ledger, null, 2));
}

/**
 * Get balance for a URI
 * @param {object} ledger - WebLedger object
 * @param {string} uri - Agent URI (e.g. did:nostr:...)
 * @param {string} [currency] - Currency code (e.g. 'tbtc3', 'tbtc4'). If omitted, reads default/simple amount.
 * @returns {number} Balance as integer
 */
export function getBalance(ledger, uri, currency) {
  const entry = ledger.entries.find(e => e.url === uri);
  if (!entry) return 0;
  // Handle array amount format
  if (Array.isArray(entry.amount)) {
    const target = currency
      ? entry.amount.find(a => a.currency === currency)
      : entry.amount.find(a => a.currency === 'satoshi' || a.currency === 'sat');
    return target ? parseInt(target.value, 10) || 0 : 0;
  }
  // Simple string format  only if no specific currency requested, or currency matches default
  if (currency) return 0;
  return parseInt(entry.amount, 10) || 0;
}

/**
 * Set balance for a URI
 * @param {object} ledger - WebLedger object
 * @param {string} uri - Agent URI
 * @param {number} amount - New balance
 * @param {string} [currency] - Currency code. If provided, uses array amount format.
 */
export function setBalance(ledger, uri, amount, currency) {
  let entry = ledger.entries.find(e => e.url === uri);
  if (!currency) {
    // Simple string format (backward compatible)
    if (entry) {
      entry.amount = String(amount);
    } else {
      ledger.entries.push({ type: 'Entry', url: uri, amount: String(amount) });
    }
    return;
  }
  // Multi-currency: use array format
  if (!entry) {
    entry = { type: 'Entry', url: uri, amount: [] };
    ledger.entries.push(entry);
  }
  // Migrate simple string to array if needed
  if (!Array.isArray(entry.amount)) {
    const oldVal = parseInt(entry.amount, 10) || 0;
    entry.amount = oldVal > 0 ? [{ currency: 'satoshi', value: String(oldVal) }] : [];
  }
  const existing = entry.amount.find(a => a.currency === currency);
  if (existing) {
    existing.value = String(amount);
  } else {
    entry.amount.push({ currency, value: String(amount) });
  }
}

/**
 * Credit (add to) a balance
 * @param {object} ledger - WebLedger object
 * @param {string} uri - Agent URI
 * @param {number} amount - Amount to add
 * @param {string} [currency] - Currency code
 * @returns {number} New balance
 */
export function credit(ledger, uri, amount, currency) {
  const current = getBalance(ledger, uri, currency);
  const newBalance = current + amount;
  setBalance(ledger, uri, newBalance, currency);
  return newBalance;
}

/**
 * Debit (subtract from) a balance
 * @param {object} ledger - WebLedger object
 * @param {string} uri - Agent URI
 * @param {number} amount - Amount to subtract
 * @param {string} [currency] - Currency code
 * @returns {{success: boolean, balance: number}} Result
 */
export function debit(ledger, uri, amount, currency) {
  const current = getBalance(ledger, uri, currency);
  if (current < amount) {
    return { success: false, balance: current };
  }
  const newBalance = current - amount;
  setBalance(ledger, uri, newBalance, currency);
  return { success: true, balance: newBalance };
}

/**
 * List all entries with non-zero balances
 * @param {object} ledger - WebLedger object
 * @returns {Array}
 */
export function listBalances(ledger) {
  return ledger.entries
    .map(e => ({ url: e.url, amount: getBalance(ledger, e.url) }))
    .filter(e => e.amount > 0);
}

/**
 * Remove entries with zero balance
 * @param {object} ledger - WebLedger object
 */
export function compact(ledger) {
  ledger.entries = ledger.entries.filter(e => {
    const bal = getBalance(ledger, e.url);
    return bal > 0;
  });
}

// Re-export the default path for consumers
export { DEFAULT_PATH as LEDGER_PATH };

Web Proxy Viewer  |  New URL  |  Original Page