Add BTCPay Bitcoin payment integration
- Store ID: D98SMWWKGxgF5QxvQ8dTHc1YrG8Po4Jho5omDvbLd7rT - API endpoints: /api/btcpay/create-invoice, /api/webhook/btcpay, /api/btcpay/invoice/:id - Upgrade flow now creates real BTCPay invoices instead of mock payments - Webhook auto-activates PRO subscription on payment settlement - /month pricing via Bitcoin on-chain
This commit is contained in:
206
server.ts
206
server.ts
@@ -16,6 +16,54 @@ import {
|
|||||||
import { HardwareProfiles, DEFAULT_ANDROID_APPS } from './src/data/deviceProfiles.js';
|
import { HardwareProfiles, DEFAULT_ANDROID_APPS } from './src/data/deviceProfiles.js';
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
// ==========================================
|
||||||
|
// BTCPAY SERVER INTEGRATION
|
||||||
|
// ==========================================
|
||||||
|
const BTCPAY_URL = 'https://btcpay.thetempleofdoom.com';
|
||||||
|
const BTCPAY_STORE_ID = 'D98SMWWKGxgF5QxvQ8dTHc1YrG8Po4Jho5omDvbLd7rT';
|
||||||
|
const BTCPAY_API_KEY = '9cd2f26f2d73f6f670df3a80138e889e08c89b33';
|
||||||
|
const SUBSCRIPTION_PRICE_USD = 5.00; // $5/month for Pro tier
|
||||||
|
|
||||||
|
// Track pending BTCPay invoices: invoiceId -> { userId, targetPlan }
|
||||||
|
const pendingInvoices: Record<string, { targetPlan: string; createdAt: string }> = {};
|
||||||
|
|
||||||
|
// BTCPay helper: create invoice
|
||||||
|
async function createBtcpayInvoice(amount: number, metadata: Record<string, any>): Promise<any> {
|
||||||
|
const https = await import('https');
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const agent = new https.Agent({ rejectUnauthorized: false });
|
||||||
|
const payload = JSON.stringify({
|
||||||
|
amount: amount.toString(),
|
||||||
|
currency: 'USD',
|
||||||
|
metadata,
|
||||||
|
paymentMethods: ['BTC-CHAIN'],
|
||||||
|
checkout: { redirectAutomatically: true },
|
||||||
|
});
|
||||||
|
const url = new URL(`${BTCPAY_URL}/api/v1/stores/${BTCPAY_STORE_ID}/invoices`);
|
||||||
|
const req = https.request({
|
||||||
|
hostname: url.hostname,
|
||||||
|
port: url.port || 443,
|
||||||
|
path: url.pathname,
|
||||||
|
method: 'POST',
|
||||||
|
agent,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `token ${BTCPAY_API_KEY}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
}, (res) => {
|
||||||
|
let data = '';
|
||||||
|
res.on('data', (chunk) => data += chunk);
|
||||||
|
res.on('end', () => {
|
||||||
|
try { resolve(JSON.parse(data)); }
|
||||||
|
catch (e) { reject(new Error(`BTCPay parse error: ${data}`)); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on('error', reject);
|
||||||
|
req.write(payload);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
@@ -147,33 +195,57 @@ app.get('/api/user/subscription', (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 3. Upgrade or Switch Subscription Tier ($5/mo for 5 VMs)
|
// 3. Upgrade or Switch Subscription Tier ($5/mo for 5 VMs)
|
||||||
app.post('/api/user/subscription/upgrade', (req, res) => {
|
app.post('/api/user/subscription/upgrade', async (req, res) => {
|
||||||
const { targetPlan, paymentMethod } = req.body;
|
const { targetPlan } = req.body;
|
||||||
|
|
||||||
if (targetPlan === 'pro') {
|
if (targetPlan === 'pro') {
|
||||||
userSubscription = {
|
// Create BTCPay invoice instead of mock upgrade
|
||||||
|
try {
|
||||||
|
const metadata = {
|
||||||
|
orderId: `droidfleet-${Date.now()}`,
|
||||||
plan: 'pro',
|
plan: 'pro',
|
||||||
maxVms: 5, // 5 VMs for $5/mo
|
itemDesc: 'DroidFleet Visor - PRO Tier Subscription',
|
||||||
pricePerMonth: 5,
|
buyerEmail: 'droidfleet-user',
|
||||||
activeVmCount: virtualMachines.length,
|
|
||||||
renewsAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
paymentMethod: paymentMethod || 'Visa ending in 4242',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const invoice = await createBtcpayInvoice(SUBSCRIPTION_PRICE_USD, metadata);
|
||||||
|
|
||||||
|
pendingInvoices[invoice.id] = {
|
||||||
|
targetPlan: 'pro',
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
message: 'Pay with Bitcoin to activate PRO tier.',
|
||||||
|
invoiceId: invoice.id,
|
||||||
|
checkoutUrl: invoice.checkoutLink,
|
||||||
|
amount: `$${SUBSCRIPTION_PRICE_USD}`,
|
||||||
|
plan: 'pro',
|
||||||
|
subscription: {
|
||||||
|
...userSubscription,
|
||||||
|
status: 'awaiting_payment',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('BTCPay error:', err.message);
|
||||||
|
return res.status(500).json({ error: 'Bitcoin payment unavailable', detail: err.message });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Downgrading to free is instant
|
||||||
userSubscription = {
|
userSubscription = {
|
||||||
plan: 'free',
|
plan: 'free',
|
||||||
maxVms: 1, // 1 Free VM
|
maxVms: 1,
|
||||||
pricePerMonth: 0,
|
pricePerMonth: 0,
|
||||||
activeVmCount: virtualMachines.length,
|
activeVmCount: virtualMachines.length,
|
||||||
renewsAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
renewsAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
paymentMethod: 'Free Tier Active',
|
paymentMethod: 'Free Tier Active',
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
res.json({
|
return res.json({
|
||||||
message: `Successfully updated subscription to ${userSubscription.plan.toUpperCase()} tier!`,
|
message: `Successfully downgraded to FREE tier.`,
|
||||||
subscription: userSubscription,
|
subscription: userSubscription,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4. List All Virtual Machines
|
// 4. List All Virtual Machines
|
||||||
@@ -783,6 +855,116 @@ app.get('/api/openapi.json', (req, res) => {
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
// MODEL CONTEXT PROTOCOL (MCP) SERVER ENGINE
|
// MODEL CONTEXT PROTOCOL (MCP) SERVER ENGINE
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
|
// BTCPay Invoice Creation Endpoint
|
||||||
|
app.post('/api/btcpay/create-invoice', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { targetPlan } = req.body;
|
||||||
|
if (targetPlan !== 'pro') {
|
||||||
|
return res.status(400).json({ error: 'Invalid plan. Only "pro" is available.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadata = {
|
||||||
|
orderId: `droidfleet-${Date.now()}`,
|
||||||
|
plan: targetPlan,
|
||||||
|
itemDesc: `DroidFleet Visor - ${targetPlan.toUpperCase()} Tier Subscription`,
|
||||||
|
buyerEmail: 'droidfleet-user',
|
||||||
|
};
|
||||||
|
|
||||||
|
const invoice = await createBtcpayInvoice(SUBSCRIPTION_PRICE_USD, metadata);
|
||||||
|
|
||||||
|
// Track this invoice
|
||||||
|
pendingInvoices[invoice.id] = {
|
||||||
|
targetPlan,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
invoiceId: invoice.id,
|
||||||
|
checkoutUrl: invoice.checkoutLink,
|
||||||
|
amount: `$${SUBSCRIPTION_PRICE_USD}`,
|
||||||
|
plan: targetPlan,
|
||||||
|
message: 'Pay with Bitcoin. Your subscription activates on payment confirmation.',
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('BTCPay invoice error:', err.message);
|
||||||
|
res.status(500).json({ error: 'Failed to create Bitcoin invoice', detail: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// BTCPay Webhook Endpoint (payment confirmation)
|
||||||
|
app.post('/api/webhook/btcpay', (req, res) => {
|
||||||
|
try {
|
||||||
|
const body = req.body;
|
||||||
|
const eventType = body?.type || '';
|
||||||
|
const invoiceId = body?.invoiceId || '';
|
||||||
|
const metadata = body?.metadata || {};
|
||||||
|
|
||||||
|
console.log(`BTCPay webhook: ${eventType} for invoice ${invoiceId}`);
|
||||||
|
|
||||||
|
if (eventType === 'InvoiceSettled' || eventType === 'InvoiceProcessing') {
|
||||||
|
const pending = pendingInvoices[invoiceId];
|
||||||
|
if (pending) {
|
||||||
|
// Activate the Pro subscription
|
||||||
|
userSubscription = {
|
||||||
|
plan: pending.targetPlan,
|
||||||
|
maxVms: 5,
|
||||||
|
pricePerMonth: SUBSCRIPTION_PRICE_USD,
|
||||||
|
activeVmCount: virtualMachines.length,
|
||||||
|
renewsAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
paymentMethod: `Bitcoin (BTCPay invoice ${invoiceId.slice(0, 12)}...)`,
|
||||||
|
};
|
||||||
|
console.log(`Subscription activated: ${pending.targetPlan} via BTCPay invoice ${invoiceId}`);
|
||||||
|
delete pendingInvoices[invoiceId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always return 200 to prevent BTCPay retry spam
|
||||||
|
res.json({ status: 'ok' });
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Webhook error:', err.message);
|
||||||
|
res.status(200).json({ status: 'error_received_but_acked' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check invoice status
|
||||||
|
app.get('/api/btcpay/invoice/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const https = await import('https');
|
||||||
|
const agent = new https.Agent({ rejectUnauthorized: false });
|
||||||
|
const url = new URL(`${BTCPAY_URL}/api/v1/stores/${BTCPAY_STORE_ID}/invoices/${req.params.id}`);
|
||||||
|
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
https.get({
|
||||||
|
hostname: url.hostname,
|
||||||
|
port: url.port || 443,
|
||||||
|
path: url.pathname,
|
||||||
|
agent,
|
||||||
|
headers: { 'Authorization': `token ${BTCPAY_API_KEY}` },
|
||||||
|
}, (resp) => {
|
||||||
|
let data = '';
|
||||||
|
resp.on('data', (chunk) => data += chunk);
|
||||||
|
resp.on('end', () => {
|
||||||
|
try {
|
||||||
|
const inv = JSON.parse(data);
|
||||||
|
res.json({
|
||||||
|
invoiceId: inv.id,
|
||||||
|
status: inv.status,
|
||||||
|
amount: `${inv.amount} ${inv.currency}`,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).json({ error: 'Parse error' });
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}).on('error', (e) => { reject(e); });
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
const MCP_TOOLS_DEFINITIONS = [
|
const MCP_TOOLS_DEFINITIONS = [
|
||||||
{
|
{
|
||||||
name: 'list_vms',
|
name: 'list_vms',
|
||||||
|
|||||||
Reference in New Issue
Block a user