From 377e4f7da6175e9ff94f5a92a0dd2310628e81b2 Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Tue, 3 Mar 2026 12:06:10 +0100 Subject: [PATCH 1/3] docs: Expand webhook documentation with comprehensive developer guide Add extensive documentation covering: - Webhook types: per-node vs per-developer webhooks with use cases - Event types: invoice_payment and node_stuck with full payload schemas - Signature verification: examples in Python, Rust, and Node.js - Delivery guarantees: retry behavior with exponential backoff - Best practices: idempotency, async processing, security - Mermaid diagrams for webhook architecture and delivery flow The documentation serves as the authoritative guide for developers integrating with Greenlight webhooks. --- docs/src/reference/webhooks.md | 732 +++++++++++++++++++++++++++------ 1 file changed, 609 insertions(+), 123 deletions(-) diff --git a/docs/src/reference/webhooks.md b/docs/src/reference/webhooks.md index 6ebbf8e95..d71c8ce0d 100644 --- a/docs/src/reference/webhooks.md +++ b/docs/src/reference/webhooks.md @@ -1,186 +1,672 @@ -## Webhooks +# Webhooks -Webhooks are URLs that receive HTTP requests containing event-related data. They allow applications to notify external systems of important events in real-time. The event source can be part of the same hosting application or an entirely different system. +Webhooks allow your application to receive real-time HTTP notifications when events occur on your Greenlight nodes. Instead of polling for changes, Greenlight pushes event data to your endpoints as soon as events happen. -With Greenlight, you can use webhooks to subscribe to events related to a given node. Each node supports up to **20 webhooks**, and duplicate URLs are allowed to facilitate secret rotations. +**Quick links:** [Event Types](#event-types) | [Signature Verification](#signature-verification) | [Managing Webhooks](#managing-webhooks) | [Best Practices](#best-practices) -## Events +## Webhook Types -Events are sent as HTTP `POST` requests with JSON payloads containing event details. The payload structure is as follows: +Greenlight supports two types of webhooks that serve different use cases: + +### Per-Node Webhooks + +Per-node webhooks are registered via the Scheduler API and apply to a single node. Use these when you need fine-grained control over which endpoints receive events from specific nodes. + +- Registered using `add_outgoing_webhook()` API +- Maximum **20 webhooks per node** +- Managed individually per node +- **Use case:** Monitoring specific nodes, per-customer webhook endpoints + +### Per-Developer Webhooks + +Per-developer webhooks are registered via the Greenlight Developer Console and automatically receive events from **all nodes** registered with your developer certificate. + +- Registered in the Developer Console +- Applies to all nodes sharing your `referrer_pubkey` +- Single endpoint receives events from your entire fleet +- **Use case:** App-wide monitoring, centralized event processing for wallet apps + +``` mermaid +graph TB + subgraph "Your Application" + EP1[Per-Node Endpoint
node-a.example.com/webhook] + EP2[Per-Node Endpoint
node-b.example.com/webhook] + EP3[App-Wide Endpoint
app.example.com/webhooks] + end + + subgraph "Per-Node Webhooks" + NodeA[Node A] -->|events| EP1 + NodeB[Node B] -->|events| EP2 + end + + subgraph "Per-Developer Webhook" + NodeA -.->|events| EP3 + NodeB -.->|events| EP3 + NodeC[Node C] -.->|events| EP3 + end + + DC[Developer Console] -->|registered| EP3 +``` + +Both webhook types use the same payload format and signature scheme, so your endpoint code works identically regardless of how the webhook was registered. + +--- + +## Event Types + +### Payload Structure + +All webhook payloads share a common structure: ```json { - "version": , - "node_id": , - "event_type": + "event_id": "7293847502938475029", + "node_id": "02abc123def456...", + "event_type": "invoice_payment", + "timestamp": 1704067200, + ...event-specific fields... } ``` -## Adding a Webhook to a Greenlight Node +| Field | Type | Description | +|-------|------|-------------| +| `event_id` | string | Unique identifier (snowflake ID) for deduplication | +| `node_id` | string | Hex-encoded 33-byte compressed public key | +| `event_type` | string | Event type: `invoice_payment` or `node_stuck` | +| `timestamp` | integer | Unix timestamp (seconds) when the event occurred | -### Prerequisites +Additional fields are included depending on the event type. -Before adding a webhook, ensure the following: +--- -- A **public TLS-secured endpoint** that can receive webhook events. -- Access to a Greenlight node's **device certificate**. +### invoice_payment -### Step 1: Initialize a Scheduler +Triggered when your node receives an incoming payment. -To add a webhook, first initialize a scheduler using the node ID and device certificate (obtained during node registration). +| Field | Type | Description | +|-------|------|-------------| +| `payment_hash` | string | Hex-encoded 32-byte payment hash | +| `preimage` | string | Hex-encoded 32-byte payment preimage (proof of payment) | +| `amount_msat` | integer | Amount received in millisatoshis | +| `bolt11` | string | The BOLT11 invoice that was paid | +| `label` | string | Invoice label (if set during creation) | -=== "Rust" -```rust -let device_cert = include_bytes!("path-to-device-cert"); -let device_key = include_bytes!("path-to-device-key"); - -let credentials = Builder::as_device() - .with_identity(device_cert, device_key) - .build() - .expect("Failed to build Device credentials"); - -let node_id = hex::decode("hex-node-id").unwrap(); - -let scheduler = Scheduler::with_credentials( - node_id, - gl_client::bitcoin::Network::Bitcoin, - utils::scheduler_uri(), - credentials -) -.await -.unwrap(); -``` +**Example payload:** -=== "Python" -```python -from pathlib import Path -from glclient import Credentials, Scheduler - -certpath = Path("device.pem") -keypath = Path("device-key.pem") -capath = Path("ca.pem") -runepath = Path("rune") - -creds = Credentials.from_parts( - certpath.open(mode="rb").read(), - keypath.open(mode="rb").read(), - capath.open(mode="rb").read(), - runepath.open(mode="rb").read(), -) - -node_id = bytes.fromhex("hex-node-id") - -scheduler = Scheduler( - node_id=node_id, - network="bitcoin", - creds=creds -) +```json +{ + "event_id": "7293847502938475029", + "node_id": "02a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", + "event_type": "invoice_payment", + "timestamp": 1704067200, + "payment_hash": "a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890", + "preimage": "fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321", + "amount_msat": 100000, + "bolt11": "lnbc1u1pjkx3xypp5...", + "label": "order-12345" +} ``` -### Step 2: Add the Webhook +**Common use cases:** -Once the scheduler is initialized, add a webhook using `add_outgoing_webhook`. Ensure that your webhook URL is correctly formatted. +- Confirming payment receipt in e-commerce applications +- Triggering order fulfillment workflows +- Updating user balances in custody applications +- Sending payment confirmation notifications to users -!!! warning "Secure Your Webhook Secret" - The `add_outgoing_webhook` method returns a **secret** used for webhook request validation. - Store this securely as it **cannot be recovered** if lost. +--- -=== "Rust" -```rust -use gl_client::scheduler::Scheduler; -use gl_client::bitcoin::Network; +### node_stuck -let webhook_uri = "https://example.com"; -let add_webhook_response = scheduler.add_outgoing_webhook(webhook_uri).await.unwrap(); +Triggered when a node falls behind the blockchain tip and cannot process new blocks. -save_secret_to_db(signer.node_id(), &add_webhook_response.secret); -``` +| Field | Type | Description | +|-------|------|-------------| +| `blockheight` | integer | Node's current synced block height | +| `headheight` | integer | Current blockchain tip height | +| `lag` | integer | Number of blocks behind (`headheight - blockheight`) | -=== "Python" -```python -scheduler.add_outgoing_webhook("https://example.com") +**Example payload:** -save_secret_to_db(signer.node_id(), add_webhook_response.secret) +```json +{ + "event_id": "7293847502938475030", + "node_id": "02a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", + "event_type": "node_stuck", + "timestamp": 1704067300, + "blockheight": 820000, + "headheight": 820150, + "lag": 150 +} ``` -## Verifying Webhook Payloads +**Common use cases:** + +- Alerting operations teams to sync issues +- Temporarily pausing payment processing +- Triggering automated recovery procedures +- Monitoring node health dashboards -Webhook payloads are verified using the **secret** returned from `add_outgoing_webhook`. This secret is unique per node and is used to validate payloads via **HMAC-SHA256 hashing**. The generated hash should match the **base64-encoded** value in the `gl-signature` header. +--- + +## Signature Verification + +Every webhook request includes an HMAC-SHA256 signature in the `gl-signature` header. **Always verify this signature before processing the payload.** + +### Signature Details + +| Property | Value | +|----------|-------| +| Header | `gl-signature` | +| Algorithm | HMAC-SHA256 | +| Encoding | Base64 (standard alphabet) | +| Input | Raw request body bytes | +| Secret | Returned from `add_outgoing_webhook()` | + +!!! warning "Verify Before Parsing" + The signature is computed over the **raw request body bytes**. You must verify + the signature before parsing JSON. Any whitespace changes or re-encoding + will invalidate the signature. + +### HTTP Headers + +Greenlight sends these headers with every webhook request: + +| Header | Description | +|--------|-------------| +| `Content-Type` | `application/json` | +| `gl-signature` | Base64-encoded HMAC-SHA256 signature | +| `X-Greenlight-Event` | Event type (e.g., `invoice_payment`) | + +### Verification Examples + +=== "Python" + + ```python + import hmac + import hashlib + import base64 + from flask import Flask, request, abort + + app = Flask(__name__) + WEBHOOK_SECRET = "your-webhook-secret" # From add_outgoing_webhook() + + def verify_signature(payload: bytes, signature: str, secret: str) -> bool: + """Verify the gl-signature header matches the payload.""" + expected = hmac.new( + secret.encode("utf-8"), + payload, + digestmod=hashlib.sha256 + ) + computed = base64.b64encode(expected.digest()).decode("utf-8") + return hmac.compare_digest(computed, signature) + + @app.route("/webhook", methods=["POST"]) + def handle_webhook(): + # Get raw body BEFORE parsing + raw_body = request.get_data() + signature = request.headers.get("gl-signature", "") + + if not verify_signature(raw_body, signature, WEBHOOK_SECRET): + abort(401, "Invalid signature") + + # Safe to parse now + event = request.get_json() + + # Handle event based on type + if event["event_type"] == "invoice_payment": + handle_payment(event) + elif event["event_type"] == "node_stuck": + handle_stuck_node(event) + + return "", 200 + ``` === "Rust" -```rust -use base64::Engine; -use hmac::{Hmac, Mac}; -use sha2::Sha256; - -fn verify_signature(secret: &String, gl_signature: &String) -> Result { - let mut hmac = Hmac::::new_from_slice(secret.as_bytes()).expect("Failed to create HMAC"); - hmac.update(&message.as_bytes()); - let hmac_output_bytes = hmac.finalize().into_bytes(); + + ```rust + use axum::{ + body::Bytes, + http::{HeaderMap, StatusCode}, + routing::post, + Router, + }; + use base64::Engine; + use hmac::{Hmac, Mac}; + use sha2::Sha256; + + const WEBHOOK_SECRET: &str = "your-webhook-secret"; + + fn verify_signature(payload: &[u8], signature: &str, secret: &str) -> bool { + let Ok(mut mac) = Hmac::::new_from_slice(secret.as_bytes()) else { + return false; + }; + mac.update(payload); + let result = mac.finalize(); + + let engine = base64::engine::general_purpose::STANDARD; + let computed = engine.encode(result.into_bytes()); + + // Constant-time comparison + computed == signature + } + + async fn handle_webhook( + headers: HeaderMap, + body: Bytes, + ) -> StatusCode { + let signature = headers + .get("gl-signature") + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); + + if !verify_signature(&body, signature, WEBHOOK_SECRET) { + return StatusCode::UNAUTHORIZED; + } + + // Safe to parse + let event: serde_json::Value = match serde_json::from_slice(&body) { + Ok(e) => e, + Err(_) => return StatusCode::BAD_REQUEST, + }; + + // Handle event... + println!("Received event: {}", event["event_type"]); + + StatusCode::OK + } + ``` + +=== "Node.js" + + ```javascript + const express = require('express'); + const crypto = require('crypto'); + + const app = express(); + const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET; + + function verifySignature(payload, signature, secret) { + const hmac = crypto.createHmac('sha256', secret); + hmac.update(payload); + const computed = hmac.digest('base64'); + + // Timing-safe comparison + try { + return crypto.timingSafeEqual( + Buffer.from(computed), + Buffer.from(signature) + ); + } catch { + return false; + } + } + + // Use raw body parser for signature verification + app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { + const signature = req.headers['gl-signature'] || ''; + + if (!verifySignature(req.body, signature, WEBHOOK_SECRET)) { + return res.status(401).send('Invalid signature'); + } + + // Safe to parse + const event = JSON.parse(req.body.toString()); + + console.log(`Received ${event.event_type} for node ${event.node_id}`); + + // Handle event based on type + switch (event.event_type) { + case 'invoice_payment': + handlePayment(event); + break; + case 'node_stuck': + handleStuckNode(event); + break; + } + + res.sendStatus(200); + }); + + app.listen(3000); + ``` + +--- + +## Delivery Guarantees + +### Automatic Retries + +Failed webhook deliveries are automatically retried with exponential backoff: + +| Setting | Default Value | +|---------|---------------| +| Maximum attempts | 5 | +| Base delay | 60 seconds | +| Backoff multiplier | 2x | +| HTTP timeout | 30 seconds | + +**Retry schedule:** + +| Attempt | Delay After Failure | +|---------|---------------------| +| 1 | Immediate | +| 2 | 60 seconds | +| 3 | 2 minutes | +| 4 | 4 minutes | +| 5 | 8 minutes | + +After 5 failed attempts, the event is dropped. + +``` mermaid +sequenceDiagram + participant S as Greenlight Service + participant H as Webhook Dispatcher + participant E as Your Endpoint + + S->>H: Event occurs + H->>E: POST webhook - let engine = base64::engine::general_purpose::STANDARD; - Ok(engine.encode(&hmac_output_bytes) == *gl_signature) -} + alt Success (2xx) + E-->>H: 200 OK + Note over H: Delivery complete + else Failure (5xx/timeout) + E-->>H: 500 Error + Note over H: Queue for retry + H->>H: Wait 60s + H->>E: Retry POST + E-->>H: 200 OK + Note over H: Delivery complete + end ``` +### What Triggers Retries + +Deliveries are **retried** on: + +- HTTP 5xx responses (server errors) +- Connection timeouts +- Network errors (connection refused, DNS failure) + +Deliveries are **not retried** on: + +- HTTP 2xx responses (success) +- HTTP 4xx responses (client errors - fix your endpoint) + +!!! tip "Return 500 for Temporary Failures" + If your endpoint encounters a temporary issue (database unavailable, rate limited), + return HTTP 500 or 503 to trigger a retry. Return 4xx only for permanent failures + like invalid signatures. + +--- + +## Managing Webhooks + +### Prerequisites + +Before adding a webhook: + +- A **public HTTPS endpoint** that can receive webhook events +- Access to your node's **device certificate** + +### Adding a Webhook + +Initialize a scheduler and register your webhook endpoint: + === "Python" -```python -import hmac, hashlib, base64 -def verify_signature(secret: str, body, sig) -> bool: - payload_hmac = hmac.new( - bytes(secret, "UTF-8"), body, digestmod=hashlib.sha256 + ```python + from pathlib import Path + from glclient import Credentials, Scheduler + + # Load credentials + creds = Credentials.from_parts( + Path("device.pem").read_bytes(), + Path("device-key.pem").read_bytes(), + Path("ca.pem").read_bytes(), + Path("rune").read_bytes(), ) - return base64.b64encode(payload_hmac.digest()).decode() == sig -``` -## Managing Webhooks + node_id = bytes.fromhex("02a1b2c3...") + + scheduler = Scheduler( + node_id=node_id, + network="bitcoin", + creds=creds + ) + + # Add webhook + response = scheduler.add_outgoing_webhook("https://example.com/webhook") + + # Store the secret securely - it cannot be recovered! + print(f"Webhook ID: {response.id}") + print(f"Secret: {response.secret}") + ``` + +=== "Rust" + + ```rust + use gl_client::credentials::Builder; + use gl_client::scheduler::Scheduler; + use gl_client::bitcoin::Network; + + let credentials = Builder::as_device() + .with_identity(device_cert, device_key) + .build() + .expect("Failed to build credentials"); + + let scheduler = Scheduler::with_credentials( + node_id, + Network::Bitcoin, + scheduler_uri, + credentials + ).await?; + + // Add webhook + let response = scheduler + .add_outgoing_webhook("https://example.com/webhook") + .await?; + + // Store the secret securely - it cannot be recovered! + println!("Webhook ID: {}", response.id); + println!("Secret: {}", response.secret); + ``` + +!!! warning "Secure Your Secret" + The webhook secret is returned **only once** when you register the webhook. + Store it securely in your secrets manager. If lost, you must delete the + webhook and create a new one. ### Listing Webhooks -To retrieve registered webhooks for a node, use `list_outgoing_webhooks`. +Retrieve all registered webhooks for a node: + +=== "Python" + + ```python + webhooks = scheduler.list_outgoing_webhooks() + for webhook in webhooks.outgoing_webhooks: + print(f"ID: {webhook.id}, URL: {webhook.uri}") + ``` === "Rust" -```rust -let outgoing_webhooks = scheduler.list_outgoing_webhooks().await.unwrap(); -``` + + ```rust + let webhooks = scheduler.list_outgoing_webhooks().await?; + for webhook in webhooks.outgoing_webhooks { + println!("ID: {}, URL: {}", webhook.id, webhook.uri); + } + ``` + +!!! note + Secrets are not included in the list response for security reasons. + +### Deleting Webhooks + +Remove webhooks by their IDs: === "Python" + + ```python + scheduler.delete_outgoing_webhooks([1, 2, 3]) + ``` + +=== "Rust" + + ```rust + scheduler.delete_webhooks(vec![1, 2, 3]).await?; + ``` + +### Rotating Secrets + +To rotate a webhook secret without downtime: + +1. **Add a new webhook** with the same URL +2. **Update your endpoint** to accept both secrets temporarily +3. **Delete the old webhook** (or rotate its secret) +4. **Remove the old secret** from your endpoint + +=== "Python" + + ```python + # Option A: Add new webhook, delete old + new_response = scheduler.add_outgoing_webhook("https://example.com/webhook") + # Update your endpoint to use new_response.secret + scheduler.delete_outgoing_webhooks([old_webhook_id]) + + # Option B: Rotate existing webhook's secret + response = scheduler.rotate_outgoing_webhook_secret(webhook_id) + # response.secret contains the new secret + ``` + +=== "Rust" + + ```rust + // Rotate existing webhook's secret + let response = scheduler + .rotate_outgoing_webhook_secret(webhook_id) + .await?; + // response.secret contains the new secret + ``` + +**Handling rotation in your endpoint:** + ```python -outgoing_webhooks = scheduler.list_outgoing_webhooks() +# During rotation: accept multiple secrets +SECRETS = [ + os.environ["WEBHOOK_SECRET_OLD"], + os.environ["WEBHOOK_SECRET_NEW"], +] + +def verify_signature(payload: bytes, signature: str) -> bool: + for secret in SECRETS: + expected = hmac.new(secret.encode(), payload, hashlib.sha256) + computed = base64.b64encode(expected.digest()).decode() + if hmac.compare_digest(computed, signature): + return True + return False ``` -### Deleting Webhooks +--- -To delete webhooks, pass the webhook ID(s) to `delete_outgoing_webhook`. +## Best Practices -=== "Rust" -```rust -scheduler.delete_outgoing_webhooks(vec![1,2,3]).await.unwrap(); +### Idempotency + +Webhooks may be delivered more than once due to retries or network issues. Design your handlers to be idempotent using the `event_id` field. + +```python +import redis + +r = redis.Redis() +EVENT_TTL = 86400 # 24 hours + +def handle_webhook(event): + event_id = event["event_id"] + + # Check if already processed (atomic set-if-not-exists) + if not r.set(f"webhook:{event_id}", "1", nx=True, ex=EVENT_TTL): + return # Already processed, skip + + # Process the event + process_payment(event) ``` -=== "Python" +!!! tip "Event ID Retention" + Keep event IDs for at least 24 hours to handle delayed retries. + Redis with TTL or a database table with cleanup jobs works well. + +### Response Time + +Return a response quickly (within 30 seconds). Perform heavy processing asynchronously. + ```python -scheduler.delete_outgoing_webhooks([1,2,3]) +from celery import Celery + +celery = Celery('tasks', broker='redis://localhost') + +@app.route("/webhook", methods=["POST"]) +def handle_webhook(): + event = verify_and_parse(request) + + # Queue for background processing + process_event.delay(event) + + # Return immediately + return "", 200 + +@celery.task +def process_event(event): + # Heavy processing happens here + ... ``` -## Rotating Webhook Secrets +### Error Handling -Rotating a webhook secret **invalidates** the old secret and generates a new one. To ensure a smooth transition: +Use appropriate HTTP status codes: -1. Add a **new webhook** with the same URL. -2. Rotate the **old webhook secret**. -3. Delete either the new or old webhook, depending on whether you need a **static webhook ID**. +| Response | When to Use | +|----------|-------------| +| `200 OK` | Event processed successfully | +| `202 Accepted` | Event received, processing async | +| `400 Bad Request` | Malformed payload (won't retry) | +| `401 Unauthorized` | Invalid signature (won't retry) | +| `500 Internal Server Error` | Temporary failure (will retry) | +| `503 Service Unavailable` | Overloaded (will retry) | -!!! note - Ensure payload verification occurs **before** JSON parsing. Any modification to the payload before hashing will invalidate the signature. +### Security -=== "Rust" -```rust -let secret_response = scheduler.rotate_outgoing_webhook_secret(1).await.unwrap(); +- **HTTPS required** - Webhook endpoints must use HTTPS with valid certificates +- **Verify signatures** - Always verify the `gl-signature` header before processing +- **Validate early** - Verify signatures before parsing JSON +- **Use constant-time comparison** - Prevents timing attacks on signature verification +- **Store secrets securely** - Use environment variables or a secrets manager + +### Testing + +For local development, use a tunneling service: + +```bash +# Using ngrok +ngrok http 8000 + +# Register the ngrok URL as your webhook endpoint +# https://abc123.ngrok.io/webhook ``` -=== "Python" +Test signature verification independently: + ```python -secret_response = scheduler.rotate_outgoing_webhook_secret(1) +def test_signature_verification(): + secret = "test-secret" + payload = b'{"event_id":"123","event_type":"invoice_payment"}' + + # Compute expected signature + expected = base64.b64encode( + hmac.new(secret.encode(), payload, hashlib.sha256).digest() + ).decode() + + assert verify_signature(payload, expected, secret) + assert not verify_signature(payload, "wrong", secret) + assert not verify_signature(b"tampered", expected, secret) ``` From 57e764779aa5206414c1e1f4e5b701f8319dbede Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Tue, 3 Mar 2026 18:37:43 +0100 Subject: [PATCH 2/3] Revert "Remove deprecated StreamIncoming API in favor of StreamNodeEvents" This reverts commit c4d7acd1a4a0a95b8a583d0f60d8e41d732e694b. --- libs/gl-client-py/glclient/__init__.py | 8 ++ libs/gl-client-py/glclient/greenlight.proto | 4 + libs/gl-client-py/glclient/greenlight_pb2.py | 92 ++++++++++--------- libs/gl-client-py/glclient/greenlight_pb2.pyi | 12 +++ .../glclient/greenlight_pb2_grpc.py | 46 +++++++++- .../glclient/greenlight_pb2_grpc.pyi | 29 ++++-- libs/gl-client-py/src/node.rs | 21 +++++ .../proto/glclient/greenlight.proto | 4 + .../proto/glclient/greenlight.proto | 10 ++ libs/gl-plugin/src/node/mod.rs | 24 +++++ libs/gl-plugin/src/node/wrapper.rs | 14 ++- .../proto/glclient/greenlight.proto | 3 + libs/proto/glclient/greenlight.proto | 4 + 13 files changed, 211 insertions(+), 60 deletions(-) diff --git a/libs/gl-client-py/glclient/__init__.py b/libs/gl-client-py/glclient/__init__.py index 247c3a441..5def639c5 100644 --- a/libs/gl-client-py/glclient/__init__.py +++ b/libs/gl-client-py/glclient/__init__.py @@ -448,6 +448,14 @@ def stream_log(self): break yield nodepb.LogEntry.FromString(bytes(n)) + def stream_incoming(self): + stream = self.inner.stream_incoming(b"") + while True: + n = stream.next() + if n is None: + break + yield nodepb.IncomingPayment.FromString(bytes(n)) + def stream_custommsg(self): stream = self.inner.stream_custommsg(b"") while True: diff --git a/libs/gl-client-py/glclient/greenlight.proto b/libs/gl-client-py/glclient/greenlight.proto index ba9528a6d..94d2cfacd 100644 --- a/libs/gl-client-py/glclient/greenlight.proto +++ b/libs/gl-client-py/glclient/greenlight.proto @@ -25,6 +25,7 @@ service Node { // // Currently includes off-chain payments received matching an // invoice or spontaneus paymens through keysend. + rpc StreamIncoming(StreamIncomingFilter) returns (stream IncomingPayment) {} // Stream the logs as they are produced by the node // @@ -120,6 +121,9 @@ message Amount { } } +// Options to stream_incoming to specify what to stream. +message StreamIncomingFilter { +} message TlvField { uint64 type = 1; diff --git a/libs/gl-client-py/glclient/greenlight_pb2.py b/libs/gl-client-py/glclient/greenlight_pb2.py index ebb94eb6c..1c0619a63 100644 --- a/libs/gl-client-py/glclient/greenlight_pb2.py +++ b/libs/gl-client-py/glclient/greenlight_pb2.py @@ -24,7 +24,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19glclient/greenlight.proto\x12\ngreenlight\"H\n\x11HsmRequestContext\x12\x0f\n\x07node_id\x18\x01 \x01(\x0c\x12\x0c\n\x04\x64\x62id\x18\x02 \x01(\x04\x12\x14\n\x0c\x63\x61pabilities\x18\x03 \x01(\x04\"q\n\x0bHsmResponse\x12\x12\n\nrequest_id\x18\x01 \x01(\r\x12\x0b\n\x03raw\x18\x02 \x01(\x0c\x12\x32\n\x0csigner_state\x18\x05 \x03(\x0b\x32\x1c.greenlight.SignerStateEntry\x12\r\n\x05\x65rror\x18\x06 \x01(\t\"\xbf\x01\n\nHsmRequest\x12\x12\n\nrequest_id\x18\x01 \x01(\r\x12.\n\x07\x63ontext\x18\x02 \x01(\x0b\x32\x1d.greenlight.HsmRequestContext\x12\x0b\n\x03raw\x18\x03 \x01(\x0c\x12\x32\n\x0csigner_state\x18\x04 \x03(\x0b\x32\x1c.greenlight.SignerStateEntry\x12,\n\x08requests\x18\x05 \x03(\x0b\x32\x1a.greenlight.PendingRequest\"\x07\n\x05\x45mpty\"l\n\x06\x41mount\x12\x16\n\x0cmillisatoshi\x18\x01 \x01(\x04H\x00\x12\x11\n\x07satoshi\x18\x02 \x01(\x04H\x00\x12\x11\n\x07\x62itcoin\x18\x03 \x01(\x04H\x00\x12\r\n\x03\x61ll\x18\x04 \x01(\x08H\x00\x12\r\n\x03\x61ny\x18\x05 \x01(\x08H\x00\x42\x06\n\x04unit\"\'\n\x08TlvField\x12\x0c\n\x04type\x18\x01 \x01(\x04\x12\r\n\x05value\x18\x02 \x01(\x0c\"\xa5\x01\n\x0fOffChainPayment\x12\r\n\x05label\x18\x01 \x01(\t\x12\x10\n\x08preimage\x18\x02 \x01(\x0c\x12\"\n\x06\x61mount\x18\x03 \x01(\x0b\x32\x12.greenlight.Amount\x12\'\n\textratlvs\x18\x04 \x03(\x0b\x32\x14.greenlight.TlvField\x12\x14\n\x0cpayment_hash\x18\x05 \x01(\x0c\x12\x0e\n\x06\x62olt11\x18\x06 \x01(\t\"M\n\x0fIncomingPayment\x12/\n\x08offchain\x18\x01 \x01(\x0b\x32\x1b.greenlight.OffChainPaymentH\x00\x42\t\n\x07\x64\x65tails\"\x12\n\x10StreamLogRequest\"\x18\n\x08LogEntry\x12\x0c\n\x04line\x18\x01 \x01(\t\"?\n\x10SignerStateEntry\x12\x0f\n\x07version\x18\x01 \x01(\x04\x12\x0b\n\x03key\x18\x02 \x01(\t\x12\r\n\x05value\x18\x03 \x01(\x0c\"r\n\x0ePendingRequest\x12\x0f\n\x07request\x18\x01 \x01(\x0c\x12\x0b\n\x03uri\x18\x02 \x01(\t\x12\x11\n\tsignature\x18\x03 \x01(\x0c\x12\x0e\n\x06pubkey\x18\x04 \x01(\x0c\x12\x11\n\ttimestamp\x18\x05 \x01(\x04\x12\x0c\n\x04rune\x18\x06 \x01(\x0c\"=\n\nNodeConfig\x12/\n\x0bstartupmsgs\x18\x01 \x03(\x0b\x32\x1a.greenlight.StartupMessage\"!\n\x08GlConfig\x12\x15\n\rclose_to_addr\x18\x01 \x01(\t\"3\n\x0eStartupMessage\x12\x0f\n\x07request\x18\x01 \x01(\x0c\x12\x10\n\x08response\x18\x02 \x01(\x0c\"\x18\n\x16StreamCustommsgRequest\"-\n\tCustommsg\x12\x0f\n\x07peer_id\x18\x01 \x01(\x0c\x12\x0f\n\x07payload\x18\x02 \x01(\x0c\"\xa4\x01\n\x14TrampolinePayRequest\x12\x0e\n\x06\x62olt11\x18\x01 \x01(\t\x12\x1a\n\x12trampoline_node_id\x18\x02 \x01(\x0c\x12\x13\n\x0b\x61mount_msat\x18\x03 \x01(\x04\x12\r\n\x05label\x18\x04 \x01(\t\x12\x15\n\rmaxfeepercent\x18\x05 \x01(\x02\x12\x10\n\x08maxdelay\x18\x06 \x01(\r\x12\x13\n\x0b\x64\x65scription\x18\x07 \x01(\t\"\xd5\x01\n\x15TrampolinePayResponse\x12\x18\n\x10payment_preimage\x18\x01 \x01(\x0c\x12\x14\n\x0cpayment_hash\x18\x02 \x01(\x0c\x12\x12\n\ncreated_at\x18\x03 \x01(\x01\x12\r\n\x05parts\x18\x04 \x01(\r\x12\x13\n\x0b\x61mount_msat\x18\x05 \x01(\x04\x12\x18\n\x10\x61mount_sent_msat\x18\x06 \x01(\x04\x12\x13\n\x0b\x64\x65stination\x18\x07 \x01(\x0c\"%\n\tPayStatus\x12\x0c\n\x08\x43OMPLETE\x10\x00\x12\n\n\x06\x46\x41ILED\x10\x02\"k\n\x11LspInvoiceRequest\x12\x0e\n\x06lsp_id\x18\x01 \x01(\t\x12\r\n\x05token\x18\x02 \x01(\t\x12\x13\n\x0b\x61mount_msat\x18\x03 \x01(\x04\x12\x13\n\x0b\x64\x65scription\x18\x04 \x01(\t\x12\r\n\x05label\x18\x05 \x01(\t\"}\n\x12LspInvoiceResponse\x12\x0e\n\x06\x62olt11\x18\x01 \x01(\t\x12\x15\n\rcreated_index\x18\x02 \x01(\r\x12\x12\n\nexpires_at\x18\x03 \x01(\r\x12\x14\n\x0cpayment_hash\x18\x04 \x01(\x0c\x12\x16\n\x0epayment_secret\x18\x05 \x01(\x0c\"\x13\n\x11NodeEventsRequest\"E\n\tNodeEvent\x12/\n\x0cinvoice_paid\x18\x01 \x01(\x0b\x32\x17.greenlight.InvoicePaidH\x00\x42\x07\n\x05\x65vent\"\x92\x01\n\x0bInvoicePaid\x12\x14\n\x0cpayment_hash\x18\x01 \x01(\x0c\x12\x0e\n\x06\x62olt11\x18\x02 \x01(\t\x12\x10\n\x08preimage\x18\x03 \x01(\x0c\x12\r\n\x05label\x18\x04 \x01(\t\x12\x13\n\x0b\x61mount_msat\x18\x05 \x01(\x04\x12\'\n\textratlvs\x18\x06 \x03(\x0b\x32\x14.greenlight.TlvField2\xd1\x04\n\x04Node\x12M\n\nLspInvoice\x12\x1d.greenlight.LspInvoiceRequest\x1a\x1e.greenlight.LspInvoiceResponse\"\x00\x12\x43\n\tStreamLog\x12\x1c.greenlight.StreamLogRequest\x1a\x14.greenlight.LogEntry\"\x00\x30\x01\x12P\n\x0fStreamCustommsg\x12\".greenlight.StreamCustommsgRequest\x1a\x15.greenlight.Custommsg\"\x00\x30\x01\x12L\n\x10StreamNodeEvents\x12\x1d.greenlight.NodeEventsRequest\x1a\x15.greenlight.NodeEvent\"\x00\x30\x01\x12\x42\n\x11StreamHsmRequests\x12\x11.greenlight.Empty\x1a\x16.greenlight.HsmRequest\"\x00\x30\x01\x12\x41\n\x11RespondHsmRequest\x12\x17.greenlight.HsmResponse\x1a\x11.greenlight.Empty\"\x00\x12\x36\n\tConfigure\x12\x14.greenlight.GlConfig\x1a\x11.greenlight.Empty\"\x00\x12V\n\rTrampolinePay\x12 .greenlight.TrampolinePayRequest\x1a!.greenlight.TrampolinePayResponse\"\x00\x32s\n\x03Hsm\x12<\n\x07Request\x12\x16.greenlight.HsmRequest\x1a\x17.greenlight.HsmResponse\"\x00\x12.\n\x04Ping\x12\x11.greenlight.Empty\x1a\x11.greenlight.Empty\"\x00\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19glclient/greenlight.proto\x12\ngreenlight\"H\n\x11HsmRequestContext\x12\x0f\n\x07node_id\x18\x01 \x01(\x0c\x12\x0c\n\x04\x64\x62id\x18\x02 \x01(\x04\x12\x14\n\x0c\x63\x61pabilities\x18\x03 \x01(\x04\"q\n\x0bHsmResponse\x12\x12\n\nrequest_id\x18\x01 \x01(\r\x12\x0b\n\x03raw\x18\x02 \x01(\x0c\x12\x32\n\x0csigner_state\x18\x05 \x03(\x0b\x32\x1c.greenlight.SignerStateEntry\x12\r\n\x05\x65rror\x18\x06 \x01(\t\"\xbf\x01\n\nHsmRequest\x12\x12\n\nrequest_id\x18\x01 \x01(\r\x12.\n\x07\x63ontext\x18\x02 \x01(\x0b\x32\x1d.greenlight.HsmRequestContext\x12\x0b\n\x03raw\x18\x03 \x01(\x0c\x12\x32\n\x0csigner_state\x18\x04 \x03(\x0b\x32\x1c.greenlight.SignerStateEntry\x12,\n\x08requests\x18\x05 \x03(\x0b\x32\x1a.greenlight.PendingRequest\"\x07\n\x05\x45mpty\"l\n\x06\x41mount\x12\x16\n\x0cmillisatoshi\x18\x01 \x01(\x04H\x00\x12\x11\n\x07satoshi\x18\x02 \x01(\x04H\x00\x12\x11\n\x07\x62itcoin\x18\x03 \x01(\x04H\x00\x12\r\n\x03\x61ll\x18\x04 \x01(\x08H\x00\x12\r\n\x03\x61ny\x18\x05 \x01(\x08H\x00\x42\x06\n\x04unit\"\x16\n\x14StreamIncomingFilter\"\'\n\x08TlvField\x12\x0c\n\x04type\x18\x01 \x01(\x04\x12\r\n\x05value\x18\x02 \x01(\x0c\"\xa5\x01\n\x0fOffChainPayment\x12\r\n\x05label\x18\x01 \x01(\t\x12\x10\n\x08preimage\x18\x02 \x01(\x0c\x12\"\n\x06\x61mount\x18\x03 \x01(\x0b\x32\x12.greenlight.Amount\x12\'\n\textratlvs\x18\x04 \x03(\x0b\x32\x14.greenlight.TlvField\x12\x14\n\x0cpayment_hash\x18\x05 \x01(\x0c\x12\x0e\n\x06\x62olt11\x18\x06 \x01(\t\"M\n\x0fIncomingPayment\x12/\n\x08offchain\x18\x01 \x01(\x0b\x32\x1b.greenlight.OffChainPaymentH\x00\x42\t\n\x07\x64\x65tails\"\x12\n\x10StreamLogRequest\"\x18\n\x08LogEntry\x12\x0c\n\x04line\x18\x01 \x01(\t\"?\n\x10SignerStateEntry\x12\x0f\n\x07version\x18\x01 \x01(\x04\x12\x0b\n\x03key\x18\x02 \x01(\t\x12\r\n\x05value\x18\x03 \x01(\x0c\"r\n\x0ePendingRequest\x12\x0f\n\x07request\x18\x01 \x01(\x0c\x12\x0b\n\x03uri\x18\x02 \x01(\t\x12\x11\n\tsignature\x18\x03 \x01(\x0c\x12\x0e\n\x06pubkey\x18\x04 \x01(\x0c\x12\x11\n\ttimestamp\x18\x05 \x01(\x04\x12\x0c\n\x04rune\x18\x06 \x01(\x0c\"=\n\nNodeConfig\x12/\n\x0bstartupmsgs\x18\x01 \x03(\x0b\x32\x1a.greenlight.StartupMessage\"!\n\x08GlConfig\x12\x15\n\rclose_to_addr\x18\x01 \x01(\t\"3\n\x0eStartupMessage\x12\x0f\n\x07request\x18\x01 \x01(\x0c\x12\x10\n\x08response\x18\x02 \x01(\x0c\"\x18\n\x16StreamCustommsgRequest\"-\n\tCustommsg\x12\x0f\n\x07peer_id\x18\x01 \x01(\x0c\x12\x0f\n\x07payload\x18\x02 \x01(\x0c\"\xa4\x01\n\x14TrampolinePayRequest\x12\x0e\n\x06\x62olt11\x18\x01 \x01(\t\x12\x1a\n\x12trampoline_node_id\x18\x02 \x01(\x0c\x12\x13\n\x0b\x61mount_msat\x18\x03 \x01(\x04\x12\r\n\x05label\x18\x04 \x01(\t\x12\x15\n\rmaxfeepercent\x18\x05 \x01(\x02\x12\x10\n\x08maxdelay\x18\x06 \x01(\r\x12\x13\n\x0b\x64\x65scription\x18\x07 \x01(\t\"\xd5\x01\n\x15TrampolinePayResponse\x12\x18\n\x10payment_preimage\x18\x01 \x01(\x0c\x12\x14\n\x0cpayment_hash\x18\x02 \x01(\x0c\x12\x12\n\ncreated_at\x18\x03 \x01(\x01\x12\r\n\x05parts\x18\x04 \x01(\r\x12\x13\n\x0b\x61mount_msat\x18\x05 \x01(\x04\x12\x18\n\x10\x61mount_sent_msat\x18\x06 \x01(\x04\x12\x13\n\x0b\x64\x65stination\x18\x07 \x01(\x0c\"%\n\tPayStatus\x12\x0c\n\x08\x43OMPLETE\x10\x00\x12\n\n\x06\x46\x41ILED\x10\x02\"k\n\x11LspInvoiceRequest\x12\x0e\n\x06lsp_id\x18\x01 \x01(\t\x12\r\n\x05token\x18\x02 \x01(\t\x12\x13\n\x0b\x61mount_msat\x18\x03 \x01(\x04\x12\x13\n\x0b\x64\x65scription\x18\x04 \x01(\t\x12\r\n\x05label\x18\x05 \x01(\t\"}\n\x12LspInvoiceResponse\x12\x0e\n\x06\x62olt11\x18\x01 \x01(\t\x12\x15\n\rcreated_index\x18\x02 \x01(\r\x12\x12\n\nexpires_at\x18\x03 \x01(\r\x12\x14\n\x0cpayment_hash\x18\x04 \x01(\x0c\x12\x16\n\x0epayment_secret\x18\x05 \x01(\x0c\"\x13\n\x11NodeEventsRequest\"E\n\tNodeEvent\x12/\n\x0cinvoice_paid\x18\x01 \x01(\x0b\x32\x17.greenlight.InvoicePaidH\x00\x42\x07\n\x05\x65vent\"\x92\x01\n\x0bInvoicePaid\x12\x14\n\x0cpayment_hash\x18\x01 \x01(\x0c\x12\x0e\n\x06\x62olt11\x18\x02 \x01(\t\x12\x10\n\x08preimage\x18\x03 \x01(\x0c\x12\r\n\x05label\x18\x04 \x01(\t\x12\x13\n\x0b\x61mount_msat\x18\x05 \x01(\x04\x12\'\n\textratlvs\x18\x06 \x03(\x0b\x32\x14.greenlight.TlvField2\xa6\x05\n\x04Node\x12M\n\nLspInvoice\x12\x1d.greenlight.LspInvoiceRequest\x1a\x1e.greenlight.LspInvoiceResponse\"\x00\x12S\n\x0eStreamIncoming\x12 .greenlight.StreamIncomingFilter\x1a\x1b.greenlight.IncomingPayment\"\x00\x30\x01\x12\x43\n\tStreamLog\x12\x1c.greenlight.StreamLogRequest\x1a\x14.greenlight.LogEntry\"\x00\x30\x01\x12P\n\x0fStreamCustommsg\x12\".greenlight.StreamCustommsgRequest\x1a\x15.greenlight.Custommsg\"\x00\x30\x01\x12L\n\x10StreamNodeEvents\x12\x1d.greenlight.NodeEventsRequest\x1a\x15.greenlight.NodeEvent\"\x00\x30\x01\x12\x42\n\x11StreamHsmRequests\x12\x11.greenlight.Empty\x1a\x16.greenlight.HsmRequest\"\x00\x30\x01\x12\x41\n\x11RespondHsmRequest\x12\x17.greenlight.HsmResponse\x1a\x11.greenlight.Empty\"\x00\x12\x36\n\tConfigure\x12\x14.greenlight.GlConfig\x1a\x11.greenlight.Empty\"\x00\x12V\n\rTrampolinePay\x12 .greenlight.TrampolinePayRequest\x1a!.greenlight.TrampolinePayResponse\"\x00\x32s\n\x03Hsm\x12<\n\x07Request\x12\x16.greenlight.HsmRequest\x1a\x17.greenlight.HsmResponse\"\x00\x12.\n\x04Ping\x12\x11.greenlight.Empty\x1a\x11.greenlight.Empty\"\x00\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -41,48 +41,50 @@ _globals['_EMPTY']._serialized_end=431 _globals['_AMOUNT']._serialized_start=433 _globals['_AMOUNT']._serialized_end=541 - _globals['_TLVFIELD']._serialized_start=543 - _globals['_TLVFIELD']._serialized_end=582 - _globals['_OFFCHAINPAYMENT']._serialized_start=585 - _globals['_OFFCHAINPAYMENT']._serialized_end=750 - _globals['_INCOMINGPAYMENT']._serialized_start=752 - _globals['_INCOMINGPAYMENT']._serialized_end=829 - _globals['_STREAMLOGREQUEST']._serialized_start=831 - _globals['_STREAMLOGREQUEST']._serialized_end=849 - _globals['_LOGENTRY']._serialized_start=851 - _globals['_LOGENTRY']._serialized_end=875 - _globals['_SIGNERSTATEENTRY']._serialized_start=877 - _globals['_SIGNERSTATEENTRY']._serialized_end=940 - _globals['_PENDINGREQUEST']._serialized_start=942 - _globals['_PENDINGREQUEST']._serialized_end=1056 - _globals['_NODECONFIG']._serialized_start=1058 - _globals['_NODECONFIG']._serialized_end=1119 - _globals['_GLCONFIG']._serialized_start=1121 - _globals['_GLCONFIG']._serialized_end=1154 - _globals['_STARTUPMESSAGE']._serialized_start=1156 - _globals['_STARTUPMESSAGE']._serialized_end=1207 - _globals['_STREAMCUSTOMMSGREQUEST']._serialized_start=1209 - _globals['_STREAMCUSTOMMSGREQUEST']._serialized_end=1233 - _globals['_CUSTOMMSG']._serialized_start=1235 - _globals['_CUSTOMMSG']._serialized_end=1280 - _globals['_TRAMPOLINEPAYREQUEST']._serialized_start=1283 - _globals['_TRAMPOLINEPAYREQUEST']._serialized_end=1447 - _globals['_TRAMPOLINEPAYRESPONSE']._serialized_start=1450 - _globals['_TRAMPOLINEPAYRESPONSE']._serialized_end=1663 - _globals['_TRAMPOLINEPAYRESPONSE_PAYSTATUS']._serialized_start=1626 - _globals['_TRAMPOLINEPAYRESPONSE_PAYSTATUS']._serialized_end=1663 - _globals['_LSPINVOICEREQUEST']._serialized_start=1665 - _globals['_LSPINVOICEREQUEST']._serialized_end=1772 - _globals['_LSPINVOICERESPONSE']._serialized_start=1774 - _globals['_LSPINVOICERESPONSE']._serialized_end=1899 - _globals['_NODEEVENTSREQUEST']._serialized_start=1901 - _globals['_NODEEVENTSREQUEST']._serialized_end=1920 - _globals['_NODEEVENT']._serialized_start=1922 - _globals['_NODEEVENT']._serialized_end=1991 - _globals['_INVOICEPAID']._serialized_start=1994 - _globals['_INVOICEPAID']._serialized_end=2140 - _globals['_NODE']._serialized_start=2143 - _globals['_NODE']._serialized_end=2736 - _globals['_HSM']._serialized_start=2738 - _globals['_HSM']._serialized_end=2853 + _globals['_STREAMINCOMINGFILTER']._serialized_start=543 + _globals['_STREAMINCOMINGFILTER']._serialized_end=565 + _globals['_TLVFIELD']._serialized_start=567 + _globals['_TLVFIELD']._serialized_end=606 + _globals['_OFFCHAINPAYMENT']._serialized_start=609 + _globals['_OFFCHAINPAYMENT']._serialized_end=774 + _globals['_INCOMINGPAYMENT']._serialized_start=776 + _globals['_INCOMINGPAYMENT']._serialized_end=853 + _globals['_STREAMLOGREQUEST']._serialized_start=855 + _globals['_STREAMLOGREQUEST']._serialized_end=873 + _globals['_LOGENTRY']._serialized_start=875 + _globals['_LOGENTRY']._serialized_end=899 + _globals['_SIGNERSTATEENTRY']._serialized_start=901 + _globals['_SIGNERSTATEENTRY']._serialized_end=964 + _globals['_PENDINGREQUEST']._serialized_start=966 + _globals['_PENDINGREQUEST']._serialized_end=1080 + _globals['_NODECONFIG']._serialized_start=1082 + _globals['_NODECONFIG']._serialized_end=1143 + _globals['_GLCONFIG']._serialized_start=1145 + _globals['_GLCONFIG']._serialized_end=1178 + _globals['_STARTUPMESSAGE']._serialized_start=1180 + _globals['_STARTUPMESSAGE']._serialized_end=1231 + _globals['_STREAMCUSTOMMSGREQUEST']._serialized_start=1233 + _globals['_STREAMCUSTOMMSGREQUEST']._serialized_end=1257 + _globals['_CUSTOMMSG']._serialized_start=1259 + _globals['_CUSTOMMSG']._serialized_end=1304 + _globals['_TRAMPOLINEPAYREQUEST']._serialized_start=1307 + _globals['_TRAMPOLINEPAYREQUEST']._serialized_end=1471 + _globals['_TRAMPOLINEPAYRESPONSE']._serialized_start=1474 + _globals['_TRAMPOLINEPAYRESPONSE']._serialized_end=1687 + _globals['_TRAMPOLINEPAYRESPONSE_PAYSTATUS']._serialized_start=1650 + _globals['_TRAMPOLINEPAYRESPONSE_PAYSTATUS']._serialized_end=1687 + _globals['_LSPINVOICEREQUEST']._serialized_start=1689 + _globals['_LSPINVOICEREQUEST']._serialized_end=1796 + _globals['_LSPINVOICERESPONSE']._serialized_start=1798 + _globals['_LSPINVOICERESPONSE']._serialized_end=1923 + _globals['_NODEEVENTSREQUEST']._serialized_start=1925 + _globals['_NODEEVENTSREQUEST']._serialized_end=1944 + _globals['_NODEEVENT']._serialized_start=1946 + _globals['_NODEEVENT']._serialized_end=2015 + _globals['_INVOICEPAID']._serialized_start=2018 + _globals['_INVOICEPAID']._serialized_end=2164 + _globals['_NODE']._serialized_start=2167 + _globals['_NODE']._serialized_end=2845 + _globals['_HSM']._serialized_start=2847 + _globals['_HSM']._serialized_end=2962 # @@protoc_insertion_point(module_scope) diff --git a/libs/gl-client-py/glclient/greenlight_pb2.pyi b/libs/gl-client-py/glclient/greenlight_pb2.pyi index 0c58312fb..a09488cbf 100644 --- a/libs/gl-client-py/glclient/greenlight_pb2.pyi +++ b/libs/gl-client-py/glclient/greenlight_pb2.pyi @@ -160,6 +160,18 @@ class Amount(_message.Message): Global___Amount: _TypeAlias = Amount # noqa: Y015 +@_typing.final +class StreamIncomingFilter(_message.Message): + """Options to stream_incoming to specify what to stream.""" + + DESCRIPTOR: _descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +Global___StreamIncomingFilter: _TypeAlias = StreamIncomingFilter # noqa: Y015 + @_typing.final class TlvField(_message.Message): DESCRIPTOR: _descriptor.Descriptor diff --git a/libs/gl-client-py/glclient/greenlight_pb2_grpc.py b/libs/gl-client-py/glclient/greenlight_pb2_grpc.py index d53fa077a..b95e8c9e0 100644 --- a/libs/gl-client-py/glclient/greenlight_pb2_grpc.py +++ b/libs/gl-client-py/glclient/greenlight_pb2_grpc.py @@ -54,6 +54,11 @@ def __init__(self, channel): request_serializer=glclient_dot_greenlight__pb2.LspInvoiceRequest.SerializeToString, response_deserializer=glclient_dot_greenlight__pb2.LspInvoiceResponse.FromString, _registered_method=True) + self.StreamIncoming = channel.unary_stream( + '/greenlight.Node/StreamIncoming', + request_serializer=glclient_dot_greenlight__pb2.StreamIncomingFilter.SerializeToString, + response_deserializer=glclient_dot_greenlight__pb2.IncomingPayment.FromString, + _registered_method=True) self.StreamLog = channel.unary_stream( '/greenlight.Node/StreamLog', request_serializer=glclient_dot_greenlight__pb2.StreamLogRequest.SerializeToString, @@ -117,13 +122,18 @@ def LspInvoice(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') - def StreamLog(self, request, context): + def StreamIncoming(self, request, context): """Stream incoming payments Currently includes off-chain payments received matching an invoice or spontaneus paymens through keysend. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') - Stream the logs as they are produced by the node + def StreamLog(self, request, context): + """Stream the logs as they are produced by the node Mainly intended for debugging clients by tailing the log as they are written on the node. The logs start streaming from @@ -200,6 +210,11 @@ def add_NodeServicer_to_server(servicer, server): request_deserializer=glclient_dot_greenlight__pb2.LspInvoiceRequest.FromString, response_serializer=glclient_dot_greenlight__pb2.LspInvoiceResponse.SerializeToString, ), + 'StreamIncoming': grpc.unary_stream_rpc_method_handler( + servicer.StreamIncoming, + request_deserializer=glclient_dot_greenlight__pb2.StreamIncomingFilter.FromString, + response_serializer=glclient_dot_greenlight__pb2.IncomingPayment.SerializeToString, + ), 'StreamLog': grpc.unary_stream_rpc_method_handler( servicer.StreamLog, request_deserializer=glclient_dot_greenlight__pb2.StreamLogRequest.FromString, @@ -288,6 +303,33 @@ def LspInvoice(request, metadata, _registered_method=True) + @staticmethod + def StreamIncoming(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_stream( + request, + target, + '/greenlight.Node/StreamIncoming', + glclient_dot_greenlight__pb2.StreamIncomingFilter.SerializeToString, + glclient_dot_greenlight__pb2.IncomingPayment.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + @staticmethod def StreamLog(request, target, diff --git a/libs/gl-client-py/glclient/greenlight_pb2_grpc.pyi b/libs/gl-client-py/glclient/greenlight_pb2_grpc.pyi index 12f961281..0ed792eb3 100644 --- a/libs/gl-client-py/glclient/greenlight_pb2_grpc.pyi +++ b/libs/gl-client-py/glclient/greenlight_pb2_grpc.pyi @@ -52,13 +52,14 @@ class NodeStub: """Create an invoice to request an incoming payment. Includes LSP negotiation to open a channel on-demand when needed. """ - StreamLog: _grpc.UnaryStreamMultiCallable[_greenlight_pb2.StreamLogRequest, _greenlight_pb2.LogEntry] + StreamIncoming: _grpc.UnaryStreamMultiCallable[_greenlight_pb2.StreamIncomingFilter, _greenlight_pb2.IncomingPayment] """Stream incoming payments Currently includes off-chain payments received matching an invoice or spontaneus paymens through keysend. - - Stream the logs as they are produced by the node + """ + StreamLog: _grpc.UnaryStreamMultiCallable[_greenlight_pb2.StreamLogRequest, _greenlight_pb2.LogEntry] + """Stream the logs as they are produced by the node Mainly intended for debugging clients by tailing the log as they are written on the node. The logs start streaming from @@ -121,13 +122,14 @@ class NodeAsyncStub(NodeStub): """Create an invoice to request an incoming payment. Includes LSP negotiation to open a channel on-demand when needed. """ - StreamLog: _aio.UnaryStreamMultiCallable[_greenlight_pb2.StreamLogRequest, _greenlight_pb2.LogEntry] # type: ignore[assignment] + StreamIncoming: _aio.UnaryStreamMultiCallable[_greenlight_pb2.StreamIncomingFilter, _greenlight_pb2.IncomingPayment] # type: ignore[assignment] """Stream incoming payments Currently includes off-chain payments received matching an invoice or spontaneus paymens through keysend. - - Stream the logs as they are produced by the node + """ + StreamLog: _aio.UnaryStreamMultiCallable[_greenlight_pb2.StreamLogRequest, _greenlight_pb2.LogEntry] # type: ignore[assignment] + """Stream the logs as they are produced by the node Mainly intended for debugging clients by tailing the log as they are written on the node. The logs start streaming from @@ -195,17 +197,24 @@ class NodeServicer(metaclass=_abc_1.ABCMeta): """ @_abc_1.abstractmethod - def StreamLog( + def StreamIncoming( self, - request: _greenlight_pb2.StreamLogRequest, + request: _greenlight_pb2.StreamIncomingFilter, context: _ServicerContext, - ) -> _typing.Union[_abc.Iterator[_greenlight_pb2.LogEntry], _abc.AsyncIterator[_greenlight_pb2.LogEntry]]: + ) -> _typing.Union[_abc.Iterator[_greenlight_pb2.IncomingPayment], _abc.AsyncIterator[_greenlight_pb2.IncomingPayment]]: """Stream incoming payments Currently includes off-chain payments received matching an invoice or spontaneus paymens through keysend. + """ - Stream the logs as they are produced by the node + @_abc_1.abstractmethod + def StreamLog( + self, + request: _greenlight_pb2.StreamLogRequest, + context: _ServicerContext, + ) -> _typing.Union[_abc.Iterator[_greenlight_pb2.LogEntry], _abc.AsyncIterator[_greenlight_pb2.LogEntry]]: + """Stream the logs as they are produced by the node Mainly intended for debugging clients by tailing the log as they are written on the node. The logs start streaming from diff --git a/libs/gl-client-py/src/node.rs b/libs/gl-client-py/src/node.rs index 3a94414cd..0fbfc8145 100644 --- a/libs/gl-client-py/src/node.rs +++ b/libs/gl-client-py/src/node.rs @@ -40,6 +40,15 @@ impl Node { Ok(LogStream { inner: stream }) } + fn stream_incoming(&self, args: &[u8]) -> PyResult { + let req = pb::StreamIncomingFilter::decode(args).map_err(error_decoding_request)?; + + let stream = exec(self.client.clone().stream_incoming(req)) + .map(|x| x.into_inner()) + .map_err(error_starting_stream)?; + Ok(IncomingStream { inner: stream }) + } + fn stream_custommsg(&self, args: &[u8]) -> PyResult { let req = pb::StreamCustommsgRequest::decode(args).map_err(error_decoding_request)?; let stream = exec(self.client.clone().stream_custommsg(req)) @@ -137,6 +146,18 @@ impl LogStream { } } +#[pyclass] +struct IncomingStream { + inner: tonic::codec::Streaming, +} + +#[pymethods] +impl IncomingStream { + fn next(&mut self) -> PyResult>> { + convert_stream_entry(exec(async { self.inner.message().await })) + } +} + #[pyclass] struct CustommsgStream { inner: tonic::codec::Streaming, diff --git a/libs/gl-client/.resources/proto/glclient/greenlight.proto b/libs/gl-client/.resources/proto/glclient/greenlight.proto index ba9528a6d..94d2cfacd 100644 --- a/libs/gl-client/.resources/proto/glclient/greenlight.proto +++ b/libs/gl-client/.resources/proto/glclient/greenlight.proto @@ -25,6 +25,7 @@ service Node { // // Currently includes off-chain payments received matching an // invoice or spontaneus paymens through keysend. + rpc StreamIncoming(StreamIncomingFilter) returns (stream IncomingPayment) {} // Stream the logs as they are produced by the node // @@ -120,6 +121,9 @@ message Amount { } } +// Options to stream_incoming to specify what to stream. +message StreamIncomingFilter { +} message TlvField { uint64 type = 1; diff --git a/libs/gl-plugin/.resources/proto/glclient/greenlight.proto b/libs/gl-plugin/.resources/proto/glclient/greenlight.proto index 7ed7b145c..94d2cfacd 100644 --- a/libs/gl-plugin/.resources/proto/glclient/greenlight.proto +++ b/libs/gl-plugin/.resources/proto/glclient/greenlight.proto @@ -21,6 +21,12 @@ service Node { // negotiation to open a channel on-demand when needed. rpc LspInvoice(LspInvoiceRequest) returns (LspInvoiceResponse) {} + // Stream incoming payments + // + // Currently includes off-chain payments received matching an + // invoice or spontaneus paymens through keysend. + rpc StreamIncoming(StreamIncomingFilter) returns (stream IncomingPayment) {} + // Stream the logs as they are produced by the node // // Mainly intended for debugging clients by tailing the log as @@ -115,6 +121,10 @@ message Amount { } } +// Options to stream_incoming to specify what to stream. +message StreamIncomingFilter { +} + message TlvField { uint64 type = 1; // length is implied since the value field carries its own diff --git a/libs/gl-plugin/src/node/mod.rs b/libs/gl-plugin/src/node/mod.rs index 41303fc80..1c559a066 100644 --- a/libs/gl-plugin/src/node/mod.rs +++ b/libs/gl-plugin/src/node/mod.rs @@ -595,6 +595,30 @@ impl Node for PluginNodeServer { Ok(Response::new(pb::Empty::default())) } + type StreamIncomingStream = ReceiverStream>; + + async fn stream_incoming( + &self, + _req: tonic::Request, + ) -> Result, Status> { + // TODO See if we can just return the broadcast::Receiver + // instead of pulling off broadcast and into an mpsc. + let (tx, rx) = mpsc::channel(1); + let mut bcast = self.events.subscribe(); + tokio::spawn(async move { + while let Ok(p) = bcast.recv().await { + match p { + super::Event::IncomingPayment(p) => { + let _ = tx.send(Ok(p)).await; + } + _ => {} + } + } + }); + + return Ok(Response::new(ReceiverStream::new(rx))); + } + type StreamNodeEventsStream = ReceiverStream>; async fn stream_node_events( diff --git a/libs/gl-plugin/src/node/wrapper.rs b/libs/gl-plugin/src/node/wrapper.rs index a4e125f74..6c573daea 100644 --- a/libs/gl-plugin/src/node/wrapper.rs +++ b/libs/gl-plugin/src/node/wrapper.rs @@ -1191,9 +1191,9 @@ impl WrappedNodeServer { } use crate::pb::{ - node_server::Node as GlNode, Custommsg, Empty, HsmRequest, HsmResponse, LogEntry, - LspInvoiceRequest, LspInvoiceResponse, NodeEvent, NodeEventsRequest, StreamCustommsgRequest, - StreamLogRequest, + node_server::Node as GlNode, Custommsg, Empty, HsmRequest, HsmResponse, IncomingPayment, + LogEntry, LspInvoiceRequest, LspInvoiceResponse, NodeEvent, NodeEventsRequest, + StreamCustommsgRequest, StreamIncomingFilter, StreamLogRequest, }; #[tonic::async_trait] @@ -1201,6 +1201,7 @@ impl GlNode for WrappedNodeServer { type StreamCustommsgStream = ReceiverStream>; type StreamHsmRequestsStream = ReceiverStream>; type StreamLogStream = ReceiverStream>; + type StreamIncomingStream = ReceiverStream>; type StreamNodeEventsStream = ReceiverStream>; async fn lsp_invoice( @@ -1210,6 +1211,13 @@ impl GlNode for WrappedNodeServer { self.node_server.lsp_invoice(req).await } + async fn stream_incoming( + &self, + req: tonic::Request, + ) -> Result, Status> { + self.node_server.stream_incoming(req).await + } + async fn respond_hsm_request( &self, req: Request, diff --git a/libs/gl-signerproxy/.resources/proto/glclient/greenlight.proto b/libs/gl-signerproxy/.resources/proto/glclient/greenlight.proto index bc0651ca3..a75224793 100644 --- a/libs/gl-signerproxy/.resources/proto/glclient/greenlight.proto +++ b/libs/gl-signerproxy/.resources/proto/glclient/greenlight.proto @@ -20,6 +20,7 @@ service Node { // // Currently includes off-chain payments received matching an // invoice or spontaneus paymens through keysend. + rpc StreamIncoming(StreamIncomingFilter) returns (stream IncomingPayment) {} // Stream the logs as they are produced by the node // @@ -108,6 +109,8 @@ message Amount { } // Options to stream_incoming to specify what to stream. +message StreamIncomingFilter { +} message TlvField { uint64 type = 1; diff --git a/libs/proto/glclient/greenlight.proto b/libs/proto/glclient/greenlight.proto index a643717b9..9454ec4c3 100644 --- a/libs/proto/glclient/greenlight.proto +++ b/libs/proto/glclient/greenlight.proto @@ -25,6 +25,7 @@ service Node { // // Currently includes off-chain payments received matching an // invoice or spontaneus paymens through keysend. + rpc StreamIncoming(StreamIncomingFilter) returns (stream IncomingPayment) {} // Stream the logs as they are produced by the node // @@ -120,6 +121,9 @@ message Amount { } } +// Options to stream_incoming to specify what to stream. +message StreamIncomingFilter { +} message TlvField { uint64 type = 1; From 0a13ef0a5e6537ce5a285d8739d7fa6a5a8b5fc3 Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Wed, 4 Mar 2026 12:22:54 +0100 Subject: [PATCH 3/3] gl-client: Fix initial VLS state sync to nodelet Create prestate_sketch from incoming state (nodelet's view) instead of the signer's merged state. This ensures that initial VLS state created during Signer::new() is properly sent back to the nodelet on the first request, fixing the issue where nodes/, nodestates/, allowlists/, and trackers/ entries were never synced to the tower. --- libs/gl-client/src/signer/mod.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/libs/gl-client/src/signer/mod.rs b/libs/gl-client/src/signer/mod.rs index d5522f667..f79a80d5c 100644 --- a/libs/gl-client/src/signer/mod.rs +++ b/libs/gl-client/src/signer/mod.rs @@ -488,14 +488,19 @@ impl Signer { async fn process_request(&self, req: HsmRequest) -> Result { debug!("Processing request {:?}", req); - let diff: crate::persist::State = req.signer_state.clone().into(); + let incoming_state: crate::persist::State = req.signer_state.clone().into(); - let (prestate_sketch, prestate_log) = { + // Create sketch from incoming state (nodelet's view) so we can + // send back any entries the nodelet doesn't know about yet, + // including the initial VLS state created during Signer::new(). + let prestate_sketch = incoming_state.sketch(); + + let prestate_log = { debug!("Updating local signer state with state from node"); let mut state = self.state.lock().map_err(|e| { Error::Other(anyhow!("Failed to acquire state lock: {:?}", e)) })?; - let merge_res = state.merge(&diff).map_err(|e| { + let merge_res = state.merge(&incoming_state).map_err(|e| { Error::Other(anyhow!("Failed to merge signer state: {:?}", e)) })?; if merge_res.has_conflicts() { @@ -505,12 +510,9 @@ impl Signer { ); } trace!("Processing request {}", hex::encode(&req.raw)); - let log_state = serde_json::to_string(&*state).map_err(|e| { + serde_json::to_string(&*state).map_err(|e| { Error::Other(anyhow!("Failed to serialize signer state for logging: {:?}", e)) - })?; - - - (state.sketch(), log_state) + })? }; // The first two bytes represent the message type. Check that