@paytaca/opencode-plugin 0.2.1 → 0.3.0

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.
@@ -110,6 +110,43 @@ async function getWalletBalance() {
110
110
  }
111
111
  }
112
112
 
113
+ // LIFT token balance in base units (2 decimals); null when unavailable.
114
+ const LIFT_TOKEN_ID = '5932b2fd4915d6a75d3ec53282cd49118149a2176ee67ed68b1111ff0786f7fc';
115
+ async function getLiftBalance() {
116
+ try {
117
+ const output = await runCommand(PAYTACA_CMD, ['token', 'info', LIFT_TOKEN_ID]);
118
+ const match = output.match(/Balance:\\s*([\\d.]+)\\s*LIFT/i);
119
+ if (match) return Math.round(parseFloat(match[1]) * 100);
120
+ return null;
121
+ } catch (err) {
122
+ log('Failed to get LIFT balance: ' + err.message);
123
+ return null;
124
+ }
125
+ }
126
+
127
+ // LIFT payment discount percent advertised by the backend (/v1/config), cached
128
+ // briefly (30s) so a server-side rate change is picked up quickly. Returns 0
129
+ // when unset/unavailable so callers can fall back to no-discount messaging.
130
+ let liftDiscountCache = { at: 0, percent: 0 };
131
+ async function getLiftDiscountPercent() {
132
+ const now = Date.now();
133
+ if (liftDiscountCache.at && now - liftDiscountCache.at < 30000) {
134
+ return liftDiscountCache.percent;
135
+ }
136
+ let percent = 0;
137
+ try {
138
+ const configRes = await fetch(BACKEND_URL + '/v1/config');
139
+ if (configRes.ok) {
140
+ const data = await configRes.json();
141
+ percent = Number(data.lift_payment_discount_percent) || 0;
142
+ }
143
+ } catch (err) {
144
+ log('Failed to fetch LIFT discount config: ' + err.message);
145
+ }
146
+ liftDiscountCache = { at: now, percent };
147
+ return percent;
148
+ }
149
+
113
150
  // Utility: get receiving address
114
151
  async function getReceivingAddress() {
115
152
  try {
@@ -236,16 +273,6 @@ async function streamTierSelectionBody(res, walletHash, modelName, tiers, includ
236
273
  choices: [{ index: 0, delta: { content: 'šŸ’³ Select a plan for **' + (modelName || 'AI Model') + '**\\n\\n' }, finish_reason: null }],
237
274
  });
238
275
 
239
- // If other models still have paid credits, tell the user they can switch
240
- // instead of buying a new plan (only when there is something to suggest).
241
- if (otherModels && otherModels.length > 0) {
242
- sseLine(res, {
243
- id: 'tier-9b',
244
- object: 'chat.completion.chunk',
245
- choices: [{ index: 0, delta: { content: otherModelsHint(otherModels) }, finish_reason: null }],
246
- });
247
- }
248
-
249
276
  // Build all tier lines into one string so backtick markdown renders
250
277
  // consistently (same as the 'plans' command).
251
278
  let tiersContent = '';
@@ -265,12 +292,32 @@ async function streamTierSelectionBody(res, walletHash, modelName, tiers, includ
265
292
  choices: [{ index: 0, delta: { content: tiersContent }, finish_reason: null }],
266
293
  });
267
294
 
295
+ // Advertise the LIFT discount when the backend advertises one.
296
+ const liftPercent = await getLiftDiscountPercent();
297
+ if (liftPercent > 0) {
298
+ sseLine(res, {
299
+ id: 'tier-10b',
300
+ object: 'chat.completion.chunk',
301
+ choices: [{ index: 0, delta: { content: '\\nšŸ’” **' + liftPercent + '% off** when you pay with LIFT tokens — type \`LIFT\` to pay with LIFT and get the discount.\\n' }, finish_reason: null }],
302
+ });
303
+ }
304
+
268
305
  sseLine(res, {
269
306
  id: 'tier-11',
270
307
  object: 'chat.completion.chunk',
271
- choices: [{ index: 0, delta: { content: '\\nEnter a number (1-' + tiers.length + '), e.g. type ' + tiers[0].minutes + ':' }, finish_reason: 'stop' }],
308
+ choices: [{ index: 0, delta: { content: '\\nEnter a number (1-' + tiers.length + ') to pay with BCH, or type \`LIFT\` to pay with LIFT tokens' + (liftPercent > 0 ? ' and get ' + liftPercent + '% off' : '') + ':\\n' }, finish_reason: 'stop' }],
272
309
  });
273
310
 
