OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / unit / lightning-provider.test.js

191 lines · javascript ref:
import test from 'brittle'
import { MockProvider } from 'p2p-hiverelay/incentive/payment/mock-provider.js'
import { PaymentManager } from 'p2p-hiverelay/incentive/payment/index.js'

// ─── MockProvider tests ────────────────────────────────────────────

test('MockProvider - connect and disconnect', async (t) => {
  const provider = new MockProvider()
  t.is(provider.connected, false, 'starts disconnected')

  await provider.connect()
  t.is(provider.connected, true, 'connected')

  await provider.disconnect()
  t.is(provider.connected, false, 'disconnected')
})

test('MockProvider - pay records payment and deducts balance', async (t) => {
  const provider = new MockProvider({ balance: 10000 })
  await provider.connect()

  const result = await provider.pay('lnbc1000...', 500)
  t.is(result.amount, 500, 'payment amount correct')
  t.ok(result.preimage, 'has preimage')
  t.is(provider.balance, 9500, 'balance deducted')
  t.is(provider.payments.length, 1, 'payment recorded')
})

test('MockProvider - pay fails on insufficient balance', async (t) => {
  const provider = new MockProvider({ balance: 100 })
  await provider.connect()

  try {
    await provider.pay('lnbc...', 500)
    t.fail('should have thrown')
  } catch (err) {
    t.ok(err.message.includes('INSUFFICIENT_BALANCE'), 'correct error')
  }
})

test('MockProvider - pay fails when failNext is set', async (t) => {
  const provider = new MockProvider()
  await provider.connect()

  provider.failNext = true
  try {
    await provider.pay('lnbc...', 100)
    t.fail('should have thrown')
  } catch (err) {
    t.ok(err.message.includes('MOCK_PAYMENT_FAILED'), 'simulated failure')
  }

  // Next payment should succeed
  const result = await provider.pay('lnbc...', 100)
  t.is(result.amount, 100, 'second payment succeeded')
})

test('MockProvider - createInvoice returns bolt11', async (t) => {
  const provider = new MockProvider()
  await provider.connect()

  const invoice = await provider.createInvoice(1000, 'test memo')
  t.ok(invoice.bolt11.startsWith('lnbc'), 'bolt11 format')
  t.is(invoice.amount, 1000, 'correct amount')
  t.is(invoice.memo, 'test memo', 'correct memo')
  t.is(provider.invoices.length, 1, 'invoice recorded')
})

test('MockProvider - getBalance returns configured balance', async (t) => {
  const provider = new MockProvider({ balance: 50000 })
  await provider.connect()

  const balance = await provider.getBalance()
  t.is(balance.confirmed, 50000, 'correct balance')
  t.is(balance.unconfirmed, 0, 'no unconfirmed')
})

test('MockProvider - getInfo returns node info', async (t) => {
  const provider = new MockProvider()
  await provider.connect()

  const info = await provider.getInfo()
  t.ok(info.pubkey, 'has pubkey')
  t.is(info.alias, 'mock-node', 'correct alias')
  t.ok(info.channels, 'has channels')
})

test('MockProvider - throws when not connected', async (t) => {
  const provider = new MockProvider()

  try {
    await provider.pay('lnbc...', 100)
    t.fail('should throw')
  } catch (err) {
    t.ok(err.message.includes('not connected'), 'correct error')
  }
})

// ─── PaymentManager + MockProvider integration ─────────────────────

test('PaymentManager + MockProvider - full settlement flow', async (t) => {
  const provider = new MockProvider({ balance: 100_000 })
  await provider.connect()

  const pm = new PaymentManager({ paymentProvider: provider })

  // Register a relay that's been active for 10+ months (0% hold)
  const relay = 'a'.repeat(64)
  const account = pm.registerRelay(relay, 'lnbc_payment_addr')
  // Backdate registration to 10 months ago
  account.registeredAt = Date.now() - (10 * 30 * 24 * 3600 * 1000)

  // Record some earnings
  const earning = pm.recordEarnings(relay, 5000, 'bandwidth fees')
  t.is(earning.amountSats, 5000, 'earned 5000 sats')
  t.is(earning.heldAmount, 0, 'no hold after month 10')
  t.is(earning.payableAmount, 5000, 'all payable')

  // Settle
  const settlement = await pm.settle(relay)
  t.is(settlement.paid, 5000, 'paid 5000 sats')

  // Verify provider state
  t.is(provider.payments.length, 1, 'one payment made')
  t.is(provider.payments[0].amount, 5000, 'correct payment amount')
  t.is(provider.balance, 95000, 'provider balance reduced')

  // Settle again — nothing to pay
  const settlement2 = await pm.settle(relay)
  t.is(settlement2.paid, 0, 'nothing to settle')

  await provider.disconnect()
})

test('PaymentManager + MockProvider - settlement failure and retry', async (t) => {
  const provider = new MockProvider({ balance: 100_000 })
  await provider.connect()

  const pm = new PaymentManager({ paymentProvider: provider })
  const relay = 'b'.repeat(64)
  const account = pm.registerRelay(relay, 'lnbc_addr')
  account.registeredAt = Date.now() - (10 * 30 * 24 * 3600 * 1000)

  pm.recordEarnings(relay, 3000, 'relay fees')

  // Make first attempt fail
  provider.failNext = true
  try {
    await pm.settle(relay)
    t.fail('should throw')
  } catch (err) {
    t.ok(err.message.includes('MOCK_PAYMENT_FAILED'), 'settlement failed')
  }

  // Verify nothing was paid
  const summary = pm.getAccountSummary(relay)
  t.is(summary.totalPaid, 0, 'nothing paid yet')

  // Retry should succeed
  const result = await pm.settle(relay)
  t.is(result.paid, 3000, 'retry succeeded')
  t.is(provider.payments.length, 1, 'one successful payment')

  await provider.disconnect()
})

test('PaymentManager + MockProvider - ledger records match payments', async (t) => {
  const provider = new MockProvider({ balance: 100_000 })
  await provider.connect()

  const pm = new PaymentManager({ paymentProvider: provider })
  const relay = 'c'.repeat(64)
  const account = pm.registerRelay(relay, 'lnbc_addr')
  account.registeredAt = Date.now() - (10 * 30 * 24 * 3600 * 1000)

  pm.recordEarnings(relay, 2000, 'storage')
  pm.recordEarnings(relay, 3000, 'bandwidth')
  await pm.settle(relay)

  const ledger = account.ledger
  t.is(ledger.length, 3, '2 earnings + 1 settlement')
  t.is(ledger[0].type, 'earning', 'first is earning')
  t.is(ledger[0].amount, 2000, 'first earning amount')
  t.is(ledger[1].type, 'earning', 'second is earning')
  t.is(ledger[1].amount, 3000, 'second earning amount')
  t.is(ledger[2].type, 'settlement', 'third is settlement')
  t.is(ledger[2].amount, 5000, 'settlement = sum of earnings')

  await provider.disconnect()
})