@potswap_io/mcp 0.1.0 → 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.
- package/README.md +132 -91
- package/dist/agentTools.d.ts +44 -0
- package/dist/agentTools.js +179 -0
- package/dist/api.d.ts +21 -0
- package/dist/api.js +7 -3
- package/dist/config.d.ts +14 -1
- package/dist/config.js +14 -1
- package/dist/history.d.ts +72 -0
- package/dist/history.js +53 -0
- package/dist/index.js +23 -5
- package/dist/pnl.d.ts +16 -0
- package/dist/pnl.js +45 -0
- package/dist/runtime.d.ts +15 -0
- package/dist/runtime.js +39 -0
- package/dist/server.d.ts +13 -2
- package/dist/server.js +18 -84
- package/dist/shared/fee.d.ts +24 -0
- package/dist/shared/fee.js +42 -0
- package/dist/strategies/engine.d.ts +43 -0
- package/dist/strategies/engine.js +117 -0
- package/dist/strategies/lock.d.ts +7 -0
- package/dist/strategies/lock.js +30 -0
- package/dist/strategies/scheduler.d.ts +39 -0
- package/dist/strategies/scheduler.js +242 -0
- package/dist/strategies/store.d.ts +26 -0
- package/dist/strategies/store.js +53 -0
- package/dist/strategies/tools.d.ts +10 -0
- package/dist/strategies/tools.js +162 -0
- package/dist/strategies/types.d.ts +78 -0
- package/dist/strategies/types.js +2 -0
- package/dist/trade.d.ts +86 -0
- package/dist/trade.js +86 -0
- package/dist/transfer.d.ts +49 -0
- package/dist/transfer.js +86 -0
- package/dist/wallet.d.ts +13 -1
- package/dist/wallet.js +41 -12
- package/package.json +59 -55
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { NO_WALLET } from '../config.js';
|
|
3
|
+
import { redact, redactValue } from '../redact.js';
|
|
4
|
+
import { newId, SOL_MINT, nextRunAfter } from './engine.js';
|
|
5
|
+
import { findStrategy, runsOf } from './store.js';
|
|
6
|
+
const MINT = z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/);
|
|
7
|
+
const MAX_STRATEGIES = Number(process.env.POTSWAP_MAX_STRATEGIES) || 10;
|
|
8
|
+
const ok = (data, summary) => ({ content: [{ type: 'text', text: redact(summary ?? JSON.stringify(data, null, 2)) }], structuredContent: redactValue(data) });
|
|
9
|
+
const fail = (message, extra = {}) => ({ content: [{ type: 'text', text: redact(message) }], structuredContent: redactValue({ error: message, ...extra }), isError: true });
|
|
10
|
+
export function registerStrategyTools(server, deps) {
|
|
11
|
+
const { trade } = deps;
|
|
12
|
+
if (!deps.store)
|
|
13
|
+
return; // strategies disabled (no state dir)
|
|
14
|
+
const store = deps.store;
|
|
15
|
+
const scheduler = deps.scheduler;
|
|
16
|
+
const now = () => Date.now();
|
|
17
|
+
const base = (kind, maxSlippageBps) => ({
|
|
18
|
+
id: newId(kind, now()), createdAt: new Date(now()).toISOString(), status: 'active', nextRunAt: new Date(now()).toISOString(),
|
|
19
|
+
consecutiveFailures: 0, totals: { runs: 0, volumeUsd: 0, feeUsd: 0, pointsAdded: 0 }, maxSlippageBps,
|
|
20
|
+
});
|
|
21
|
+
const guard = (confirm) => {
|
|
22
|
+
if (!trade.signer)
|
|
23
|
+
return fail(NO_WALLET);
|
|
24
|
+
if (!confirm)
|
|
25
|
+
return fail('Set confirm=true to create a strategy. Nothing was created and no funds were debited.');
|
|
26
|
+
if (store.load().strategies.filter((s) => s.status === 'active').length >= MAX_STRATEGIES)
|
|
27
|
+
return fail(`Limit of ${MAX_STRATEGIES} active strategies reached. Pause or delete one first.`);
|
|
28
|
+
return null;
|
|
29
|
+
};
|
|
30
|
+
const created = async (s, summary) => {
|
|
31
|
+
store.update((st) => { st.strategies.push(s); });
|
|
32
|
+
// First leg right away (P11.1) when a scheduler is attached.
|
|
33
|
+
if (scheduler)
|
|
34
|
+
await scheduler.tick();
|
|
35
|
+
const fresh = findStrategy(store.load(), s.id) ?? s;
|
|
36
|
+
return ok({ strategy: fresh, schedulerActive: !!scheduler?.active, runs: runsOf(store.load(), s.id, 3) }, `${summary}\nid=${s.id}${scheduler?.active ? '' : '\nNote: no scheduler is running in this process; run `npx @potswap_io/mcp --daemon` to execute strategies 24/7.'}`);
|
|
37
|
+
};
|
|
38
|
+
server.registerTool('dca_create', {
|
|
39
|
+
title: 'Create a DCA strategy',
|
|
40
|
+
description: 'Buy a fixed amount of a token on a schedule (first run immediately). Every leg pays the Potswap fee and earns POT points. Runs while this host is open, or 24/7 with `npx @potswap_io/mcp --daemon`.',
|
|
41
|
+
inputSchema: {
|
|
42
|
+
inputMint: MINT, outputMint: MINT,
|
|
43
|
+
amountPerRun: z.number().positive().describe('UI units of the input token per run (1 = 1 SOL)'),
|
|
44
|
+
intervalMinutes: z.number().int().min(5).max(60 * 24 * 30),
|
|
45
|
+
totalRuns: z.number().int().min(1).max(1000).optional(),
|
|
46
|
+
maxSlippageBps: z.number().int().min(1).max(5000).optional(),
|
|
47
|
+
confirm: z.boolean().default(false),
|
|
48
|
+
},
|
|
49
|
+
}, async (a) => {
|
|
50
|
+
const g = guard(a.confirm);
|
|
51
|
+
if (g)
|
|
52
|
+
return g;
|
|
53
|
+
if (a.inputMint === a.outputMint)
|
|
54
|
+
return fail('inputMint and outputMint must differ. Nothing was created.');
|
|
55
|
+
const s = { ...base('dca', a.maxSlippageBps), kind: 'dca', inputMint: a.inputMint, outputMint: a.outputMint, amountPerRun: a.amountPerRun, intervalMinutes: a.intervalMinutes, totalRuns: a.totalRuns, runsDone: 0 };
|
|
56
|
+
return created(s, `DCA created: ${a.amountPerRun} per run every ${a.intervalMinutes} min${a.totalRuns ? ` × ${a.totalRuns}` : ''}.`);
|
|
57
|
+
});
|
|
58
|
+
server.registerTool('rebalance_create', {
|
|
59
|
+
title: 'Create a rebalance strategy',
|
|
60
|
+
description: 'Keep the wallet at target percentages (2–6 assets incl. SOL). Checks on an interval; trades only when an asset drifts more than driftPct, routing through SOL.',
|
|
61
|
+
inputSchema: {
|
|
62
|
+
targets: z.record(MINT, z.number().min(0).max(100)).describe('mint → percent; must sum to 100 and include SOL'),
|
|
63
|
+
driftPct: z.number().min(1).max(50).default(5),
|
|
64
|
+
checkIntervalMinutes: z.number().int().min(15).max(60 * 24 * 7).default(60),
|
|
65
|
+
minTradeUsd: z.number().min(1).default(5),
|
|
66
|
+
maxSlippageBps: z.number().int().min(1).max(5000).optional(),
|
|
67
|
+
confirm: z.boolean().default(false),
|
|
68
|
+
},
|
|
69
|
+
}, async (a) => {
|
|
70
|
+
const g = guard(a.confirm);
|
|
71
|
+
if (g)
|
|
72
|
+
return g;
|
|
73
|
+
const mints = Object.keys(a.targets);
|
|
74
|
+
const sum = Object.values(a.targets).reduce((x, y) => x + y, 0);
|
|
75
|
+
if (mints.length < 2 || mints.length > 6)
|
|
76
|
+
return fail('targets must list 2–6 assets. Nothing was created.');
|
|
77
|
+
if (Math.abs(sum - 100) > 0.5)
|
|
78
|
+
return fail(`targets must sum to 100 (got ${sum}). Nothing was created.`);
|
|
79
|
+
if (!(SOL_MINT in a.targets))
|
|
80
|
+
return fail('targets must include SOL (So11111111111111111111111111111111111111112). Nothing was created.');
|
|
81
|
+
const s = { ...base('rebalance', a.maxSlippageBps), kind: 'rebalance', targets: a.targets, driftPct: a.driftPct, checkIntervalMinutes: a.checkIntervalMinutes, minTradeUsd: a.minTradeUsd };
|
|
82
|
+
return created(s, `Rebalance created: ${mints.map((m) => `${m === SOL_MINT ? 'SOL' : m.slice(0, 4)} ${a.targets[m]}%`).join(' / ')}, drift ${a.driftPct}%, every ${a.checkIntervalMinutes} min.`);
|
|
83
|
+
});
|
|
84
|
+
server.registerTool('tpsl_create', {
|
|
85
|
+
title: 'Create a take-profit / stop-loss',
|
|
86
|
+
description: 'Sell a percentage of a position when its price reaches a take-profit, stop-loss or trailing-stop level (entry = price now). Checks every 30 s while a scheduler runs.',
|
|
87
|
+
inputSchema: {
|
|
88
|
+
mint: MINT, sellPct: z.number().min(1).max(100).default(100),
|
|
89
|
+
takeProfitPct: z.number().positive().optional(), stopLossPct: z.number().positive().max(99).optional(), trailingPct: z.number().positive().max(99).optional(),
|
|
90
|
+
exitMint: MINT.default(SOL_MINT), maxSlippageBps: z.number().int().min(1).max(5000).optional(),
|
|
91
|
+
confirm: z.boolean().default(false),
|
|
92
|
+
},
|
|
93
|
+
}, async (a) => {
|
|
94
|
+
const g = guard(a.confirm);
|
|
95
|
+
if (g)
|
|
96
|
+
return g;
|
|
97
|
+
if (a.takeProfitPct === undefined && a.stopLossPct === undefined && a.trailingPct === undefined)
|
|
98
|
+
return fail('Set at least one of takeProfitPct, stopLossPct, trailingPct. Nothing was created.');
|
|
99
|
+
if (a.mint === a.exitMint)
|
|
100
|
+
return fail('mint and exitMint must differ. Nothing was created.');
|
|
101
|
+
let entry;
|
|
102
|
+
try {
|
|
103
|
+
entry = (await trade.api.prices([a.mint]))[a.mint];
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
entry = undefined;
|
|
107
|
+
}
|
|
108
|
+
if (!entry)
|
|
109
|
+
return fail('No price available for this token right now; try again in a moment. Nothing was created.');
|
|
110
|
+
const s = { ...base('tpsl', a.maxSlippageBps), kind: 'tpsl', mint: a.mint, sellPct: a.sellPct, exitMint: a.exitMint, entryPrice: entry, highWater: entry, takeProfitPct: a.takeProfitPct, stopLossPct: a.stopLossPct, trailingPct: a.trailingPct, nextRunAt: null };
|
|
111
|
+
return created(s, `TP/SL created at entry ${entry}: ${[a.takeProfitPct && `TP +${a.takeProfitPct}%`, a.stopLossPct && `SL −${a.stopLossPct}%`, a.trailingPct && `trailing ${a.trailingPct}%`].filter(Boolean).join(', ')}, sells ${a.sellPct}%.`);
|
|
112
|
+
});
|
|
113
|
+
server.registerTool('limit_order_create', {
|
|
114
|
+
title: 'Create a limit order (price-triggered swap)',
|
|
115
|
+
description: 'Buy or sell a token when its USD price crosses a level. Runs through the strategy scheduler (needs this host open or the daemon). Fills once at the best route with the normal fee and points, then completes.',
|
|
116
|
+
inputSchema: {
|
|
117
|
+
side: z.enum(['buy', 'sell']), mint: MINT, quoteMint: MINT.default(SOL_MINT),
|
|
118
|
+
amount: z.number().positive().describe('Input amount in UI units: quote (SOL) amount for buys, token amount for sells'),
|
|
119
|
+
triggerPriceUsd: z.number().positive(), condition: z.enum(['above', 'below']),
|
|
120
|
+
expiresAt: z.string().datetime().optional(), maxSlippageBps: z.number().int().min(1).max(5000).optional(),
|
|
121
|
+
confirm: z.boolean().default(false),
|
|
122
|
+
},
|
|
123
|
+
}, async (a) => {
|
|
124
|
+
const g = guard(a.confirm);
|
|
125
|
+
if (g)
|
|
126
|
+
return g;
|
|
127
|
+
if (a.mint === a.quoteMint)
|
|
128
|
+
return fail('mint and quoteMint must differ. Nothing was created.');
|
|
129
|
+
const s = { ...base('limit', a.maxSlippageBps), kind: 'limit', side: a.side, mint: a.mint, quoteMint: a.quoteMint, amount: a.amount, triggerPriceUsd: a.triggerPriceUsd, condition: a.condition, expiresAt: a.expiresAt, nextRunAt: null };
|
|
130
|
+
return created(s, `Limit ${a.side} created: ${a.amount} ${a.side === 'buy' ? 'quote' : 'tokens'} when price ${a.condition} $${a.triggerPriceUsd}${a.expiresAt ? ` until ${a.expiresAt}` : ''}.`);
|
|
131
|
+
});
|
|
132
|
+
server.registerTool('strategy_list', { title: 'List strategies', description: 'All strategies with status, next run, totals and last result.', inputSchema: {} }, async () => {
|
|
133
|
+
const st = store.load();
|
|
134
|
+
const rows = st.strategies.map((s) => ({ ...s, lastRun: runsOf(st, s.id, 1)[0] ?? null }));
|
|
135
|
+
return ok({ schedulerActive: !!scheduler?.active, stateFile: store.location, strategies: rows }, rows.length ? rows.map((s) => `${s.id} ${s.kind} ${s.status}${s.pausedReason ? ` (${s.pausedReason})` : ''} next=${s.nextRunAt ?? '—'} runs=${s.totals.runs} fees=$${s.totals.feeUsd.toFixed(2)}`).join('\n') : 'No strategies yet.');
|
|
136
|
+
});
|
|
137
|
+
const byId = { id: z.string().min(3) };
|
|
138
|
+
server.registerTool('strategy_pause', { title: 'Pause a strategy', description: 'Stops future legs; keeps history.', inputSchema: byId }, async ({ id }) => mutate(id, (s) => { s.status = 'paused'; s.pausedReason = 'paused by user'; s.nextRunAt = null; }));
|
|
139
|
+
server.registerTool('strategy_resume', { title: 'Resume a strategy', description: 'Re-activates a paused strategy; the next run is scheduled from now.', inputSchema: byId }, async ({ id }) => mutate(id, (s) => { if (s.status === 'completed')
|
|
140
|
+
throw new Error('Completed strategies cannot be resumed; create a new one.'); s.status = 'active'; s.pausedReason = undefined; s.consecutiveFailures = 0; s.nextRunAt = s.kind === 'tpsl' || s.kind === 'limit' ? null : nextRunAfter(s, now()); }));
|
|
141
|
+
server.registerTool('strategy_delete', { title: 'Delete a strategy', description: 'Removes the strategy; its run history is kept.', inputSchema: byId }, async ({ id }) => {
|
|
142
|
+
const existed = store.update((st) => { const n = st.strategies.length; st.strategies = st.strategies.filter((s) => s.id !== id); return n !== st.strategies.length; });
|
|
143
|
+
return existed ? ok({ deleted: id }, `Deleted ${id}.`) : fail(`No strategy ${id}.`);
|
|
144
|
+
});
|
|
145
|
+
server.registerTool('strategy_runs', { title: 'Strategy run log', description: 'Recent legs with amounts, signatures, fees, points, or the skip/fail reason.', inputSchema: { id: z.string().min(3), limit: z.number().int().min(1).max(200).default(20) } }, async ({ id, limit }) => {
|
|
146
|
+
const st = store.load();
|
|
147
|
+
if (!findStrategy(st, id))
|
|
148
|
+
return fail(`No strategy ${id}.`);
|
|
149
|
+
const runs = runsOf(st, id, limit);
|
|
150
|
+
return ok({ id, runs }, runs.length ? runs.map((r) => `${r.at} ${r.outcome}: ${r.action}${r.signature ? ` sig=${r.signature}` : ''}${r.message ? ` — ${r.message}` : ''}`).join('\n') : 'No runs yet.');
|
|
151
|
+
});
|
|
152
|
+
function mutate(id, fn) {
|
|
153
|
+
try {
|
|
154
|
+
const s = store.update((st) => { const cur = findStrategy(st, id); if (!cur)
|
|
155
|
+
return null; fn(cur); return cur; });
|
|
156
|
+
return s ? ok({ strategy: s }, `${s.id} is now ${s.status}.`) : fail(`No strategy ${id}.`);
|
|
157
|
+
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
return fail(e instanceof Error ? e.message : String(e));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export type StrategyStatus = 'active' | 'paused' | 'completed';
|
|
2
|
+
interface Base {
|
|
3
|
+
id: string;
|
|
4
|
+
createdAt: string;
|
|
5
|
+
status: StrategyStatus;
|
|
6
|
+
pausedReason?: string;
|
|
7
|
+
nextRunAt: string | null;
|
|
8
|
+
consecutiveFailures: number;
|
|
9
|
+
totals: {
|
|
10
|
+
runs: number;
|
|
11
|
+
volumeUsd: number;
|
|
12
|
+
feeUsd: number;
|
|
13
|
+
pointsAdded: number;
|
|
14
|
+
};
|
|
15
|
+
maxSlippageBps?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface DcaStrategy extends Base {
|
|
18
|
+
kind: 'dca';
|
|
19
|
+
inputMint: string;
|
|
20
|
+
outputMint: string;
|
|
21
|
+
amountPerRun: number;
|
|
22
|
+
intervalMinutes: number;
|
|
23
|
+
totalRuns?: number;
|
|
24
|
+
runsDone: number;
|
|
25
|
+
}
|
|
26
|
+
export interface RebalanceStrategy extends Base {
|
|
27
|
+
kind: 'rebalance';
|
|
28
|
+
targets: Record<string, number>;
|
|
29
|
+
driftPct: number;
|
|
30
|
+
checkIntervalMinutes: number;
|
|
31
|
+
minTradeUsd: number;
|
|
32
|
+
}
|
|
33
|
+
export interface TpSlStrategy extends Base {
|
|
34
|
+
kind: 'tpsl';
|
|
35
|
+
mint: string;
|
|
36
|
+
sellPct: number;
|
|
37
|
+
exitMint: string;
|
|
38
|
+
entryPrice: number;
|
|
39
|
+
highWater: number;
|
|
40
|
+
takeProfitPct?: number;
|
|
41
|
+
stopLossPct?: number;
|
|
42
|
+
trailingPct?: number;
|
|
43
|
+
}
|
|
44
|
+
export interface LimitStrategy extends Base {
|
|
45
|
+
kind: 'limit';
|
|
46
|
+
side: 'buy' | 'sell';
|
|
47
|
+
mint: string;
|
|
48
|
+
quoteMint: string;
|
|
49
|
+
amount: number;
|
|
50
|
+
triggerPriceUsd: number;
|
|
51
|
+
condition: 'above' | 'below';
|
|
52
|
+
expiresAt?: string;
|
|
53
|
+
}
|
|
54
|
+
export type Strategy = DcaStrategy | RebalanceStrategy | TpSlStrategy | LimitStrategy;
|
|
55
|
+
export type RunOutcome = 'ok' | 'skipped' | 'failed' | 'dry-run' | 'no-op' | 'completed';
|
|
56
|
+
export interface Run {
|
|
57
|
+
id: string;
|
|
58
|
+
strategyId: string;
|
|
59
|
+
at: string;
|
|
60
|
+
action: string;
|
|
61
|
+
outcome: RunOutcome;
|
|
62
|
+
message?: string;
|
|
63
|
+
signature?: string;
|
|
64
|
+
inAmount?: number;
|
|
65
|
+
inputSymbol?: string;
|
|
66
|
+
outAmount?: number;
|
|
67
|
+
outputSymbol?: string;
|
|
68
|
+
feeUsd?: number;
|
|
69
|
+
pointsAdded?: number;
|
|
70
|
+
solEquivalent?: number;
|
|
71
|
+
}
|
|
72
|
+
export interface StrategyState {
|
|
73
|
+
version: 1;
|
|
74
|
+
strategies: Strategy[];
|
|
75
|
+
runs: Run[];
|
|
76
|
+
}
|
|
77
|
+
export declare const EMPTY_STATE: StrategyState;
|
|
78
|
+
export {};
|
package/dist/trade.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { PotswapApi } from './api.js';
|
|
2
|
+
import { type Config } from './config.js';
|
|
3
|
+
import type { Signer } from './wallet.js';
|
|
4
|
+
export type BalancesFn = (wallet: string) => Promise<Record<string, number>>;
|
|
5
|
+
export interface TradeContext {
|
|
6
|
+
api: PotswapApi;
|
|
7
|
+
config: Config;
|
|
8
|
+
signer: Signer | null;
|
|
9
|
+
balances: BalancesFn;
|
|
10
|
+
}
|
|
11
|
+
export interface SwapArgs {
|
|
12
|
+
inputMint: string;
|
|
13
|
+
outputMint: string;
|
|
14
|
+
amount: number;
|
|
15
|
+
slippageBps?: number;
|
|
16
|
+
confirm?: boolean;
|
|
17
|
+
/** Extra SOL-equivalent already spent in the current window (strategies' daily cap). */
|
|
18
|
+
extraSolCap?: {
|
|
19
|
+
remainingSol: number;
|
|
20
|
+
label: string;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export interface Quote {
|
|
24
|
+
inputMint: string;
|
|
25
|
+
inputSymbol: string;
|
|
26
|
+
outputMint: string;
|
|
27
|
+
outputSymbol: string;
|
|
28
|
+
inAmount: number;
|
|
29
|
+
inAmountRaw: string;
|
|
30
|
+
outAmount: number;
|
|
31
|
+
price: number;
|
|
32
|
+
priceImpactPct?: number;
|
|
33
|
+
slippageBps?: number;
|
|
34
|
+
feeBps: number;
|
|
35
|
+
feeTier: string | null;
|
|
36
|
+
feeLabel: string | null;
|
|
37
|
+
scheduledFeeBps: number;
|
|
38
|
+
inputUsd?: number;
|
|
39
|
+
router: string;
|
|
40
|
+
requestId: string;
|
|
41
|
+
expiresAt: string | null;
|
|
42
|
+
}
|
|
43
|
+
export interface SwapOk {
|
|
44
|
+
signature: string;
|
|
45
|
+
explorerUrl: string;
|
|
46
|
+
inAmount: number;
|
|
47
|
+
inputSymbol: string;
|
|
48
|
+
outAmount: number;
|
|
49
|
+
outputSymbol: string;
|
|
50
|
+
router: string;
|
|
51
|
+
feeBps: number;
|
|
52
|
+
feeUsd?: number;
|
|
53
|
+
pointsAdded?: number;
|
|
54
|
+
solEquivalent?: number;
|
|
55
|
+
}
|
|
56
|
+
export type SwapOutcome = {
|
|
57
|
+
kind: 'no-wallet';
|
|
58
|
+
message: string;
|
|
59
|
+
} | {
|
|
60
|
+
kind: 'refused';
|
|
61
|
+
message: string;
|
|
62
|
+
quote: Quote;
|
|
63
|
+
} | {
|
|
64
|
+
kind: 'quote-only';
|
|
65
|
+
quote: Quote;
|
|
66
|
+
} | {
|
|
67
|
+
kind: 'dry-run';
|
|
68
|
+
quote: Quote;
|
|
69
|
+
} | {
|
|
70
|
+
kind: 'failed';
|
|
71
|
+
message: string;
|
|
72
|
+
quote?: Quote;
|
|
73
|
+
signature?: string;
|
|
74
|
+
explorerUrl?: string;
|
|
75
|
+
} | {
|
|
76
|
+
kind: 'ok';
|
|
77
|
+
result: SwapOk;
|
|
78
|
+
quote: Quote;
|
|
79
|
+
};
|
|
80
|
+
export declare function buildQuote(api: PotswapApi, a: {
|
|
81
|
+
inputMint: string;
|
|
82
|
+
outputMint: string;
|
|
83
|
+
amount: number;
|
|
84
|
+
slippageBps?: number;
|
|
85
|
+
}): Promise<Quote>;
|
|
86
|
+
export declare function executeSwap(ctx: TradeContext, a: SwapArgs): Promise<SwapOutcome>;
|
package/dist/trade.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Swap execution shared by the `swap` tool (P10) and the strategy scheduler (P11).
|
|
2
|
+
// One path: quote → limits → fresh order as taker → local signature → execute.
|
|
3
|
+
import { VersionedTransaction } from '@solana/web3.js';
|
|
4
|
+
import { NO_WALLET, SOL_MINT, TOKEN_NOT_ALLOWED } from './config.js';
|
|
5
|
+
import { checkLimits } from './limits.js';
|
|
6
|
+
import { explorer, fromRaw, toRaw } from './format.js';
|
|
7
|
+
import { describeOrderError, friendlyError } from './shared/errors.js';
|
|
8
|
+
import { feeBpsFor } from './shared/fee.js';
|
|
9
|
+
export async function buildQuote(api, a) {
|
|
10
|
+
const [inTok, outTok, cfg, px] = await Promise.all([
|
|
11
|
+
api.token(a.inputMint), api.token(a.outputMint), api.config(), api.prices([a.inputMint]).catch(() => ({})),
|
|
12
|
+
]);
|
|
13
|
+
const inAmountRaw = toRaw(a.amount, inTok.decimals);
|
|
14
|
+
// PRD P8.12–P8.15: the fee tier depends on the USD value of the input at quote time.
|
|
15
|
+
const inputUsd = px[a.inputMint] ? a.amount * px[a.inputMint] : undefined;
|
|
16
|
+
const feeDecision = feeBpsFor(cfg.fee, inputUsd);
|
|
17
|
+
const o = await api.order({ inputMint: a.inputMint, outputMint: a.outputMint, amount: inAmountRaw, slippageBps: a.slippageBps, feeBps: feeDecision.bps });
|
|
18
|
+
if (o.errorMessage && !o.outAmount)
|
|
19
|
+
throw new Error(`${describeOrderError(o)} No funds were debited.`);
|
|
20
|
+
const outAmount = fromRaw(o.outAmount, outTok.decimals);
|
|
21
|
+
return {
|
|
22
|
+
inputMint: a.inputMint, inputSymbol: inTok.symbol, outputMint: a.outputMint, outputSymbol: outTok.symbol,
|
|
23
|
+
inAmount: a.amount, inAmountRaw, outAmount, price: outAmount / a.amount,
|
|
24
|
+
priceImpactPct: o.priceImpactPct !== undefined ? Math.abs(Number(o.priceImpactPct)) : undefined,
|
|
25
|
+
slippageBps: o.slippageBps, feeBps: o.platformFee?.feeBps ?? o.feeBps ?? 0,
|
|
26
|
+
feeTier: cfg.referralAccount ? feeDecision.tier : null, feeLabel: cfg.referralAccount ? feeDecision.label : null,
|
|
27
|
+
scheduledFeeBps: feeDecision.bps, inputUsd, router: o.router, requestId: o.requestId, expiresAt: o.expireAt ?? null,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export async function executeSwap(ctx, a) {
|
|
31
|
+
const { api, config, signer } = ctx;
|
|
32
|
+
if (!signer)
|
|
33
|
+
return { kind: 'no-wallet', message: NO_WALLET };
|
|
34
|
+
if (config.allowedTokens.size && (!config.allowedTokens.has(a.inputMint) || !config.allowedTokens.has(a.outputMint))) {
|
|
35
|
+
return { kind: 'failed', message: TOKEN_NOT_ALLOWED };
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const quote = await buildQuote(api, a);
|
|
39
|
+
const prices = await api.prices([SOL_MINT, a.inputMint]).catch(() => ({}));
|
|
40
|
+
let balanceUi;
|
|
41
|
+
try {
|
|
42
|
+
balanceUi = (await ctx.balances(signer.publicKey))[a.inputMint] ?? 0;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
balanceUi = undefined;
|
|
46
|
+
}
|
|
47
|
+
const inputUsd = prices[a.inputMint] ? a.amount * prices[a.inputMint] : undefined;
|
|
48
|
+
const solEquivalent = a.inputMint === SOL_MINT ? a.amount : inputUsd !== undefined && prices[SOL_MINT] ? inputUsd / prices[SOL_MINT] : undefined;
|
|
49
|
+
const refusal = checkLimits({
|
|
50
|
+
amountUi: a.amount, inputMint: a.inputMint, inputSymbol: quote.inputSymbol, isSol: a.inputMint === SOL_MINT,
|
|
51
|
+
inputUsd, solUsd: prices[SOL_MINT], slippageBps: a.slippageBps,
|
|
52
|
+
maxTradeSol: config.maxTradeSol, maxSlippageBps: config.maxSlippageBps, balanceUi,
|
|
53
|
+
});
|
|
54
|
+
if (refusal)
|
|
55
|
+
return { kind: 'refused', message: refusal, quote };
|
|
56
|
+
if (a.extraSolCap && solEquivalent !== undefined && solEquivalent > a.extraSolCap.remainingSol) {
|
|
57
|
+
return { kind: 'refused', message: `${a.extraSolCap.label} No funds were debited.`, quote };
|
|
58
|
+
}
|
|
59
|
+
if (!a.confirm)
|
|
60
|
+
return { kind: 'quote-only', quote };
|
|
61
|
+
if (config.dryRun)
|
|
62
|
+
return { kind: 'dry-run', quote };
|
|
63
|
+
const order = await api.order({ inputMint: a.inputMint, outputMint: a.outputMint, amount: quote.inAmountRaw, taker: signer.publicKey, slippageBps: a.slippageBps, feeBps: quote.scheduledFeeBps });
|
|
64
|
+
if (!order.transaction)
|
|
65
|
+
return { kind: 'failed', message: `${describeOrderError(order)} No funds were debited.`, quote };
|
|
66
|
+
const tx = VersionedTransaction.deserialize(Buffer.from(order.transaction, 'base64'));
|
|
67
|
+
signer.sign(tx);
|
|
68
|
+
const res = await api.execute(Buffer.from(tx.serialize()).toString('base64'), order.requestId);
|
|
69
|
+
if (res.status !== 'Success') {
|
|
70
|
+
return { kind: 'failed', message: friendlyError(res.error || `Swap failed (code ${res.code})`), quote, signature: res.signature, explorerUrl: res.signature ? explorer(res.signature) : undefined };
|
|
71
|
+
}
|
|
72
|
+
const outDec = (await api.token(a.outputMint)).decimals;
|
|
73
|
+
const outAmount = fromRaw(res.totalOutputAmount ?? order.outAmount, outDec);
|
|
74
|
+
const feeBps = order.platformFee?.feeBps ?? order.feeBps ?? 0;
|
|
75
|
+
const feeUsd = inputUsd !== undefined ? inputUsd * feeBps / 10_000 : undefined;
|
|
76
|
+
const cfg = await api.config();
|
|
77
|
+
const result = {
|
|
78
|
+
signature: res.signature, explorerUrl: explorer(res.signature), inAmount: a.amount, inputSymbol: quote.inputSymbol,
|
|
79
|
+
outAmount, outputSymbol: quote.outputSymbol, router: order.router, feeBps, feeUsd, pointsAdded: cfg.referralAccount ? feeUsd : 0, solEquivalent,
|
|
80
|
+
};
|
|
81
|
+
return { kind: 'ok', result, quote };
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
return { kind: 'failed', message: friendlyError(e instanceof Error ? e.message : String(e)) };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Connection, type Commitment } from '@solana/web3.js';
|
|
2
|
+
import { type Config } from './config.js';
|
|
3
|
+
import type { Signer } from './wallet.js';
|
|
4
|
+
/** Minimal RPC surface so tests can mock it. */
|
|
5
|
+
export interface Rpc {
|
|
6
|
+
getLatestBlockhash(commitment?: Commitment): Promise<{
|
|
7
|
+
blockhash: string;
|
|
8
|
+
lastValidBlockHeight: number;
|
|
9
|
+
}>;
|
|
10
|
+
getAccountOwner(address: string): Promise<string | null>;
|
|
11
|
+
sendRawTransaction(raw: Uint8Array): Promise<string>;
|
|
12
|
+
confirmTransaction(sig: string, blockhash: string, lastValidBlockHeight: number): Promise<{
|
|
13
|
+
err: unknown | null;
|
|
14
|
+
}>;
|
|
15
|
+
}
|
|
16
|
+
export declare function rpcFromConnection(conn: Connection): Rpc;
|
|
17
|
+
export interface TransferArgs {
|
|
18
|
+
to: string;
|
|
19
|
+
mint?: string;
|
|
20
|
+
amount: number;
|
|
21
|
+
decimals?: number;
|
|
22
|
+
confirm: boolean;
|
|
23
|
+
}
|
|
24
|
+
export type TransferOutcome = {
|
|
25
|
+
kind: 'no-wallet' | 'refused' | 'failed';
|
|
26
|
+
message: string;
|
|
27
|
+
} | {
|
|
28
|
+
kind: 'quote-only';
|
|
29
|
+
summary: string;
|
|
30
|
+
} | {
|
|
31
|
+
kind: 'dry-run';
|
|
32
|
+
summary: string;
|
|
33
|
+
} | {
|
|
34
|
+
kind: 'ok';
|
|
35
|
+
signature: string;
|
|
36
|
+
explorerUrl: string;
|
|
37
|
+
summary: string;
|
|
38
|
+
};
|
|
39
|
+
export interface TransferContext {
|
|
40
|
+
config: Config;
|
|
41
|
+
signer: Signer | null;
|
|
42
|
+
rpc: Rpc;
|
|
43
|
+
balances: (wallet: string) => Promise<Record<string, number>>;
|
|
44
|
+
prices: (mints: string[]) => Promise<Record<string, number>>;
|
|
45
|
+
decimalsOf: (mint: string) => Promise<number>;
|
|
46
|
+
/** Program that owns the mint (Token vs Token-2022); defaults to the classic Token program. */
|
|
47
|
+
getMintOwner?: (mint: string) => Promise<string | null>;
|
|
48
|
+
}
|
|
49
|
+
export declare function executeTransfer(ctx: TransferContext, a: TransferArgs): Promise<TransferOutcome>;
|
package/dist/transfer.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// PRD P13.2/P13.3 — SOL and SPL transfers built locally, no Potswap fee, recipient allowlist + caps.
|
|
2
|
+
import { Connection, PublicKey, SystemProgram, Transaction } from '@solana/web3.js';
|
|
3
|
+
import { createAssociatedTokenAccountIdempotentInstruction, createTransferCheckedInstruction, getAssociatedTokenAddressSync, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@solana/spl-token';
|
|
4
|
+
import { DEFAULTS, NO_WALLET, RECIPIENT_NOT_ALLOWED, SOL_MINT } from './config.js';
|
|
5
|
+
import { toRaw } from './format.js';
|
|
6
|
+
export function rpcFromConnection(conn) {
|
|
7
|
+
return {
|
|
8
|
+
getLatestBlockhash: (c) => conn.getLatestBlockhash(c ?? 'confirmed'),
|
|
9
|
+
getAccountOwner: async (a) => { const info = await conn.getAccountInfo(new PublicKey(a)); return info ? info.owner.toBase58() : null; },
|
|
10
|
+
sendRawTransaction: (raw) => conn.sendRawTransaction(raw, { skipPreflight: false, maxRetries: 3 }),
|
|
11
|
+
confirmTransaction: async (signature, blockhash, lastValidBlockHeight) => { const r = await conn.confirmTransaction({ signature, blockhash, lastValidBlockHeight }, 'confirmed'); return { err: r.value.err }; },
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
const BASE58 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
15
|
+
export async function executeTransfer(ctx, a) {
|
|
16
|
+
const { config, signer } = ctx;
|
|
17
|
+
if (!signer)
|
|
18
|
+
return { kind: 'no-wallet', message: NO_WALLET };
|
|
19
|
+
if (!BASE58.test(a.to))
|
|
20
|
+
return { kind: 'refused', message: 'Invalid recipient address. No funds were debited.' };
|
|
21
|
+
if (config.allowedRecipients.size && !config.allowedRecipients.has(a.to))
|
|
22
|
+
return { kind: 'refused', message: RECIPIENT_NOT_ALLOWED };
|
|
23
|
+
if (!(a.amount > 0))
|
|
24
|
+
return { kind: 'refused', message: 'Amount must be positive. No funds were debited.' };
|
|
25
|
+
const mint = a.mint ?? SOL_MINT;
|
|
26
|
+
const isSol = mint === SOL_MINT;
|
|
27
|
+
// Caps: per-transfer in SOL-equivalent, SOL reserve, balance.
|
|
28
|
+
let prices = {};
|
|
29
|
+
try {
|
|
30
|
+
prices = await ctx.prices([SOL_MINT, mint]);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
prices = {};
|
|
34
|
+
}
|
|
35
|
+
const solEq = isSol ? a.amount : prices[mint] && prices[SOL_MINT] ? (a.amount * prices[mint]) / prices[SOL_MINT] : undefined;
|
|
36
|
+
if (solEq !== undefined && solEq > config.maxTradeSol)
|
|
37
|
+
return { kind: 'refused', message: `Refused by safety limit: ≈ ${solEq.toFixed(3)} SOL is above POTSWAP_MAX_TRADE_SOL (${config.maxTradeSol}). No funds were debited.` };
|
|
38
|
+
let bal = {};
|
|
39
|
+
try {
|
|
40
|
+
bal = await ctx.balances(signer.publicKey);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
bal = {};
|
|
44
|
+
}
|
|
45
|
+
const have = bal[mint] ?? 0;
|
|
46
|
+
const spendable = isSol ? Math.max(have - DEFAULTS.solReserve, 0) : have;
|
|
47
|
+
if (a.amount > spendable)
|
|
48
|
+
return { kind: 'refused', message: isSol ? `Insufficient SOL: balance ${have.toFixed(4)}, keep at least ${DEFAULTS.solReserve} SOL for fees. No funds were debited.` : `Insufficient balance (${have}). No funds were debited.` };
|
|
49
|
+
const summary = `${a.amount} ${isSol ? 'SOL' : mint.slice(0, 4) + '…'} → ${a.to}`;
|
|
50
|
+
if (!a.confirm)
|
|
51
|
+
return { kind: 'quote-only', summary: `Transfer not sent (confirm=true to send): ${summary}` };
|
|
52
|
+
if (config.dryRun)
|
|
53
|
+
return { kind: 'dry-run', summary: `DRY RUN, nothing signed: ${summary}` };
|
|
54
|
+
try {
|
|
55
|
+
const from = new PublicKey(signer.publicKey);
|
|
56
|
+
const to = new PublicKey(a.to);
|
|
57
|
+
const tx = new Transaction();
|
|
58
|
+
if (isSol) {
|
|
59
|
+
tx.add(SystemProgram.transfer({ fromPubkey: from, toPubkey: to, lamports: BigInt(toRaw(a.amount, 9)) }));
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
const mintPk = new PublicKey(mint);
|
|
63
|
+
const decimals = a.decimals ?? (await ctx.decimalsOf(mint));
|
|
64
|
+
const owner = await ctx.getMintOwner?.(mint);
|
|
65
|
+
const program = owner === TOKEN_2022_PROGRAM_ID.toBase58() ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID;
|
|
66
|
+
const src = getAssociatedTokenAddressSync(mintPk, from, false, program);
|
|
67
|
+
const dst = getAssociatedTokenAddressSync(mintPk, to, true, program);
|
|
68
|
+
if (!(await ctx.rpc.getAccountOwner(dst.toBase58())))
|
|
69
|
+
tx.add(createAssociatedTokenAccountIdempotentInstruction(from, dst, to, mintPk, program));
|
|
70
|
+
tx.add(createTransferCheckedInstruction(src, mintPk, dst, from, BigInt(toRaw(a.amount, decimals)), decimals, [], program));
|
|
71
|
+
}
|
|
72
|
+
const { blockhash, lastValidBlockHeight } = await ctx.rpc.getLatestBlockhash('confirmed');
|
|
73
|
+
tx.recentBlockhash = blockhash;
|
|
74
|
+
tx.feePayer = from;
|
|
75
|
+
signer.signLegacy(tx);
|
|
76
|
+
const signature = await ctx.rpc.sendRawTransaction(tx.serialize());
|
|
77
|
+
const conf = await ctx.rpc.confirmTransaction(signature, blockhash, lastValidBlockHeight);
|
|
78
|
+
if (conf.err)
|
|
79
|
+
return { kind: 'failed', message: `Transfer failed on-chain: ${JSON.stringify(conf.err)}. Check https://solscan.io/tx/${signature}` };
|
|
80
|
+
return { kind: 'ok', signature, explorerUrl: `https://solscan.io/tx/${signature}`, summary: `Sent ${summary}` };
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
const m = e instanceof Error ? e.message : String(e);
|
|
84
|
+
return { kind: 'failed', message: `${m}${/blockhash|expired/i.test(m) ? ' The transaction expired before landing; no funds were debited.' : ''}` };
|
|
85
|
+
}
|
|
86
|
+
}
|
package/dist/wallet.d.ts
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
|
-
import { type VersionedTransaction } from '@solana/web3.js';
|
|
1
|
+
import { type VersionedTransaction, type Transaction } from '@solana/web3.js';
|
|
2
2
|
export interface Signer {
|
|
3
3
|
publicKey: string;
|
|
4
4
|
sign(tx: VersionedTransaction): VersionedTransaction;
|
|
5
|
+
signLegacy(tx: Transaction): Transaction;
|
|
5
6
|
}
|
|
7
|
+
export declare const WALLET_FILE = "wallet.json";
|
|
6
8
|
export declare function parseSecret(raw: string): Uint8Array;
|
|
9
|
+
/**
|
|
10
|
+
* Resolution order: SOLANA_PRIVATE_KEY → SOLANA_KEYPAIR_PATH → `<stateDir>/wallet.json`
|
|
11
|
+
* (created by `wallet_create`). Returns null when none exists.
|
|
12
|
+
*/
|
|
7
13
|
export declare function loadSigner(opts: {
|
|
8
14
|
privateKey?: string;
|
|
9
15
|
keypairPath?: string;
|
|
16
|
+
stateDir?: string;
|
|
10
17
|
}): Signer | null;
|
|
18
|
+
/** PRD P13.1 — creates `<stateDir>/wallet.json` (0600). Refuses to overwrite. */
|
|
19
|
+
export declare function createWallet(stateDir: string): {
|
|
20
|
+
signer: Signer;
|
|
21
|
+
file: string;
|
|
22
|
+
};
|
package/dist/wallet.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
// PRD P10.2/P10.9 — load the agent's keypair; the secret never leaves this module.
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
1
|
+
// PRD P10.2 / P10.9 / P13.1 — load or create the agent's keypair; the secret never leaves this module.
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
3
4
|
import { Keypair } from '@solana/web3.js';
|
|
4
5
|
import bs58 from 'bs58';
|
|
5
6
|
import { registerSecret } from './redact.js';
|
|
7
|
+
export const WALLET_FILE = 'wallet.json';
|
|
6
8
|
export function parseSecret(raw) {
|
|
7
9
|
const s = raw.trim();
|
|
8
10
|
if (s.startsWith('[')) {
|
|
@@ -24,6 +26,21 @@ export function parseSecret(raw) {
|
|
|
24
26
|
return Keypair.fromSeed(bytes).secretKey;
|
|
25
27
|
throw new Error(`Private key has ${bytes.length} bytes; expected 64 (or a 32-byte seed)`);
|
|
26
28
|
}
|
|
29
|
+
function toSigner(kp, raw) {
|
|
30
|
+
// Register every encoding the secret could leak in, not just the one supplied.
|
|
31
|
+
registerSecret(raw.trim());
|
|
32
|
+
registerSecret(bs58.encode(kp.secretKey));
|
|
33
|
+
registerSecret(JSON.stringify(Array.from(kp.secretKey)));
|
|
34
|
+
return {
|
|
35
|
+
publicKey: kp.publicKey.toBase58(),
|
|
36
|
+
sign(tx) { tx.sign([kp]); return tx; },
|
|
37
|
+
signLegacy(tx) { tx.sign(kp); return tx; },
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolution order: SOLANA_PRIVATE_KEY → SOLANA_KEYPAIR_PATH → `<stateDir>/wallet.json`
|
|
42
|
+
* (created by `wallet_create`). Returns null when none exists.
|
|
43
|
+
*/
|
|
27
44
|
export function loadSigner(opts) {
|
|
28
45
|
let raw = opts.privateKey;
|
|
29
46
|
if (!raw && opts.keypairPath) {
|
|
@@ -31,18 +48,30 @@ export function loadSigner(opts) {
|
|
|
31
48
|
raw = readFileSync(opts.keypairPath, 'utf8');
|
|
32
49
|
}
|
|
33
50
|
catch {
|
|
34
|
-
throw new Error(
|
|
51
|
+
throw new Error('Cannot read keypair file at SOLANA_KEYPAIR_PATH');
|
|
35
52
|
}
|
|
36
53
|
}
|
|
54
|
+
if (!raw && opts.stateDir) {
|
|
55
|
+
const file = join(opts.stateDir, WALLET_FILE);
|
|
56
|
+
if (existsSync(file))
|
|
57
|
+
raw = readFileSync(file, 'utf8');
|
|
58
|
+
}
|
|
37
59
|
if (!raw)
|
|
38
60
|
return null;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
61
|
+
return toSigner(Keypair.fromSecretKey(parseSecret(raw)), raw);
|
|
62
|
+
}
|
|
63
|
+
/** PRD P13.1 — creates `<stateDir>/wallet.json` (0600). Refuses to overwrite. */
|
|
64
|
+
export function createWallet(stateDir) {
|
|
65
|
+
mkdirSync(stateDir, { recursive: true });
|
|
66
|
+
const file = join(stateDir, WALLET_FILE);
|
|
67
|
+
if (existsSync(file))
|
|
68
|
+
throw new Error(`A wallet already exists at ${file}. Back it up and remove it first if you really want a new one.`);
|
|
69
|
+
const kp = Keypair.generate();
|
|
70
|
+
const raw = JSON.stringify(Array.from(kp.secretKey));
|
|
71
|
+
writeFileSync(file, raw, { mode: 0o600 });
|
|
72
|
+
try {
|
|
73
|
+
chmodSync(file, 0o600);
|
|
74
|
+
}
|
|
75
|
+
catch { /* Windows */ }
|
|
76
|
+
return { signer: toSigner(kp, raw), file };
|
|
48
77
|
}
|