311
+ // If other models still have paid credits, tell the user they can switch
312
+ // instead of buying a new plan (only when there is something to suggest).
313
+ if (otherModels && otherModels.length > 0) {
314
+ sseLine(res, {
315
+ id: 'tier-9b',
316
+ object: 'chat.completion.chunk',
317
+ choices: [{ index: 0, delta: { content: otherModelsHint(otherModels) }, finish_reason: null }],
318
+ });
319
+ }
320
+
274
321
  sseLine(res, {
275
322
  id: 'tier-12',
276
323
  object: 'chat.completion.chunk',
@@ -731,7 +778,7 @@ async function streamPaymentFailureAndRetry(res, walletHash, pendingPayload, mes
731
778
  }
732
779
 
733
780
  // Run paytaca pay internally and return the response
734
- function runPaytacaPay(djangoUrl, body, walletHash, extraHeaders, callback) {
781
+ function runPaytacaPay(djangoUrl, body, walletHash, extraHeaders, paymentMethod, callback) {
735
782
  const url = djangoUrl + '/chat/completions?wallet_hash=' + encodeURIComponent(walletHash || '');
736
783
  const payBody = forceNonStreaming(body);
737
784
 
@@ -753,6 +800,9 @@ function runPaytacaPay(djangoUrl, body, walletHash, extraHeaders, callback) {
753
800
  bodyFile,
754
801
  confirmed: true,
755
802
  };
803
+ if (paymentMethod === 'lift') {
804
+ config.paymentMethod = 'lift';
805
+ }
756
806
 
757
807
  try {
758
808
  fs.writeFileSync(configFile, JSON.stringify(config), 'utf8');
@@ -1167,20 +1217,43 @@ const server = http.createServer(async (req, res) => {
1167
1217
  await handlePricingCommand(res);
1168
1218
  return;
1169
1219
  }
1220
+
1221
+ // LIFT payment option: user typed "LIFT" (optionally followed by a
1222
+ // tier number, e.g. "LIFT 2"). Defaults to the first tier.
1223
+ let paymentMethod = 'bch';
1224
+ if (timeCmd === 'lift' || (timeCmd && /^lift[\s]+\\d+$/.test(timeCmd))) {
1225
+ paymentMethod = 'lift';
1226
+ }
1227
+ let liftTierIndex = -1;
1228
+ if (timeCmd && /^lift[\s]+\\d+$/.test(timeCmd)) {
1229
+ const liftNum = parseInt(timeCmd.split(/\\s+/)[1], 10);
1230
+ if (!isNaN(liftNum) && liftNum >= 1 && liftNum <= pendingPayload.tiers.length) {
1231
+ liftTierIndex = liftNum - 1;
1232
+ }
1233
+ }
1234
+
1170
1235
  let selectedIndex = -1;
1171
1236
 
1172
- // Try to parse user input as a number (1-based)
1173
- const num = parseInt(userInput, 10);
1174
- if (!isNaN(num) && num >= 1 && num <= pendingPayload.tiers.length) {
1175
- selectedIndex = num - 1;
1237
+ // "LIFT" alone selects the first (cheapest) tier paid with LIFT;
1238
+ // "LIFT N" selects tier N.
1239
+ if (paymentMethod === 'lift' && liftTierIndex >= 0) {
1240
+ selectedIndex = liftTierIndex;
1241
+ } else if (paymentMethod === 'lift') {
1242
+ selectedIndex = 0;
1176
1243
  } else {
1177
- // Try to match by duration minutes
1178
- for (let i = 0; i < pendingPayload.tiers.length; i++) {
1179
- if (userInput === String(pendingPayload.tiers[i].minutes) ||
1180
- userInput === pendingPayload.tiers[i].minutes + ' minutes' ||
1181
- userInput === pendingPayload.tiers[i].minutes + ' min') {
1182
- selectedIndex = i;
1183
- break;
1244
+ // Try to parse user input as a number (1-based)
1245
+ const num = parseInt(userInput, 10);
1246
+ if (!isNaN(num) && num >= 1 && num <= pendingPayload.tiers.length) {
1247
+ selectedIndex = num - 1;
1248
+ } else {
1249
+ // Try to match by duration minutes
1250
+ for (let i = 0; i < pendingPayload.tiers.length; i++) {
1251
+ if (userInput === String(pendingPayload.tiers[i].minutes) ||
1252
+ userInput === pendingPayload.tiers[i].minutes + ' minutes' ||
1253
+ userInput === pendingPayload.tiers[i].minutes + ' min') {
1254
+ selectedIndex = i;
1255
+ break;
1256
+ }
1184
1257
  }
1185
1258
  }
1186
1259
  }
@@ -1188,9 +1261,10 @@ const server = http.createServer(async (req, res) => {
1188
1261
  if (selectedIndex >= 0) {
1189
1262
  const selectedTier = pendingPayload.tiers[selectedIndex];
1190
1263
  pendingPayload.durationMinutes = selectedTier.minutes;
1264
+ pendingPayload.paymentMethod = paymentMethod;
1191
1265
  pendingPayload.step = 'processing';
1192
1266
 
1193
- log('Tier selected: ' + selectedTier.minutes + ' min for wallet ' + walletHash?.substring(0, 16) + '...');
1267
+ log('Tier selected: ' + selectedTier.minutes + ' min (' + paymentMethod + ') for wallet ' + walletHash?.substring(0, 16) + '...');
1194
1268
 
1195
1269
  // Build extra headers for payment wrapper
1196
1270
  const extraHeaders = {};
@@ -1198,28 +1272,56 @@ const server = http.createServer(async (req, res) => {
1198
1272
  extraHeaders['X-Model-Id'] = pendingPayload.modelId;
1199
1273
  }
1200
1274
  extraHeaders['X-Duration-Minutes'] = String(selectedTier.minutes);
1275
+ if (paymentMethod === 'lift') {
1276
+ extraHeaders['X-Payment-Method'] = 'lift';
1277
+ }
1201
1278
 
1202
- // Check wallet balance before attempting payment
1203
- const currentBalanceSats = await getWalletBalance();
1204
- if (currentBalanceSats !== null && selectedTier.price_sats && currentBalanceSats < selectedTier.price_sats) {
1205
- log('Insufficient balance for wallet ' + walletHash?.substring(0, 16) + '...: ' + currentBalanceSats + ' sats < ' + selectedTier.price_sats + ' sats needed');
1206
- pendingPayments.delete(walletHash);
1207
- const addr = await getReceivingAddress();
1208
- const neededBch = (selectedTier.price_sats - currentBalanceSats) / 100000000;
1209
- const neededLine = addr ? '\\n\\nšŸ“„ **Fund your wallet:** \\\`' + addr + '\\\`\\nOr run: paytaca receive (in another terminal) for QR code' : '';
1210
- sseLine(res, {
1211
- id: 'balance-err',
1212
- object: 'chat.completion.chunk',
1213
- choices: [{ index: 0, delta: { content: PROXY_MARKER + '\\n\\nāŒ **Insufficient balance** — You have **' + (currentBalanceSats / 100000000).toFixed(8) + ' BCH** but need **' + (selectedTier.price_sats / 100000000).toFixed(8) + ' BCH** for this plan. Top up at least **' + neededBch.toFixed(8) + ' BCH** more.' + neededLine + '\\n\\nType \\\`balance\\\` to re-check or try a different plan:' }, finish_reason: 'stop' }],
1214
- });
1215
- sseLine(res, {
1216
- id: 'balance-err-done',
1217
- object: 'chat.completion.chunk',
1218
- choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
1219
- });
1220
- sseDone(res);
1221
- res.end();
1222
- return;
1279
+ // Check wallet balance before attempting payment — only for BCH.
1280
+ // The LIFT path sells tokens, so no BCH balance is required.
1281
+ if (paymentMethod !== 'lift') {
1282
+ const currentBalanceSats = await getWalletBalance();
1283
+ if (currentBalanceSats !== null && selectedTier.price_sats && currentBalanceSats < selectedTier.price_sats) {
1284
+ log('Insufficient balance for wallet ' + walletHash?.substring(0, 16) + '...: ' + currentBalanceSats + ' sats < ' + selectedTier.price_sats + ' sats needed');
1285
+ pendingPayments.delete(walletHash);
1286
+ const addr = await getReceivingAddress();
1287
+ const neededBch = (selectedTier.price_sats - currentBalanceSats) / 100000000;
1288
+ const neededLine = addr ? '\\n\\nšŸ“„ **Fund your wallet:** \\\`' + addr + '\\\`\\nOr run: paytaca receive (in another terminal) for QR code' : '';
1289
+ sseLine(res, {
1290
+ id: 'balance-err',
1291
+ object: 'chat.completion.chunk',
1292
+ choices: [{ index: 0, delta: { content: PROXY_MARKER + '\\n\\nāŒ **Insufficient balance** — You have **' + (currentBalanceSats / 100000000).toFixed(8) + ' BCH** but need **' + (selectedTier.price_sats / 100000000).toFixed(8) + ' BCH** for this plan. Top up at least **' + neededBch.toFixed(8) + ' BCH** more.' + neededLine + '\\n\\nType \\\`balance\\\` to re-check or try a different plan:' }, finish_reason: 'stop' }],
1293
+ });
1294
+ sseLine(res, {
1295
+ id: 'balance-err-done',
1296
+ object: 'chat.completion.chunk',
1297
+ choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
1298
+ });
1299
+ sseDone(res);
1300
+ res.end();
1301
+ return;
1302
+ }
1303
+ } else {
1304
+ // LIFT path: fail fast if the wallet holds no LIFT tokens.
1305
+ const liftBalanceUnits = await getLiftBalance();
1306
+ if (liftBalanceUnits !== null && liftBalanceUnits <= 0) {
1307
+ log('No LIFT tokens for wallet ' + walletHash?.substring(0, 16) + '...');
1308
+ pendingPayments.delete(walletHash);
1309
+ const addr = await getReceivingAddress();
1310
+ const fundLine = addr ? '\\n\\nšŸ“„ **Add LIFT to your wallet:** \\\`' + addr + '\\\` (send LIFT tokens) or buy LIFT on the Cauldron DEX' : '';
1311
+ sseLine(res, {
1312
+ id: 'lift-err',
1313
+ object: 'chat.completion.chunk',
1314
+ choices: [{ index: 0, delta: { content: PROXY_MARKER + '\\n\\nāŒ **No LIFT tokens** — you need LIFT to pay with tokens. Add LIFT to your wallet, then type a plan number above or \\\`LIFT\\\` again.' + fundLine + '\\n\\nType \\\`balance\\\` to re-check:' }, finish_reason: 'stop' }],
1315
+ });
1316
+ sseLine(res, {
1317
+ id: 'lift-err-done',
1318
+ object: 'chat.completion.chunk',
1319
+ choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
1320
+ });
1321
+ sseDone(res);
1322
+ res.end();
1323
+ return;
1324
+ }
1223
1325
  }
1224
1326
 
1225
1327
  // Keepalive during payment processing
@@ -1236,7 +1338,7 @@ const server = http.createServer(async (req, res) => {
1236
1338
  res.write(': keepalive\\n\\n');
1237
1339
  }, 2000);
1238
1340
 
1239
- runPaytacaPay(BACKEND_URL + '/v1', pendingPayload.body, walletHash, extraHeaders, async (err, responseJson) => {
1341
+ runPaytacaPay(BACKEND_URL + '/v1', pendingPayload.body, walletHash, extraHeaders, pendingPayload.paymentMethod || 'bch', async (err, responseJson) => {
1240
1342
  pendingPayments.delete(walletHash);
1241
1343
  clearInterval(keepalive);
1242
1344
 
@@ -1299,7 +1401,18 @@ const server = http.createServer(async (req, res) => {
1299
1401
 
1300
1402
  if (wasStreaming) {
1301
1403
  try {
1302
- jsonToSse(res, chatCompletion, { prependContent: '\\nšŸ’³ Payment successful — generating your response...\\n\\n' });
1404
+ let prepend = '\\nšŸ’³ Payment successful — generating your response...\\n\\n';
1405
+ if (pendingPayload.paymentMethod === 'lift') {
1406
+ const liftPercent = await getLiftDiscountPercent();
1407
+ const selectedTier = (pendingPayload.tiers || []).find((t) => t.minutes === pendingPayload.durationMinutes);
1408
+ if (liftPercent > 0 && selectedTier && selectedTier.price_sats) {
1409
+ const savedBch = (selectedTier.price_sats * (liftPercent / 100) / 100000000).toFixed(8);
1410
+ prepend = '\\nšŸ’³ Payment successful — paid with LIFT (**' + liftPercent + '% off**, saved **' + savedBch + ' BCH**). Generating your response...\\n\\n';
1411
+ } else {
1412
+ prepend = '\\nšŸ’³ Payment successful — paid with LIFT tokens. Generating your response...\\n\\n';
1413
+ }
1414
+ }
1415
+ jsonToSse(res, chatCompletion, { prependContent: prepend });
1303
1416
  } catch (e) { log('jsonToSse threw: ' + e.message); }
1304
1417
  } else {
1305
1418
  try {
@@ -1345,7 +1458,7 @@ const server = http.createServer(async (req, res) => {
1345
1458
  res.write(': keepalive\\n\\n');
1346
1459
  }, 2000);
1347
1460
 
1348
- runPaytacaPay(BACKEND_URL + '/v1', pendingPayload.body, walletHash, extraHeaders, (err, responseJson) => {
1461
+ runPaytacaPay(BACKEND_URL + '/v1', pendingPayload.body, walletHash, extraHeaders, 'bch', (err, responseJson) => {
1349
1462
  clearInterval(keepalive);
1350
1463
  if (err) {
1351
1464
  log('paytaca pay failed: ' + err.message);
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.js","sourceRoot":"","sources":["../../src/bundled/proxy.ts"],"names":[],"mappings":";AAAA,0DAA0D;AAC1D,6DAA6D;;;AAEhD,QAAA,oBAAoB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAonDnC,CAAC"}
1
+ {"version":3,"file":"proxy.js","sourceRoot":"","sources":["../../src/bundled/proxy.ts"],"names":[],"mappings":";AAAA,0DAA0D;AAC1D,6DAA6D;;;AAEhD,QAAA,oBAAoB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAquDnC,CAAC"}
@@ -1,2 +1,2 @@
1
- export declare const WRAPPER_SCRIPT_CONTENT = "#!/usr/bin/env node\n/**\n * Paytaca Pay Wrapper \u2014 handles large request bodies by reading from a file.\n * Imports paytaca-cli modules directly (avoids CLI argument size limits).\n */\n\nimport { readFileSync } from 'fs';\nimport { execSync } from 'child_process';\nimport { fileURLToPath } from 'url';\nimport { dirname, join } from 'path';\n\n// How long to wait for the server to respond before treating the payment as timed out.\n// Default 240s so heavy non-streaming generations (large context / long output) can\n// complete; override with PAYTACA_PAY_TIMEOUT_MS.\nconst PAY_TIMEOUT_MS = Number(process.env.PAYTACA_PAY_TIMEOUT_MS || 240000);\n\n// Find paytaca-cli installation\nfunction findPaytacaCliPath() {\n const possiblePaths = [];\n \n // Try to get global npm root\n try {\n const globalPath = execSync('npm root -g', { encoding: 'utf8' }).trim();\n possiblePaths.push(\n join(globalPath, 'paytaca-cli'),\n join(globalPath, 'opencode-plugin', 'node_modules', 'paytaca-cli'),\n );\n } catch {}\n \n // Common global locations\n possiblePaths.push(\n '/usr/lib/node_modules/paytaca-cli',\n '/usr/local/lib/node_modules/paytaca-cli',\n '/opt/homebrew/lib/node_modules/paytaca-cli',\n );\n \n // Try current file's node_modules (for bundled installs)\n try {\n const currentFile = fileURLToPath(import.meta.url);\n const currentDir = dirname(currentFile);\n possiblePaths.push(\n join(currentDir, '..', 'node_modules', 'paytaca-cli'),\n join(currentDir, '..', '..', 'node_modules', 'paytaca-cli'),\n );\n } catch {}\n \n // Find first valid path\n for (const basePath of possiblePaths) {\n try {\n const walletPath = join(basePath, 'dist', 'wallet', 'index.js');\n readFileSync(walletPath);\n return basePath;\n } catch {}\n }\n \n throw new Error('paytaca-cli not found. Try reinstalling opencode-plugin: npm install @paytaca/opencode-plugin');\n}\n\n// Load paytaca-cli modules\nlet loadMnemonic, loadWallet, LibauthHDWallet, X402Payer, parsePaymentRequiredJson, selectBchPaymentRequirements, BCH_DERIVATION_PATH;\n\ntry {\n const basePath = findPaytacaCliPath();\n \n ({ loadMnemonic, loadWallet } = await import(join(basePath, 'dist', 'wallet', 'index.js')));\n ({ LibauthHDWallet } = await import(join(basePath, 'dist', 'wallet', 'keys.js')));\n ({ X402Payer } = await import(join(basePath, 'dist', 'wallet', 'x402.js')));\n ({ parsePaymentRequiredJson, selectBchPaymentRequirements } = await import(join(basePath, 'dist', 'utils', 'x402.js')));\n ({ BCH_DERIVATION_PATH } = await import(join(basePath, 'dist', 'utils', 'network.js')));\n} catch (err) {\n console.log(JSON.stringify({ success: false, error: 'Failed to load paytaca-cli: ' + err.message }));\n process.exit(1);\n}\n\nasync function main() {\n const configPath = process.argv[2];\n if (!configPath) {\n console.log(JSON.stringify({ success: false, error: 'Usage: node paytaca-pay-wrapper.mjs <config.json>' }));\n process.exit(1);\n }\n\n const config = JSON.parse(readFileSync(configPath, 'utf8'));\n const { url, method, headers, bodyFile, chipnet, confirmed } = config;\n\n const body = readFileSync(bodyFile, 'utf8');\n\n const data = loadMnemonic();\n if (!data) {\n console.log(JSON.stringify({ success: false, error: 'No wallet found. Run paytaca wallet create first.' }));\n process.exit(1);\n }\n\n const wallet = loadWallet();\n const isChipnet = Boolean(chipnet);\n const bchWallet = wallet.forNetwork(isChipnet);\n const hdWallet = new LibauthHDWallet(data.mnemonic, BCH_DERIVATION_PATH, isChipnet ? 'chipnet' : 'mainnet');\n const x402Payer = new X402Payer({ hdWallet, addressIndex: 0 });\n\n try {\n const result = await executePay(url, method, headers, body, bchWallet, x402Payer, isChipnet, confirmed);\n console.log(JSON.stringify(result, null, 2));\n } catch (err) {\n console.log(JSON.stringify({ success: false, error: err.message || String(err) }, null, 2));\n process.exit(1);\n }\n}\n\nasync function executePay(url, method, headers, body, bchWallet, x402Payer, isChipnet, confirmed) {\n const response = await fetch(url, {\n method,\n headers,\n body: ['POST', 'PUT', 'PATCH'].includes(method) ? body : undefined,\n signal: AbortSignal.timeout(PAY_TIMEOUT_MS),\n });\n\n const responseHeaders = {};\n response.headers.forEach((value, key) => { responseHeaders[key] = value; });\n const responseText = await response.text();\n let responseData;\n try { responseData = JSON.parse(responseText); } catch { responseData = responseText; }\n\n if (response.status === 402) {\n const paymentRequired = parsePaymentRequiredJson(responseData);\n if (!paymentRequired) {\n return { success: false, status: 402, error: 'Could not parse PaymentRequired from 402 response body' };\n }\n const requirements = selectBchPaymentRequirements(paymentRequired, isChipnet ? 'chipnet' : 'mainnet');\n if (!requirements) {\n return {\n success: false, status: 402, error: 'Server does not accept BCH payment',\n data: { acceptedSchemes: paymentRequired.accepts.map(a => ({ scheme: a.scheme, network: a.network })) },\n };\n }\n\n const payerAddress = x402Payer.getPayerAddress();\n const address = requirements.payTo;\n const amountBch = Number(requirements.amount) / 1e8;\n const changeAddressSet = bchWallet.getAddressSetAt(0);\n const changeAddress = changeAddressSet.change;\n\n if (!confirmed) {\n return {\n success: false, status: 402, error: 'Payment not confirmed.',\n payment: { required: true, amount: requirements.amount, payTo: address },\n };\n }\n\n const sendResult = await bchWallet.sendBch(amountBch, address, changeAddress);\n if (!sendResult.success) {\n return { success: false, status: 402, payment: { required: true, error: sendResult.error }, error: sendResult.error };\n }\n\n const txid = sendResult.txid;\n const paymentPayload = await x402Payer.createPaymentPayload(requirements, paymentRequired.resource.url, txid, 0, requirements.amount);\n headers['PAYMENT-SIGNATURE'] = JSON.stringify(paymentPayload);\n\n let retryResponse;\n try {\n retryResponse = await fetch(url, {\n method,\n headers,\n body: ['POST', 'PUT', 'PATCH'].includes(method) ? body : undefined,\n signal: AbortSignal.timeout(PAY_TIMEOUT_MS),\n });\n } catch (e) {\n if (e.name === 'AbortError') {\n return { success: false, timeout: true, error: 'Response timed out from server.' };\n }\n throw e;\n }\n const retryResponseHeaders = {};\n retryResponse.headers.forEach((value, key) => { retryResponseHeaders[key] = value; });\n const retryResponseText = await retryResponse.text();\n let retryResponseData;\n try { retryResponseData = JSON.parse(retryResponseText); } catch { retryResponseData = retryResponseText; }\n\n return {\n success: retryResponse.ok,\n status: retryResponse.status,\n statusText: retryResponse.statusText,\n headers: retryResponseHeaders,\n data: retryResponseData,\n payment: { required: true, txid, recipientAddress: address },\n };\n }\n\n return {\n success: response.ok,\n status: response.status,\n statusText: response.statusText,\n headers: responseHeaders,\n data: responseData,\n payment: { required: false },\n };\n}\n\nmain();\n";
1
+ export declare const WRAPPER_SCRIPT_CONTENT = "#!/usr/bin/env node\n/**\n * Paytaca Pay Wrapper \u2014 handles large request bodies by reading from a file.\n * Imports paytaca-cli modules directly (avoids CLI argument size limits).\n */\n\nimport { readFileSync } from 'fs';\nimport { execSync } from 'child_process';\nimport { fileURLToPath } from 'url';\nimport { dirname, join } from 'path';\n\n// How long to wait for the server to respond before treating the payment as timed out.\n// Default 240s so heavy non-streaming generations (large context / long output) can\n// complete; override with PAYTACA_PAY_TIMEOUT_MS.\nconst PAY_TIMEOUT_MS = Number(process.env.PAYTACA_PAY_TIMEOUT_MS || 240000);\n\n// Find paytaca-cli installation\nfunction findPaytacaCliPath() {\n const possiblePaths = [];\n \n // Try to get global npm root\n try {\n const globalPath = execSync('npm root -g', { encoding: 'utf8' }).trim();\n possiblePaths.push(\n join(globalPath, 'paytaca-cli'),\n join(globalPath, 'opencode-plugin', 'node_modules', 'paytaca-cli'),\n );\n } catch {}\n \n // Common global locations\n possiblePaths.push(\n '/usr/lib/node_modules/paytaca-cli',\n '/usr/local/lib/node_modules/paytaca-cli',\n '/opt/homebrew/lib/node_modules/paytaca-cli',\n );\n \n // Try current file's node_modules (for bundled installs)\n try {\n const currentFile = fileURLToPath(import.meta.url);\n const currentDir = dirname(currentFile);\n possiblePaths.push(\n join(currentDir, '..', 'node_modules', 'paytaca-cli'),\n join(currentDir, '..', '..', 'node_modules', 'paytaca-cli'),\n );\n } catch {}\n \n // Find first valid path\n for (const basePath of possiblePaths) {\n try {\n const walletPath = join(basePath, 'dist', 'wallet', 'index.js');\n readFileSync(walletPath);\n return basePath;\n } catch {}\n }\n \n throw new Error('paytaca-cli not found. Try reinstalling opencode-plugin: npm install @paytaca/opencode-plugin');\n}\n\n// Load paytaca-cli modules\nlet loadMnemonic, loadWallet, LibauthHDWallet, X402Payer, parsePaymentRequiredJson, selectBchPaymentRequirements, BCH_DERIVATION_PATH;\n\ntry {\n const basePath = findPaytacaCliPath();\n \n ({ loadMnemonic, loadWallet } = await import(join(basePath, 'dist', 'wallet', 'index.js')));\n ({ LibauthHDWallet } = await import(join(basePath, 'dist', 'wallet', 'keys.js')));\n ({ X402Payer } = await import(join(basePath, 'dist', 'wallet', 'x402.js')));\n ({ parsePaymentRequiredJson, selectBchPaymentRequirements } = await import(join(basePath, 'dist', 'utils', 'x402.js')));\n ({ BCH_DERIVATION_PATH } = await import(join(basePath, 'dist', 'utils', 'network.js')));\n} catch (err) {\n console.log(JSON.stringify({ success: false, error: 'Failed to load paytaca-cli: ' + err.message }));\n process.exit(1);\n}\n\n// Cauldron payment support (opt-in via config.paymentMethod === 'lift').\n// The LIFT token is sold in a single swap transaction whose output pays the\n// x402 payTo address directly. Uses the same machinery as paytaca-cli's\n// \"paytaca swap\" command, imported via absolute paths because the wrapper runs\n// outside any node_modules tree.\nconst LIFT_TOKEN_ID = process.env.PAYTACA_PAYMENT_TOKEN_ID || '5932b2fd4915d6a75d3ec53282cd49118149a2176ee67ed68b1111ff0786f7fc';\nlet cauldronLoaded = false;\nlet fetchPoolsForToken, apiPoolToMicroPool, microPoolToPoolV0, attemptTrade, watchtowerUtxosToSpendableCoins, ExchangeLab, PayoutAmountRuleType, cashAddressToLockingBytecode, binToHex;\ntry {\n const basePath = findPaytacaCliPath();\n const cauldronDir = join(basePath, 'dist', 'wallet', 'cauldron');\n const cashlabDir = join(basePath, 'node_modules', '@cashlab');\n ({ fetchPoolsForToken } = await import(join(cauldronDir, 'api.js')));\n ({ apiPoolToMicroPool, microPoolToPoolV0 } = await import(join(cauldronDir, 'pools.js')));\n ({ attemptTrade, watchtowerUtxosToSpendableCoins } = await import(join(cauldronDir, 'transact.js')));\n ({ default: ExchangeLab } = await import(join(cashlabDir, 'cauldron', 'out', 'exchange-lab.js')));\n ({ PayoutAmountRuleType } = await import(join(cashlabDir, 'common', 'out', 'constants.js')));\n ({ cashAddressToLockingBytecode, binToHex } = await import(join(cashlabDir, 'common', 'out', 'libauth.js')));\n cauldronLoaded = true;\n} catch (err) {\n // Cauldron modules are only needed for LIFT payments; BCH payments still work.\n cauldronLoaded = false;\n}\n\nasync function main() {\n const configPath = process.argv[2];\n if (!configPath) {\n console.log(JSON.stringify({ success: false, error: 'Usage: node paytaca-pay-wrapper.mjs <config.json>' }));\n process.exit(1);\n }\n\n const config = JSON.parse(readFileSync(configPath, 'utf8'));\n const { url, method, headers, bodyFile, chipnet, confirmed, paymentMethod } = config;\n\n const body = readFileSync(bodyFile, 'utf8');\n\n const data = loadMnemonic();\n if (!data) {\n console.log(JSON.stringify({ success: false, error: 'No wallet found. Run paytaca wallet create first.' }));\n process.exit(1);\n }\n\n const wallet = loadWallet();\n const isChipnet = Boolean(chipnet);\n const bchWallet = wallet.forNetwork(isChipnet);\n const hdWallet = new LibauthHDWallet(data.mnemonic, BCH_DERIVATION_PATH, isChipnet ? 'chipnet' : 'mainnet');\n const x402Payer = new X402Payer({ hdWallet, addressIndex: 0 });\n\n try {\n const result = await executePay(url, method, headers, body, bchWallet, hdWallet, x402Payer, isChipnet, confirmed, paymentMethod);\n console.log(JSON.stringify(result, null, 2));\n } catch (err) {\n console.log(JSON.stringify({ success: false, error: err.message || String(err) }, null, 2));\n process.exit(1);\n }\n}\n\n// Sell LIFT tokens via Cauldron in a single swap transaction that pays the\n// x402 payTo address directly. Returns { txid, vout } for the payment payload.\nasync function payWithLift(bchWallet, hdWallet, requirements, changeAddress) {\n if (!cauldronLoaded) {\n throw new Error('Cauldron payment modules unavailable. Update paytaca-cli to 0.5.0+ to pay with LIFT.');\n }\n const tokenId = LIFT_TOKEN_ID;\n const amountSats = BigInt(requirements.amount);\n\n const [apiPools, allUtxos, tokenUtxos] = await Promise.all([\n fetchPoolsForToken(tokenId),\n bchWallet.getUtxos(),\n bchWallet.getUtxos({ category: tokenId }),\n ]);\n if (!apiPools || apiPools.length === 0) {\n throw new Error('No active Cauldron pools for the payment token.');\n }\n const pools = apiPools.map(apiPoolToMicroPool).map(microPoolToPoolV0);\n\n const tokenBalance = (tokenUtxos || []).reduce((sum, u) => sum + BigInt(u.amount || 0), 0n);\n if (tokenBalance <= 0n) {\n throw new Error('No LIFT tokens in the wallet. Add LIFT to pay this plan with tokens, or pay with BCH.');\n }\n\n const bchUtxos = allUtxos.filter((utxo) => !utxo.is_cashtoken);\n const spendableCoins = watchtowerUtxosToSpendableCoins({\n utxos: [...bchUtxos, ...(tokenUtxos || [])],\n wallet: hdWallet,\n });\n if (spendableCoins.length === 0) {\n throw new Error('No spendable UTXOs available.');\n }\n\n const payToDecoded = cashAddressToLockingBytecode(requirements.payTo);\n if (!payToDecoded || typeof payToDecoded === 'string' || !payToDecoded.bytecode) {\n throw new Error('Invalid payment address: ' + requirements.payTo);\n }\n const changeDecoded = cashAddressToLockingBytecode(changeAddress);\n if (!changeDecoded || typeof changeDecoded === 'string' || !changeDecoded.bytecode) {\n throw new Error('Invalid change address: ' + changeAddress);\n }\n\n const exlab = new ExchangeLab();\n const payoutRules = [\n { type: PayoutAmountRuleType.FIXED, locking_bytecode: payToDecoded.bytecode, amount: amountSats },\n { type: PayoutAmountRuleType.CHANGE, locking_bytecode: changeDecoded.bytecode, allow_mixing_native_and_token: false, allow_mixing_native_and_token_when_bch_change_is_dust: false, add_change_to_txfee_when_bch_change_is_dust: true },\n ];\n\n // Back-compute the token supply for a demand target slightly above the plan\n // cost so the received BCH covers the fixed payout plus fees (excess becomes\n // change). Retry with a bigger buffer if the first target leaves no change.\n let trade = null;\n let tradeTx = null;\n let lastError = null;\n for (const buffer of [2000n, 20000n, 100000n]) {\n try {\n trade = attemptTrade({ pools, isBuyingToken: false, supply: undefined, demand: amountSats + buffer });\n tradeTx = exlab.createTradeTx(trade.entries, spendableCoins, payoutRules, null, 1n);\n exlab.verifyTradeTx(tradeTx);\n break;\n } catch (e) {\n lastError = e;\n }\n }\n if (!tradeTx) {\n const supply = trade?.summary?.supply;\n if (supply && tokenBalance < supply) {\n throw new Error('Insufficient LIFT balance: this payment needs ' + supply + ' base units but the wallet has ' + tokenBalance + '.');\n }\n throw new Error('Could not fund the payment by selling LIFT: ' + (lastError?.message || 'unknown error'));\n }\n\n const tx = tradeTx.libauth_generated_transaction;\n const payToHex = binToHex(payToDecoded.bytecode);\n const vout = tx.outputs.findIndex((o) => binToHex(o.lockingBytecode) === payToHex);\n if (vout === -1) {\n throw new Error('Payment output missing from built transaction.');\n }\n\n const txHex = binToHex(tradeTx.txbin);\n const broadcastResponse = await bchWallet.watchtower.BCH._api.post('broadcast/', { transaction: txHex });\n const data = broadcastResponse.data;\n if (data?.result) {\n data[data.success ? 'txid' : 'error'] = data.result;\n delete data.result;\n }\n if (!data?.success || !data?.txid) {\n throw new Error(data?.error || 'Broadcast failed');\n }\n return { txid: data.txid, vout };\n}\n\nasync function executePay(url, method, headers, body, bchWallet, hdWallet, x402Payer, isChipnet, confirmed, paymentMethod) {\n // Tell the backend the payment method so it can apply the LIFT discount and\n // record how the plan was paid. Set once here \u2014 the same headers object is\n // reused for the 402 fetch and the PAYMENT-SIGNATURE retry.\n if (paymentMethod === 'lift') {\n headers['X-Payment-Method'] = 'lift';\n }\n\n const response = await fetch(url, {\n method,\n headers,\n body: ['POST', 'PUT', 'PATCH'].includes(method) ? body : undefined,\n signal: AbortSignal.timeout(PAY_TIMEOUT_MS),\n });\n\n const responseHeaders = {};\n response.headers.forEach((value, key) => { responseHeaders[key] = value; });\n const responseText = await response.text();\n let responseData;\n try { responseData = JSON.parse(responseText); } catch { responseData = responseText; }\n\n if (response.status === 402) {\n const paymentRequired = parsePaymentRequiredJson(responseData);\n if (!paymentRequired) {\n return { success: false, status: 402, error: 'Could not parse PaymentRequired from 402 response body' };\n }\n const requirements = selectBchPaymentRequirements(paymentRequired, isChipnet ? 'chipnet' : 'mainnet');\n if (!requirements) {\n return {\n success: false, status: 402, error: 'Server does not accept BCH payment',\n data: { acceptedSchemes: paymentRequired.accepts.map(a => ({ scheme: a.scheme, network: a.network })) },\n };\n }\n\n const payerAddress = x402Payer.getPayerAddress();\n const address = requirements.payTo;\n const amountBch = Number(requirements.amount) / 1e8;\n const changeAddressSet = bchWallet.getAddressSetAt(0);\n const changeAddress = changeAddressSet.change;\n\n if (!confirmed) {\n return {\n success: false, status: 402, error: 'Payment not confirmed.',\n payment: { required: true, amount: requirements.amount, payTo: address },\n };\n }\n\n let txid, vout = 0;\n if (paymentMethod === 'lift') {\n // Sell LIFT via Cauldron; the swap transaction pays the plan directly.\n const liftPayment = await payWithLift(bchWallet, hdWallet, requirements, changeAddress);\n txid = liftPayment.txid;\n vout = liftPayment.vout;\n } else {\n const sendResult = await bchWallet.sendBch(amountBch, address, changeAddress);\n if (!sendResult.success) {\n return { success: false, status: 402, payment: { required: true, error: sendResult.error }, error: sendResult.error };\n }\n txid = sendResult.txid;\n }\n\n const paymentPayload = await x402Payer.createPaymentPayload(requirements, paymentRequired.resource.url, txid, vout, requirements.amount);\n headers['PAYMENT-SIGNATURE'] = JSON.stringify(paymentPayload);\n\n let retryResponse;\n try {\n retryResponse = await fetch(url, {\n method,\n headers,\n body: ['POST', 'PUT', 'PATCH'].includes(method) ? body : undefined,\n signal: AbortSignal.timeout(PAY_TIMEOUT_MS),\n });\n } catch (e) {\n if (e.name === 'AbortError') {\n return { success: false, timeout: true, error: 'Response timed out from server.' };\n }\n throw e;\n }\n const retryResponseHeaders = {};\n retryResponse.headers.forEach((value, key) => { retryResponseHeaders[key] = value; });\n const retryResponseText = await retryResponse.text();\n let retryResponseData;\n try { retryResponseData = JSON.parse(retryResponseText); } catch { retryResponseData = retryResponseText; }\n\n return {\n success: retryResponse.ok,\n status: retryResponse.status,\n statusText: retryResponse.statusText,\n headers: retryResponseHeaders,\n data: retryResponseData,\n payment: { required: true, txid, recipientAddress: address, method: paymentMethod === 'lift' ? 'lift' : 'bch' },\n };\n }\n\n return {\n success: response.ok,\n status: response.status,\n statusText: response.statusText,\n headers: responseHeaders,\n data: responseData,\n payment: { required: false },\n };\n}\n\nmain();\n";
2
2
  //# sourceMappingURL=wrapper.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"wrapper.d.ts","sourceRoot":"","sources":["../../src/bundled/wrapper.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB,ozOAqMlC,CAAC"}
1
+ {"version":3,"file":"wrapper.d.ts","sourceRoot":"","sources":["../../src/bundled/wrapper.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB,6lbAwUlC,CAAC"}
@@ -78,6 +78,30 @@ try {
78
78
  process.exit(1);
79
79
  }
80
80
 
81
+ // Cauldron payment support (opt-in via config.paymentMethod === 'lift').
82
+ // The LIFT token is sold in a single swap transaction whose output pays the
83
+ // x402 payTo address directly. Uses the same machinery as paytaca-cli's
84
+ // "paytaca swap" command, imported via absolute paths because the wrapper runs
85
+ // outside any node_modules tree.
86
+ const LIFT_TOKEN_ID = process.env.PAYTACA_PAYMENT_TOKEN_ID || '5932b2fd4915d6a75d3ec53282cd49118149a2176ee67ed68b1111ff0786f7fc';
87
+ let cauldronLoaded = false;
88
+ let fetchPoolsForToken, apiPoolToMicroPool, microPoolToPoolV0, attemptTrade, watchtowerUtxosToSpendableCoins, ExchangeLab, PayoutAmountRuleType, cashAddressToLockingBytecode, binToHex;
89
+ try {
90
+ const basePath = findPaytacaCliPath();
91
+ const cauldronDir = join(basePath, 'dist', 'wallet', 'cauldron');
92
+ const cashlabDir = join(basePath, 'node_modules', '@cashlab');
93
+ ({ fetchPoolsForToken } = await import(join(cauldronDir, 'api.js')));
94
+ ({ apiPoolToMicroPool, microPoolToPoolV0 } = await import(join(cauldronDir, 'pools.js')));
95
+ ({ attemptTrade, watchtowerUtxosToSpendableCoins } = await import(join(cauldronDir, 'transact.js')));
96
+ ({ default: ExchangeLab } = await import(join(cashlabDir, 'cauldron', 'out', 'exchange-lab.js')));
97
+ ({ PayoutAmountRuleType } = await import(join(cashlabDir, 'common', 'out', 'constants.js')));
98
+ ({ cashAddressToLockingBytecode, binToHex } = await import(join(cashlabDir, 'common', 'out', 'libauth.js')));
99
+ cauldronLoaded = true;
100
+ } catch (err) {
101
+ // Cauldron modules are only needed for LIFT payments; BCH payments still work.
102
+ cauldronLoaded = false;
103
+ }
104
+
81
105
  async function main() {
82
106
  const configPath = process.argv[2];
83
107
  if (!configPath) {
@@ -86,7 +110,7 @@ async function main() {
86
110
  }
87
111
 
88
112
  const config = JSON.parse(readFileSync(configPath, 'utf8'));
89
- const { url, method, headers, bodyFile, chipnet, confirmed } = config;
113
+ const { url, method, headers, bodyFile, chipnet, confirmed, paymentMethod } = config;
90
114
 
91
115
  const body = readFileSync(bodyFile, 'utf8');
92
116
 
@@ -103,7 +127,7 @@ async function main() {
103
127
  const x402Payer = new X402Payer({ hdWallet, addressIndex: 0 });
104
128
 
105
129
  try {
106
- const result = await executePay(url, method, headers, body, bchWallet, x402Payer, isChipnet, confirmed);
130
+ const result = await executePay(url, method, headers, body, bchWallet, hdWallet, x402Payer, isChipnet, confirmed, paymentMethod);
107
131
  console.log(JSON.stringify(result, null, 2));
108
132
  } catch (err) {
109
133
  console.log(JSON.stringify({ success: false, error: err.message || String(err) }, null, 2));
@@ -111,7 +135,106 @@ async function main() {
111
135
  }
112
136
  }
113
137
 
114
- async function executePay(url, method, headers, body, bchWallet, x402Payer, isChipnet, confirmed) {
138
+ // Sell LIFT tokens via Cauldron in a single swap transaction that pays the
139
+ // x402 payTo address directly. Returns { txid, vout } for the payment payload.
140
+ async function payWithLift(bchWallet, hdWallet, requirements, changeAddress) {
141
+ if (!cauldronLoaded) {
142
+ throw new Error('Cauldron payment modules unavailable. Update paytaca-cli to 0.5.0+ to pay with LIFT.');
143
+ }
144
+ const tokenId = LIFT_TOKEN_ID;
145
+ const amountSats = BigInt(requirements.amount);
146
+
147
+ const [apiPools, allUtxos, tokenUtxos] = await Promise.all([
148
+ fetchPoolsForToken(tokenId),
149
+ bchWallet.getUtxos(),
150
+ bchWallet.getUtxos({ category: tokenId }),
151
+ ]);
152
+ if (!apiPools || apiPools.length === 0) {
153
+ throw new Error('No active Cauldron pools for the payment token.');
154
+ }
155
+ const pools = apiPools.map(apiPoolToMicroPool).map(microPoolToPoolV0);
156
+
157
+ const tokenBalance = (tokenUtxos || []).reduce((sum, u) => sum + BigInt(u.amount || 0), 0n);
158
+ if (tokenBalance <= 0n) {
159
+ throw new Error('No LIFT tokens in the wallet. Add LIFT to pay this plan with tokens, or pay with BCH.');
160
+ }
161
+
162
+ const bchUtxos = allUtxos.filter((utxo) => !utxo.is_cashtoken);
163
+ const spendableCoins = watchtowerUtxosToSpendableCoins({
164
+ utxos: [...bchUtxos, ...(tokenUtxos || [])],
165
+ wallet: hdWallet,
166
+ });
167
+ if (spendableCoins.length === 0) {
168
+ throw new Error('No spendable UTXOs available.');
169
+ }
170
+
171
+ const payToDecoded = cashAddressToLockingBytecode(requirements.payTo);
172
+ if (!payToDecoded || typeof payToDecoded === 'string' || !payToDecoded.bytecode) {
173
+ throw new Error('Invalid payment address: ' + requirements.payTo);
174
+ }
175
+ const changeDecoded = cashAddressToLockingBytecode(changeAddress);
176
+ if (!changeDecoded || typeof changeDecoded === 'string' || !changeDecoded.bytecode) {
177
+ throw new Error('Invalid change address: ' + changeAddress);
178
+ }
179
+
180
+ const exlab = new ExchangeLab();
181
+ const payoutRules = [
182
+ { type: PayoutAmountRuleType.FIXED, locking_bytecode: payToDecoded.bytecode, amount: amountSats },
183
+ { type: PayoutAmountRuleType.CHANGE, locking_bytecode: changeDecoded.bytecode, allow_mixing_native_and_token: false, allow_mixing_native_and_token_when_bch_change_is_dust: false, add_change_to_txfee_when_bch_change_is_dust: true },
184
+ ];
185
+
186
+ // Back-compute the token supply for a demand target slightly above the plan
187
+ // cost so the received BCH covers the fixed payout plus fees (excess becomes
188
+ // change). Retry with a bigger buffer if the first target leaves no change.
189
+ let trade = null;
190
+ let tradeTx = null;
191
+ let lastError = null;
192
+ for (const buffer of [2000n, 20000n, 100000n]) {
193
+ try {
194
+ trade = attemptTrade({ pools, isBuyingToken: false, supply: undefined, demand: amountSats + buffer });
195
+ tradeTx = exlab.createTradeTx(trade.entries, spendableCoins, payoutRules, null, 1n);
196
+ exlab.verifyTradeTx(tradeTx);
197
+ break;
198
+ } catch (e) {
199
+ lastError = e;
200
+ }
201
+ }
202
+ if (!tradeTx) {
203
+ const supply = trade?.summary?.supply;
204
+ if (supply && tokenBalance < supply) {
205
+ throw new Error('Insufficient LIFT balance: this payment needs ' + supply + ' base units but the wallet has ' + tokenBalance + '.');
206
+ }
207
+ throw new Error('Could not fund the payment by selling LIFT: ' + (lastError?.message || 'unknown error'));
208
+ }
209
+
210
+ const tx = tradeTx.libauth_generated_transaction;
211
+ const payToHex = binToHex(payToDecoded.bytecode);
212
+ const vout = tx.outputs.findIndex((o) => binToHex(o.lockingBytecode) === payToHex);
213
+ if (vout === -1) {
214
+ throw new Error('Payment output missing from built transaction.');
215
+ }
216
+
217
+ const txHex = binToHex(tradeTx.txbin);
218
+ const broadcastResponse = await bchWallet.watchtower.BCH._api.post('broadcast/', { transaction: txHex });
219
+ const data = broadcastResponse.data;
220
+ if (data?.result) {
221
+ data[data.success ? 'txid' : 'error'] = data.result;
222
+ delete data.result;
223
+ }
224
+ if (!data?.success || !data?.txid) {
225
+ throw new Error(data?.error || 'Broadcast failed');
226
+ }
227
+ return { txid: data.txid, vout };
228
+ }
229
+
230
+ async function executePay(url, method, headers, body, bchWallet, hdWallet, x402Payer, isChipnet, confirmed, paymentMethod) {
231
+ // Tell the backend the payment method so it can apply the LIFT discount and
232
+ // record how the plan was paid. Set once here — the same headers object is
233
+ // reused for the 402 fetch and the PAYMENT-SIGNATURE retry.
234
+ if (paymentMethod === 'lift') {
235
+ headers['X-Payment-Method'] = 'lift';
236
+ }
237
+
115
238
  const response = await fetch(url, {
116
239
  method,
117
240
  headers,
@@ -151,13 +274,21 @@ async function executePay(url, method, headers, body, bchWallet, x402Payer, isCh
151
274
  };
152
275
  }
153
276
 
154
- const sendResult = await bchWallet.sendBch(amountBch, address, changeAddress);
155
- if (!sendResult.success) {
156
- return { success: false, status: 402, payment: { required: true, error: sendResult.error }, error: sendResult.error };
277
+ let txid, vout = 0;
278
+ if (paymentMethod === 'lift') {
279
+ // Sell LIFT via Cauldron; the swap transaction pays the plan directly.
280
+ const liftPayment = await payWithLift(bchWallet, hdWallet, requirements, changeAddress);
281
+ txid = liftPayment.txid;
282
+ vout = liftPayment.vout;
283
+ } else {
284
+ const sendResult = await bchWallet.sendBch(amountBch, address, changeAddress);
285
+ if (!sendResult.success) {
286
+ return { success: false, status: 402, payment: { required: true, error: sendResult.error }, error: sendResult.error };
287
+ }
288
+ txid = sendResult.txid;
157
289
  }
158
290
 
159
- const txid = sendResult.txid;
160
- const paymentPayload = await x402Payer.createPaymentPayload(requirements, paymentRequired.resource.url, txid, 0, requirements.amount);
291
+ const paymentPayload = await x402Payer.createPaymentPayload(requirements, paymentRequired.resource.url, txid, vout, requirements.amount);
161
292
  headers['PAYMENT-SIGNATURE'] = JSON.stringify(paymentPayload);
162
293
 
163
294
  let retryResponse;
@@ -186,7 +317,7 @@ async function executePay(url, method, headers, body, bchWallet, x402Payer, isCh
186
317
  statusText: retryResponse.statusText,
187
318
  headers: retryResponseHeaders,
188
319
  data: retryResponseData,
189
- payment: { required: true, txid, recipientAddress: address },
320
+ payment: { required: true, txid, recipientAddress: address, method: paymentMethod === 'lift' ? 'lift' : 'bch' },
190
321
  };
191
322
  }
192
323
 
@@ -1 +1 @@
1
- {"version":3,"file":"wrapper.js","sourceRoot":"","sources":["../../src/bundled/wrapper.ts"],"names":[],"mappings":";AAAA,oEAAoE;AACpE,4EAA4E;AAC5E,mFAAmF;;;AAEtE,QAAA,sBAAsB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqMrC,CAAC"}
1
+ {"version":3,"file":"wrapper.js","sourceRoot":"","sources":["../../src/bundled/wrapper.ts"],"names":[],"mappings":";AAAA,oEAAoE;AACpE,4EAA4E;AAC5E,mFAAmF;;;AAEtE,QAAA,sBAAsB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwUrC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAc5C,wBAAgB,iBAAiB,IAAI,MAAM,CA4C1C;AAED,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAYpE;AAuCD,wBAAsB,iBAAiB,CAAC,SAAS,GAAE,MAAa,EAAE,OAAO,GAAE,MAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAOzG;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAOrD;AAED,wBAAsB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAiBlH;AAED,wBAAsB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CA+GtF;AAqBD,wBAAsB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAU5G"}
1
+ {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAc5C,wBAAgB,iBAAiB,IAAI,MAAM,CA4C1C;AAED,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAYpE;AAuCD,wBAAsB,iBAAiB,CAAC,SAAS,GAAE,MAAa,EAAE,OAAO,GAAE,MAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAOzG;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAOrD;AAED,wBAAsB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAiBlH;AAED,wBAAsB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAiHtF;AAqBD,wBAAsB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAU5G"}
package/dist/proxy.js CHANGED
@@ -173,17 +173,21 @@ async function startProxy(configDir, config) {
173
173
  const logFile = (0, config_1.getLogFile)(configDir);
174
174
  const wrapperScript = (0, config_1.getWrapperScript)(configDir);
175
175
  (0, config_1.ensureConfigDir)(configDir);
176
+ // Always refresh the payment wrapper on disk. It is executed fresh on every
177
+ // payment, so it must never go stale even when the proxy is reused/restarted.
178
+ fs.writeFileSync(wrapperScript, wrapper_1.WRAPPER_SCRIPT_CONTENT, 'utf8');
179
+ fs.chmodSync(wrapperScript, '755');
176
180
  const currentHash = scriptHash();
177
181
  const persistedConfig = (0, config_1.loadConfig)(configDir);
178
182
  let existingStatus = await getProxyStatus(configDir);
179
183
  const needsRestart = existingStatus.running &&
180
184
  (existingStatus.pid || 0) > 0 &&
181
185
  persistedConfig.proxyScriptHash !== currentHash;
182
- fs.writeFileSync(proxyScript, proxy_1.PROXY_SCRIPT_CONTENT, 'utf8');
183
- fs.chmodSync(proxyScript, '755');
184
186
  if (existingStatus.running && existingStatus.pid && existingStatus.port && !needsRestart) {
185
187
  return await reuseExistingProxy(configDir, config, { port: existingStatus.port, pid: existingStatus.pid }, currentHash);
186
188
  }
189
+ fs.writeFileSync(proxyScript, proxy_1.PROXY_SCRIPT_CONTENT, 'utf8');
190
+ fs.chmodSync(proxyScript, '755');
187
191
  if (existingStatus.running && existingStatus.pid) {
188
192
  try {
189
193
  process.kill(existingStatus.pid);
@@ -197,8 +201,6 @@ async function startProxy(configDir, config) {
197
201
  if (adopted) {
198
202
  return adopted;
199
203
  }
200
- fs.writeFileSync(wrapperScript, wrapper_1.WRAPPER_SCRIPT_CONTENT, 'utf8');
201
- fs.chmodSync(wrapperScript, '755');
202
204
  const paytacaCmd = getPaytacaCommand();
203
205
  const MAX_ATTEMPTS = 3;
204
206
  for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {