Release v1.1.0 - Add WebLN API support for Lightning wallet integration
- Add window.webln API for web app Lightning wallet integration
- Implement webln.enable(), getInfo(), sendPayment(), makeInvoice() methods
- Add WebLN permission prompts with proper amount display for payments
- Dispatch webln:ready and webln:enabled events per WebLN standard
- Add NWC client caching for persistent wallet connections
- Implement inline BOLT11 invoice amount parsing
- Always prompt for sendPayment (security-critical, irreversible)
- Add signMessage/verifyMessage stubs that return "not supported"
- Fix response handling for undefined returns in content script
Files modified:
- projects/common/src/lib/models/nostr.ts (WeblnMethod, ExtensionMethod types)
- projects/common/src/lib/models/webln.ts (new - WebLN response types)
- projects/common/src/public-api.ts (export webln types)
- projects/{chrome,firefox}/src/plebian-signer-extension.ts (window.webln)
- projects/{chrome,firefox}/src/background.ts (WebLN handlers)
- projects/{chrome,firefox}/src/background-common.ts (WebLN permissions)
- projects/{chrome,firefox}/public/prompt.html (WebLN cards)
- projects/{chrome,firefox}/src/prompt.ts (WebLN method handling)
- projects/common/src/lib/services/storage/types.ts (ExtensionMethod type)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "plebeian-signer",
|
||||
"version": "v1.0.11",
|
||||
"version": "v1.1.0",
|
||||
"custom": {
|
||||
"chrome": {
|
||||
"version": "v1.0.11"
|
||||
"version": "v1.1.0"
|
||||
},
|
||||
"firefox": {
|
||||
"version": "v1.0.11"
|
||||
"version": "v1.1.0"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 3,
|
||||
"name": "Plebeian Signer - Nostr Identity Manager & Signer",
|
||||
"description": "Manage and switch between multiple identities while interacting with Nostr apps",
|
||||
"version": "1.0.11",
|
||||
"version": "1.1.0",
|
||||
"homepage_url": "https://github.com/PlebeianApp/plebeian-signer",
|
||||
"options_page": "options.html",
|
||||
"permissions": [
|
||||
|
||||
@@ -211,6 +211,53 @@
|
||||
<div id="card2Nip44Decrypt" class="card sam-mt sam-ml sam-mr">
|
||||
<div id="card2Nip44Decrypt_text" class="text"></div>
|
||||
</div>
|
||||
|
||||
<!-- Card for webln.enable -->
|
||||
<div id="cardWeblnEnable" class="card sam-mt sam-ml sam-mr">
|
||||
<p class="description">
|
||||
<b class="host-INSERT color-primary"></b> is requesting permission to
|
||||
<b class="color-primary">connect to your Lightning wallet</b>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Card for webln.getInfo -->
|
||||
<div id="cardWeblnGetInfo" class="card sam-mt sam-ml sam-mr">
|
||||
<p class="description">
|
||||
<b class="host-INSERT color-primary"></b> is requesting permission to
|
||||
<b class="color-primary">read your wallet info</b>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Card for webln.sendPayment -->
|
||||
<div id="cardWeblnSendPayment" class="card sam-mt sam-ml sam-mr">
|
||||
<p class="description">
|
||||
<b class="host-INSERT color-primary"></b> is requesting permission to
|
||||
<b class="color-primary">send a Lightning payment</b> of
|
||||
<b id="paymentAmountSpan" class="color-primary"></b>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Card2 for webln.sendPayment (shows invoice) -->
|
||||
<div id="card2WeblnSendPayment" class="card sam-mt sam-ml sam-mr">
|
||||
<div id="card2WeblnSendPayment_json" class="json"></div>
|
||||
</div>
|
||||
|
||||
<!-- Card for webln.makeInvoice -->
|
||||
<div id="cardWeblnMakeInvoice" class="card sam-mt sam-ml sam-mr">
|
||||
<p class="description">
|
||||
<b class="host-INSERT color-primary"></b> is requesting permission to
|
||||
<b class="color-primary">create a Lightning invoice</b>
|
||||
<span id="invoiceAmountSpan"></span>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Card for webln.keysend -->
|
||||
<div id="cardWeblnKeysend" class="card sam-mt sam-ml sam-mr">
|
||||
<p class="description">
|
||||
<b class="host-INSERT color-primary"></b> is requesting permission to
|
||||
<b class="color-primary">send a keysend payment</b>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!------------->
|
||||
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
CashuMint_DECRYPTED,
|
||||
CashuMint_ENCRYPTED,
|
||||
deriveKeyArgon2,
|
||||
ExtensionMethod,
|
||||
WeblnMethod,
|
||||
} from '@common';
|
||||
import { ChromeMetaHandler } from './app/common/data/chrome-meta-handler';
|
||||
import { Event, EventTemplate, finalizeEvent, nip04, nip44 } from 'nostr-tools';
|
||||
@@ -55,7 +57,7 @@ export interface PromptResponseMessage {
|
||||
}
|
||||
|
||||
export interface BackgroundRequestMessage {
|
||||
method: Nip07Method;
|
||||
method: ExtensionMethod;
|
||||
params: any;
|
||||
host: string;
|
||||
}
|
||||
@@ -218,11 +220,51 @@ export const checkPermissions = function (
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a method is a WebLN method
|
||||
*/
|
||||
export const isWeblnMethod = function (method: ExtensionMethod): method is WeblnMethod {
|
||||
return method.startsWith('webln.');
|
||||
};
|
||||
|
||||
/**
|
||||
* Check WebLN permissions for a host.
|
||||
* Note: WebLN permissions are NOT tied to identities since the wallet is global.
|
||||
* For sendPayment, always returns undefined (require user prompt for security).
|
||||
*/
|
||||
export const checkWeblnPermissions = function (
|
||||
browserSessionData: BrowserSessionData,
|
||||
host: string,
|
||||
method: WeblnMethod
|
||||
): boolean | undefined {
|
||||
// sendPayment ALWAYS requires user approval (security-critical, irreversible)
|
||||
if (method === 'webln.sendPayment') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// keysend also requires approval
|
||||
if (method === 'webln.keysend') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// For other WebLN methods, check stored permissions
|
||||
// WebLN permissions use 'webln' as the identityId
|
||||
const permissions = browserSessionData.permissions.filter(
|
||||
(x) => x.identityId === 'webln' && x.host === host && x.method === method
|
||||
);
|
||||
|
||||
if (permissions.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return permissions.every((x) => x.methodPolicy === 'allow');
|
||||
};
|
||||
|
||||
export const storePermission = async function (
|
||||
browserSessionData: BrowserSessionData,
|
||||
identity: Identity_DECRYPTED,
|
||||
identity: Identity_DECRYPTED | null,
|
||||
host: string,
|
||||
method: Nip07Method,
|
||||
method: ExtensionMethod,
|
||||
methodPolicy: Nip07MethodPolicy,
|
||||
kind?: number
|
||||
) {
|
||||
@@ -231,11 +273,14 @@ export const storePermission = async function (
|
||||
throw new Error(`Could not retrieve sync data`);
|
||||
}
|
||||
|
||||
// For WebLN methods, use 'webln' as identityId since wallet is global
|
||||
const identityId = identity?.id ?? 'webln';
|
||||
|
||||
const permission: Permission_DECRYPTED = {
|
||||
id: crypto.randomUUID(),
|
||||
identityId: identity.id,
|
||||
identityId,
|
||||
host,
|
||||
method,
|
||||
method: method as Nip07Method, // Cast for storage compatibility
|
||||
methodPolicy,
|
||||
kind,
|
||||
};
|
||||
|
||||
@@ -3,14 +3,23 @@ import {
|
||||
backgroundLogNip07Action,
|
||||
backgroundLogPermissionStored,
|
||||
NostrHelper,
|
||||
NwcClient,
|
||||
NwcConnection_DECRYPTED,
|
||||
WeblnMethod,
|
||||
Nip07Method,
|
||||
GetInfoResponse,
|
||||
SendPaymentResponse,
|
||||
RequestInvoiceResponse,
|
||||
} from '@common';
|
||||
import {
|
||||
BackgroundRequestMessage,
|
||||
checkPermissions,
|
||||
checkWeblnPermissions,
|
||||
debug,
|
||||
getBrowserSessionData,
|
||||
getPosition,
|
||||
handleUnlockRequest,
|
||||
isWeblnMethod,
|
||||
nip04Decrypt,
|
||||
nip04Encrypt,
|
||||
nip44Decrypt,
|
||||
@@ -27,6 +36,73 @@ import {
|
||||
import browser from 'webextension-polyfill';
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
// Cache for NWC clients to avoid reconnecting for each request
|
||||
const nwcClientCache = new Map<string, NwcClient>();
|
||||
|
||||
/**
|
||||
* Get or create an NWC client for a connection
|
||||
*/
|
||||
async function getNwcClient(connection: NwcConnection_DECRYPTED): Promise<NwcClient> {
|
||||
const cached = nwcClientCache.get(connection.id);
|
||||
if (cached && cached.isConnected()) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const client = new NwcClient({
|
||||
walletPubkey: connection.walletPubkey,
|
||||
relayUrl: connection.relayUrl,
|
||||
secret: connection.secret,
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
nwcClientCache.set(connection.id, client);
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse invoice amount from a BOLT11 invoice string
|
||||
* Returns amount in satoshis, or undefined if no amount specified
|
||||
*/
|
||||
function parseInvoiceAmount(invoice: string): number | undefined {
|
||||
try {
|
||||
// BOLT11 invoices start with 'ln' followed by network prefix and amount
|
||||
// Format: ln[network][amount][multiplier]1[data]
|
||||
// Examples: lnbc1500n1... (1500 sat), lnbc1m1... (0.001 BTC = 100000 sat)
|
||||
const match = invoice.toLowerCase().match(/^ln(bc|tb|tbs|bcrt)(\d+)([munp])?1/);
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const amountStr = match[2];
|
||||
const multiplier = match[3];
|
||||
|
||||
let amount = parseInt(amountStr, 10);
|
||||
|
||||
// Apply multiplier (amount is in BTC by default)
|
||||
switch (multiplier) {
|
||||
case 'm': // milli-bitcoin (0.001 BTC)
|
||||
amount = amount * 100000;
|
||||
break;
|
||||
case 'u': // micro-bitcoin (0.000001 BTC)
|
||||
amount = amount * 100;
|
||||
break;
|
||||
case 'n': // nano-bitcoin (0.000000001 BTC) = 0.1 sat
|
||||
amount = Math.floor(amount / 10);
|
||||
break;
|
||||
case 'p': // pico-bitcoin (0.000000000001 BTC) = 0.0001 sat
|
||||
amount = Math.floor(amount / 10000);
|
||||
break;
|
||||
default:
|
||||
// No multiplier means BTC
|
||||
amount = amount * 100000000;
|
||||
}
|
||||
|
||||
return amount;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
type Relays = Record<string, { read: boolean; write: boolean }>;
|
||||
|
||||
const openPrompts = new Map<
|
||||
@@ -116,8 +192,12 @@ browser.runtime.onMessage.addListener(async (message /*, sender*/) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Process the NIP-07 request
|
||||
return processNip07Request(request as BackgroundRequestMessage);
|
||||
// Process the request (NIP-07 or WebLN)
|
||||
const req = request as BackgroundRequestMessage;
|
||||
if (isWeblnMethod(req.method)) {
|
||||
return processWeblnRequest(req);
|
||||
}
|
||||
return processNip07Request(req);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -149,7 +229,7 @@ async function processNip07Request(req: BackgroundRequestMessage): Promise<any>
|
||||
browserSessionData,
|
||||
currentIdentity,
|
||||
req.host,
|
||||
req.method,
|
||||
req.method as Nip07Method,
|
||||
req.params
|
||||
);
|
||||
debug(`permissionState result: ${permissionState}`);
|
||||
@@ -282,3 +362,146 @@ async function processNip07Request(req: BackgroundRequestMessage): Promise<any>
|
||||
throw new Error(`Not supported request method '${req.method}'.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a WebLN request after vault is unlocked
|
||||
*/
|
||||
async function processWeblnRequest(req: BackgroundRequestMessage): Promise<any> {
|
||||
const browserSessionData = await getBrowserSessionData();
|
||||
|
||||
if (!browserSessionData) {
|
||||
throw new Error('Plebeian Signer vault not unlocked by the user.');
|
||||
}
|
||||
|
||||
const nwcConnections = browserSessionData.nwcConnections ?? [];
|
||||
const method = req.method as WeblnMethod;
|
||||
|
||||
// webln.enable just checks if NWC is configured
|
||||
if (method === 'webln.enable') {
|
||||
if (nwcConnections.length === 0) {
|
||||
throw new Error('No wallet configured. Please add an NWC connection in Plebeian Signer settings.');
|
||||
}
|
||||
debug('WebLN enabled');
|
||||
return { enabled: true }; // Return explicit value (undefined gets filtered by content script)
|
||||
}
|
||||
|
||||
// All other methods require an NWC connection
|
||||
const defaultConnection = nwcConnections[0];
|
||||
if (!defaultConnection) {
|
||||
throw new Error('No wallet configured. Please add an NWC connection in Plebeian Signer settings.');
|
||||
}
|
||||
|
||||
// Check reckless mode (but still prompt for payments)
|
||||
const recklessApprove = await shouldRecklessModeApprove(req.host);
|
||||
|
||||
// Check WebLN permissions
|
||||
const permissionState = recklessApprove && method !== 'webln.sendPayment' && method !== 'webln.keysend'
|
||||
? true
|
||||
: checkWeblnPermissions(browserSessionData, req.host, method);
|
||||
|
||||
if (permissionState === false) {
|
||||
throw new Error('Permission denied');
|
||||
}
|
||||
|
||||
if (permissionState === undefined) {
|
||||
// Ask user for permission
|
||||
const width = 375;
|
||||
const height = 600;
|
||||
const { top, left } = await getPosition(width, height);
|
||||
|
||||
// For sendPayment, include the invoice amount in the prompt data
|
||||
let promptParams = req.params ?? {};
|
||||
if (method === 'webln.sendPayment' && req.params?.paymentRequest) {
|
||||
const amountSats = parseInvoiceAmount(req.params.paymentRequest);
|
||||
promptParams = { ...promptParams, amountSats };
|
||||
}
|
||||
|
||||
const base64Event = Buffer.from(
|
||||
JSON.stringify(promptParams, undefined, 2)
|
||||
).toString('base64');
|
||||
|
||||
const response = await new Promise<PromptResponse>((resolve, reject) => {
|
||||
const id = crypto.randomUUID();
|
||||
openPrompts.set(id, { resolve, reject });
|
||||
browser.windows.create({
|
||||
type: 'popup',
|
||||
url: `prompt.html?method=${method}&host=${req.host}&id=${id}&nick=WebLN&event=${base64Event}`,
|
||||
height,
|
||||
width,
|
||||
top,
|
||||
left,
|
||||
});
|
||||
});
|
||||
|
||||
debug(response);
|
||||
|
||||
// Store permission for non-payment methods
|
||||
if ((response === 'approve' || response === 'reject') && method !== 'webln.sendPayment' && method !== 'webln.keysend') {
|
||||
const policy = response === 'approve' ? 'allow' : 'deny';
|
||||
await storePermission(
|
||||
browserSessionData,
|
||||
null, // WebLN has no identity
|
||||
req.host,
|
||||
method,
|
||||
policy
|
||||
);
|
||||
await backgroundLogPermissionStored(req.host, method, policy);
|
||||
}
|
||||
|
||||
if (['reject', 'reject-once'].includes(response)) {
|
||||
throw new Error('Permission denied');
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the WebLN method
|
||||
let result: any;
|
||||
const client = await getNwcClient(defaultConnection);
|
||||
|
||||
switch (method) {
|
||||
case 'webln.getInfo': {
|
||||
const info = await client.getInfo();
|
||||
result = {
|
||||
node: {
|
||||
alias: info.alias,
|
||||
pubkey: info.pubkey,
|
||||
color: info.color,
|
||||
},
|
||||
} as GetInfoResponse;
|
||||
debug('webln.getInfo result:');
|
||||
debug(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
case 'webln.sendPayment': {
|
||||
const invoice = req.params.paymentRequest;
|
||||
const payResult = await client.payInvoice({ invoice });
|
||||
result = { preimage: payResult.preimage } as SendPaymentResponse;
|
||||
debug('webln.sendPayment result:');
|
||||
debug(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
case 'webln.makeInvoice': {
|
||||
// Convert sats to millisats (NWC uses millisats)
|
||||
const amountSats = typeof req.params.amount === 'string'
|
||||
? parseInt(req.params.amount, 10)
|
||||
: req.params.amount ?? req.params.defaultAmount ?? 0;
|
||||
const amountMsat = amountSats * 1000;
|
||||
|
||||
const invoiceResult = await client.makeInvoice({
|
||||
amount: amountMsat,
|
||||
description: req.params.defaultMemo,
|
||||
});
|
||||
result = { paymentRequest: invoiceResult.invoice } as RequestInvoiceResponse;
|
||||
debug('webln.makeInvoice result:');
|
||||
debug(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
case 'webln.keysend':
|
||||
throw new Error('keysend is not yet supported');
|
||||
|
||||
default:
|
||||
throw new Error(`Not supported WebLN method '${method}'.`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Event, EventTemplate } from 'nostr-tools';
|
||||
import { Nip07Method } from '@common';
|
||||
import { Event as NostrEvent, EventTemplate } from 'nostr-tools';
|
||||
import { ExtensionMethod } from '@common';
|
||||
|
||||
// Extend Window interface for NIP-07
|
||||
// Extend Window interface for NIP-07 and WebLN
|
||||
declare global {
|
||||
interface Window {
|
||||
nostr?: any;
|
||||
webln?: any;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +39,7 @@ class Messenger {
|
||||
window.addEventListener('message', this.#handleCallResponse.bind(this));
|
||||
}
|
||||
|
||||
async request(method: Nip07Method, params: any): Promise<any> {
|
||||
async request(method: ExtensionMethod, params: any): Promise<any> {
|
||||
const id = generateUUID();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -89,7 +90,7 @@ const nostr = {
|
||||
return pubkey;
|
||||
},
|
||||
|
||||
async signEvent(event: EventTemplate): Promise<Event> {
|
||||
async signEvent(event: EventTemplate): Promise<NostrEvent> {
|
||||
debug('signEvent received');
|
||||
const signedEvent = await this.messenger.request('signEvent', event);
|
||||
debug('signEvent response:');
|
||||
@@ -158,6 +159,92 @@ const nostr = {
|
||||
|
||||
window.nostr = nostr as any;
|
||||
|
||||
// WebLN types (inline to avoid build issues with @common types in injected script)
|
||||
interface RequestInvoiceArgs {
|
||||
amount?: string | number;
|
||||
defaultAmount?: string | number;
|
||||
minimumAmount?: string | number;
|
||||
maximumAmount?: string | number;
|
||||
defaultMemo?: string;
|
||||
}
|
||||
|
||||
interface KeysendArgs {
|
||||
destination: string;
|
||||
amount: string | number;
|
||||
customRecords?: Record<string, string>;
|
||||
}
|
||||
|
||||
// Create a shared messenger instance for WebLN
|
||||
const weblnMessenger = nostr.messenger;
|
||||
|
||||
const webln = {
|
||||
enabled: false,
|
||||
|
||||
async enable(): Promise<void> {
|
||||
debug('webln.enable received');
|
||||
await weblnMessenger.request('webln.enable', {});
|
||||
this.enabled = true;
|
||||
debug('webln.enable completed');
|
||||
// Dispatch webln:enabled event as per WebLN spec
|
||||
window.dispatchEvent(new Event('webln:enabled'));
|
||||
},
|
||||
|
||||
async getInfo(): Promise<{ node: { alias?: string; pubkey?: string; color?: string } }> {
|
||||
debug('webln.getInfo received');
|
||||
const info = await weblnMessenger.request('webln.getInfo', {});
|
||||
debug('webln.getInfo response:');
|
||||
debug(info);
|
||||
return info;
|
||||
},
|
||||
|
||||
async sendPayment(paymentRequest: string): Promise<{ preimage: string }> {
|
||||
debug('webln.sendPayment received');
|
||||
const result = await weblnMessenger.request('webln.sendPayment', { paymentRequest });
|
||||
debug('webln.sendPayment response:');
|
||||
debug(result);
|
||||
return result;
|
||||
},
|
||||
|
||||
async keysend(args: KeysendArgs): Promise<{ preimage: string }> {
|
||||
debug('webln.keysend received');
|
||||
const result = await weblnMessenger.request('webln.keysend', args);
|
||||
debug('webln.keysend response:');
|
||||
debug(result);
|
||||
return result;
|
||||
},
|
||||
|
||||
async makeInvoice(
|
||||
args: string | number | RequestInvoiceArgs
|
||||
): Promise<{ paymentRequest: string }> {
|
||||
debug('webln.makeInvoice received');
|
||||
// Normalize args to RequestInvoiceArgs
|
||||
let normalizedArgs: RequestInvoiceArgs;
|
||||
if (typeof args === 'string' || typeof args === 'number') {
|
||||
normalizedArgs = { amount: args };
|
||||
} else {
|
||||
normalizedArgs = args;
|
||||
}
|
||||
const result = await weblnMessenger.request('webln.makeInvoice', normalizedArgs);
|
||||
debug('webln.makeInvoice response:');
|
||||
debug(result);
|
||||
return result;
|
||||
},
|
||||
|
||||
signMessage(): Promise<{ message: string; signature: string }> {
|
||||
throw new Error('signMessage is not supported - NWC does not provide node signing capabilities');
|
||||
},
|
||||
|
||||
verifyMessage(): Promise<void> {
|
||||
throw new Error('verifyMessage is not supported - NWC does not provide message verification');
|
||||
},
|
||||
};
|
||||
|
||||
window.webln = webln as any;
|
||||
|
||||
// Dispatch webln:ready event to signal that webln is available
|
||||
// This is dispatched on document as per the WebLN standard
|
||||
document.dispatchEvent(new Event('webln:ready'));
|
||||
|
||||
const debug = function (value: any) {
|
||||
console.log(JSON.stringify(value));
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import browser from 'webextension-polyfill';
|
||||
import { Nip07Method } from '@common';
|
||||
import { ExtensionMethod } from '@common';
|
||||
import { PromptResponse, PromptResponseMessage } from './background-common';
|
||||
|
||||
/**
|
||||
@@ -14,7 +14,7 @@ function base64ToUtf8(base64: string): string {
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
const id = params.get('id') as string;
|
||||
const method = params.get('method') as Nip07Method;
|
||||
const method = params.get('method') as ExtensionMethod;
|
||||
const host = params.get('host') as string;
|
||||
const nick = params.get('nick') as string;
|
||||
|
||||
@@ -58,6 +58,26 @@ switch (method) {
|
||||
title = 'Get Relays';
|
||||
break;
|
||||
|
||||
case 'webln.enable':
|
||||
title = 'Enable WebLN';
|
||||
break;
|
||||
|
||||
case 'webln.getInfo':
|
||||
title = 'Wallet Info';
|
||||
break;
|
||||
|
||||
case 'webln.sendPayment':
|
||||
title = 'Send Payment';
|
||||
break;
|
||||
|
||||
case 'webln.makeInvoice':
|
||||
title = 'Create Invoice';
|
||||
break;
|
||||
|
||||
case 'webln.keysend':
|
||||
title = 'Keysend Payment';
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -185,6 +205,65 @@ if (cardNip44DecryptElement && card2Nip44DecryptElement) {
|
||||
}
|
||||
}
|
||||
|
||||
// WebLN card visibility
|
||||
const cardWeblnEnableElement = document.getElementById('cardWeblnEnable');
|
||||
if (cardWeblnEnableElement) {
|
||||
if (method !== 'webln.enable') {
|
||||
cardWeblnEnableElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
const cardWeblnGetInfoElement = document.getElementById('cardWeblnGetInfo');
|
||||
if (cardWeblnGetInfoElement) {
|
||||
if (method !== 'webln.getInfo') {
|
||||
cardWeblnGetInfoElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
const cardWeblnSendPaymentElement = document.getElementById('cardWeblnSendPayment');
|
||||
const card2WeblnSendPaymentElement = document.getElementById('card2WeblnSendPayment');
|
||||
if (cardWeblnSendPaymentElement && card2WeblnSendPaymentElement) {
|
||||
if (method === 'webln.sendPayment') {
|
||||
// Display amount in sats
|
||||
const paymentAmountSpan = document.getElementById('paymentAmountSpan');
|
||||
if (paymentAmountSpan && eventParsed.amountSats !== undefined) {
|
||||
paymentAmountSpan.innerText = `${eventParsed.amountSats.toLocaleString()} sats`;
|
||||
} else if (paymentAmountSpan) {
|
||||
paymentAmountSpan.innerText = 'unknown amount';
|
||||
}
|
||||
// Show invoice in json card
|
||||
const card2WeblnSendPayment_jsonElement = document.getElementById('card2WeblnSendPayment_json');
|
||||
if (card2WeblnSendPayment_jsonElement && eventParsed.paymentRequest) {
|
||||
card2WeblnSendPayment_jsonElement.innerText = eventParsed.paymentRequest;
|
||||
}
|
||||
} else {
|
||||
cardWeblnSendPaymentElement.style.display = 'none';
|
||||
card2WeblnSendPaymentElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
const cardWeblnMakeInvoiceElement = document.getElementById('cardWeblnMakeInvoice');
|
||||
if (cardWeblnMakeInvoiceElement) {
|
||||
if (method === 'webln.makeInvoice') {
|
||||
const invoiceAmountSpan = document.getElementById('invoiceAmountSpan');
|
||||
if (invoiceAmountSpan) {
|
||||
const amount = eventParsed.amount ?? eventParsed.defaultAmount;
|
||||
if (amount) {
|
||||
invoiceAmountSpan.innerText = ` for ${Number(amount).toLocaleString()} sats`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cardWeblnMakeInvoiceElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
const cardWeblnKeysendElement = document.getElementById('cardWeblnKeysend');
|
||||
if (cardWeblnKeysendElement) {
|
||||
if (method !== 'webln.keysend') {
|
||||
cardWeblnKeysendElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Functions
|
||||
//
|
||||
|
||||
@@ -8,3 +8,12 @@ export type Nip07Method =
|
||||
| 'nip44.decrypt';
|
||||
|
||||
export type Nip07MethodPolicy = 'allow' | 'deny';
|
||||
|
||||
export type WeblnMethod =
|
||||
| 'webln.enable'
|
||||
| 'webln.getInfo'
|
||||
| 'webln.sendPayment'
|
||||
| 'webln.makeInvoice'
|
||||
| 'webln.keysend';
|
||||
|
||||
export type ExtensionMethod = Nip07Method | WeblnMethod;
|
||||
|
||||
41
projects/common/src/lib/models/webln.ts
Normal file
41
projects/common/src/lib/models/webln.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* WebLN API Types
|
||||
* Based on the WebLN specification: https://webln.dev/
|
||||
*/
|
||||
|
||||
export interface WebLNNode {
|
||||
alias?: string;
|
||||
pubkey?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export interface GetInfoResponse {
|
||||
node: WebLNNode;
|
||||
}
|
||||
|
||||
export interface SendPaymentResponse {
|
||||
preimage: string;
|
||||
}
|
||||
|
||||
export interface RequestInvoiceArgs {
|
||||
amount?: string | number;
|
||||
defaultAmount?: string | number;
|
||||
minimumAmount?: string | number;
|
||||
maximumAmount?: string | number;
|
||||
defaultMemo?: string;
|
||||
}
|
||||
|
||||
export interface RequestInvoiceResponse {
|
||||
paymentRequest: string;
|
||||
}
|
||||
|
||||
export interface KeysendArgs {
|
||||
destination: string;
|
||||
amount: string | number;
|
||||
customRecords?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface SignMessageResponse {
|
||||
message: string;
|
||||
signature: string;
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Nip07Method, Nip07MethodPolicy } from '@common';
|
||||
import { ExtensionMethod, Nip07MethodPolicy } from '@common';
|
||||
|
||||
export interface Permission_DECRYPTED {
|
||||
id: string;
|
||||
identityId: string;
|
||||
host: string;
|
||||
method: Nip07Method;
|
||||
method: ExtensionMethod;
|
||||
methodPolicy: Nip07MethodPolicy;
|
||||
kind?: number;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export * from './lib/helpers/nip05-validator';
|
||||
|
||||
// Models
|
||||
export * from './lib/models/nostr';
|
||||
export * from './lib/models/webln';
|
||||
|
||||
// Services (and related)
|
||||
export * from './lib/services/storage/storage.service';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 3,
|
||||
"name": "Plebeian Signer",
|
||||
"description": "Nostr Identity Manager & Signer",
|
||||
"version": "1.0.11",
|
||||
"version": "1.1.0",
|
||||
"homepage_url": "https://github.com/PlebeianApp/plebeian-signer",
|
||||
"options_page": "options.html",
|
||||
"permissions": [
|
||||
|
||||
@@ -211,6 +211,53 @@
|
||||
<div id="card2Nip44Decrypt" class="card sam-mt sam-ml sam-mr">
|
||||
<div id="card2Nip44Decrypt_text" class="text"></div>
|
||||
</div>
|
||||
|
||||
<!-- Card for webln.enable -->
|
||||
<div id="cardWeblnEnable" class="card sam-mt sam-ml sam-mr">
|
||||
<p class="description">
|
||||
<b class="host-INSERT color-primary"></b> is requesting permission to
|
||||
<b class="color-primary">connect to your Lightning wallet</b>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Card for webln.getInfo -->
|
||||
<div id="cardWeblnGetInfo" class="card sam-mt sam-ml sam-mr">
|
||||
<p class="description">
|
||||
<b class="host-INSERT color-primary"></b> is requesting permission to
|
||||
<b class="color-primary">read your wallet info</b>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Card for webln.sendPayment -->
|
||||
<div id="cardWeblnSendPayment" class="card sam-mt sam-ml sam-mr">
|
||||
<p class="description">
|
||||
<b class="host-INSERT color-primary"></b> is requesting permission to
|
||||
<b class="color-primary">send a Lightning payment</b> of
|
||||
<b id="paymentAmountSpan" class="color-primary"></b>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Card2 for webln.sendPayment (shows invoice) -->
|
||||
<div id="card2WeblnSendPayment" class="card sam-mt sam-ml sam-mr">
|
||||
<div id="card2WeblnSendPayment_json" class="json"></div>
|
||||
</div>
|
||||
|
||||
<!-- Card for webln.makeInvoice -->
|
||||
<div id="cardWeblnMakeInvoice" class="card sam-mt sam-ml sam-mr">
|
||||
<p class="description">
|
||||
<b class="host-INSERT color-primary"></b> is requesting permission to
|
||||
<b class="color-primary">create a Lightning invoice</b>
|
||||
<span id="invoiceAmountSpan"></span>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Card for webln.keysend -->
|
||||
<div id="cardWeblnKeysend" class="card sam-mt sam-ml sam-mr">
|
||||
<p class="description">
|
||||
<b class="host-INSERT color-primary"></b> is requesting permission to
|
||||
<b class="color-primary">send a keysend payment</b>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!------------->
|
||||
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
CashuMint_DECRYPTED,
|
||||
CashuMint_ENCRYPTED,
|
||||
deriveKeyArgon2,
|
||||
ExtensionMethod,
|
||||
WeblnMethod,
|
||||
} from '@common';
|
||||
import { FirefoxMetaHandler } from './app/common/data/firefox-meta-handler';
|
||||
import { Event, EventTemplate, finalizeEvent, nip04, nip44 } from 'nostr-tools';
|
||||
@@ -56,7 +58,7 @@ export interface PromptResponseMessage {
|
||||
}
|
||||
|
||||
export interface BackgroundRequestMessage {
|
||||
method: Nip07Method;
|
||||
method: ExtensionMethod;
|
||||
params: any;
|
||||
host: string;
|
||||
}
|
||||
@@ -219,11 +221,51 @@ export const checkPermissions = function (
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a method is a WebLN method
|
||||
*/
|
||||
export const isWeblnMethod = function (method: ExtensionMethod): method is WeblnMethod {
|
||||
return method.startsWith('webln.');
|
||||
};
|
||||
|
||||
/**
|
||||
* Check WebLN permissions for a host.
|
||||
* Note: WebLN permissions are NOT tied to identities since the wallet is global.
|
||||
* For sendPayment, always returns undefined (require user prompt for security).
|
||||
*/
|
||||
export const checkWeblnPermissions = function (
|
||||
browserSessionData: BrowserSessionData,
|
||||
host: string,
|
||||
method: WeblnMethod
|
||||
): boolean | undefined {
|
||||
// sendPayment ALWAYS requires user approval (security-critical, irreversible)
|
||||
if (method === 'webln.sendPayment') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// keysend also requires approval
|
||||
if (method === 'webln.keysend') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// For other WebLN methods, check stored permissions
|
||||
// WebLN permissions use 'webln' as the identityId
|
||||
const permissions = browserSessionData.permissions.filter(
|
||||
(x) => x.identityId === 'webln' && x.host === host && x.method === method
|
||||
);
|
||||
|
||||
if (permissions.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return permissions.every((x) => x.methodPolicy === 'allow');
|
||||
};
|
||||
|
||||
export const storePermission = async function (
|
||||
browserSessionData: BrowserSessionData,
|
||||
identity: Identity_DECRYPTED,
|
||||
identity: Identity_DECRYPTED | null,
|
||||
host: string,
|
||||
method: Nip07Method,
|
||||
method: ExtensionMethod,
|
||||
methodPolicy: Nip07MethodPolicy,
|
||||
kind?: number
|
||||
) {
|
||||
@@ -232,11 +274,14 @@ export const storePermission = async function (
|
||||
throw new Error(`Could not retrieve sync data`);
|
||||
}
|
||||
|
||||
// For WebLN methods, use 'webln' as identityId since wallet is global
|
||||
const identityId = identity?.id ?? 'webln';
|
||||
|
||||
const permission: Permission_DECRYPTED = {
|
||||
id: crypto.randomUUID(),
|
||||
identityId: identity.id,
|
||||
identityId,
|
||||
host,
|
||||
method,
|
||||
method: method as Nip07Method, // Cast for storage compatibility
|
||||
methodPolicy,
|
||||
kind,
|
||||
};
|
||||
|
||||
@@ -3,14 +3,23 @@ import {
|
||||
backgroundLogNip07Action,
|
||||
backgroundLogPermissionStored,
|
||||
NostrHelper,
|
||||
NwcClient,
|
||||
NwcConnection_DECRYPTED,
|
||||
WeblnMethod,
|
||||
Nip07Method,
|
||||
GetInfoResponse,
|
||||
SendPaymentResponse,
|
||||
RequestInvoiceResponse,
|
||||
} from '@common';
|
||||
import {
|
||||
BackgroundRequestMessage,
|
||||
checkPermissions,
|
||||
checkWeblnPermissions,
|
||||
debug,
|
||||
getBrowserSessionData,
|
||||
getPosition,
|
||||
handleUnlockRequest,
|
||||
isWeblnMethod,
|
||||
nip04Decrypt,
|
||||
nip04Encrypt,
|
||||
nip44Decrypt,
|
||||
@@ -27,6 +36,73 @@ import {
|
||||
import browser from 'webextension-polyfill';
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
// Cache for NWC clients to avoid reconnecting for each request
|
||||
const nwcClientCache = new Map<string, NwcClient>();
|
||||
|
||||
/**
|
||||
* Get or create an NWC client for a connection
|
||||
*/
|
||||
async function getNwcClient(connection: NwcConnection_DECRYPTED): Promise<NwcClient> {
|
||||
const cached = nwcClientCache.get(connection.id);
|
||||
if (cached && cached.isConnected()) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const client = new NwcClient({
|
||||
walletPubkey: connection.walletPubkey,
|
||||
relayUrl: connection.relayUrl,
|
||||
secret: connection.secret,
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
nwcClientCache.set(connection.id, client);
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse invoice amount from a BOLT11 invoice string
|
||||
* Returns amount in satoshis, or undefined if no amount specified
|
||||
*/
|
||||
function parseInvoiceAmount(invoice: string): number | undefined {
|
||||
try {
|
||||
// BOLT11 invoices start with 'ln' followed by network prefix and amount
|
||||
// Format: ln[network][amount][multiplier]1[data]
|
||||
// Examples: lnbc1500n1... (1500 sat), lnbc1m1... (0.001 BTC = 100000 sat)
|
||||
const match = invoice.toLowerCase().match(/^ln(bc|tb|tbs|bcrt)(\d+)([munp])?1/);
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const amountStr = match[2];
|
||||
const multiplier = match[3];
|
||||
|
||||
let amount = parseInt(amountStr, 10);
|
||||
|
||||
// Apply multiplier (amount is in BTC by default)
|
||||
switch (multiplier) {
|
||||
case 'm': // milli-bitcoin (0.001 BTC)
|
||||
amount = amount * 100000;
|
||||
break;
|
||||
case 'u': // micro-bitcoin (0.000001 BTC)
|
||||
amount = amount * 100;
|
||||
break;
|
||||
case 'n': // nano-bitcoin (0.000000001 BTC) = 0.1 sat
|
||||
amount = Math.floor(amount / 10);
|
||||
break;
|
||||
case 'p': // pico-bitcoin (0.000000000001 BTC) = 0.0001 sat
|
||||
amount = Math.floor(amount / 10000);
|
||||
break;
|
||||
default:
|
||||
// No multiplier means BTC
|
||||
amount = amount * 100000000;
|
||||
}
|
||||
|
||||
return amount;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
type Relays = Record<string, { read: boolean; write: boolean }>;
|
||||
|
||||
const openPrompts = new Map<
|
||||
@@ -116,8 +192,12 @@ browser.runtime.onMessage.addListener(async (message /*, sender*/) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Process the NIP-07 request
|
||||
return processNip07Request(request as BackgroundRequestMessage);
|
||||
// Process the request (NIP-07 or WebLN)
|
||||
const req = request as BackgroundRequestMessage;
|
||||
if (isWeblnMethod(req.method)) {
|
||||
return processWeblnRequest(req);
|
||||
}
|
||||
return processNip07Request(req);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -149,7 +229,7 @@ async function processNip07Request(req: BackgroundRequestMessage): Promise<any>
|
||||
browserSessionData,
|
||||
currentIdentity,
|
||||
req.host,
|
||||
req.method,
|
||||
req.method as Nip07Method,
|
||||
req.params
|
||||
);
|
||||
debug(`permissionState result: ${permissionState}`);
|
||||
@@ -282,3 +362,146 @@ async function processNip07Request(req: BackgroundRequestMessage): Promise<any>
|
||||
throw new Error(`Not supported request method '${req.method}'.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a WebLN request after vault is unlocked
|
||||
*/
|
||||
async function processWeblnRequest(req: BackgroundRequestMessage): Promise<any> {
|
||||
const browserSessionData = await getBrowserSessionData();
|
||||
|
||||
if (!browserSessionData) {
|
||||
throw new Error('Plebeian Signer vault not unlocked by the user.');
|
||||
}
|
||||
|
||||
const nwcConnections = browserSessionData.nwcConnections ?? [];
|
||||
const method = req.method as WeblnMethod;
|
||||
|
||||
// webln.enable just checks if NWC is configured
|
||||
if (method === 'webln.enable') {
|
||||
if (nwcConnections.length === 0) {
|
||||
throw new Error('No wallet configured. Please add an NWC connection in Plebeian Signer settings.');
|
||||
}
|
||||
debug('WebLN enabled');
|
||||
return { enabled: true }; // Return explicit value (undefined gets filtered by content script)
|
||||
}
|
||||
|
||||
// All other methods require an NWC connection
|
||||
const defaultConnection = nwcConnections[0];
|
||||
if (!defaultConnection) {
|
||||
throw new Error('No wallet configured. Please add an NWC connection in Plebeian Signer settings.');
|
||||
}
|
||||
|
||||
// Check reckless mode (but still prompt for payments)
|
||||
const recklessApprove = await shouldRecklessModeApprove(req.host);
|
||||
|
||||
// Check WebLN permissions
|
||||
const permissionState = recklessApprove && method !== 'webln.sendPayment' && method !== 'webln.keysend'
|
||||
? true
|
||||
: checkWeblnPermissions(browserSessionData, req.host, method);
|
||||
|
||||
if (permissionState === false) {
|
||||
throw new Error('Permission denied');
|
||||
}
|
||||
|
||||
if (permissionState === undefined) {
|
||||
// Ask user for permission
|
||||
const width = 375;
|
||||
const height = 600;
|
||||
const { top, left } = await getPosition(width, height);
|
||||
|
||||
// For sendPayment, include the invoice amount in the prompt data
|
||||
let promptParams = req.params ?? {};
|
||||
if (method === 'webln.sendPayment' && req.params?.paymentRequest) {
|
||||
const amountSats = parseInvoiceAmount(req.params.paymentRequest);
|
||||
promptParams = { ...promptParams, amountSats };
|
||||
}
|
||||
|
||||
const base64Event = Buffer.from(
|
||||
JSON.stringify(promptParams, undefined, 2)
|
||||
).toString('base64');
|
||||
|
||||
const response = await new Promise<PromptResponse>((resolve, reject) => {
|
||||
const id = crypto.randomUUID();
|
||||
openPrompts.set(id, { resolve, reject });
|
||||
browser.windows.create({
|
||||
type: 'popup',
|
||||
url: `prompt.html?method=${method}&host=${req.host}&id=${id}&nick=WebLN&event=${base64Event}`,
|
||||
height,
|
||||
width,
|
||||
top,
|
||||
left,
|
||||
});
|
||||
});
|
||||
|
||||
debug(response);
|
||||
|
||||
// Store permission for non-payment methods
|
||||
if ((response === 'approve' || response === 'reject') && method !== 'webln.sendPayment' && method !== 'webln.keysend') {
|
||||
const policy = response === 'approve' ? 'allow' : 'deny';
|
||||
await storePermission(
|
||||
browserSessionData,
|
||||
null, // WebLN has no identity
|
||||
req.host,
|
||||
method,
|
||||
policy
|
||||
);
|
||||
await backgroundLogPermissionStored(req.host, method, policy);
|
||||
}
|
||||
|
||||
if (['reject', 'reject-once'].includes(response)) {
|
||||
throw new Error('Permission denied');
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the WebLN method
|
||||
let result: any;
|
||||
const client = await getNwcClient(defaultConnection);
|
||||
|
||||
switch (method) {
|
||||
case 'webln.getInfo': {
|
||||
const info = await client.getInfo();
|
||||
result = {
|
||||
node: {
|
||||
alias: info.alias,
|
||||
pubkey: info.pubkey,
|
||||
color: info.color,
|
||||
},
|
||||
} as GetInfoResponse;
|
||||
debug('webln.getInfo result:');
|
||||
debug(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
case 'webln.sendPayment': {
|
||||
const invoice = req.params.paymentRequest;
|
||||
const payResult = await client.payInvoice({ invoice });
|
||||
result = { preimage: payResult.preimage } as SendPaymentResponse;
|
||||
debug('webln.sendPayment result:');
|
||||
debug(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
case 'webln.makeInvoice': {
|
||||
// Convert sats to millisats (NWC uses millisats)
|
||||
const amountSats = typeof req.params.amount === 'string'
|
||||
? parseInt(req.params.amount, 10)
|
||||
: req.params.amount ?? req.params.defaultAmount ?? 0;
|
||||
const amountMsat = amountSats * 1000;
|
||||
|
||||
const invoiceResult = await client.makeInvoice({
|
||||
amount: amountMsat,
|
||||
description: req.params.defaultMemo,
|
||||
});
|
||||
result = { paymentRequest: invoiceResult.invoice } as RequestInvoiceResponse;
|
||||
debug('webln.makeInvoice result:');
|
||||
debug(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
case 'webln.keysend':
|
||||
throw new Error('keysend is not yet supported');
|
||||
|
||||
default:
|
||||
throw new Error(`Not supported WebLN method '${method}'.`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Event, EventTemplate } from 'nostr-tools';
|
||||
import { Nip07Method } from '@common';
|
||||
import { Event as NostrEvent, EventTemplate } from 'nostr-tools';
|
||||
import { ExtensionMethod } from '@common';
|
||||
|
||||
// Extend Window interface for NIP-07
|
||||
// Extend Window interface for NIP-07 and WebLN
|
||||
declare global {
|
||||
interface Window {
|
||||
nostr?: any;
|
||||
webln?: any;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +39,7 @@ class Messenger {
|
||||
window.addEventListener('message', this.#handleCallResponse.bind(this));
|
||||
}
|
||||
|
||||
async request(method: Nip07Method, params: any): Promise<any> {
|
||||
async request(method: ExtensionMethod, params: any): Promise<any> {
|
||||
const id = generateUUID();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -89,7 +90,7 @@ const nostr = {
|
||||
return pubkey;
|
||||
},
|
||||
|
||||
async signEvent(event: EventTemplate): Promise<Event> {
|
||||
async signEvent(event: EventTemplate): Promise<NostrEvent> {
|
||||
debug('signEvent received');
|
||||
const signedEvent = await this.messenger.request('signEvent', event);
|
||||
debug('signEvent response:');
|
||||
@@ -158,6 +159,92 @@ const nostr = {
|
||||
|
||||
window.nostr = nostr as any;
|
||||
|
||||
// WebLN types (inline to avoid build issues with @common types in injected script)
|
||||
interface RequestInvoiceArgs {
|
||||
amount?: string | number;
|
||||
defaultAmount?: string | number;
|
||||
minimumAmount?: string | number;
|
||||
maximumAmount?: string | number;
|
||||
defaultMemo?: string;
|
||||
}
|
||||
|
||||
interface KeysendArgs {
|
||||
destination: string;
|
||||
amount: string | number;
|
||||
customRecords?: Record<string, string>;
|
||||
}
|
||||
|
||||
// Create a shared messenger instance for WebLN
|
||||
const weblnMessenger = nostr.messenger;
|
||||
|
||||
const webln = {
|
||||
enabled: false,
|
||||
|
||||
async enable(): Promise<void> {
|
||||
debug('webln.enable received');
|
||||
await weblnMessenger.request('webln.enable', {});
|
||||
this.enabled = true;
|
||||
debug('webln.enable completed');
|
||||
// Dispatch webln:enabled event as per WebLN spec
|
||||
window.dispatchEvent(new Event('webln:enabled'));
|
||||
},
|
||||
|
||||
async getInfo(): Promise<{ node: { alias?: string; pubkey?: string; color?: string } }> {
|
||||
debug('webln.getInfo received');
|
||||
const info = await weblnMessenger.request('webln.getInfo', {});
|
||||
debug('webln.getInfo response:');
|
||||
debug(info);
|
||||
return info;
|
||||
},
|
||||
|
||||
async sendPayment(paymentRequest: string): Promise<{ preimage: string }> {
|
||||
debug('webln.sendPayment received');
|
||||
const result = await weblnMessenger.request('webln.sendPayment', { paymentRequest });
|
||||
debug('webln.sendPayment response:');
|
||||
debug(result);
|
||||
return result;
|
||||
},
|
||||
|
||||
async keysend(args: KeysendArgs): Promise<{ preimage: string }> {
|
||||
debug('webln.keysend received');
|
||||
const result = await weblnMessenger.request('webln.keysend', args);
|
||||
debug('webln.keysend response:');
|
||||
debug(result);
|
||||
return result;
|
||||
},
|
||||
|
||||
async makeInvoice(
|
||||
args: string | number | RequestInvoiceArgs
|
||||
): Promise<{ paymentRequest: string }> {
|
||||
debug('webln.makeInvoice received');
|
||||
// Normalize args to RequestInvoiceArgs
|
||||
let normalizedArgs: RequestInvoiceArgs;
|
||||
if (typeof args === 'string' || typeof args === 'number') {
|
||||
normalizedArgs = { amount: args };
|
||||
} else {
|
||||
normalizedArgs = args;
|
||||
}
|
||||
const result = await weblnMessenger.request('webln.makeInvoice', normalizedArgs);
|
||||
debug('webln.makeInvoice response:');
|
||||
debug(result);
|
||||
return result;
|
||||
},
|
||||
|
||||
signMessage(): Promise<{ message: string; signature: string }> {
|
||||
throw new Error('signMessage is not supported - NWC does not provide node signing capabilities');
|
||||
},
|
||||
|
||||
verifyMessage(): Promise<void> {
|
||||
throw new Error('verifyMessage is not supported - NWC does not provide message verification');
|
||||
},
|
||||
};
|
||||
|
||||
window.webln = webln as any;
|
||||
|
||||
// Dispatch webln:ready event to signal that webln is available
|
||||
// This is dispatched on document as per the WebLN standard
|
||||
document.dispatchEvent(new Event('webln:ready'));
|
||||
|
||||
const debug = function (value: any) {
|
||||
console.log(JSON.stringify(value));
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import browser from 'webextension-polyfill';
|
||||
import { Nip07Method } from '@common';
|
||||
import { ExtensionMethod } from '@common';
|
||||
import { PromptResponse, PromptResponseMessage } from './background-common';
|
||||
|
||||
/**
|
||||
@@ -14,7 +14,7 @@ function base64ToUtf8(base64: string): string {
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
const id = params.get('id') as string;
|
||||
const method = params.get('method') as Nip07Method;
|
||||
const method = params.get('method') as ExtensionMethod;
|
||||
const host = params.get('host') as string;
|
||||
const nick = params.get('nick') as string;
|
||||
|
||||
@@ -58,6 +58,26 @@ switch (method) {
|
||||
title = 'Get Relays';
|
||||
break;
|
||||
|
||||
case 'webln.enable':
|
||||
title = 'Enable WebLN';
|
||||
break;
|
||||
|
||||
case 'webln.getInfo':
|
||||
title = 'Wallet Info';
|
||||
break;
|
||||
|
||||
case 'webln.sendPayment':
|
||||
title = 'Send Payment';
|
||||
break;
|
||||
|
||||
case 'webln.makeInvoice':
|
||||
title = 'Create Invoice';
|
||||
break;
|
||||
|
||||
case 'webln.keysend':
|
||||
title = 'Keysend Payment';
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -186,6 +206,65 @@ if (cardNip44DecryptElement && card2Nip44DecryptElement) {
|
||||
}
|
||||
}
|
||||
|
||||
// WebLN card visibility
|
||||
const cardWeblnEnableElement = document.getElementById('cardWeblnEnable');
|
||||
if (cardWeblnEnableElement) {
|
||||
if (method !== 'webln.enable') {
|
||||
cardWeblnEnableElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
const cardWeblnGetInfoElement = document.getElementById('cardWeblnGetInfo');
|
||||
if (cardWeblnGetInfoElement) {
|
||||
if (method !== 'webln.getInfo') {
|
||||
cardWeblnGetInfoElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
const cardWeblnSendPaymentElement = document.getElementById('cardWeblnSendPayment');
|
||||
const card2WeblnSendPaymentElement = document.getElementById('card2WeblnSendPayment');
|
||||
if (cardWeblnSendPaymentElement && card2WeblnSendPaymentElement) {
|
||||
if (method === 'webln.sendPayment') {
|
||||
// Display amount in sats
|
||||
const paymentAmountSpan = document.getElementById('paymentAmountSpan');
|
||||
if (paymentAmountSpan && eventParsed.amountSats !== undefined) {
|
||||
paymentAmountSpan.innerText = `${eventParsed.amountSats.toLocaleString()} sats`;
|
||||
} else if (paymentAmountSpan) {
|
||||
paymentAmountSpan.innerText = 'unknown amount';
|
||||
}
|
||||
// Show invoice in json card
|
||||
const card2WeblnSendPayment_jsonElement = document.getElementById('card2WeblnSendPayment_json');
|
||||
if (card2WeblnSendPayment_jsonElement && eventParsed.paymentRequest) {
|
||||
card2WeblnSendPayment_jsonElement.innerText = eventParsed.paymentRequest;
|
||||
}
|
||||
} else {
|
||||
cardWeblnSendPaymentElement.style.display = 'none';
|
||||
card2WeblnSendPaymentElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
const cardWeblnMakeInvoiceElement = document.getElementById('cardWeblnMakeInvoice');
|
||||
if (cardWeblnMakeInvoiceElement) {
|
||||
if (method === 'webln.makeInvoice') {
|
||||
const invoiceAmountSpan = document.getElementById('invoiceAmountSpan');
|
||||
if (invoiceAmountSpan) {
|
||||
const amount = eventParsed.amount ?? eventParsed.defaultAmount;
|
||||
if (amount) {
|
||||
invoiceAmountSpan.innerText = ` for ${Number(amount).toLocaleString()} sats`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cardWeblnMakeInvoiceElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
const cardWeblnKeysendElement = document.getElementById('cardWeblnKeysend');
|
||||
if (cardWeblnKeysendElement) {
|
||||
if (method !== 'webln.keysend') {
|
||||
cardWeblnKeysendElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Functions
|
||||
//
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user