paytaca-cli 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -6
- package/dist/ai/autoRefill.d.ts +58 -0
- package/dist/ai/autoRefill.d.ts.map +1 -1
- package/dist/ai/autoRefill.js +193 -2
- package/dist/ai/autoRefill.js.map +1 -1
- package/dist/ai/config.d.ts +1 -0
- package/dist/ai/config.d.ts.map +1 -1
- package/dist/ai/config.js +1 -0
- package/dist/ai/config.js.map +1 -1
- package/dist/ai/credits.d.ts.map +1 -1
- package/dist/ai/credits.js +20 -11
- package/dist/ai/credits.js.map +1 -1
- package/dist/ai/images.d.ts +128 -0
- package/dist/ai/images.d.ts.map +1 -0
- package/dist/ai/images.js +399 -0
- package/dist/ai/images.js.map +1 -0
- package/dist/commands/ai.d.ts.map +1 -1
- package/dist/commands/ai.js +283 -7
- package/dist/commands/ai.js.map +1 -1
- package/dist/commands/update.d.ts +32 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +200 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/commands/wallet.js +1 -1
- package/dist/commands/wallet.js.map +1 -1
- package/dist/commands/web.d.ts +3 -0
- package/dist/commands/web.d.ts.map +1 -0
- package/dist/commands/web.js +71 -0
- package/dist/commands/web.js.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +29 -1
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools.d.ts +2 -0
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +288 -9
- package/dist/mcp/tools.js.map +1 -1
- package/dist/wallet/cauldron/swap.d.ts +15 -0
- package/dist/wallet/cauldron/swap.d.ts.map +1 -1
- package/dist/wallet/cauldron/swap.js +31 -0
- package/dist/wallet/cauldron/swap.js.map +1 -1
- package/dist/wallet/index.d.ts +16 -1
- package/dist/wallet/index.d.ts.map +1 -1
- package/dist/wallet/index.js +74 -8
- package/dist/wallet/index.js.map +1 -1
- package/dist/web/assets.d.ts +3 -0
- package/dist/web/assets.d.ts.map +1 -0
- package/dist/web/assets.js +3 -0
- package/dist/web/assets.js.map +1 -0
- package/dist/web/page.d.ts +2 -0
- package/dist/web/page.d.ts.map +1 -0
- package/dist/web/page.js +638 -0
- package/dist/web/page.js.map +1 -0
- package/dist/web/server.d.ts +79 -0
- package/dist/web/server.d.ts.map +1 -0
- package/dist/web/server.js +665 -0
- package/dist/web/server.js.map +1 -0
- package/package.json +2 -1
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { renderPage } from './page.js';
|
|
6
|
+
import { getBalanceView, getTokenBalances, getHistoryView, getReceiveAddressView, sendBch, sendToken, isValidBchAddress, isTokenAddress, } from '../core/wallet.js';
|
|
7
|
+
import { loadWalletRef, loadWallet, loadMnemonic } from '../wallet/index.js';
|
|
8
|
+
import { BCH_DERIVATION_PATH } from '../utils/network.js';
|
|
9
|
+
import { getBchUsdPrice } from '../utils/prices.js';
|
|
10
|
+
import { WalletNotConfiguredError } from '../core/context.js';
|
|
11
|
+
import { LIFT_TOKEN_ID } from '../ai/config.js';
|
|
12
|
+
import { getConfig, getWalletStatus, } from '../ai/client.js';
|
|
13
|
+
import { listPlans, formatDuration, } from '../ai/models.js';
|
|
14
|
+
import { getSessions, } from '../ai/credits.js';
|
|
15
|
+
import { buyPlan, } from '../ai/purchase.js';
|
|
16
|
+
import { readAutoRefillState, armAutoRefill, disarmAutoRefill, deleteAutoRefill, remainingBudget, autoRefillTick, } from '../ai/autoRefill.js';
|
|
17
|
+
import { createRefillTickDeps } from '../mcp/tools.js';
|
|
18
|
+
import { listImageModels, createImageOrder, fulfillImageOrder, getImageHistory, IMAGE_DIR, MEDIA_TYPE_EXTENSIONS, } from '../ai/images.js';
|
|
19
|
+
import { estimateSwap, executeSwap, formatQuote, estimateTokenNeededForBch, } from '../wallet/cauldron/swap.js';
|
|
20
|
+
import { fetchTokenData } from '../wallet/cauldron/api.js';
|
|
21
|
+
import { formatTokenAmount } from '../utils/format.js';
|
|
22
|
+
function defaultDeps(isChipnet, backendUrl) {
|
|
23
|
+
return {
|
|
24
|
+
async getWalletState() {
|
|
25
|
+
const wallet = loadWalletRef();
|
|
26
|
+
if (!wallet)
|
|
27
|
+
throw new WalletNotConfiguredError();
|
|
28
|
+
const walletHash = wallet.walletHash;
|
|
29
|
+
const [balance, tokens, config, status] = await Promise.all([
|
|
30
|
+
getBalanceView(isChipnet),
|
|
31
|
+
getTokenBalances(isChipnet),
|
|
32
|
+
getConfig({ backendUrl }),
|
|
33
|
+
getWalletStatus(walletHash, { backendUrl }),
|
|
34
|
+
]);
|
|
35
|
+
let bchPriceUsd = null;
|
|
36
|
+
try {
|
|
37
|
+
bchPriceUsd = await getBchUsdPrice(isChipnet);
|
|
38
|
+
}
|
|
39
|
+
catch { }
|
|
40
|
+
const lift = tokens.tokens.find((t) => t.category === LIFT_TOKEN_ID);
|
|
41
|
+
const sessions = getSessions(status).map((s) => ({
|
|
42
|
+
model: s.model_id || s.ai_model || null,
|
|
43
|
+
displayName: s.display_name || null,
|
|
44
|
+
active: s.model_active === true || s.session_active === true,
|
|
45
|
+
remainingSeconds: s.time_remaining_seconds ?? 0,
|
|
46
|
+
usedSeconds: s.time_used_seconds ?? 0,
|
|
47
|
+
creditsSeconds: s.time_credits_seconds ?? 0,
|
|
48
|
+
}));
|
|
49
|
+
const plans = listPlans(config).map((p) => ({
|
|
50
|
+
modelId: p.id,
|
|
51
|
+
displayName: p.displayName,
|
|
52
|
+
tiers: (p.plans || []).map((t) => ({
|
|
53
|
+
minutes: t.minutes,
|
|
54
|
+
durationDisplay: formatDuration(t.minutes),
|
|
55
|
+
priceUsd: t.price_usd,
|
|
56
|
+
priceSats: t.price_sats,
|
|
57
|
+
})),
|
|
58
|
+
}));
|
|
59
|
+
const autoRefill = readAutoRefillState();
|
|
60
|
+
let imageModels = [];
|
|
61
|
+
try {
|
|
62
|
+
imageModels = await listImageModels({ backendUrl });
|
|
63
|
+
}
|
|
64
|
+
catch { }
|
|
65
|
+
let imageHistory = [];
|
|
66
|
+
try {
|
|
67
|
+
const hist = await getImageHistory({ backendUrl, page: 1, pageSize: 20 });
|
|
68
|
+
imageHistory = (hist.data || []).map((e) => ({
|
|
69
|
+
...e,
|
|
70
|
+
filepath: localImageFile(e.id) ?? undefined,
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
catch { }
|
|
74
|
+
return {
|
|
75
|
+
walletHash,
|
|
76
|
+
bchPriceUsd,
|
|
77
|
+
network: balance.network,
|
|
78
|
+
balance: {
|
|
79
|
+
spendableSats: balance.spendableSats,
|
|
80
|
+
spendableBch: balance.spendableBch,
|
|
81
|
+
usd: balance.usd,
|
|
82
|
+
},
|
|
83
|
+
tokens: tokens.tokens,
|
|
84
|
+
lift: lift
|
|
85
|
+
? { category: lift.category, symbol: lift.symbol, displayBalance: lift.displayBalance, rawBalance: lift.rawBalance }
|
|
86
|
+
: null,
|
|
87
|
+
usage: sessions,
|
|
88
|
+
plans,
|
|
89
|
+
liftDiscountPercent: config.lift_payment_discount_percent ?? 0,
|
|
90
|
+
autoRefill: autoRefill
|
|
91
|
+
? { ...autoRefill, remainingMinutes: remainingBudget(autoRefill) }
|
|
92
|
+
: null,
|
|
93
|
+
imageModels,
|
|
94
|
+
imageHistory,
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
async getWalletHistory(page, type, tokenId) {
|
|
98
|
+
return getHistoryView({ page, recordType: type, tokenId }, isChipnet);
|
|
99
|
+
},
|
|
100
|
+
async getReceiveView(opts) {
|
|
101
|
+
return getReceiveAddressView({ index: opts.index, token: opts.token, category: opts.category, amount: opts.amount }, isChipnet);
|
|
102
|
+
},
|
|
103
|
+
async sendBchPayment(opts) {
|
|
104
|
+
let amountBch = opts.amount;
|
|
105
|
+
if (opts.currency === 'sats' || opts.currency === 'satoshis') {
|
|
106
|
+
amountBch = amountBch / 1e8;
|
|
107
|
+
}
|
|
108
|
+
else if (opts.currency === 'usd') {
|
|
109
|
+
const usdPrice = await getBchUsdPrice(isChipnet);
|
|
110
|
+
if (usdPrice === null)
|
|
111
|
+
throw new Error('Unable to fetch BCH-USD price');
|
|
112
|
+
amountBch = amountBch / usdPrice;
|
|
113
|
+
}
|
|
114
|
+
return sendBch({ address: opts.address, amountBch }, isChipnet);
|
|
115
|
+
},
|
|
116
|
+
async sendTokenPayment(opts) {
|
|
117
|
+
const amount = BigInt(opts.amount);
|
|
118
|
+
if (amount <= 0n)
|
|
119
|
+
throw new Error('Amount must be positive');
|
|
120
|
+
return sendToken({ category: opts.category, amount, address: opts.address }, isChipnet);
|
|
121
|
+
},
|
|
122
|
+
async swapQuote(opts) {
|
|
123
|
+
if (isChipnet)
|
|
124
|
+
throw new Error('Cauldron swaps are not supported on chipnet');
|
|
125
|
+
const parsedAmount = parseFloat(opts.amount);
|
|
126
|
+
if (isNaN(parsedAmount) || parsedAmount <= 0)
|
|
127
|
+
throw new Error('Invalid amount');
|
|
128
|
+
const tokenData = await fetchTokenData(opts.tokenId);
|
|
129
|
+
if (!tokenData)
|
|
130
|
+
throw new Error('No token data found');
|
|
131
|
+
const decimals = tokenData.bcmr.token.decimals;
|
|
132
|
+
const symbol = tokenData.bcmr.token.symbol || tokenData.display_symbol;
|
|
133
|
+
const amount = BigInt(Math.round(parsedAmount * 10 ** decimals));
|
|
134
|
+
const direction = opts.direction === 'buy' ? 'buy' : 'sell';
|
|
135
|
+
const quote = await estimateSwap({ tokenId: opts.tokenId, direction, amount });
|
|
136
|
+
const platformFeeSats = quote.platformFee ? Number(quote.platformFee.amount).toString() : null;
|
|
137
|
+
return {
|
|
138
|
+
tokenId: opts.tokenId,
|
|
139
|
+
symbol,
|
|
140
|
+
decimals,
|
|
141
|
+
direction: opts.direction,
|
|
142
|
+
rate: quote.rate,
|
|
143
|
+
tokenAmount: Number(quote.tokenAmount).toString(),
|
|
144
|
+
bchAmountSats: Number(quote.bchAmount).toString(),
|
|
145
|
+
tradeFeeSats: Number(quote.tradeFee).toString(),
|
|
146
|
+
platformFeeSats,
|
|
147
|
+
formatted: formatQuote(quote),
|
|
148
|
+
};
|
|
149
|
+
},
|
|
150
|
+
async swapExecute(opts) {
|
|
151
|
+
if (isChipnet)
|
|
152
|
+
throw new Error('Cauldron swaps are not supported on chipnet');
|
|
153
|
+
const parsedAmount = parseFloat(opts.amount);
|
|
154
|
+
if (isNaN(parsedAmount) || parsedAmount <= 0)
|
|
155
|
+
throw new Error('Invalid amount');
|
|
156
|
+
const mnemonicData = loadMnemonic();
|
|
157
|
+
if (!mnemonicData)
|
|
158
|
+
throw new WalletNotConfiguredError();
|
|
159
|
+
const wallet = loadWallet();
|
|
160
|
+
if (!wallet)
|
|
161
|
+
throw new WalletNotConfiguredError();
|
|
162
|
+
const bchWallet = wallet.forNetwork(false);
|
|
163
|
+
const tokenData = await fetchTokenData(opts.tokenId);
|
|
164
|
+
if (!tokenData)
|
|
165
|
+
throw new Error('No token data found');
|
|
166
|
+
const decimals = tokenData.bcmr.token.decimals;
|
|
167
|
+
const amount = BigInt(Math.round(parsedAmount * 10 ** decimals));
|
|
168
|
+
const direction = opts.direction === 'buy' ? 'buy' : 'sell';
|
|
169
|
+
const result = await executeSwap({
|
|
170
|
+
tokenId: opts.tokenId,
|
|
171
|
+
direction,
|
|
172
|
+
amount,
|
|
173
|
+
bchWallet,
|
|
174
|
+
mnemonic: mnemonicData.mnemonic,
|
|
175
|
+
derivationPath: BCH_DERIVATION_PATH,
|
|
176
|
+
});
|
|
177
|
+
return {
|
|
178
|
+
success: result.success,
|
|
179
|
+
txid: result.txid || null,
|
|
180
|
+
error: result.error || null,
|
|
181
|
+
};
|
|
182
|
+
},
|
|
183
|
+
async purchasePlan(opts) {
|
|
184
|
+
return buyPlan({
|
|
185
|
+
model: opts.model,
|
|
186
|
+
minutes: opts.minutes,
|
|
187
|
+
paymentMethod: opts.method || 'bch',
|
|
188
|
+
isChipnet,
|
|
189
|
+
backendUrl,
|
|
190
|
+
confirmed: true,
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
async quoteLiftNeeded(opts) {
|
|
194
|
+
if (isChipnet)
|
|
195
|
+
throw new Error('LIFT swaps are not supported on chipnet');
|
|
196
|
+
const sats = Number(opts.sats);
|
|
197
|
+
if (!Number.isFinite(sats) || !Number.isInteger(sats) || sats <= 0) {
|
|
198
|
+
throw new Error('Invalid sats amount');
|
|
199
|
+
}
|
|
200
|
+
const est = await estimateTokenNeededForBch({
|
|
201
|
+
tokenId: LIFT_TOKEN_ID,
|
|
202
|
+
bchDemandSats: BigInt(sats),
|
|
203
|
+
isChipnet,
|
|
204
|
+
});
|
|
205
|
+
return {
|
|
206
|
+
sats,
|
|
207
|
+
rawAmount: Number(est.tokenAmount).toString(),
|
|
208
|
+
display: formatTokenAmount(Number(est.tokenAmount), est.decimals),
|
|
209
|
+
symbol: est.symbol,
|
|
210
|
+
decimals: est.decimals,
|
|
211
|
+
};
|
|
212
|
+
},
|
|
213
|
+
async setAutoRefill(opts) {
|
|
214
|
+
if (opts.delete) {
|
|
215
|
+
return { deleted: true, existed: deleteAutoRefill() };
|
|
216
|
+
}
|
|
217
|
+
if (opts.enabled) {
|
|
218
|
+
const state = armAutoRefill({
|
|
219
|
+
model: opts.model,
|
|
220
|
+
minutes: opts.minutes,
|
|
221
|
+
maxMinutes: opts.maxMinutes,
|
|
222
|
+
paymentMethod: opts.paymentMethod || 'bch',
|
|
223
|
+
});
|
|
224
|
+
let initialTick = null;
|
|
225
|
+
const wallet = loadWalletRef();
|
|
226
|
+
if (wallet?.canSign) {
|
|
227
|
+
try {
|
|
228
|
+
initialTick = await autoRefillTick(createRefillTickDeps(isChipnet));
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
initialTick = null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return initialTick ? { ...state, initialTick } : { ...state };
|
|
235
|
+
}
|
|
236
|
+
return { ...disarmAutoRefill() };
|
|
237
|
+
},
|
|
238
|
+
async getImageModels() {
|
|
239
|
+
return listImageModels({ backendUrl });
|
|
240
|
+
},
|
|
241
|
+
async getImageHistory(page) {
|
|
242
|
+
return getImageHistory({ backendUrl, page, pageSize: 20 });
|
|
243
|
+
},
|
|
244
|
+
async createImageQuote(opts) {
|
|
245
|
+
return createImageOrder({
|
|
246
|
+
prompt: opts.prompt,
|
|
247
|
+
model: opts.model,
|
|
248
|
+
aspectRatio: opts.aspectRatio,
|
|
249
|
+
quality: opts.quality,
|
|
250
|
+
resolution: opts.resolution,
|
|
251
|
+
isChipnet,
|
|
252
|
+
backendUrl,
|
|
253
|
+
});
|
|
254
|
+
},
|
|
255
|
+
async fulfillImage(orderId) {
|
|
256
|
+
throw new Error('Fulfillment requires the original quote. Use POST /api/ai/images/fulfill with stored quote.');
|
|
257
|
+
},
|
|
258
|
+
async serveImageFile(id) {
|
|
259
|
+
const filePath = localImageFile(id);
|
|
260
|
+
if (!filePath)
|
|
261
|
+
return null;
|
|
262
|
+
const ext = filePath.split('.').pop() || '';
|
|
263
|
+
return {
|
|
264
|
+
filePath,
|
|
265
|
+
mediaType: IMAGE_EXT_TO_MEDIA[ext] || 'application/octet-stream',
|
|
266
|
+
};
|
|
267
|
+
},
|
|
268
|
+
async deleteImageFile(id) {
|
|
269
|
+
const filePath = localImageFile(id);
|
|
270
|
+
if (!filePath)
|
|
271
|
+
return false;
|
|
272
|
+
try {
|
|
273
|
+
fs.unlinkSync(filePath);
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function localImageFile(id) {
|
|
283
|
+
const safeId = id.replace(/[^a-zA-Z0-9_-]/g, '');
|
|
284
|
+
if (!safeId)
|
|
285
|
+
return null;
|
|
286
|
+
try {
|
|
287
|
+
const files = fs.readdirSync(IMAGE_DIR).filter((f) => f.startsWith(safeId + '.'));
|
|
288
|
+
return files.length ? path.join(IMAGE_DIR, files[0]) : null;
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const IMAGE_EXT_TO_MEDIA = Object.fromEntries(Object.entries(MEDIA_TYPE_EXTENSIONS).map(([mediaType, ext]) => [ext, mediaType]));
|
|
295
|
+
function createDefaultDepsWithQuotes(isChipnet, backendUrl) {
|
|
296
|
+
const base = defaultDeps(isChipnet, backendUrl);
|
|
297
|
+
const quoteStore = new Map();
|
|
298
|
+
return {
|
|
299
|
+
...base,
|
|
300
|
+
async fulfillImage(orderId) {
|
|
301
|
+
const quote = quoteStore.get(orderId);
|
|
302
|
+
if (!quote)
|
|
303
|
+
throw new Error('No quote found for order. Request a new quote first.');
|
|
304
|
+
quoteStore.delete(orderId);
|
|
305
|
+
return fulfillImageOrder(quote, { isChipnet, backendUrl });
|
|
306
|
+
},
|
|
307
|
+
async createImageQuote(opts) {
|
|
308
|
+
const quote = await createImageOrder({
|
|
309
|
+
prompt: opts.prompt,
|
|
310
|
+
model: opts.model,
|
|
311
|
+
aspectRatio: opts.aspectRatio,
|
|
312
|
+
quality: opts.quality,
|
|
313
|
+
resolution: opts.resolution,
|
|
314
|
+
isChipnet,
|
|
315
|
+
backendUrl,
|
|
316
|
+
});
|
|
317
|
+
quoteStore.set(quote.orderId, quote);
|
|
318
|
+
if (quoteStore.size > 50) {
|
|
319
|
+
const oldest = quoteStore.keys().next().value;
|
|
320
|
+
if (oldest)
|
|
321
|
+
quoteStore.delete(oldest);
|
|
322
|
+
}
|
|
323
|
+
let amountUsd = quote.amountUsd;
|
|
324
|
+
if (amountUsd === undefined) {
|
|
325
|
+
try {
|
|
326
|
+
const usdPerBch = await getBchUsdPrice(isChipnet);
|
|
327
|
+
if (usdPerBch !== null) {
|
|
328
|
+
amountUsd = Number(((quote.amountSats / 1e8) * usdPerBch).toFixed(2));
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
amountUsd = undefined;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return { ...quote, amountUsd };
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
const MAX_BODY_BYTES = 1024 * 1024;
|
|
340
|
+
async function readBody(req) {
|
|
341
|
+
const chunks = [];
|
|
342
|
+
let total = 0;
|
|
343
|
+
for await (const chunk of req) {
|
|
344
|
+
const buf = typeof chunk === 'string' ? Buffer.from(chunk) : chunk;
|
|
345
|
+
total += buf.length;
|
|
346
|
+
if (total > MAX_BODY_BYTES) {
|
|
347
|
+
req.destroy();
|
|
348
|
+
throw new Error('Request body too large');
|
|
349
|
+
}
|
|
350
|
+
chunks.push(buf);
|
|
351
|
+
}
|
|
352
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
353
|
+
if (!raw)
|
|
354
|
+
return {};
|
|
355
|
+
try {
|
|
356
|
+
return JSON.parse(raw);
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
return {};
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function json(res, status, data) {
|
|
363
|
+
const body = JSON.stringify(data);
|
|
364
|
+
res.writeHead(status, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) });
|
|
365
|
+
res.end(body);
|
|
366
|
+
}
|
|
367
|
+
function getClientHost(req) {
|
|
368
|
+
return (req.headers.host || '').toLowerCase().split(':')[0];
|
|
369
|
+
}
|
|
370
|
+
export async function startWebServer(options = {}) {
|
|
371
|
+
const token = crypto.randomBytes(16).toString('hex');
|
|
372
|
+
const isChipnet = Boolean(options.isChipnet);
|
|
373
|
+
const backendUrl = options.backendUrl;
|
|
374
|
+
const deps = options.deps || createDefaultDepsWithQuotes(isChipnet, backendUrl);
|
|
375
|
+
const pageHtml = renderPage();
|
|
376
|
+
function checkAuth(req) {
|
|
377
|
+
return req.headers['x-paytaca-token'] === token;
|
|
378
|
+
}
|
|
379
|
+
function checkHost(req) {
|
|
380
|
+
const host = getClientHost(req);
|
|
381
|
+
return host === '127.0.0.1' || host === 'localhost';
|
|
382
|
+
}
|
|
383
|
+
const server = http.createServer(async (req, res) => {
|
|
384
|
+
try {
|
|
385
|
+
const url = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
|
|
386
|
+
const pathname = url.pathname;
|
|
387
|
+
if (!checkHost(req)) {
|
|
388
|
+
return json(res, 403, { error: 'Forbidden: invalid host' });
|
|
389
|
+
}
|
|
390
|
+
if (pathname === '/') {
|
|
391
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
392
|
+
res.end(pageHtml);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
// ── wallet routes ──────────────────────────────────────────────────
|
|
396
|
+
if (pathname === '/api/wallet/state' && req.method === 'GET') {
|
|
397
|
+
if (!checkAuth(req))
|
|
398
|
+
return json(res, 401, { error: 'Unauthorized' });
|
|
399
|
+
const state = await deps.getWalletState();
|
|
400
|
+
return json(res, 200, state);
|
|
401
|
+
}
|
|
402
|
+
if (pathname === '/api/wallet/history' && req.method === 'GET') {
|
|
403
|
+
if (!checkAuth(req))
|
|
404
|
+
return json(res, 401, { error: 'Unauthorized' });
|
|
405
|
+
const page = Number(url.searchParams.get('page')) || 1;
|
|
406
|
+
const type = String(url.searchParams.get('type') || 'all');
|
|
407
|
+
const tokenId = url.searchParams.get('tokenId') || undefined;
|
|
408
|
+
const result = await deps.getWalletHistory(page, type, tokenId);
|
|
409
|
+
return json(res, 200, result);
|
|
410
|
+
}
|
|
411
|
+
if (pathname === '/api/wallet/receive' && req.method === 'GET') {
|
|
412
|
+
if (!checkAuth(req))
|
|
413
|
+
return json(res, 401, { error: 'Unauthorized' });
|
|
414
|
+
const index = url.searchParams.has('index') ? Number(url.searchParams.get('index')) : undefined;
|
|
415
|
+
const token = url.searchParams.has('token') ? url.searchParams.get('token') === '1' || url.searchParams.get('token') === 'true' : undefined;
|
|
416
|
+
const category = url.searchParams.get('category') || undefined;
|
|
417
|
+
const amount = url.searchParams.has('amount') ? Number(url.searchParams.get('amount')) : undefined;
|
|
418
|
+
const view = await deps.getReceiveView({ index, token, category, amount });
|
|
419
|
+
return json(res, 200, view);
|
|
420
|
+
}
|
|
421
|
+
if (pathname === '/api/wallet/send' && req.method === 'POST') {
|
|
422
|
+
if (!checkAuth(req))
|
|
423
|
+
return json(res, 401, { error: 'Unauthorized' });
|
|
424
|
+
if (req.headers['content-type'] !== 'application/json') {
|
|
425
|
+
return json(res, 400, { error: 'Content-Type must be application/json' });
|
|
426
|
+
}
|
|
427
|
+
const body = await readBody(req);
|
|
428
|
+
const address = String(body.address || '');
|
|
429
|
+
const amount = Number(body.amount);
|
|
430
|
+
const currency = String(body.currency || 'bch');
|
|
431
|
+
if (!address)
|
|
432
|
+
return json(res, 400, { error: 'Missing address' });
|
|
433
|
+
if (!isValidBchAddress(address, isChipnet))
|
|
434
|
+
return json(res, 400, { error: 'Invalid BCH address' });
|
|
435
|
+
if (!Number.isFinite(amount) || amount <= 0)
|
|
436
|
+
return json(res, 400, { error: 'Invalid amount' });
|
|
437
|
+
if (!['bch', 'sats', 'satoshis', 'usd'].includes(currency)) {
|
|
438
|
+
return json(res, 400, { error: 'Currency must be "bch", "sats", "satoshis", or "usd"' });
|
|
439
|
+
}
|
|
440
|
+
const result = await deps.sendBchPayment({ address, amount, currency });
|
|
441
|
+
return json(res, 200, result);
|
|
442
|
+
}
|
|
443
|
+
if (pathname === '/api/wallet/send-token' && req.method === 'POST') {
|
|
444
|
+
if (!checkAuth(req))
|
|
445
|
+
return json(res, 401, { error: 'Unauthorized' });
|
|
446
|
+
if (req.headers['content-type'] !== 'application/json') {
|
|
447
|
+
return json(res, 400, { error: 'Content-Type must be application/json' });
|
|
448
|
+
}
|
|
449
|
+
const body = await readBody(req);
|
|
450
|
+
const category = String(body.category || '');
|
|
451
|
+
const amount = String(body.amount || '');
|
|
452
|
+
const address = String(body.address || '');
|
|
453
|
+
if (!category || !/^[a-fA-F0-9]{64}$/.test(category))
|
|
454
|
+
return json(res, 400, { error: 'Invalid category (64-char hex)' });
|
|
455
|
+
if (!amount)
|
|
456
|
+
return json(res, 400, { error: 'Missing amount' });
|
|
457
|
+
if (!address)
|
|
458
|
+
return json(res, 400, { error: 'Missing address' });
|
|
459
|
+
if (!isValidBchAddress(address, isChipnet))
|
|
460
|
+
return json(res, 400, { error: 'Invalid BCH address' });
|
|
461
|
+
let bigAmount;
|
|
462
|
+
try {
|
|
463
|
+
bigAmount = BigInt(amount);
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
return json(res, 400, { error: 'Amount must be a valid integer' });
|
|
467
|
+
}
|
|
468
|
+
if (bigAmount <= 0n)
|
|
469
|
+
return json(res, 400, { error: 'Amount must be positive' });
|
|
470
|
+
const tokenWarning = !isTokenAddress(address);
|
|
471
|
+
const result = await deps.sendTokenPayment({ category, amount, address });
|
|
472
|
+
return json(res, 200, { ...result, tokenWarning });
|
|
473
|
+
}
|
|
474
|
+
// ── swap routes ────────────────────────────────────────────────────
|
|
475
|
+
if (pathname === '/api/swap/quote' && req.method === 'POST') {
|
|
476
|
+
if (!checkAuth(req))
|
|
477
|
+
return json(res, 401, { error: 'Unauthorized' });
|
|
478
|
+
if (req.headers['content-type'] !== 'application/json') {
|
|
479
|
+
return json(res, 400, { error: 'Content-Type must be application/json' });
|
|
480
|
+
}
|
|
481
|
+
const body = await readBody(req);
|
|
482
|
+
const tokenId = String(body.tokenId || '');
|
|
483
|
+
const direction = String(body.direction || 'sell');
|
|
484
|
+
const amount = String(body.amount || '');
|
|
485
|
+
if (!tokenId || !/^[a-fA-F0-9]{64}$/.test(tokenId))
|
|
486
|
+
return json(res, 400, { error: 'Invalid tokenId' });
|
|
487
|
+
if (!['buy', 'sell'].includes(direction))
|
|
488
|
+
return json(res, 400, { error: 'Direction must be "buy" or "sell"' });
|
|
489
|
+
if (!amount || isNaN(parseFloat(amount)) || parseFloat(amount) <= 0)
|
|
490
|
+
return json(res, 400, { error: 'Invalid amount' });
|
|
491
|
+
const quote = await deps.swapQuote({ tokenId, direction, amount });
|
|
492
|
+
return json(res, 200, quote);
|
|
493
|
+
}
|
|
494
|
+
if (pathname === '/api/swap/execute' && req.method === 'POST') {
|
|
495
|
+
if (!checkAuth(req))
|
|
496
|
+
return json(res, 401, { error: 'Unauthorized' });
|
|
497
|
+
if (req.headers['content-type'] !== 'application/json') {
|
|
498
|
+
return json(res, 400, { error: 'Content-Type must be application/json' });
|
|
499
|
+
}
|
|
500
|
+
const body = await readBody(req);
|
|
501
|
+
const tokenId = String(body.tokenId || '');
|
|
502
|
+
const direction = String(body.direction || 'sell');
|
|
503
|
+
const amount = String(body.amount || '');
|
|
504
|
+
if (!tokenId || !/^[a-fA-F0-9]{64}$/.test(tokenId))
|
|
505
|
+
return json(res, 400, { error: 'Invalid tokenId' });
|
|
506
|
+
if (!['buy', 'sell'].includes(direction))
|
|
507
|
+
return json(res, 400, { error: 'Direction must be "buy" or "sell"' });
|
|
508
|
+
if (!amount || isNaN(parseFloat(amount)) || parseFloat(amount) <= 0)
|
|
509
|
+
return json(res, 400, { error: 'Invalid amount' });
|
|
510
|
+
const result = await deps.swapExecute({ tokenId, direction, amount });
|
|
511
|
+
return json(res, 200, result);
|
|
512
|
+
}
|
|
513
|
+
// ── ai routes ──────────────────────────────────────────────────────
|
|
514
|
+
if (pathname === '/api/ai/state' && req.method === 'GET') {
|
|
515
|
+
if (!checkAuth(req))
|
|
516
|
+
return json(res, 401, { error: 'Unauthorized' });
|
|
517
|
+
const state = await deps.getWalletState();
|
|
518
|
+
return json(res, 200, state);
|
|
519
|
+
}
|
|
520
|
+
if (pathname === '/api/ai/lift-quote' && req.method === 'GET') {
|
|
521
|
+
if (!checkAuth(req))
|
|
522
|
+
return json(res, 401, { error: 'Unauthorized' });
|
|
523
|
+
const sats = Number(url.searchParams.get('sats'));
|
|
524
|
+
if (!Number.isFinite(sats) || !Number.isInteger(sats) || sats <= 0) {
|
|
525
|
+
return json(res, 400, { error: 'Invalid sats amount' });
|
|
526
|
+
}
|
|
527
|
+
const quote = await deps.quoteLiftNeeded({ sats });
|
|
528
|
+
return json(res, 200, quote);
|
|
529
|
+
}
|
|
530
|
+
if (pathname === '/api/ai/purchase' && req.method === 'POST') {
|
|
531
|
+
if (!checkAuth(req))
|
|
532
|
+
return json(res, 401, { error: 'Unauthorized' });
|
|
533
|
+
if (req.headers['content-type'] !== 'application/json') {
|
|
534
|
+
return json(res, 400, { error: 'Content-Type must be application/json' });
|
|
535
|
+
}
|
|
536
|
+
const body = await readBody(req);
|
|
537
|
+
const model = String(body.model || '');
|
|
538
|
+
const minutes = Number(body.minutes);
|
|
539
|
+
const method = String(body.method || 'bch');
|
|
540
|
+
if (!model)
|
|
541
|
+
return json(res, 400, { error: 'Missing model' });
|
|
542
|
+
if (!Number.isFinite(minutes) || minutes <= 0)
|
|
543
|
+
return json(res, 400, { error: 'Invalid minutes' });
|
|
544
|
+
if (method !== 'bch' && method !== 'lift')
|
|
545
|
+
return json(res, 400, { error: 'Method must be "bch" or "lift"' });
|
|
546
|
+
const result = await deps.purchasePlan({ model, minutes, method });
|
|
547
|
+
return json(res, 200, result);
|
|
548
|
+
}
|
|
549
|
+
if (pathname === '/api/ai/auto-refill' && req.method === 'POST') {
|
|
550
|
+
if (!checkAuth(req))
|
|
551
|
+
return json(res, 401, { error: 'Unauthorized' });
|
|
552
|
+
if (req.headers['content-type'] !== 'application/json') {
|
|
553
|
+
return json(res, 400, { error: 'Content-Type must be application/json' });
|
|
554
|
+
}
|
|
555
|
+
const body = await readBody(req);
|
|
556
|
+
const enabled = Boolean(body.enabled);
|
|
557
|
+
const result = await deps.setAutoRefill({
|
|
558
|
+
enabled,
|
|
559
|
+
delete: Boolean(body.delete),
|
|
560
|
+
model: body.model != null ? String(body.model) : undefined,
|
|
561
|
+
minutes: body.minutes != null ? Number(body.minutes) : undefined,
|
|
562
|
+
maxMinutes: body.maxMinutes != null ? Number(body.maxMinutes) : undefined,
|
|
563
|
+
paymentMethod: body.paymentMethod != null ? String(body.paymentMethod) : undefined,
|
|
564
|
+
});
|
|
565
|
+
return json(res, 200, result);
|
|
566
|
+
}
|
|
567
|
+
if (pathname === '/api/ai/image-models' && req.method === 'GET') {
|
|
568
|
+
if (!checkAuth(req))
|
|
569
|
+
return json(res, 401, { error: 'Unauthorized' });
|
|
570
|
+
const models = await deps.getImageModels();
|
|
571
|
+
return json(res, 200, { models });
|
|
572
|
+
}
|
|
573
|
+
if (pathname === '/api/ai/images/history' && req.method === 'GET') {
|
|
574
|
+
if (!checkAuth(req))
|
|
575
|
+
return json(res, 401, { error: 'Unauthorized' });
|
|
576
|
+
const page = url.searchParams.get('page') ? Number(url.searchParams.get('page')) : undefined;
|
|
577
|
+
const result = await deps.getImageHistory(page);
|
|
578
|
+
return json(res, 200, result);
|
|
579
|
+
}
|
|
580
|
+
if (pathname === '/api/ai/images/quote' && req.method === 'POST') {
|
|
581
|
+
if (!checkAuth(req))
|
|
582
|
+
return json(res, 401, { error: 'Unauthorized' });
|
|
583
|
+
if (req.headers['content-type'] !== 'application/json') {
|
|
584
|
+
return json(res, 400, { error: 'Content-Type must be application/json' });
|
|
585
|
+
}
|
|
586
|
+
const body = await readBody(req);
|
|
587
|
+
const prompt = String(body.prompt || '').trim();
|
|
588
|
+
if (!prompt)
|
|
589
|
+
return json(res, 400, { error: 'Missing prompt' });
|
|
590
|
+
const quote = await deps.createImageQuote({
|
|
591
|
+
prompt,
|
|
592
|
+
model: body.model != null ? String(body.model) : undefined,
|
|
593
|
+
aspectRatio: body.aspectRatio != null ? String(body.aspectRatio) : undefined,
|
|
594
|
+
quality: body.quality != null ? String(body.quality) : undefined,
|
|
595
|
+
resolution: body.resolution != null ? String(body.resolution) : undefined,
|
|
596
|
+
});
|
|
597
|
+
return json(res, 200, quote);
|
|
598
|
+
}
|
|
599
|
+
if (pathname === '/api/ai/images/fulfill' && req.method === 'POST') {
|
|
600
|
+
if (!checkAuth(req))
|
|
601
|
+
return json(res, 401, { error: 'Unauthorized' });
|
|
602
|
+
if (req.headers['content-type'] !== 'application/json') {
|
|
603
|
+
return json(res, 400, { error: 'Content-Type must be application/json' });
|
|
604
|
+
}
|
|
605
|
+
const body = await readBody(req);
|
|
606
|
+
const orderId = String(body.orderId || '');
|
|
607
|
+
if (!orderId)
|
|
608
|
+
return json(res, 400, { error: 'Missing orderId' });
|
|
609
|
+
const result = await deps.fulfillImage(orderId);
|
|
610
|
+
return json(res, 200, result);
|
|
611
|
+
}
|
|
612
|
+
const imageFileMatch = pathname.match(/^\/api\/ai\/images\/([^/]+)\/file$/);
|
|
613
|
+
if (imageFileMatch && req.method === 'GET') {
|
|
614
|
+
if (!checkAuth(req))
|
|
615
|
+
return json(res, 401, { error: 'Unauthorized' });
|
|
616
|
+
const id = decodeURIComponent(imageFileMatch[1]);
|
|
617
|
+
const file = await deps.serveImageFile(id);
|
|
618
|
+
if (!file)
|
|
619
|
+
return json(res, 404, { error: 'Image not found' });
|
|
620
|
+
const data = fs.readFileSync(file.filePath);
|
|
621
|
+
res.writeHead(200, {
|
|
622
|
+
'Content-Type': file.mediaType,
|
|
623
|
+
'Content-Length': data.length,
|
|
624
|
+
'Cache-Control': 'public, max-age=3600',
|
|
625
|
+
});
|
|
626
|
+
res.end(data);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
const imageDeleteMatch = pathname.match(/^\/api\/ai\/images\/([^/]+)$/);
|
|
630
|
+
if (imageDeleteMatch && req.method === 'DELETE') {
|
|
631
|
+
if (!checkAuth(req))
|
|
632
|
+
return json(res, 401, { error: 'Unauthorized' });
|
|
633
|
+
const id = decodeURIComponent(imageDeleteMatch[1]);
|
|
634
|
+
const deleted = await deps.deleteImageFile(id);
|
|
635
|
+
return json(res, 200, { deleted });
|
|
636
|
+
}
|
|
637
|
+
json(res, 404, { error: 'Not found' });
|
|
638
|
+
}
|
|
639
|
+
catch (err) {
|
|
640
|
+
console.error('[paytaca web]', err?.stack || err?.message || err);
|
|
641
|
+
json(res, 500, { error: 'Internal server error' });
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
const port = options.port || 7474;
|
|
645
|
+
return new Promise((resolve, reject) => {
|
|
646
|
+
server.on('error', reject);
|
|
647
|
+
server.listen(port, '127.0.0.1', () => {
|
|
648
|
+
const addr = server.address();
|
|
649
|
+
const actualPort = typeof addr === 'object' && addr ? addr.port : port;
|
|
650
|
+
const url = `http://127.0.0.1:${actualPort}/?token=${token}`;
|
|
651
|
+
resolve({
|
|
652
|
+
server,
|
|
653
|
+
port: actualPort,
|
|
654
|
+
token,
|
|
655
|
+
url,
|
|
656
|
+
close() {
|
|
657
|
+
return new Promise((res, rej) => {
|
|
658
|
+
server.close((err) => (err ? rej(err) : res()));
|
|
659
|
+
});
|
|
660
|
+
},
|
|
661
|
+
});
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
//# sourceMappingURL=server.js.map
|