@lightninglabs/pi-402 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightninglabs/pi-402",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Pay for Pi inference with Lightning: an L402 + walletdk extension for the Pi coding agent",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/format.ts ADDED
@@ -0,0 +1,4 @@
1
+ // formatSat groups the digits of a sat amount for display ("58,000").
2
+ export function formatSat(sat: number): string {
3
+ return sat.toLocaleString("en-US");
4
+ }
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { join } from "node:path";
2
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import type { ExtensionAPI, ExtensionUIContext } from "@earendil-works/pi-coding-agent";
3
3
  import { registerReceiptCommands, registerWalletCommands } from "./commands.ts";
4
+ import { formatSat } from "./format.ts";
4
5
  import { MockInvoicePayer, WalletInvoicePayer } from "./payer.ts";
5
6
  import { buildBudgetPolicy } from "./policy.ts";
6
7
  import { makeStreamSimple } from "./provider.ts";
@@ -23,10 +24,35 @@ export default function (pi: ExtensionAPI): void {
23
24
  // The wallet holds the embedded walletdk instance for the whole session.
24
25
  const wallet = new WalletService();
25
26
 
27
+ // The footer status shows the wallet balance, and a one-line notice marks
28
+ // each payment, so an agent that spends on its own never does it silently.
29
+ // The UI handle arrives with session_start; it is a convenience only, so a
30
+ // failure here (no UI, a stale session, a balance read that errors) is
31
+ // swallowed and never reaches a payment.
32
+ let ui: ExtensionUIContext | undefined;
33
+ const showBalance = async (paidSat?: number): Promise<void> => {
34
+ const target = ui;
35
+ if (!target) {
36
+ return;
37
+ }
38
+
39
+ try {
40
+ const balance = await wallet.confirmedBalanceSat();
41
+ target.setStatus("pi-402", `⚡ ${formatSat(balance)} sat`);
42
+ if (paidSat !== undefined) {
43
+ target.notify(`pi-402 paid ${formatSat(paidSat)} sat (${formatSat(balance)} sat left)`, "info");
44
+ }
45
+ } catch {
46
+ // Leave the last status in place.
47
+ }
48
+ };
49
+
26
50
  // The mock payer exercises the buyer loop end to end against a mock proxy
27
51
  // with no Lightning at all; the real payer settles through walletdk.
28
52
  const mockMode = process.env.PI402_MOCK_PAYER === "1";
29
- const payer = mockMode ? new MockInvoicePayer() : new WalletInvoicePayer(wallet);
53
+ const payer = mockMode
54
+ ? new MockInvoicePayer()
55
+ : new WalletInvoicePayer(wallet, ({ amountPaidSat }) => showBalance(amountPaidSat));
30
56
 
31
57
  // Durable state: paid tokens and the receipt ledger live under the data
32
58
  // dir, so a paid bundle survives a restart instead of being re-purchased
@@ -44,8 +70,10 @@ export default function (pi: ExtensionAPI): void {
44
70
  // Bootstrap the wallet once per session. In mock mode there is no wallet
45
71
  // to load, so the session starts without touching the WASM at all.
46
72
  if (!mockMode) {
47
- pi.on("session_start", async () => {
73
+ pi.on("session_start", async (_event, ctx) => {
48
74
  await wallet.startAndUnlock();
75
+ ui = ctx.ui;
76
+ await showBalance();
49
77
  });
50
78
 
51
79
  // Release the wallet when Pi tears the extension down. Without this
package/src/payer.ts CHANGED
@@ -8,6 +8,9 @@ interface PayInvoiceInput {
8
8
  challengeId: string;
9
9
  }
10
10
 
11
+ // PaidHook is told about each payment the wallet settles.
12
+ export type PaidHook = (paid: { amountPaidSat: number }) => void | Promise<void>;
13
+
11
14
  // MockInvoicePayer returns a fixed preimage without paying anything. It exists
12
15
  // so the buyer loop can be exercised end to end with no wallet and no
13
16
  // Lightning at all.
@@ -23,12 +26,26 @@ export class MockInvoicePayer implements InvoicePayer {
23
26
  // is read off the settled activity entry once the swap completes.
24
27
  export class WalletInvoicePayer implements InvoicePayer {
25
28
  private readonly wallet: WalletService;
29
+ private readonly onPaid?: PaidHook;
26
30
 
27
- constructor(wallet: WalletService) {
31
+ // onPaid runs after each successful payment, for display only: it is not
32
+ // awaited and a failure in it is swallowed, so it can never delay a
33
+ // payment's preimage or turn a settled payment into an error.
34
+ constructor(wallet: WalletService, onPaid?: PaidHook) {
28
35
  this.wallet = wallet;
36
+ this.onPaid = onPaid;
29
37
  }
30
38
 
31
39
  async payInvoice(input: PayInvoiceInput): Promise<{ preimageHex: string; amountPaidSat: number }> {
32
- return this.wallet.payInvoice(input.invoice, input.amountSat);
40
+ const result = await this.wallet.payInvoice(input.invoice, input.amountSat);
41
+
42
+ if (this.onPaid) {
43
+ const hook = this.onPaid;
44
+ void Promise.resolve()
45
+ .then(() => hook({ amountPaidSat: result.amountPaidSat }))
46
+ .catch(() => {});
47
+ }
48
+
49
+ return result;
33
50
  }
34
51
  }