@paytaca/opencode-plugin 0.3.0 → 0.3.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 +30 -1
- package/dist/bundled/mcp.d.ts +1 -1
- package/dist/bundled/mcp.d.ts.map +1 -1
- package/dist/bundled/mcp.js +118 -2
- package/dist/bundled/mcp.js.map +1 -1
- package/dist/bundled/proxy.d.ts +1 -1
- package/dist/bundled/proxy.d.ts.map +1 -1
- package/dist/bundled/proxy.js +243 -6
- package/dist/bundled/proxy.js.map +1 -1
- package/dist/bundled/wrapper.d.ts +1 -1
- package/dist/bundled/wrapper.d.ts.map +1 -1
- package/dist/bundled/wrapper.js +116 -3
- package/dist/bundled/wrapper.js.map +1 -1
- package/dist/credits.d.ts +9 -0
- package/dist/credits.d.ts.map +1 -0
- package/dist/credits.js +120 -0
- package/dist/credits.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/bundled/proxy.js
CHANGED
|
@@ -64,7 +64,7 @@ let requestCounter = 0;
|
|
|
64
64
|
// Utility: run shell command and return output
|
|
65
65
|
function runCommand(cmd, args = []) {
|
|
66
66
|
return new Promise((resolve, reject) => {
|
|
67
|
-
const child = spawn(cmd, args, { shell:
|
|
67
|
+
const child = spawn(cmd, args, { shell: process.platform === 'win32' });
|
|
68
68
|
let stdout = '';
|
|
69
69
|
let stderr = '';
|
|
70
70
|
|
|
@@ -115,7 +115,7 @@ const LIFT_TOKEN_ID = '5932b2fd4915d6a75d3ec53282cd49118149a2176ee67ed68b1111ff0
|
|
|
115
115
|
async function getLiftBalance() {
|
|
116
116
|
try {
|
|
117
117
|
const output = await runCommand(PAYTACA_CMD, ['token', 'info', LIFT_TOKEN_ID]);
|
|
118
|
-
const match = output.match(/Balance
|
|
118
|
+
const match = output.match(/Balance:\s*([\d.]+)\s*LIFT/i);
|
|
119
119
|
if (match) return Math.round(parseFloat(match[1]) * 100);
|
|
120
120
|
return null;
|
|
121
121
|
} catch (err) {
|
|
@@ -124,6 +124,23 @@ async function getLiftBalance() {
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
// Short-lived cache of wallet + LIFT balances so we don't shell out to the CLI
|
|
128
|
+
// on every forwarded prompt. The backend concierge reads these headers to give
|
|
129
|
+
// free balance answers when the wallet has no paid capacity.
|
|
130
|
+
const BALANCE_CACHE_TTL = 15000;
|
|
131
|
+
let balanceCache = { at: 0, sats: null, lift: null };
|
|
132
|
+
|
|
133
|
+
async function getCachedBalances() {
|
|
134
|
+
const now = Date.now();
|
|
135
|
+
if (balanceCache.at && now - balanceCache.at < BALANCE_CACHE_TTL) {
|
|
136
|
+
return { sats: balanceCache.sats, lift: balanceCache.lift };
|
|
137
|
+
}
|
|
138
|
+
const sats = await getWalletBalance();
|
|
139
|
+
const lift = await getLiftBalance();
|
|
140
|
+
balanceCache = { at: now, sats, lift };
|
|
141
|
+
return { sats, lift };
|
|
142
|
+
}
|
|
143
|
+
|
|
127
144
|
// LIFT payment discount percent advertised by the backend (/v1/config), cached
|
|
128
145
|
// briefly (30s) so a server-side rate change is picked up quickly. Returns 0
|
|
129
146
|
// when unset/unavailable so callers can fall back to no-discount messaging.
|
|
@@ -381,7 +398,8 @@ async function streamLowBalanceNotice(res, modelName, otherModels) {
|
|
|
381
398
|
}
|
|
382
399
|
|
|
383
400
|
// Forward request to Django and return response (buffered, for non-streaming)
|
|
384
|
-
function forwardToDjango(req, body, callback) {
|
|
401
|
+
async function forwardToDjango(req, body, callback) {
|
|
402
|
+
const balances = await getCachedBalances();
|
|
385
403
|
const options = {
|
|
386
404
|
hostname: DJANGO_HOST,
|
|
387
405
|
port: DJANGO_PORT,
|
|
@@ -390,6 +408,8 @@ function forwardToDjango(req, body, callback) {
|
|
|
390
408
|
headers: {
|
|
391
409
|
'Content-Type': req.headers['content-type'] || 'application/json',
|
|
392
410
|
'X-Wallet-Hash': req.headers['x-wallet-hash'] || '',
|
|
411
|
+
'X-Wallet-Balance-Sats': balances.sats !== null ? String(balances.sats) : '',
|
|
412
|
+
'X-Lift-Balance-Units': balances.lift !== null ? String(balances.lift) : '',
|
|
393
413
|
'Content-Length': Buffer.byteLength(body),
|
|
394
414
|
},
|
|
395
415
|
};
|
|
@@ -433,7 +453,8 @@ function forwardToDjango(req, body, callback) {
|
|
|
433
453
|
}
|
|
434
454
|
|
|
435
455
|
// Forward streaming request to Django
|
|
436
|
-
function forwardStreaming(req, res, body, callback) {
|
|
456
|
+
async function forwardStreaming(req, res, body, callback) {
|
|
457
|
+
const balances = await getCachedBalances();
|
|
437
458
|
const options = {
|
|
438
459
|
hostname: DJANGO_HOST,
|
|
439
460
|
port: DJANGO_PORT,
|
|
@@ -442,6 +463,8 @@ function forwardStreaming(req, res, body, callback) {
|
|
|
442
463
|
headers: {
|
|
443
464
|
'Content-Type': req.headers['content-type'] || 'application/json',
|
|
444
465
|
'X-Wallet-Hash': req.headers['x-wallet-hash'] || '',
|
|
466
|
+
'X-Wallet-Balance-Sats': balances.sats !== null ? String(balances.sats) : '',
|
|
467
|
+
'X-Lift-Balance-Units': balances.lift !== null ? String(balances.lift) : '',
|
|
445
468
|
'Content-Length': Buffer.byteLength(body),
|
|
446
469
|
},
|
|
447
470
|
};
|
|
@@ -866,6 +889,184 @@ function runPaytacaPay(djangoUrl, body, walletHash, extraHeaders, paymentMethod,
|
|
|
866
889
|
});
|
|
867
890
|
}
|
|
868
891
|
|
|
892
|
+
// ---------------------------------------------------------------------------
|
|
893
|
+
// Auto-refill: when armed via the MCP auto_refill tool, the proxy silently buys
|
|
894
|
+
// a plan of the configured size on 402 (credits exhausted) and retries instead
|
|
895
|
+
// of showing the interactive tier prompt. It stops when the cumulative budget
|
|
896
|
+
// (maxMinutes) is reached, the requested model mismatches, funds are short, or
|
|
897
|
+
// 24h pass without a refill (so a stale armed state cannot keep spending in a
|
|
898
|
+
// later session).
|
|
899
|
+
const AUTO_REFILL_FILE = path.join(LOG_DIR, 'auto-refill.json');
|
|
900
|
+
|
|
901
|
+
function getAutoRefillState() {
|
|
902
|
+
try {
|
|
903
|
+
const s = JSON.parse(fs.readFileSync(AUTO_REFILL_FILE, 'utf8'));
|
|
904
|
+
if (!s || s.enabled !== true) {
|
|
905
|
+
return null;
|
|
906
|
+
}
|
|
907
|
+
return {
|
|
908
|
+
enabled: true,
|
|
909
|
+
minutes: Number(s.minutes) || 0,
|
|
910
|
+
maxMinutes: Number(s.maxMinutes) || 0,
|
|
911
|
+
spentMinutes: Number(s.spentMinutes) || 0,
|
|
912
|
+
model: s.model ? String(s.model) : '',
|
|
913
|
+
paymentMethod: s.paymentMethod === 'lift' ? 'lift' : 'bch',
|
|
914
|
+
startedAt: s.startedAt || new Date().toISOString(),
|
|
915
|
+
lastRefillAt: s.lastRefillAt || null,
|
|
916
|
+
};
|
|
917
|
+
} catch (err) {
|
|
918
|
+
log('Failed to read auto-refill state: ' + err.message);
|
|
919
|
+
return null;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function saveAutoRefillState(s) {
|
|
924
|
+
try {
|
|
925
|
+
fs.writeFileSync(AUTO_REFILL_FILE, JSON.stringify(s, null, 2), 'utf8');
|
|
926
|
+
} catch (err) {
|
|
927
|
+
log('Failed to write auto-refill state: ' + err.message);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Can the proxy buy another plan right now? Handles budget, model scope and a
|
|
932
|
+
// staleness rule — when it returns false the caller falls through to the normal
|
|
933
|
+
// interactive tier prompt so the user can buy manually.
|
|
934
|
+
function autoRefillCanBuy(s, modelId) {
|
|
935
|
+
if (!s || !s.enabled) return false;
|
|
936
|
+
if (s.minutes <= 0 || s.maxMinutes < s.minutes) {
|
|
937
|
+
log('Auto-refill disarmed: bad config (minutes=' + s.minutes + ' maxMinutes=' + s.maxMinutes + ')');
|
|
938
|
+
s.enabled = false;
|
|
939
|
+
saveAutoRefillState(s);
|
|
940
|
+
return false;
|
|
941
|
+
}
|
|
942
|
+
if (s.spentMinutes >= s.maxMinutes) {
|
|
943
|
+
return false;
|
|
944
|
+
}
|
|
945
|
+
if (s.model && (!modelId || s.model !== modelId)) {
|
|
946
|
+
return false;
|
|
947
|
+
}
|
|
948
|
+
const now = Date.now();
|
|
949
|
+
const anchorMs = new Date(s.lastRefillAt || s.startedAt || now).getTime();
|
|
950
|
+
if (isNaN(anchorMs) || now - anchorMs > 24 * 3600 * 1000) {
|
|
951
|
+
log('Auto-refill disarmed: stale (no refill for 24h)');
|
|
952
|
+
s.enabled = false;
|
|
953
|
+
saveAutoRefillState(s);
|
|
954
|
+
return false;
|
|
955
|
+
}
|
|
956
|
+
return true;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// Buy the configured plan automatically and retry the request. Mirrors the
|
|
960
|
+
// interactive payment path (keepalive + paytaca pay + SSE delivery) but never
|
|
961
|
+
// asks the user to pick a tier. On failure it disarms auto-refill and streams
|
|
962
|
+
// the payment-error prompt so the user can retry by hand.
|
|
963
|
+
async function autoRefillAndRetry(res, walletHash, pendingPayload, refill) {
|
|
964
|
+
log('AUTO-REFILL: buying ' + refill.minutes + '-min plan for wallet ' + walletHash?.substring(0, 16) + '...');
|
|
965
|
+
pendingPayload.step = 'processing';
|
|
966
|
+
const extraHeaders = {};
|
|
967
|
+
if (pendingPayload.modelId) {
|
|
968
|
+
extraHeaders['X-Model-Id'] = pendingPayload.modelId;
|
|
969
|
+
}
|
|
970
|
+
extraHeaders['X-Duration-Minutes'] = String(refill.minutes);
|
|
971
|
+
if (refill.paymentMethod === 'lift') {
|
|
972
|
+
extraHeaders['X-Payment-Method'] = 'lift';
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
if (!res.headersSent) {
|
|
976
|
+
try {
|
|
977
|
+
res.writeHead(200, {
|
|
978
|
+
'Content-Type': 'text/event-stream',
|
|
979
|
+
'Cache-Control': 'no-cache',
|
|
980
|
+
'Connection': 'keep-alive',
|
|
981
|
+
'X-Payment-Processing': 'true',
|
|
982
|
+
});
|
|
983
|
+
} catch (e) {
|
|
984
|
+
log('autoRefillAndRetry writeHead failed: ' + e.message);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
const keepalive = setInterval(() => {
|
|
988
|
+
if (res.destroyed || res.writableEnded) { clearInterval(keepalive); return; }
|
|
989
|
+
res.write(': keepalive\\n\\n');
|
|
990
|
+
}, 2000);
|
|
991
|
+
|
|
992
|
+
runPaytacaPay(BACKEND_URL + '/v1', pendingPayload.body, walletHash, extraHeaders, refill.paymentMethod || 'bch', async (err, responseJson) => {
|
|
993
|
+
clearInterval(keepalive);
|
|
994
|
+
pendingPayments.delete(walletHash);
|
|
995
|
+
|
|
996
|
+
const disarmAndFail = async (msg) => {
|
|
997
|
+
refill.enabled = false;
|
|
998
|
+
saveAutoRefillState(refill);
|
|
999
|
+
log('AUTO-REFILL: failed (' + msg + ') — disarmed');
|
|
1000
|
+
if (res.headersSent && !res.destroyed && !res.writableEnded) {
|
|
1001
|
+
await streamPaymentFailureAndRetry(res, walletHash, pendingPayload, '\\n\\n❌ Auto-refill payment failed: ' + msg + '\\n\\n');
|
|
1002
|
+
} else if (!res.headersSent) {
|
|
1003
|
+
try {
|
|
1004
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1005
|
+
res.end(JSON.stringify({ error: 'Payment failed', message: msg }));
|
|
1006
|
+
} catch (e) { log('Failed to write auto-refill error JSON: ' + e.message); }
|
|
1007
|
+
} else {
|
|
1008
|
+
log('Cannot send auto-refill failure — response already ended or destroyed');
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
|
|
1012
|
+
if (err) {
|
|
1013
|
+
return await disarmAndFail(err.message);
|
|
1014
|
+
}
|
|
1015
|
+
if (!responseJson.success) {
|
|
1016
|
+
const msg = responseJson.timeout
|
|
1017
|
+
? 'response timed out after payment — check credits with \\'credits\\' and retry'
|
|
1018
|
+
: (responseJson.error || 'Unknown payment error');
|
|
1019
|
+
return await disarmAndFail(msg);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// Payment succeeded — count it toward the budget.
|
|
1023
|
+
const spent = Math.min(refill.spentMinutes + refill.minutes, refill.maxMinutes);
|
|
1024
|
+
const exhausted = spent >= refill.maxMinutes;
|
|
1025
|
+
refill.spentMinutes = spent;
|
|
1026
|
+
refill.lastRefillAt = new Date().toISOString();
|
|
1027
|
+
if (exhausted) {
|
|
1028
|
+
refill.enabled = false;
|
|
1029
|
+
}
|
|
1030
|
+
saveAutoRefillState(refill);
|
|
1031
|
+
|
|
1032
|
+
const chatCompletion = responseJson?.data || responseJson;
|
|
1033
|
+
let wasStreaming = false;
|
|
1034
|
+
try {
|
|
1035
|
+
wasStreaming = JSON.parse(pendingPayload.body).stream === true;
|
|
1036
|
+
} catch (e) {}
|
|
1037
|
+
|
|
1038
|
+
log('AUTO-REFILL: payment ok, spent=' + spent + ' min, exhausted=' + exhausted);
|
|
1039
|
+
|
|
1040
|
+
if (res.destroyed || res.writableEnded) {
|
|
1041
|
+
log('Auto-refill payment succeeded but response connection is gone — cannot deliver chat response');
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
let note = '⚡ Auto-refill active: bought a ' + refill.minutes + '-minute plan for ' + (pendingPayload.displayName || pendingPayload.modelId || 'this model') + ' (' + spent + '/' + refill.maxMinutes + ' min budget used)';
|
|
1046
|
+
if (exhausted) {
|
|
1047
|
+
note += ' — **budget reached, auto-refill is now OFF**. Reply to buy more manually if you need to keep going.';
|
|
1048
|
+
}
|
|
1049
|
+
note += '. Generating your response...\\n\\n';
|
|
1050
|
+
|
|
1051
|
+
if (wasStreaming) {
|
|
1052
|
+
try {
|
|
1053
|
+
jsonToSse(res, chatCompletion, { prependContent: PROXY_MARKER + '\\n' + note });
|
|
1054
|
+
} catch (e) {
|
|
1055
|
+
log('auto-refill jsonToSse threw: ' + e.message);
|
|
1056
|
+
}
|
|
1057
|
+
} else {
|
|
1058
|
+
try {
|
|
1059
|
+
if (!res.headersSent) {
|
|
1060
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1061
|
+
}
|
|
1062
|
+
res.end(JSON.stringify(chatCompletion));
|
|
1063
|
+
} catch (e) {
|
|
1064
|
+
log('auto-refill non-stream send failed: ' + e.message);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
|
|
869
1070
|
// Extract the last user message content from a chat payload
|
|
870
1071
|
function getLastUserMessageContent(body) {
|
|
871
1072
|
try {
|
|
@@ -1635,6 +1836,42 @@ const server = http.createServer(async (req, res) => {
|
|
|
1635
1836
|
});
|
|
1636
1837
|
|
|
1637
1838
|
if (tiers && tiers.length > 0) {
|
|
1839
|
+
// AUTO-REFILL: when armed, buy the configured plan and retry the
|
|
1840
|
+
// request without the interactive tier prompt. Falls through to the
|
|
1841
|
+
// prompt when not armed, model mismatched, budget exhausted, plan
|
|
1842
|
+
// no longer offered, or the wallet cannot fund the refill.
|
|
1843
|
+
const refill = getAutoRefillState();
|
|
1844
|
+
if (refill && autoRefillCanBuy(refill, modelId || requestModel)) {
|
|
1845
|
+
const refillTier = (tiers || []).find((t) => Number(t.minutes) === Number(refill.minutes));
|
|
1846
|
+
if (refillTier && refillTier.price_sats) {
|
|
1847
|
+
if (refill.paymentMethod !== 'lift') {
|
|
1848
|
+
const currentBalanceSats = await getWalletBalance();
|
|
1849
|
+
if (currentBalanceSats === null || currentBalanceSats >= Number(refillTier.price_sats)) {
|
|
1850
|
+
const pp = pendingPayments.get(walletHash);
|
|
1851
|
+
await autoRefillAndRetry(res, walletHash, pp, refill);
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
log('AUTO-REFILL: insufficient balance (' + (currentBalanceSats === null ? 'n/a' : currentBalanceSats) + ' sats < ' + refillTier.price_sats + ') — disarming');
|
|
1855
|
+
refill.enabled = false;
|
|
1856
|
+
saveAutoRefillState(refill);
|
|
1857
|
+
} else {
|
|
1858
|
+
const liftUnits = await getLiftBalance();
|
|
1859
|
+
if (liftUnits !== null && liftUnits > 0) {
|
|
1860
|
+
const pp = pendingPayments.get(walletHash);
|
|
1861
|
+
await autoRefillAndRetry(res, walletHash, pp, refill);
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
log('AUTO-REFILL: no LIFT tokens — disarming');
|
|
1865
|
+
refill.enabled = false;
|
|
1866
|
+
saveAutoRefillState(refill);
|
|
1867
|
+
}
|
|
1868
|
+
} else {
|
|
1869
|
+
log('AUTO-REFILL: configured ' + refill.minutes + '-min plan not offered — disarming');
|
|
1870
|
+
refill.enabled = false;
|
|
1871
|
+
saveAutoRefillState(refill);
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1638
1875
|
// New flow: show tier selection prompt. Also tell the user about
|
|
1639
1876
|
// other models that still have paid credits, so they can switch
|
|
1640
1877
|
// instead of buying a plan for the currently selected model.
|
|
@@ -1728,9 +1965,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
1728
1965
|
};
|
|
1729
1966
|
|
|
1730
1967
|
if (isStreaming) {
|
|
1731
|
-
forwardStreaming(req, res, body, handleResponse);
|
|
1968
|
+
await forwardStreaming(req, res, body, handleResponse);
|
|
1732
1969
|
} else {
|
|
1733
|
-
forwardToDjango(req, body, handleResponse);
|
|
1970
|
+
await forwardToDjango(req, body, handleResponse);
|
|
1734
1971
|
}
|
|
1735
1972
|
|
|
1736
1973
|
} catch (err) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxy.js","sourceRoot":"","sources":["../../src/bundled/proxy.ts"],"names":[],"mappings":";AAAA,0DAA0D;AAC1D,6DAA6D;;;AAEhD,QAAA,oBAAoB,GAAG
|
|
1
|
+
{"version":3,"file":"proxy.js","sourceRoot":"","sources":["../../src/bundled/proxy.ts"],"names":[],"mappings":";AAAA,0DAA0D;AAC1D,6DAA6D;;;AAEhD,QAAA,oBAAoB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAk9DnC,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\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";
|
|
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, realpathSync } 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// Pre-payment stability probe. Before broadcasting the payment we sample the\n// plan/config endpoint several times ~500ms apart to catch a \"flapping\"\n// backend (reachable one moment, unreachable the next, e.g. right after a\n// purchase). If any sample fails while others succeed, or the plan price\n// changes between samples, we abort BEFORE any money moves. Disable with\n// PAYTACA_PLAN_PROBE=0.\nconst PLAN_PROBE_ENABLED = process.env.PAYTACA_PLAN_PROBE !== '0';\nconst PLAN_PROBE_SAMPLES = 3;\nconst PLAN_PROBE_SPACING_MS = 500;\nconst PLAN_PROBE_TIMEOUT_MS = 6000;\n\nfunction planProbeSleep(ms) {\n return new Promise(function (resolve) { setTimeout(resolve, ms); });\n}\n\n// Probe /v1/config (models + price tiers) repeatedly. Throws when the endpoint\n// is flapping or the plan price changes between samples. Returns undefined on\n// a stable backend.\nasync function probePlanStability(url, headers) {\n const modelId = headers['X-Model-Id'];\n const minutes = headers['X-Duration-Minutes'];\n if (!modelId || !minutes) return;\n let origin = null;\n try { origin = new URL(url).origin; } catch (e) { return; }\n if (!origin) return;\n\n let okSamples = 0;\n let totalSamples = 0;\n const priceSamples = [];\n let lastError = '';\n\n for (let i = 0; i < PLAN_PROBE_SAMPLES; i++) {\n totalSamples++;\n try {\n const controller = new AbortController();\n const timer = setTimeout(function () { controller.abort(); }, PLAN_PROBE_TIMEOUT_MS);\n let res;\n try {\n res = await fetch(origin + '/v1/config', { signal: controller.signal });\n } finally {\n clearTimeout(timer);\n }\n if (!res.ok) { lastError = 'HTTP ' + res.status; continue; }\n const data = await res.json();\n const models = Array.isArray(data.models) ? data.models : [];\n const model = models.find(function (m) {\n const id = String(m.id || '').toLowerCase();\n const name = String(m.display_name || '').toLowerCase();\n const q = String(modelId).toLowerCase();\n return id.indexOf(q) !== -1 || name.indexOf(q) !== -1;\n });\n if (!model) { lastError = 'model ' + modelId + ' not in plan config'; continue; }\n const tiers = Array.isArray(model.price_tiers) ? model.price_tiers : [];\n const tier = tiers.find(function (t) { return Number(t.minutes) === Number(minutes); });\n if (!tier) { lastError = 'no ' + minutes + '-min plan for ' + modelId + ' in plan config'; continue; }\n priceSamples.push(Number(tier.price_sats) || 0);\n okSamples++;\n } catch (e) {\n lastError = (e && e.name === 'AbortError') ? 'timeout' : (e && e.message) || String(e);\n }\n if (i < PLAN_PROBE_SAMPLES - 1) await planProbeSleep(PLAN_PROBE_SPACING_MS);\n }\n\n // Stable = every sample succeeded AND every sample quoted the same price.\n if (okSamples === totalSamples && priceSamples.length > 0) {\n const first = priceSamples[0];\n const allEqual = priceSamples.every(function (p) { return p === first; });\n if (allEqual) return;\n }\n throw new Error('Backend plan endpoint looked unstable before purchase (reachable ' + okSamples + '/' + totalSamples + (lastError ? ', last error: ' + lastError : '') + '). No payment was broadcast \u2014 please retry in a few seconds.');\n}\n\n// Find paytaca-cli installation\nfunction findPaytacaCliPath() {\n const possiblePaths = [];\n\n // Resolve the paytaca command itself when it's on PATH. This covers local\n // installs (e.g. the plugin's own node_modules) and per-node asdf globals\n // that `npm root -g` may not reflect when multiple node versions exist.\n try {\n const whichCmd = process.platform === 'win32' ? 'where' : 'which';\n const bin = execSync(`${whichCmd} paytaca`, { encoding: 'utf8' }).trim();\n if (bin) {\n let dir = dirname(realpathSync(bin));\n for (let i = 0; i < 6; i++) {\n const parent = dirname(dir);\n if (parent === dir) break;\n possiblePaths.push(dir);\n dir = parent;\n }\n }\n } catch {}\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 // Windows global npm location\n if (process.platform === 'win32' && process.env.APPDATA) {\n possiblePaths.push(join(process.env.APPDATA, 'npm', '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 = findCashlabDir(basePath);\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\n// Locate the @cashlab package dir by walking up from the paytaca-cli path.\n// Handles hoisted installs (top-level node_modules) and nested installs.\nfunction findCashlabDir(cliPath) {\n for (let dir = cliPath, i = 0; i < 8; i++) {\n const candidate = join(dir, 'node_modules', '@cashlab');\n try {\n readFileSync(join(candidate, 'cauldron', 'out', 'exchange-lab.js'));\n return candidate;\n } catch {}\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return null;\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 if (PLAN_PROBE_ENABLED) {\n await probePlanStability(url, headers);\n }\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,
|
|
1
|
+
{"version":3,"file":"wrapper.d.ts","sourceRoot":"","sources":["../../src/bundled/wrapper.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB,+wkBAyblC,CAAC"}
|
package/dist/bundled/wrapper.js
CHANGED
|
@@ -10,7 +10,7 @@ exports.WRAPPER_SCRIPT_CONTENT = `#!/usr/bin/env node
|
|
|
10
10
|
* Imports paytaca-cli modules directly (avoids CLI argument size limits).
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { readFileSync } from 'fs';
|
|
13
|
+
import { readFileSync, realpathSync } from 'fs';
|
|
14
14
|
import { execSync } from 'child_process';
|
|
15
15
|
import { fileURLToPath } from 'url';
|
|
16
16
|
import { dirname, join } from 'path';
|
|
@@ -20,10 +20,99 @@ import { dirname, join } from 'path';
|
|
|
20
20
|
// complete; override with PAYTACA_PAY_TIMEOUT_MS.
|
|
21
21
|
const PAY_TIMEOUT_MS = Number(process.env.PAYTACA_PAY_TIMEOUT_MS || 240000);
|
|
22
22
|
|
|
23
|
+
// Pre-payment stability probe. Before broadcasting the payment we sample the
|
|
24
|
+
// plan/config endpoint several times ~500ms apart to catch a "flapping"
|
|
25
|
+
// backend (reachable one moment, unreachable the next, e.g. right after a
|
|
26
|
+
// purchase). If any sample fails while others succeed, or the plan price
|
|
27
|
+
// changes between samples, we abort BEFORE any money moves. Disable with
|
|
28
|
+
// PAYTACA_PLAN_PROBE=0.
|
|
29
|
+
const PLAN_PROBE_ENABLED = process.env.PAYTACA_PLAN_PROBE !== '0';
|
|
30
|
+
const PLAN_PROBE_SAMPLES = 3;
|
|
31
|
+
const PLAN_PROBE_SPACING_MS = 500;
|
|
32
|
+
const PLAN_PROBE_TIMEOUT_MS = 6000;
|
|
33
|
+
|
|
34
|
+
function planProbeSleep(ms) {
|
|
35
|
+
return new Promise(function (resolve) { setTimeout(resolve, ms); });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Probe /v1/config (models + price tiers) repeatedly. Throws when the endpoint
|
|
39
|
+
// is flapping or the plan price changes between samples. Returns undefined on
|
|
40
|
+
// a stable backend.
|
|
41
|
+
async function probePlanStability(url, headers) {
|
|
42
|
+
const modelId = headers['X-Model-Id'];
|
|
43
|
+
const minutes = headers['X-Duration-Minutes'];
|
|
44
|
+
if (!modelId || !minutes) return;
|
|
45
|
+
let origin = null;
|
|
46
|
+
try { origin = new URL(url).origin; } catch (e) { return; }
|
|
47
|
+
if (!origin) return;
|
|
48
|
+
|
|
49
|
+
let okSamples = 0;
|
|
50
|
+
let totalSamples = 0;
|
|
51
|
+
const priceSamples = [];
|
|
52
|
+
let lastError = '';
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < PLAN_PROBE_SAMPLES; i++) {
|
|
55
|
+
totalSamples++;
|
|
56
|
+
try {
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
const timer = setTimeout(function () { controller.abort(); }, PLAN_PROBE_TIMEOUT_MS);
|
|
59
|
+
let res;
|
|
60
|
+
try {
|
|
61
|
+
res = await fetch(origin + '/v1/config', { signal: controller.signal });
|
|
62
|
+
} finally {
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
}
|
|
65
|
+
if (!res.ok) { lastError = 'HTTP ' + res.status; continue; }
|
|
66
|
+
const data = await res.json();
|
|
67
|
+
const models = Array.isArray(data.models) ? data.models : [];
|
|
68
|
+
const model = models.find(function (m) {
|
|
69
|
+
const id = String(m.id || '').toLowerCase();
|
|
70
|
+
const name = String(m.display_name || '').toLowerCase();
|
|
71
|
+
const q = String(modelId).toLowerCase();
|
|
72
|
+
return id.indexOf(q) !== -1 || name.indexOf(q) !== -1;
|
|
73
|
+
});
|
|
74
|
+
if (!model) { lastError = 'model ' + modelId + ' not in plan config'; continue; }
|
|
75
|
+
const tiers = Array.isArray(model.price_tiers) ? model.price_tiers : [];
|
|
76
|
+
const tier = tiers.find(function (t) { return Number(t.minutes) === Number(minutes); });
|
|
77
|
+
if (!tier) { lastError = 'no ' + minutes + '-min plan for ' + modelId + ' in plan config'; continue; }
|
|
78
|
+
priceSamples.push(Number(tier.price_sats) || 0);
|
|
79
|
+
okSamples++;
|
|
80
|
+
} catch (e) {
|
|
81
|
+
lastError = (e && e.name === 'AbortError') ? 'timeout' : (e && e.message) || String(e);
|
|
82
|
+
}
|
|
83
|
+
if (i < PLAN_PROBE_SAMPLES - 1) await planProbeSleep(PLAN_PROBE_SPACING_MS);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Stable = every sample succeeded AND every sample quoted the same price.
|
|
87
|
+
if (okSamples === totalSamples && priceSamples.length > 0) {
|
|
88
|
+
const first = priceSamples[0];
|
|
89
|
+
const allEqual = priceSamples.every(function (p) { return p === first; });
|
|
90
|
+
if (allEqual) return;
|
|
91
|
+
}
|
|
92
|
+
throw new Error('Backend plan endpoint looked unstable before purchase (reachable ' + okSamples + '/' + totalSamples + (lastError ? ', last error: ' + lastError : '') + '). No payment was broadcast — please retry in a few seconds.');
|
|
93
|
+
}
|
|
94
|
+
|
|
23
95
|
// Find paytaca-cli installation
|
|
24
96
|
function findPaytacaCliPath() {
|
|
25
97
|
const possiblePaths = [];
|
|
26
|
-
|
|
98
|
+
|
|
99
|
+
// Resolve the paytaca command itself when it's on PATH. This covers local
|
|
100
|
+
// installs (e.g. the plugin's own node_modules) and per-node asdf globals
|
|
101
|
+
// that \`npm root -g\` may not reflect when multiple node versions exist.
|
|
102
|
+
try {
|
|
103
|
+
const whichCmd = process.platform === 'win32' ? 'where' : 'which';
|
|
104
|
+
const bin = execSync(\`\${whichCmd} paytaca\`, { encoding: 'utf8' }).trim();
|
|
105
|
+
if (bin) {
|
|
106
|
+
let dir = dirname(realpathSync(bin));
|
|
107
|
+
for (let i = 0; i < 6; i++) {
|
|
108
|
+
const parent = dirname(dir);
|
|
109
|
+
if (parent === dir) break;
|
|
110
|
+
possiblePaths.push(dir);
|
|
111
|
+
dir = parent;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} catch {}
|
|
115
|
+
|
|
27
116
|
// Try to get global npm root
|
|
28
117
|
try {
|
|
29
118
|
const globalPath = execSync('npm root -g', { encoding: 'utf8' }).trim();
|
|
@@ -40,6 +129,11 @@ function findPaytacaCliPath() {
|
|
|
40
129
|
'/opt/homebrew/lib/node_modules/paytaca-cli',
|
|
41
130
|
);
|
|
42
131
|
|
|
132
|
+
// Windows global npm location
|
|
133
|
+
if (process.platform === 'win32' && process.env.APPDATA) {
|
|
134
|
+
possiblePaths.push(join(process.env.APPDATA, 'npm', 'node_modules', 'paytaca-cli'));
|
|
135
|
+
}
|
|
136
|
+
|
|
43
137
|
// Try current file's node_modules (for bundled installs)
|
|
44
138
|
try {
|
|
45
139
|
const currentFile = fileURLToPath(import.meta.url);
|
|
@@ -89,7 +183,7 @@ let fetchPoolsForToken, apiPoolToMicroPool, microPoolToPoolV0, attemptTrade, wat
|
|
|
89
183
|
try {
|
|
90
184
|
const basePath = findPaytacaCliPath();
|
|
91
185
|
const cauldronDir = join(basePath, 'dist', 'wallet', 'cauldron');
|
|
92
|
-
const cashlabDir =
|
|
186
|
+
const cashlabDir = findCashlabDir(basePath);
|
|
93
187
|
({ fetchPoolsForToken } = await import(join(cauldronDir, 'api.js')));
|
|
94
188
|
({ apiPoolToMicroPool, microPoolToPoolV0 } = await import(join(cauldronDir, 'pools.js')));
|
|
95
189
|
({ attemptTrade, watchtowerUtxosToSpendableCoins } = await import(join(cauldronDir, 'transact.js')));
|
|
@@ -102,6 +196,22 @@ try {
|
|
|
102
196
|
cauldronLoaded = false;
|
|
103
197
|
}
|
|
104
198
|
|
|
199
|
+
// Locate the @cashlab package dir by walking up from the paytaca-cli path.
|
|
200
|
+
// Handles hoisted installs (top-level node_modules) and nested installs.
|
|
201
|
+
function findCashlabDir(cliPath) {
|
|
202
|
+
for (let dir = cliPath, i = 0; i < 8; i++) {
|
|
203
|
+
const candidate = join(dir, 'node_modules', '@cashlab');
|
|
204
|
+
try {
|
|
205
|
+
readFileSync(join(candidate, 'cauldron', 'out', 'exchange-lab.js'));
|
|
206
|
+
return candidate;
|
|
207
|
+
} catch {}
|
|
208
|
+
const parent = dirname(dir);
|
|
209
|
+
if (parent === dir) break;
|
|
210
|
+
dir = parent;
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
105
215
|
async function main() {
|
|
106
216
|
const configPath = process.argv[2];
|
|
107
217
|
if (!configPath) {
|
|
@@ -127,6 +237,9 @@ async function main() {
|
|
|
127
237
|
const x402Payer = new X402Payer({ hdWallet, addressIndex: 0 });
|
|
128
238
|
|
|
129
239
|
try {
|
|
240
|
+
if (PLAN_PROBE_ENABLED) {
|
|
241
|
+
await probePlanStability(url, headers);
|
|
242
|
+
}
|
|
130
243
|
const result = await executePay(url, method, headers, body, bchWallet, hdWallet, x402Payer, isChipnet, confirmed, paymentMethod);
|
|
131
244
|
console.log(JSON.stringify(result, null, 2));
|
|
132
245
|
} catch (err) {
|
|
@@ -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
|
|
1
|
+
{"version":3,"file":"wrapper.js","sourceRoot":"","sources":["../../src/bundled/wrapper.ts"],"names":[],"mappings":";AAAA,oEAAoE;AACpE,4EAA4E;AAC5E,mFAAmF;;;AAEtE,QAAA,sBAAsB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAybrC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credits.d.ts","sourceRoot":"","sources":["../src/credits.ts"],"names":[],"mappings":"AAkCA,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,GAAG,CAAC;IACZ,UAAU,EAAE,MAAM,MAAM,CAAC;IACzB,UAAU,EAAE,MAAM,MAAM,CAAC;CAC1B;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,gBAAgB;;EAiG7D"}
|