This commit is contained in:
Arc
2026-01-10 14:50:19 +00:00
parent 6b233cc067
commit a061dda5c1
3 changed files with 93 additions and 84 deletions
+2 -2
View File
@@ -20,6 +20,7 @@ from .corelightning import CoreLightningWallet as CLightningWallet
from .corelightningrest import CoreLightningRestWallet from .corelightningrest import CoreLightningRestWallet
from .eclair import EclairWallet from .eclair import EclairWallet
from .fake import FakeWallet from .fake import FakeWallet
from .lightspark import LightsparkSparkWallet
from .lnbits import LNbitsWallet from .lnbits import LNbitsWallet
from .lndgrpc import LndWallet from .lndgrpc import LndWallet
from .lndrest import LndRestWallet from .lndrest import LndRestWallet
@@ -32,7 +33,6 @@ from .spark import SparkWallet
from .strike import StrikeWallet from .strike import StrikeWallet
from .void import VoidWallet from .void import VoidWallet
from .zbd import ZBDWallet from .zbd import ZBDWallet
from .lightspark import LightsparkSparkWallet
def set_funding_source(class_name: str | None = None) -> None: def set_funding_source(class_name: str | None = None) -> None:
@@ -70,6 +70,7 @@ __all__ = [
"FakeWallet", "FakeWallet",
"LNPayWallet", "LNPayWallet",
"LNbitsWallet", "LNbitsWallet",
"LightsparkSparkWallet",
"LnTipsWallet", "LnTipsWallet",
"LndRestWallet", "LndRestWallet",
"LndWallet", "LndWallet",
@@ -77,7 +78,6 @@ __all__ = [
"OpenNodeWallet", "OpenNodeWallet",
"PhoenixdWallet", "PhoenixdWallet",
"SparkWallet", "SparkWallet",
"LightsparkSparkWallet",
"StrikeWallet", "StrikeWallet",
"VoidWallet", "VoidWallet",
"ZBDWallet", "ZBDWallet",
+7 -3
View File
@@ -69,7 +69,7 @@ class LightsparkSparkWallet(Wallet):
except (httpx.RequestError, httpx.HTTPStatusError, json.JSONDecodeError) as exc: except (httpx.RequestError, httpx.HTTPStatusError, json.JSONDecodeError) as exc:
raise SparkSidecarError(f"Spark sidecar request failed: {exc}") from exc raise SparkSidecarError(f"Spark sidecar request failed: {exc}") from exc
if "error" in j and j["error"]: if j.get("error"):
raise SparkSidecarError(str(j["error"])) raise SparkSidecarError(str(j["error"]))
return j return j
@@ -114,7 +114,9 @@ class LightsparkSparkWallet(Wallet):
bolt11 = res.get("payment_request") bolt11 = res.get("payment_request")
checking_id = res.get("checking_id") checking_id = res.get("checking_id")
if not bolt11 or not checking_id: if not bolt11 or not checking_id:
raise SparkSidecarError("Spark sidecar invoice response missing fields.") raise SparkSidecarError(
"Spark sidecar invoice response missing fields."
)
self.pending_invoices.append(checking_id) self.pending_invoices.append(checking_id)
return InvoiceResponse( return InvoiceResponse(
@@ -133,7 +135,9 @@ class LightsparkSparkWallet(Wallet):
res = await self._request("POST", "/v1/payments", payload) res = await self._request("POST", "/v1/payments", payload)
checking_id = res.get("checking_id") checking_id = res.get("checking_id")
if not checking_id: if not checking_id:
raise SparkSidecarError("Spark sidecar payment response missing checking_id.") raise SparkSidecarError(
"Spark sidecar payment response missing checking_id."
)
status = res.get("status") status = res.get("status")
fee_msat = res.get("fee_msat") fee_msat = res.get("fee_msat")
ok = None ok = None
+84 -79
View File
@@ -1,170 +1,175 @@
import http from "node:http"; import http from 'node:http'
import { SparkWallet } from "@buildonspark/spark-sdk"; import {SparkWallet} from '@buildonspark/spark-sdk'
const PORT = parseInt(process.env.SPARK_SIDECAR_PORT || "8765", 10); const PORT = parseInt(process.env.SPARK_SIDECAR_PORT || '8765', 10)
const API_KEY = process.env.SPARK_SIDECAR_API_KEY || ""; const API_KEY = process.env.SPARK_SIDECAR_API_KEY || ''
const MNEMONIC = process.env.SPARK_MNEMONIC || ""; const MNEMONIC = process.env.SPARK_MNEMONIC || ''
const NETWORK = process.env.SPARK_NETWORK || "MAINNET"; const NETWORK = process.env.SPARK_NETWORK || 'MAINNET'
const ACCOUNT_NUMBER = process.env.SPARK_ACCOUNT_NUMBER const ACCOUNT_NUMBER = process.env.SPARK_ACCOUNT_NUMBER
? parseInt(process.env.SPARK_ACCOUNT_NUMBER, 10) ? parseInt(process.env.SPARK_ACCOUNT_NUMBER, 10)
: undefined; : undefined
if (!MNEMONIC) { if (!MNEMONIC) {
console.error("Missing SPARK_MNEMONIC for Spark sidecar."); console.error('Missing SPARK_MNEMONIC for Spark sidecar.')
process.exit(1); process.exit(1)
} }
let walletPromise; let walletPromise
async function getWallet() { async function getWallet() {
if (!walletPromise) { if (!walletPromise) {
walletPromise = SparkWallet.initialize({ walletPromise = SparkWallet.initialize({
mnemonicOrSeed: MNEMONIC, mnemonicOrSeed: MNEMONIC,
accountNumber: ACCOUNT_NUMBER, accountNumber: ACCOUNT_NUMBER,
options: { network: NETWORK }, options: {network: NETWORK}
}).then(({ wallet }) => wallet); }).then(({wallet}) => wallet)
} }
return walletPromise; return walletPromise
} }
function sendJson(res, statusCode, payload) { function sendJson(res, statusCode, payload) {
res.writeHead(statusCode, { "content-type": "application/json" }); res.writeHead(statusCode, {'content-type': 'application/json'})
res.end(JSON.stringify(payload)); res.end(JSON.stringify(payload))
} }
async function readJson(req) { async function readJson(req) {
const chunks = []; const chunks = []
for await (const chunk of req) { for await (const chunk of req) {
chunks.push(chunk); chunks.push(chunk)
} }
if (chunks.length === 0) { if (chunks.length === 0) {
return {}; return {}
} }
return JSON.parse(Buffer.concat(chunks).toString("utf8")); return JSON.parse(Buffer.concat(chunks).toString('utf8'))
} }
function feeToMsat(fee) { function feeToMsat(fee) {
if (!fee || fee.originalValue === undefined || !fee.originalUnit) { if (!fee || fee.originalValue === undefined || !fee.originalUnit) {
return null; return null
} }
const value = Number(fee.originalValue); const value = Number(fee.originalValue)
if (!Number.isFinite(value)) { if (!Number.isFinite(value)) {
return null; return null
} }
switch (fee.originalUnit) { switch (fee.originalUnit) {
case "MILLISATOSHI": case 'MILLISATOSHI':
return BigInt(Math.round(value)).toString(); return BigInt(Math.round(value)).toString()
case "SATOSHI": case 'SATOSHI':
return BigInt(Math.round(value * 1000)).toString(); return BigInt(Math.round(value * 1000)).toString()
case "BITCOIN": case 'BITCOIN':
return BigInt(Math.round(value * 100_000_000_000)).toString(); return BigInt(Math.round(value * 100_000_000_000)).toString()
default: default:
return BigInt(Math.round(value * 1000)).toString(); return BigInt(Math.round(value * 1000)).toString()
} }
} }
const server = http.createServer(async (req, res) => { const server = http.createServer(async (req, res) => {
const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`); const url = new URL(
req.url || '/',
`http://${req.headers.host || 'localhost'}`
)
if (API_KEY && req.headers["x-api-key"] !== API_KEY) { if (API_KEY && req.headers['x-api-key'] !== API_KEY) {
return sendJson(res, 401, { error: "Unauthorized" }); return sendJson(res, 401, {error: 'Unauthorized'})
} }
try { try {
if (req.method === "GET" && url.pathname === "/health") { if (req.method === 'GET' && url.pathname === '/health') {
return sendJson(res, 200, { status: "ok" }); return sendJson(res, 200, {status: 'ok'})
} }
if (req.method === "POST" && url.pathname === "/v1/balance") { if (req.method === 'POST' && url.pathname === '/v1/balance') {
const wallet = await getWallet(); const wallet = await getWallet()
const balance = await wallet.getBalance(); const balance = await wallet.getBalance()
const sats = BigInt(balance.balance); const sats = BigInt(balance.balance)
return sendJson(res, 200, { return sendJson(res, 200, {
balance_sats: sats.toString(), balance_sats: sats.toString(),
balance_msat: (sats * 1000n).toString(), balance_msat: (sats * 1000n).toString()
}); })
} }
if (req.method === "POST" && url.pathname === "/v1/invoices") { if (req.method === 'POST' && url.pathname === '/v1/invoices') {
const wallet = await getWallet(); const wallet = await getWallet()
const body = await readJson(req); const body = await readJson(req)
const amountSats = Number(body.amount_sats); const amountSats = Number(body.amount_sats)
if (!Number.isFinite(amountSats) || amountSats < 0) { if (!Number.isFinite(amountSats) || amountSats < 0) {
return sendJson(res, 400, { error: "Invalid amount_sats" }); return sendJson(res, 400, {error: 'Invalid amount_sats'})
} }
const invoice = await wallet.createLightningInvoice({ const invoice = await wallet.createLightningInvoice({
amountSats, amountSats,
memo: body.memo || undefined, memo: body.memo || undefined,
descriptionHash: body.description_hash || undefined, descriptionHash: body.description_hash || undefined,
expirySeconds: body.expiry_seconds || undefined, expirySeconds: body.expiry_seconds || undefined
}); })
return sendJson(res, 200, { return sendJson(res, 200, {
checking_id: invoice.id, checking_id: invoice.id,
payment_request: invoice.invoice.encodedInvoice, payment_request: invoice.invoice.encodedInvoice,
payment_hash: invoice.invoice.paymentHash, payment_hash: invoice.invoice.paymentHash,
status: invoice.status, status: invoice.status,
preimage: invoice.paymentPreimage || null, preimage: invoice.paymentPreimage || null
}); })
} }
if (req.method === "POST" && url.pathname === "/v1/payments") { if (req.method === 'POST' && url.pathname === '/v1/payments') {
const wallet = await getWallet(); const wallet = await getWallet()
const body = await readJson(req); const body = await readJson(req)
const bolt11 = body.bolt11; const bolt11 = body.bolt11
if (!bolt11) { if (!bolt11) {
return sendJson(res, 400, { error: "Missing bolt11" }); return sendJson(res, 400, {error: 'Missing bolt11'})
} }
const maxFeeSats = Number(body.max_fee_sats || 0); const maxFeeSats = Number(body.max_fee_sats || 0)
const amountSatsToSend = body.amount_sats ? Number(body.amount_sats) : undefined; const amountSatsToSend = body.amount_sats
? Number(body.amount_sats)
: undefined
const payment = await wallet.payLightningInvoice({ const payment = await wallet.payLightningInvoice({
invoice: bolt11, invoice: bolt11,
maxFeeSats, maxFeeSats,
amountSatsToSend, amountSatsToSend
}); })
return sendJson(res, 200, { return sendJson(res, 200, {
checking_id: payment.id, checking_id: payment.id,
status: payment.status, status: payment.status,
fee_msat: feeToMsat(payment.fee), fee_msat: feeToMsat(payment.fee),
preimage: payment.paymentPreimage || null, preimage: payment.paymentPreimage || null
}); })
} }
const parts = url.pathname.split("/").filter(Boolean); const parts = url.pathname.split('/').filter(Boolean)
if (parts.length === 3 && parts[0] === "v1" && parts[1] === "invoices") { if (parts.length === 3 && parts[0] === 'v1' && parts[1] === 'invoices') {
const wallet = await getWallet(); const wallet = await getWallet()
const invoice = await wallet.getLightningReceiveRequest(parts[2]); const invoice = await wallet.getLightningReceiveRequest(parts[2])
if (!invoice) { if (!invoice) {
return sendJson(res, 404, { error: "Not found" }); return sendJson(res, 404, {error: 'Not found'})
} }
return sendJson(res, 200, { return sendJson(res, 200, {
checking_id: invoice.id, checking_id: invoice.id,
status: invoice.status, status: invoice.status,
payment_hash: invoice.invoice.paymentHash, payment_hash: invoice.invoice.paymentHash,
preimage: invoice.paymentPreimage || null, preimage: invoice.paymentPreimage || null
}); })
} }
if (parts.length === 3 && parts[0] === "v1" && parts[1] === "payments") { if (parts.length === 3 && parts[0] === 'v1' && parts[1] === 'payments') {
const wallet = await getWallet(); const wallet = await getWallet()
const payment = await wallet.getLightningSendRequest(parts[2]); const payment = await wallet.getLightningSendRequest(parts[2])
if (!payment) { if (!payment) {
return sendJson(res, 404, { error: "Not found" }); return sendJson(res, 404, {error: 'Not found'})
} }
return sendJson(res, 200, { return sendJson(res, 200, {
checking_id: payment.id, checking_id: payment.id,
status: payment.status, status: payment.status,
fee_msat: feeToMsat(payment.fee), fee_msat: feeToMsat(payment.fee),
preimage: payment.paymentPreimage || null, preimage: payment.paymentPreimage || null
}); })
} }
return sendJson(res, 404, { error: "Not found" }); return sendJson(res, 404, {error: 'Not found'})
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : String(error); const message = error instanceof Error ? error.message : String(error)
return sendJson(res, 500, { error: message }); return sendJson(res, 500, {error: message})
} }
}); })
server.listen(PORT, () => { server.listen(PORT, () => {
console.log(`Spark sidecar listening on :${PORT}`); console.log(`Spark sidecar listening on :${PORT}`)
}); })