@thawgate/cli 0.1.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/LICENSE +22 -0
- package/README.md +72 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1092 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1092 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/index.ts
|
|
27
|
+
var import_commander = require("commander");
|
|
28
|
+
var import_sdk3 = require("@thawgate/sdk");
|
|
29
|
+
|
|
30
|
+
// src/commands.ts
|
|
31
|
+
var fs3 = __toESM(require("fs"));
|
|
32
|
+
var os3 = __toESM(require("os"));
|
|
33
|
+
var readline = __toESM(require("readline"));
|
|
34
|
+
var import_web3 = require("@solana/web3.js");
|
|
35
|
+
var import_spl_token = require("@solana/spl-token");
|
|
36
|
+
var import_anchor = require("@coral-xyz/anchor");
|
|
37
|
+
var import_sdk = require("@thawgate/sdk");
|
|
38
|
+
|
|
39
|
+
// src/config.ts
|
|
40
|
+
var fs = __toESM(require("fs"));
|
|
41
|
+
var path = __toESM(require("path"));
|
|
42
|
+
var os = __toESM(require("os"));
|
|
43
|
+
var dotenv = __toESM(require("dotenv"));
|
|
44
|
+
dotenv.config();
|
|
45
|
+
var CONFIG_DIR = path.join(os.homedir(), ".thawgate");
|
|
46
|
+
var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
47
|
+
var LEGACY_CONFIG_FILE = path.join(os.homedir(), ".sss-token", "config.json");
|
|
48
|
+
var DEFAULTS = {
|
|
49
|
+
rpcUrl: "https://api.devnet.solana.com",
|
|
50
|
+
commitment: "confirmed",
|
|
51
|
+
programId: "",
|
|
52
|
+
hookProgramId: "",
|
|
53
|
+
keypairPath: path.join(os.homedir(), ".config", "solana", "id.json"),
|
|
54
|
+
outputFormat: "text"
|
|
55
|
+
};
|
|
56
|
+
function loadConfig(overrides) {
|
|
57
|
+
let fileConfig = {};
|
|
58
|
+
const file = fs.existsSync(CONFIG_FILE) ? CONFIG_FILE : fs.existsSync(LEGACY_CONFIG_FILE) ? LEGACY_CONFIG_FILE : void 0;
|
|
59
|
+
if (file) {
|
|
60
|
+
try {
|
|
61
|
+
const raw = fs.readFileSync(file, "utf-8");
|
|
62
|
+
fileConfig = JSON.parse(raw);
|
|
63
|
+
} catch {
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const envConfig = {};
|
|
67
|
+
if (process.env.SSS_RPC_URL) envConfig.rpcUrl = process.env.SSS_RPC_URL;
|
|
68
|
+
if (process.env.SSS_COMMITMENT)
|
|
69
|
+
envConfig.commitment = process.env.SSS_COMMITMENT;
|
|
70
|
+
if (process.env.SSS_PROGRAM_ID)
|
|
71
|
+
envConfig.programId = process.env.SSS_PROGRAM_ID;
|
|
72
|
+
if (process.env.SSS_HOOK_PROGRAM_ID)
|
|
73
|
+
envConfig.hookProgramId = process.env.SSS_HOOK_PROGRAM_ID;
|
|
74
|
+
if (process.env.SSS_KEYPAIR_PATH)
|
|
75
|
+
envConfig.keypairPath = process.env.SSS_KEYPAIR_PATH;
|
|
76
|
+
const given = Object.fromEntries(Object.entries(overrides ?? {}).filter(([, v]) => v !== void 0));
|
|
77
|
+
return {
|
|
78
|
+
...DEFAULTS,
|
|
79
|
+
...fileConfig,
|
|
80
|
+
...envConfig,
|
|
81
|
+
...given
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function saveConfig(config2) {
|
|
85
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
86
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
87
|
+
}
|
|
88
|
+
const existing = fs.existsSync(CONFIG_FILE) ? JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8")) : {};
|
|
89
|
+
const merged = { ...existing, ...config2 };
|
|
90
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2));
|
|
91
|
+
}
|
|
92
|
+
function loadKeypair(keypairPath) {
|
|
93
|
+
const raw = fs.readFileSync(keypairPath, "utf-8");
|
|
94
|
+
return Uint8Array.from(JSON.parse(raw));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/logger.ts
|
|
98
|
+
var fs2 = __toESM(require("fs"));
|
|
99
|
+
var path2 = __toESM(require("path"));
|
|
100
|
+
var os2 = __toESM(require("os"));
|
|
101
|
+
var LOG_DIR = path2.join(os2.homedir(), ".thawgate");
|
|
102
|
+
var LOG_FILE = path2.join(LOG_DIR, "audit.log");
|
|
103
|
+
var Logger = class {
|
|
104
|
+
verbose;
|
|
105
|
+
jsonOutput;
|
|
106
|
+
constructor(verbose = false, jsonOutput = false) {
|
|
107
|
+
this.verbose = verbose;
|
|
108
|
+
this.jsonOutput = jsonOutput;
|
|
109
|
+
this.ensureLogDir();
|
|
110
|
+
}
|
|
111
|
+
ensureLogDir() {
|
|
112
|
+
if (!fs2.existsSync(LOG_DIR)) {
|
|
113
|
+
fs2.mkdirSync(LOG_DIR, { recursive: true });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Formats and writes a log entry.
|
|
118
|
+
*/
|
|
119
|
+
log(level, message, data) {
|
|
120
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
121
|
+
const entry = {
|
|
122
|
+
timestamp,
|
|
123
|
+
level,
|
|
124
|
+
message,
|
|
125
|
+
...data ? { data } : {}
|
|
126
|
+
};
|
|
127
|
+
const line = JSON.stringify(entry) + "\n";
|
|
128
|
+
try {
|
|
129
|
+
fs2.appendFileSync(LOG_FILE, line);
|
|
130
|
+
} catch {
|
|
131
|
+
}
|
|
132
|
+
if (this.jsonOutput) {
|
|
133
|
+
if (level !== "DEBUG" /* DEBUG */ || this.verbose) console.error(JSON.stringify(entry));
|
|
134
|
+
} else if (level !== "DEBUG" /* DEBUG */ || this.verbose) {
|
|
135
|
+
const prefix = level === "ERROR" /* ERROR */ ? "\u2717" : level === "WARN" /* WARN */ ? "\u26A0" : "\u2713";
|
|
136
|
+
console.log(`${prefix} ${message}`);
|
|
137
|
+
if (data && this.verbose) {
|
|
138
|
+
console.log(" Data:", JSON.stringify(data, null, 2));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
debug(message, data) {
|
|
143
|
+
this.log("DEBUG" /* DEBUG */, message, data);
|
|
144
|
+
}
|
|
145
|
+
info(message, data) {
|
|
146
|
+
this.log("INFO" /* INFO */, message, data);
|
|
147
|
+
}
|
|
148
|
+
warn(message, data) {
|
|
149
|
+
this.log("WARN" /* WARN */, message, data);
|
|
150
|
+
}
|
|
151
|
+
error(message, data) {
|
|
152
|
+
this.log("ERROR" /* ERROR */, message, data);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Logs a transaction result.
|
|
156
|
+
*/
|
|
157
|
+
transaction(action, signature, details) {
|
|
158
|
+
this.info(`${action}: ${signature}`, details);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Outputs structured data (for --json mode).
|
|
162
|
+
*/
|
|
163
|
+
output(data) {
|
|
164
|
+
if (this.jsonOutput) {
|
|
165
|
+
console.log(JSON.stringify(data, null, 2));
|
|
166
|
+
} else {
|
|
167
|
+
for (const [key, value] of Object.entries(data)) {
|
|
168
|
+
console.log(` ${key}: ${value}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// src/utils.ts
|
|
175
|
+
async function simulateTransaction(connection, transaction, signers) {
|
|
176
|
+
const { blockhash } = await connection.getLatestBlockhash("confirmed");
|
|
177
|
+
transaction.recentBlockhash = blockhash;
|
|
178
|
+
transaction.feePayer = signers[0].publicKey;
|
|
179
|
+
transaction.sign(...signers);
|
|
180
|
+
const simulation = await connection.simulateTransaction(transaction);
|
|
181
|
+
console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
182
|
+
console.log("DRY RUN RESULT (transaction NOT submitted)");
|
|
183
|
+
console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
184
|
+
if (simulation.value.err) {
|
|
185
|
+
console.log("Status: \u274C WOULD FAIL");
|
|
186
|
+
console.log("Error: ", JSON.stringify(simulation.value.err));
|
|
187
|
+
} else {
|
|
188
|
+
console.log("Status: \u2705 WOULD SUCCEED");
|
|
189
|
+
console.log("Units: ", simulation.value.unitsConsumed ?? "unknown");
|
|
190
|
+
}
|
|
191
|
+
if (simulation.value.logs && simulation.value.logs.length > 0) {
|
|
192
|
+
console.log("Program logs:");
|
|
193
|
+
simulation.value.logs.forEach((log) => console.log(" ", log));
|
|
194
|
+
}
|
|
195
|
+
console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
196
|
+
console.log("(No transaction submitted \u2014 dry run only)");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/commands.ts
|
|
200
|
+
function createClient(opts) {
|
|
201
|
+
const config2 = loadConfig({
|
|
202
|
+
rpcUrl: opts.rpcUrl,
|
|
203
|
+
commitment: opts.commitment,
|
|
204
|
+
keypairPath: opts.keypair
|
|
205
|
+
});
|
|
206
|
+
const logger = new Logger(Boolean(opts.verbose), Boolean(opts.json));
|
|
207
|
+
const keypairBytes = loadKeypair(config2.keypairPath);
|
|
208
|
+
const keypair = import_web3.Keypair.fromSecretKey(keypairBytes);
|
|
209
|
+
const connection = new import_web3.Connection(config2.rpcUrl, config2.commitment);
|
|
210
|
+
const client = import_sdk.SolanaStablecoin.fromConfig(
|
|
211
|
+
{
|
|
212
|
+
rpcUrl: config2.rpcUrl,
|
|
213
|
+
commitment: config2.commitment,
|
|
214
|
+
programId: config2.programId ? new import_web3.PublicKey(config2.programId) : void 0,
|
|
215
|
+
hookProgramId: config2.hookProgramId ? new import_web3.PublicKey(config2.hookProgramId) : void 0
|
|
216
|
+
},
|
|
217
|
+
keypair
|
|
218
|
+
);
|
|
219
|
+
return { client, keypair, logger, connection };
|
|
220
|
+
}
|
|
221
|
+
async function confirmSSS2Warning() {
|
|
222
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
223
|
+
return new Promise((resolve) => {
|
|
224
|
+
console.log("\n\u26A0\uFE0F WARNING: SSS-2 Compliance Notice");
|
|
225
|
+
console.log("\u2500".repeat(60));
|
|
226
|
+
console.log("SSS-2 enables PERMANENT DELEGATE and TRANSFER HOOK extensions.");
|
|
227
|
+
console.log("These extensions are IMMUTABLE after initialization.");
|
|
228
|
+
console.log("Once enabled, they CANNOT be disabled without migrating");
|
|
229
|
+
console.log("all token holders to a new mint.");
|
|
230
|
+
console.log("");
|
|
231
|
+
console.log("Ensure you understand the compliance implications before");
|
|
232
|
+
console.log("deploying this token in a production environment.");
|
|
233
|
+
console.log("\u2500".repeat(60));
|
|
234
|
+
rl.question("Type 'yes' to continue, anything else to cancel: ", (answer) => {
|
|
235
|
+
rl.close();
|
|
236
|
+
resolve(answer.toLowerCase() === "yes");
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
function auditLog(entry) {
|
|
241
|
+
const config2 = loadConfig({});
|
|
242
|
+
const logPath = config2.auditLogPath || `${os3.homedir()}/.thawgate/audit.log`;
|
|
243
|
+
const dir = logPath.substring(0, logPath.lastIndexOf("/"));
|
|
244
|
+
if (dir && !fs3.existsSync(dir)) {
|
|
245
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
246
|
+
}
|
|
247
|
+
const line = JSON.stringify({ ...entry, timestamp: (/* @__PURE__ */ new Date()).toISOString() }) + "\n";
|
|
248
|
+
fs3.appendFileSync(logPath, line, "utf-8");
|
|
249
|
+
}
|
|
250
|
+
function registerCommands(program2) {
|
|
251
|
+
program2.command("init").description("Initialize a new SSS stablecoin").option("--name <name>", "Stablecoin name (max 32 chars)").option("--symbol <symbol>", "Token symbol (max 10 chars)").option("--uri <uri>", "Metadata URI").option("--decimals <n>", "Decimal places", "6").option("--preset <preset>", "Preset: sss1 or sss2", "sss1").option("--custom <path>", "Path to custom config file (.toml or .json)").option("--hook-program <pubkey>", "Transfer hook program ID (SSS-2)").option("--confirm", "Skip confirmation prompt").action(async (opts) => {
|
|
252
|
+
if (opts.custom && opts.preset && opts.preset !== "sss1") {
|
|
253
|
+
console.error("\u274C Cannot use --custom and --preset together");
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
let name = opts.name;
|
|
257
|
+
let symbol = opts.symbol;
|
|
258
|
+
let uri = opts.uri;
|
|
259
|
+
let decimals = parseInt(opts.decimals, 10);
|
|
260
|
+
let preset = opts.preset;
|
|
261
|
+
if (opts.custom) {
|
|
262
|
+
const { extname } = require("path");
|
|
263
|
+
const raw = fs3.readFileSync(opts.custom, "utf-8");
|
|
264
|
+
const ext = extname(opts.custom).toLowerCase();
|
|
265
|
+
let customConfig;
|
|
266
|
+
if (ext === ".toml") {
|
|
267
|
+
try {
|
|
268
|
+
const toml = require("@iarna/toml");
|
|
269
|
+
customConfig = toml.parse(raw);
|
|
270
|
+
} catch {
|
|
271
|
+
customConfig = {};
|
|
272
|
+
for (const line of raw.split("\n")) {
|
|
273
|
+
const trimmed = line.trim();
|
|
274
|
+
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("[")) continue;
|
|
275
|
+
const eqIdx = trimmed.indexOf("=");
|
|
276
|
+
if (eqIdx === -1) continue;
|
|
277
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
278
|
+
let value = trimmed.slice(eqIdx + 1).trim();
|
|
279
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
280
|
+
value = value.slice(1, -1);
|
|
281
|
+
}
|
|
282
|
+
if (value === "true") customConfig[key] = true;
|
|
283
|
+
else if (value === "false") customConfig[key] = false;
|
|
284
|
+
else if (/^\d+$/.test(value)) customConfig[key] = parseInt(value, 10);
|
|
285
|
+
else customConfig[key] = value;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
} else {
|
|
289
|
+
customConfig = JSON.parse(raw);
|
|
290
|
+
}
|
|
291
|
+
console.log(`Loading custom config from ${opts.custom}`);
|
|
292
|
+
console.log("Config:", JSON.stringify(customConfig, null, 2));
|
|
293
|
+
if (!customConfig.name || !customConfig.symbol) {
|
|
294
|
+
console.error("\u274C Custom config must include: name, symbol");
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
name = customConfig.name;
|
|
298
|
+
symbol = customConfig.symbol;
|
|
299
|
+
uri = customConfig.uri || opts.uri || "";
|
|
300
|
+
decimals = customConfig.decimals ?? decimals;
|
|
301
|
+
if (customConfig.preset) preset = customConfig.preset;
|
|
302
|
+
}
|
|
303
|
+
preset = parsePreset(preset);
|
|
304
|
+
if (!name || !symbol) {
|
|
305
|
+
console.error("\u274C --name and --symbol are required (or provide --custom <path>)");
|
|
306
|
+
process.exit(1);
|
|
307
|
+
}
|
|
308
|
+
if (preset === "sss2" && !opts.confirm) {
|
|
309
|
+
const confirmed = await confirmSSS2Warning();
|
|
310
|
+
if (!confirmed) {
|
|
311
|
+
console.log("Initialization cancelled.");
|
|
312
|
+
process.exit(0);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const { client, keypair, logger, connection } = createClient(program2.opts());
|
|
316
|
+
const args = preset === "sss2" ? (0, import_sdk.sss2Preset)(
|
|
317
|
+
name,
|
|
318
|
+
symbol,
|
|
319
|
+
uri || "",
|
|
320
|
+
new import_web3.PublicKey(opts.hookProgram),
|
|
321
|
+
decimals
|
|
322
|
+
) : (0, import_sdk.sss1Preset)(name, symbol, uri || "", decimals);
|
|
323
|
+
logger.info(`Initializing ${preset.toUpperCase()} stablecoin: ${name}`);
|
|
324
|
+
const { instructions, mint, mintKeypair } = await client.initialize(
|
|
325
|
+
keypair.publicKey,
|
|
326
|
+
args
|
|
327
|
+
);
|
|
328
|
+
const tx = new import_web3.Transaction().add(...instructions);
|
|
329
|
+
const sig = await (0, import_web3.sendAndConfirmTransaction)(connection, tx, [keypair, mintKeypair]);
|
|
330
|
+
logger.transaction("init", sig, {
|
|
331
|
+
mint: mint.toBase58(),
|
|
332
|
+
preset,
|
|
333
|
+
custom: opts.custom || void 0
|
|
334
|
+
});
|
|
335
|
+
logger.output({ mint: mint.toBase58(), signature: sig });
|
|
336
|
+
auditLog({ command: "init", mint: mint.toBase58(), preset, custom: opts.custom, result: sig });
|
|
337
|
+
});
|
|
338
|
+
program2.command("mint").description("Mint tokens to a recipient").requiredOption("--mint <pubkey>", "Mint address").requiredOption("--recipient <pubkey>", "Recipient wallet address").requiredOption("--amount <amount>", "Amount to mint (raw units)").option("--dry-run", "Simulate the transaction without submitting").option("--memo <memo>", "Optional memo").action(async (opts) => {
|
|
339
|
+
const { client, keypair, logger, connection } = createClient(program2.opts());
|
|
340
|
+
const mint = new import_web3.PublicKey(opts.mint);
|
|
341
|
+
const recipient = new import_web3.PublicKey(opts.recipient);
|
|
342
|
+
const amount = new import_anchor.BN(opts.amount);
|
|
343
|
+
const instructions = await client.mintTokens(mint, keypair.publicKey, recipient, amount);
|
|
344
|
+
const tx = new import_web3.Transaction().add(...instructions);
|
|
345
|
+
if (opts.dryRun) {
|
|
346
|
+
await simulateTransaction(connection, tx, [keypair]);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
logger.info(`Minting ${opts.amount} tokens to ${recipient.toBase58()}`);
|
|
350
|
+
const sig = await (0, import_web3.sendAndConfirmTransaction)(connection, tx, [keypair]);
|
|
351
|
+
logger.transaction("mint", sig, {
|
|
352
|
+
mint: mint.toBase58(),
|
|
353
|
+
recipient: recipient.toBase58(),
|
|
354
|
+
amount: opts.amount
|
|
355
|
+
});
|
|
356
|
+
auditLog({ command: "mint", mint: mint.toBase58(), recipient: recipient.toBase58(), amount: opts.amount, result: sig });
|
|
357
|
+
});
|
|
358
|
+
program2.command("burn").description("Burn tokens from your account").requiredOption("--mint <pubkey>", "Mint address").requiredOption("--amount <amount>", "Amount to burn (raw units)").option("--dry-run", "Simulate the transaction without submitting").action(async (opts) => {
|
|
359
|
+
const { client, keypair, logger, connection } = createClient(program2.opts());
|
|
360
|
+
const mint = new import_web3.PublicKey(opts.mint);
|
|
361
|
+
const amount = new import_anchor.BN(opts.amount);
|
|
362
|
+
const instructions = await client.burnTokens(mint, keypair.publicKey, amount);
|
|
363
|
+
const tx = new import_web3.Transaction().add(...instructions);
|
|
364
|
+
if (opts.dryRun) {
|
|
365
|
+
await simulateTransaction(connection, tx, [keypair]);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
logger.info(`Burning ${opts.amount} tokens`);
|
|
369
|
+
const sig = await (0, import_web3.sendAndConfirmTransaction)(connection, tx, [keypair]);
|
|
370
|
+
logger.transaction("burn", sig, { mint: mint.toBase58(), amount: opts.amount });
|
|
371
|
+
auditLog({ command: "burn", mint: mint.toBase58(), amount: opts.amount, result: sig });
|
|
372
|
+
});
|
|
373
|
+
program2.command("freeze").description("Freeze a token account").requiredOption("--mint <pubkey>", "Mint address").requiredOption("--target <pubkey>", "Target token account to freeze").option("--dry-run", "Simulate the transaction without submitting").option("--confirm", "Confirm this irreversible action").action(async (opts) => {
|
|
374
|
+
if (!opts.confirm && !opts.dryRun) {
|
|
375
|
+
console.error(
|
|
376
|
+
`ERROR: Freeze is irreversible.
|
|
377
|
+
Re-run with --confirm to proceed:
|
|
378
|
+
thawgate freeze --mint ${opts.mint} --target ${opts.target} --confirm`
|
|
379
|
+
);
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
382
|
+
const { client, keypair, logger, connection } = createClient(program2.opts());
|
|
383
|
+
const mint = new import_web3.PublicKey(opts.mint);
|
|
384
|
+
const target = new import_web3.PublicKey(opts.target);
|
|
385
|
+
const instructions = await client.freezeAccount(mint, keypair.publicKey, target);
|
|
386
|
+
const tx = new import_web3.Transaction().add(...instructions);
|
|
387
|
+
if (opts.dryRun) {
|
|
388
|
+
await simulateTransaction(connection, tx, [keypair]);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
logger.info(`Freezing token account ${target.toBase58()}`);
|
|
392
|
+
const sig = await (0, import_web3.sendAndConfirmTransaction)(connection, tx, [keypair]);
|
|
393
|
+
logger.transaction("freeze", sig, { target: target.toBase58() });
|
|
394
|
+
auditLog({ command: "freeze", mint: mint.toBase58(), target: target.toBase58(), result: sig });
|
|
395
|
+
});
|
|
396
|
+
program2.command("thaw").description("Thaw a frozen token account").requiredOption("--mint <pubkey>", "Mint address").requiredOption("--target <pubkey>", "Target token account to thaw").option("--dry-run", "Simulate the transaction without submitting").action(async (opts) => {
|
|
397
|
+
const { client, keypair, logger, connection } = createClient(program2.opts());
|
|
398
|
+
const mint = new import_web3.PublicKey(opts.mint);
|
|
399
|
+
const target = new import_web3.PublicKey(opts.target);
|
|
400
|
+
const instructions = await client.thawAccount(mint, keypair.publicKey, target);
|
|
401
|
+
const tx = new import_web3.Transaction().add(...instructions);
|
|
402
|
+
if (opts.dryRun) {
|
|
403
|
+
await simulateTransaction(connection, tx, [keypair]);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
logger.info(`Thawing token account ${target.toBase58()}`);
|
|
407
|
+
const sig = await (0, import_web3.sendAndConfirmTransaction)(connection, tx, [keypair]);
|
|
408
|
+
logger.transaction("thaw", sig, { target: target.toBase58() });
|
|
409
|
+
auditLog({ command: "thaw", mint: mint.toBase58(), target: target.toBase58(), result: sig });
|
|
410
|
+
});
|
|
411
|
+
program2.command("pause").description("Pause all token operations").requiredOption("--mint <pubkey>", "Mint address").action(async (opts) => {
|
|
412
|
+
const { client, keypair, logger, connection } = createClient(program2.opts());
|
|
413
|
+
const mint = new import_web3.PublicKey(opts.mint);
|
|
414
|
+
logger.info("Pausing token operations");
|
|
415
|
+
const instructions = await client.pause(mint, keypair.publicKey);
|
|
416
|
+
const tx = new import_web3.Transaction().add(...instructions);
|
|
417
|
+
const sig = await (0, import_web3.sendAndConfirmTransaction)(connection, tx, [keypair]);
|
|
418
|
+
logger.transaction("pause", sig, { mint: mint.toBase58() });
|
|
419
|
+
auditLog({ command: "pause", mint: mint.toBase58(), result: sig });
|
|
420
|
+
});
|
|
421
|
+
program2.command("unpause").description("Resume token operations").requiredOption("--mint <pubkey>", "Mint address").action(async (opts) => {
|
|
422
|
+
const { client, keypair, logger, connection } = createClient(program2.opts());
|
|
423
|
+
const mint = new import_web3.PublicKey(opts.mint);
|
|
424
|
+
logger.info("Resuming token operations");
|
|
425
|
+
const instructions = await client.unpause(mint, keypair.publicKey);
|
|
426
|
+
const tx = new import_web3.Transaction().add(...instructions);
|
|
427
|
+
const sig = await (0, import_web3.sendAndConfirmTransaction)(connection, tx, [keypair]);
|
|
428
|
+
logger.transaction("unpause", sig, { mint: mint.toBase58() });
|
|
429
|
+
auditLog({ command: "unpause", mint: mint.toBase58(), result: sig });
|
|
430
|
+
});
|
|
431
|
+
program2.command("grant-role").description("Grant a role to a wallet").requiredOption("--mint <pubkey>", "Mint address").requiredOption("--holder <pubkey>", "Wallet to grant role to").requiredOption("--role <role>", "Role: minter, burner, pauser, blacklister, seizer").action(async (opts) => {
|
|
432
|
+
const { client, keypair, logger, connection } = createClient(program2.opts());
|
|
433
|
+
const mint = new import_web3.PublicKey(opts.mint);
|
|
434
|
+
const holder = new import_web3.PublicKey(opts.holder);
|
|
435
|
+
const role = parseRoleType(opts.role);
|
|
436
|
+
logger.info(`Granting ${opts.role} role to ${holder.toBase58()}`);
|
|
437
|
+
const instructions = await client.updateRoles(mint, keypair.publicKey, holder, role, true);
|
|
438
|
+
const tx = new import_web3.Transaction().add(...instructions);
|
|
439
|
+
const sig = await (0, import_web3.sendAndConfirmTransaction)(connection, tx, [keypair]);
|
|
440
|
+
logger.transaction("grant-role", sig, {
|
|
441
|
+
holder: holder.toBase58(),
|
|
442
|
+
role: opts.role
|
|
443
|
+
});
|
|
444
|
+
auditLog({ command: "grant-role", mint: mint.toBase58(), holder: holder.toBase58(), role: opts.role, result: sig });
|
|
445
|
+
});
|
|
446
|
+
program2.command("revoke-role").description("Revoke a role from a wallet").requiredOption("--mint <pubkey>", "Mint address").requiredOption("--holder <pubkey>", "Wallet to revoke role from").requiredOption("--role <role>", "Role: minter, burner, pauser, blacklister, seizer").action(async (opts) => {
|
|
447
|
+
const { client, keypair, logger, connection } = createClient(program2.opts());
|
|
448
|
+
const mint = new import_web3.PublicKey(opts.mint);
|
|
449
|
+
const holder = new import_web3.PublicKey(opts.holder);
|
|
450
|
+
const role = parseRoleType(opts.role);
|
|
451
|
+
logger.info(`Revoking ${opts.role} role from ${holder.toBase58()}`);
|
|
452
|
+
const instructions = await client.updateRoles(mint, keypair.publicKey, holder, role, false);
|
|
453
|
+
const tx = new import_web3.Transaction().add(...instructions);
|
|
454
|
+
const sig = await (0, import_web3.sendAndConfirmTransaction)(connection, tx, [keypair]);
|
|
455
|
+
logger.transaction("revoke-role", sig, {
|
|
456
|
+
holder: holder.toBase58(),
|
|
457
|
+
role: opts.role
|
|
458
|
+
});
|
|
459
|
+
auditLog({ command: "revoke-role", mint: mint.toBase58(), holder: holder.toBase58(), role: opts.role, result: sig });
|
|
460
|
+
});
|
|
461
|
+
const rolesCmd = program2.command("roles").description("Manage roles (grant or revoke). Use: roles grant | roles revoke");
|
|
462
|
+
rolesCmd.command("grant").description("Grant a role to a wallet").requiredOption("--mint <pubkey>", "Mint address").requiredOption("--holder <pubkey>", "Wallet to grant role to").requiredOption("--role <role>", "Role: minter, burner, pauser, blacklister, seizer").action(async (opts) => {
|
|
463
|
+
const { client, keypair, logger, connection } = createClient(program2.opts());
|
|
464
|
+
const mint = new import_web3.PublicKey(opts.mint);
|
|
465
|
+
const holder = new import_web3.PublicKey(opts.holder);
|
|
466
|
+
const role = parseRoleType(opts.role);
|
|
467
|
+
logger.info(`Granting ${opts.role} role to ${holder.toBase58()}`);
|
|
468
|
+
const instructions = await client.updateRoles(mint, keypair.publicKey, holder, role, true);
|
|
469
|
+
const tx = new import_web3.Transaction().add(...instructions);
|
|
470
|
+
const sig = await (0, import_web3.sendAndConfirmTransaction)(connection, tx, [keypair]);
|
|
471
|
+
logger.transaction("roles grant", sig, {
|
|
472
|
+
holder: holder.toBase58(),
|
|
473
|
+
role: opts.role
|
|
474
|
+
});
|
|
475
|
+
auditLog({ command: "roles grant", mint: mint.toBase58(), holder: holder.toBase58(), role: opts.role, result: sig });
|
|
476
|
+
});
|
|
477
|
+
rolesCmd.command("revoke").description("Revoke a role from a wallet").requiredOption("--mint <pubkey>", "Mint address").requiredOption("--holder <pubkey>", "Wallet to revoke role from").requiredOption("--role <role>", "Role: minter, burner, pauser, blacklister, seizer").action(async (opts) => {
|
|
478
|
+
const { client, keypair, logger, connection } = createClient(program2.opts());
|
|
479
|
+
const mint = new import_web3.PublicKey(opts.mint);
|
|
480
|
+
const holder = new import_web3.PublicKey(opts.holder);
|
|
481
|
+
const role = parseRoleType(opts.role);
|
|
482
|
+
logger.info(`Revoking ${opts.role} role from ${holder.toBase58()}`);
|
|
483
|
+
const instructions = await client.updateRoles(mint, keypair.publicKey, holder, role, false);
|
|
484
|
+
const tx = new import_web3.Transaction().add(...instructions);
|
|
485
|
+
const sig = await (0, import_web3.sendAndConfirmTransaction)(connection, tx, [keypair]);
|
|
486
|
+
logger.transaction("roles revoke", sig, {
|
|
487
|
+
holder: holder.toBase58(),
|
|
488
|
+
role: opts.role
|
|
489
|
+
});
|
|
490
|
+
auditLog({ command: "roles revoke", mint: mint.toBase58(), holder: holder.toBase58(), role: opts.role, result: sig });
|
|
491
|
+
});
|
|
492
|
+
rolesCmd.command("list").description("List all active role assignments").requiredOption("--mint <pubkey>", "Mint address").option("--json", "Output as JSON").action(async (opts) => {
|
|
493
|
+
const { client } = createClient(program2.opts());
|
|
494
|
+
const mint = new import_web3.PublicKey(opts.mint);
|
|
495
|
+
const allRoles = await client.program.account.roleRecord.all([
|
|
496
|
+
{ memcmp: { offset: 8, bytes: mint.toBase58() } }
|
|
497
|
+
]);
|
|
498
|
+
const active = allRoles.map((r) => r.account).filter((r) => r.active);
|
|
499
|
+
if (opts.json) {
|
|
500
|
+
console.log(JSON.stringify({ success: true, data: active, error: null }));
|
|
501
|
+
} else {
|
|
502
|
+
if (active.length === 0) {
|
|
503
|
+
console.log("No active role assignments.");
|
|
504
|
+
} else {
|
|
505
|
+
active.forEach((r) => {
|
|
506
|
+
console.log(`${r.holder.toBase58()} role: ${Object.keys(r.role)[0]}`);
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
program2.command("transfer-authority").description("Transfer MasterAuthority to a new wallet").requiredOption("--mint <pubkey>", "Mint address").requiredOption("--new-authority <pubkey>", "New MasterAuthority wallet").option("--confirm", "Confirm this irreversible action").action(async (opts) => {
|
|
512
|
+
if (!opts.confirm) {
|
|
513
|
+
console.error(
|
|
514
|
+
`ERROR: Transferring authority is irreversible without a follow-up transfer-back.
|
|
515
|
+
Re-run with --confirm to proceed:
|
|
516
|
+
thawgate transfer-authority --mint ${opts.mint} --new-authority ${opts.newAuthority} --confirm`
|
|
517
|
+
);
|
|
518
|
+
process.exit(1);
|
|
519
|
+
}
|
|
520
|
+
const { client, keypair, logger, connection } = createClient(program2.opts());
|
|
521
|
+
const mint = new import_web3.PublicKey(opts.mint);
|
|
522
|
+
const newAuthority = new import_web3.PublicKey(opts.newAuthority);
|
|
523
|
+
logger.info(`Transferring authority to ${newAuthority.toBase58()}`);
|
|
524
|
+
const instructions = await client.transferAuthority(
|
|
525
|
+
mint,
|
|
526
|
+
keypair.publicKey,
|
|
527
|
+
newAuthority
|
|
528
|
+
);
|
|
529
|
+
const tx = new import_web3.Transaction().add(...instructions);
|
|
530
|
+
const sig = await (0, import_web3.sendAndConfirmTransaction)(connection, tx, [keypair]);
|
|
531
|
+
logger.transaction("transfer-authority", sig, {
|
|
532
|
+
newAuthority: newAuthority.toBase58()
|
|
533
|
+
});
|
|
534
|
+
auditLog({ command: "transfer-authority", mint: mint.toBase58(), newAuthority: newAuthority.toBase58(), result: sig });
|
|
535
|
+
});
|
|
536
|
+
program2.command("blacklist").description("Add a wallet to the blacklist (Blacklister role); on Token ACL mints its passed account is frozen too").requiredOption("--mint <pubkey>", "Mint address").requiredOption("--target <pubkey>", "Wallet to blacklist").requiredOption("--reason <reason>", "Reason for blacklisting").option("--token-account <pubkey>", "A token account owned by the target (default: its associated token account)").option("--confirm", "Confirm this irreversible action").action(async (opts) => {
|
|
537
|
+
if (!opts.confirm) {
|
|
538
|
+
console.error(
|
|
539
|
+
`ERROR: Blacklisting is irreversible without a separate unblacklist operation.
|
|
540
|
+
Re-run with --confirm to proceed:
|
|
541
|
+
thawgate blacklist --mint ${opts.mint} --target ${opts.target} --reason "${opts.reason}" --confirm`
|
|
542
|
+
);
|
|
543
|
+
process.exit(1);
|
|
544
|
+
}
|
|
545
|
+
const { client, keypair, logger, connection } = createClient(program2.opts());
|
|
546
|
+
const mint = new import_web3.PublicKey(opts.mint);
|
|
547
|
+
const target = new import_web3.PublicKey(opts.target);
|
|
548
|
+
logger.info(`Blacklisting ${target.toBase58()}: ${opts.reason}`);
|
|
549
|
+
const compliance = client.compliance(mint);
|
|
550
|
+
const instructions = await compliance.addToBlacklist(
|
|
551
|
+
keypair.publicKey,
|
|
552
|
+
target,
|
|
553
|
+
opts.reason,
|
|
554
|
+
{ targetTokenAccount: opts.tokenAccount ? new import_web3.PublicKey(opts.tokenAccount) : void 0 }
|
|
555
|
+
);
|
|
556
|
+
const tx = new import_web3.Transaction().add(...instructions);
|
|
557
|
+
const sig = await (0, import_web3.sendAndConfirmTransaction)(connection, tx, [keypair]);
|
|
558
|
+
logger.transaction("blacklist", sig, {
|
|
559
|
+
target: target.toBase58(),
|
|
560
|
+
reason: opts.reason
|
|
561
|
+
});
|
|
562
|
+
auditLog({ command: "blacklist", mint: mint.toBase58(), target: target.toBase58(), reason: opts.reason, result: sig });
|
|
563
|
+
});
|
|
564
|
+
program2.command("unblacklist").description("Remove a wallet from the blacklist (SSS-2)").requiredOption("--mint <pubkey>", "Mint address").requiredOption("--target <pubkey>", "Wallet to remove from blacklist").option("--confirm", "Confirm this action").action(async (opts) => {
|
|
565
|
+
if (!opts.confirm) {
|
|
566
|
+
console.error(
|
|
567
|
+
`ERROR: Confirm required for this operation.
|
|
568
|
+
Re-run with --confirm:
|
|
569
|
+
thawgate unblacklist --mint ${opts.mint} --target ${opts.target} --confirm`
|
|
570
|
+
);
|
|
571
|
+
process.exit(1);
|
|
572
|
+
}
|
|
573
|
+
const { client, keypair, logger, connection } = createClient(program2.opts());
|
|
574
|
+
const mint = new import_web3.PublicKey(opts.mint);
|
|
575
|
+
const target = new import_web3.PublicKey(opts.target);
|
|
576
|
+
logger.info(`Removing ${target.toBase58()} from blacklist`);
|
|
577
|
+
const compliance = client.compliance(mint);
|
|
578
|
+
const instructions = await compliance.removeFromBlacklist(
|
|
579
|
+
keypair.publicKey,
|
|
580
|
+
target
|
|
581
|
+
);
|
|
582
|
+
const tx = new import_web3.Transaction().add(...instructions);
|
|
583
|
+
const sig = await (0, import_web3.sendAndConfirmTransaction)(connection, tx, [keypair]);
|
|
584
|
+
logger.transaction("unblacklist", sig, { target: target.toBase58() });
|
|
585
|
+
auditLog({ command: "unblacklist", mint: mint.toBase58(), target: target.toBase58(), result: sig });
|
|
586
|
+
});
|
|
587
|
+
program2.command("seize").description("Seize tokens from a blacklisted frozen account (SSS-2 only)").requiredOption("--mint <pubkey>", "Mint address").requiredOption("--source <pubkey>", "Source token account (ATA) to seize from").requiredOption("--source-authority <pubkey>", "Wallet that owns the source token account (the blacklisted address)").requiredOption("--treasury <pubkey>", "Treasury token account (ATA) to receive seized tokens").option("--confirm", "Confirm this irreversible action").action(async (opts) => {
|
|
588
|
+
if (!opts.confirm) {
|
|
589
|
+
console.error(
|
|
590
|
+
`ERROR: Seize is irreversible. Re-run with --confirm to proceed:
|
|
591
|
+
thawgate seize --mint ${opts.mint} --source ${opts.source} --source-authority ${opts.sourceAuthority} --treasury ${opts.treasury} --confirm`
|
|
592
|
+
);
|
|
593
|
+
process.exit(1);
|
|
594
|
+
}
|
|
595
|
+
const { client, keypair, logger, connection } = createClient(program2.opts());
|
|
596
|
+
const mint = new import_web3.PublicKey(opts.mint);
|
|
597
|
+
const sourceAccount = new import_web3.PublicKey(opts.source);
|
|
598
|
+
const sourceAuthority = new import_web3.PublicKey(opts.sourceAuthority);
|
|
599
|
+
const treasuryAccount = new import_web3.PublicKey(opts.treasury);
|
|
600
|
+
logger.info(`Seizing tokens from ${sourceAccount.toBase58()} to ${treasuryAccount.toBase58()}`);
|
|
601
|
+
const compliance = client.compliance(mint);
|
|
602
|
+
const instructions = await compliance.seize(
|
|
603
|
+
keypair.publicKey,
|
|
604
|
+
sourceAuthority,
|
|
605
|
+
sourceAccount,
|
|
606
|
+
treasuryAccount
|
|
607
|
+
);
|
|
608
|
+
const tx = new import_web3.Transaction().add(...instructions);
|
|
609
|
+
const sig = await (0, import_web3.sendAndConfirmTransaction)(connection, tx, [keypair]);
|
|
610
|
+
logger.transaction("seize", sig, {
|
|
611
|
+
source: sourceAccount.toBase58(),
|
|
612
|
+
treasury: treasuryAccount.toBase58()
|
|
613
|
+
});
|
|
614
|
+
auditLog({ command: "seize", mint: mint.toBase58(), source: sourceAccount.toBase58(), treasury: treasuryAccount.toBase58(), result: sig });
|
|
615
|
+
});
|
|
616
|
+
program2.command("supply").description("Display the current total token supply").requiredOption("--mint <pubkey>", "Mint address").option("--json", "Output as JSON").action(async (opts) => {
|
|
617
|
+
const { client, logger } = createClient(program2.opts());
|
|
618
|
+
const mint = new import_web3.PublicKey(opts.mint);
|
|
619
|
+
const config2 = await client.getConfig(mint);
|
|
620
|
+
const supply = config2.totalMinted;
|
|
621
|
+
if (opts.json) {
|
|
622
|
+
console.log(JSON.stringify({ success: true, data: { supply: supply.toString(), totalMinted: config2.totalMinted.toString(), totalBurned: config2.totalBurned.toString() }, error: null }));
|
|
623
|
+
} else {
|
|
624
|
+
console.log(`Total Minted: ${config2.totalMinted.toString()}`);
|
|
625
|
+
console.log(`Total Burned: ${config2.totalBurned.toString()}`);
|
|
626
|
+
console.log(`Net Supply: ${supply.toString()}`);
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
const mintersCmd = program2.command("minters").description("Manage minter roles");
|
|
630
|
+
mintersCmd.command("list").description("List all active minters and their quotas").requiredOption("--mint <pubkey>", "Mint address").option("--json", "Output as JSON").action(async (opts) => {
|
|
631
|
+
const { client } = createClient(program2.opts());
|
|
632
|
+
const mint = new import_web3.PublicKey(opts.mint);
|
|
633
|
+
const allRoles = await client.program.account.roleRecord.all([
|
|
634
|
+
{ memcmp: { offset: 8, bytes: mint.toBase58() } }
|
|
635
|
+
]);
|
|
636
|
+
const minterRoles = allRoles.map((r) => r.account).filter((r) => r.active && Object.keys(r.role)[0] === "minter");
|
|
637
|
+
if (opts.json) {
|
|
638
|
+
console.log(JSON.stringify({ success: true, data: minterRoles, error: null }));
|
|
639
|
+
} else {
|
|
640
|
+
if (minterRoles.length === 0) {
|
|
641
|
+
console.log("No active minters.");
|
|
642
|
+
} else {
|
|
643
|
+
minterRoles.forEach((m) => {
|
|
644
|
+
console.log(`${m.holder.toBase58()} limit: ${m.limit?.toString() || "unlimited"}`);
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
mintersCmd.command("add <address>").description("Add a minter role").requiredOption("--mint <pubkey>", "Mint address").requiredOption("--limit <amount>", "Minting quota limit (0 = unlimited)").option("--period <period>", "Quota period: daily|weekly|monthly|lifetime", "lifetime").option("--confirm", "Confirm the action").action(async (address, opts) => {
|
|
650
|
+
if (!opts.confirm) {
|
|
651
|
+
console.error("Error: --confirm flag required for this operation");
|
|
652
|
+
process.exit(1);
|
|
653
|
+
}
|
|
654
|
+
const { client, keypair, logger, connection } = createClient(program2.opts());
|
|
655
|
+
const mint = new import_web3.PublicKey(opts.mint);
|
|
656
|
+
const holder = new import_web3.PublicKey(address);
|
|
657
|
+
const instructions = await client.updateMinter(
|
|
658
|
+
mint,
|
|
659
|
+
keypair.publicKey,
|
|
660
|
+
holder,
|
|
661
|
+
new import_anchor.BN(opts.limit),
|
|
662
|
+
parseQuotaPeriod(opts.period)
|
|
663
|
+
);
|
|
664
|
+
const tx = new import_web3.Transaction().add(...instructions);
|
|
665
|
+
const sig = await (0, import_web3.sendAndConfirmTransaction)(connection, tx, [keypair]);
|
|
666
|
+
console.log(`Minter added. TX: ${sig}`);
|
|
667
|
+
auditLog({ command: "minters add", address, limit: opts.limit, result: sig });
|
|
668
|
+
});
|
|
669
|
+
mintersCmd.command("remove <address>").description("Remove a minter role").requiredOption("--mint <pubkey>", "Mint address").option("--confirm", "Confirm the action").action(async (address, opts) => {
|
|
670
|
+
if (!opts.confirm) {
|
|
671
|
+
console.error("Error: --confirm flag required for this operation");
|
|
672
|
+
process.exit(1);
|
|
673
|
+
}
|
|
674
|
+
const { client, keypair, logger, connection } = createClient(program2.opts());
|
|
675
|
+
const mint = new import_web3.PublicKey(opts.mint);
|
|
676
|
+
const holder = new import_web3.PublicKey(address);
|
|
677
|
+
const instructions = await client.updateRoles(mint, keypair.publicKey, holder, import_sdk.RoleType.Minter, false);
|
|
678
|
+
const tx = new import_web3.Transaction().add(...instructions);
|
|
679
|
+
const sig = await (0, import_web3.sendAndConfirmTransaction)(connection, tx, [keypair]);
|
|
680
|
+
console.log(`Minter removed. TX: ${sig}`);
|
|
681
|
+
auditLog({ command: "minters remove", address, result: sig });
|
|
682
|
+
});
|
|
683
|
+
program2.command("holders").description("List token holders").requiredOption("--mint <pubkey>", "Mint address").option("--min-balance <amount>", "Minimum balance filter", "0").option("--format <format>", "Output format: table|json|csv", "table").action(async (opts) => {
|
|
684
|
+
const { connection } = createClient(program2.opts());
|
|
685
|
+
const mint = new import_web3.PublicKey(opts.mint);
|
|
686
|
+
const accounts = await connection.getProgramAccounts(import_spl_token.TOKEN_2022_PROGRAM_ID, {
|
|
687
|
+
filters: [
|
|
688
|
+
{ dataSize: 165 },
|
|
689
|
+
{ memcmp: { offset: 0, bytes: mint.toBase58() } }
|
|
690
|
+
]
|
|
691
|
+
});
|
|
692
|
+
const minBalance = BigInt(opts.minBalance);
|
|
693
|
+
const holders = accounts.map((acc) => ({
|
|
694
|
+
address: acc.pubkey.toString(),
|
|
695
|
+
balance: BigInt(acc.account.data.readBigUInt64LE ? acc.account.data.readBigUInt64LE(64) : 0)
|
|
696
|
+
})).filter((h) => h.balance >= minBalance);
|
|
697
|
+
if (opts.format === "json") {
|
|
698
|
+
console.log(JSON.stringify({ success: true, data: holders.map((h) => ({ ...h, balance: h.balance.toString() })), error: null }));
|
|
699
|
+
} else if (opts.format === "csv") {
|
|
700
|
+
console.log("address,balance");
|
|
701
|
+
holders.forEach((h) => console.log(`${h.address},${h.balance}`));
|
|
702
|
+
} else {
|
|
703
|
+
console.table(holders.map((h) => ({ ...h, balance: h.balance.toString() })));
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
program2.command("audit-log").description("Display the local audit log").option("--action <type>", "Filter by action type").option("--from <date>", "Start date (ISO 8601)").option("--to <date>", "End date (ISO 8601)").option("--format <format>", "Output format: json|csv", "json").action((opts) => {
|
|
707
|
+
const config2 = loadConfig({});
|
|
708
|
+
const logPath = config2.auditLogPath || `${os3.homedir()}/.thawgate/audit.log`;
|
|
709
|
+
if (!fs3.existsSync(logPath)) {
|
|
710
|
+
console.log("No audit log found.");
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
const lines = fs3.readFileSync(logPath, "utf-8").split("\n").filter(Boolean).map((l) => JSON.parse(l));
|
|
714
|
+
let filtered = lines;
|
|
715
|
+
if (opts.action) filtered = filtered.filter((e) => e.command?.includes(opts.action));
|
|
716
|
+
if (opts.from) filtered = filtered.filter((e) => new Date(e.timestamp) >= new Date(opts.from));
|
|
717
|
+
if (opts.to) filtered = filtered.filter((e) => new Date(e.timestamp) <= new Date(opts.to));
|
|
718
|
+
if (opts.format === "csv") {
|
|
719
|
+
console.log("timestamp,command,result");
|
|
720
|
+
filtered.forEach((e) => console.log(`${e.timestamp},${e.command},${e.result}`));
|
|
721
|
+
} else {
|
|
722
|
+
console.log(JSON.stringify(filtered, null, 2));
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
const configCmd = program2.command("config").description("Manage CLI configuration");
|
|
726
|
+
configCmd.command("show").description("Display current configuration").action(() => {
|
|
727
|
+
const config2 = loadConfig({});
|
|
728
|
+
const safe = { ...config2, keypairPath: config2.keypairPath ? "[set]" : "[not set]" };
|
|
729
|
+
console.log(JSON.stringify(safe, null, 2));
|
|
730
|
+
});
|
|
731
|
+
configCmd.command("set").description("Set a configuration value").option("--rpc-url <url>", "Solana RPC URL").option("--keypair-path <path>", "Path to keypair JSON file").option("--program-id <id>", "SSS Token program ID").option("--mint-address <address>", "Token mint address").action((opts) => {
|
|
732
|
+
const updates = {};
|
|
733
|
+
if (opts.rpcUrl) updates.rpcUrl = opts.rpcUrl;
|
|
734
|
+
if (opts.keypairPath) updates.keypairPath = opts.keypairPath;
|
|
735
|
+
if (opts.programId) updates.programId = opts.programId;
|
|
736
|
+
if (opts.mintAddress) updates.mintAddress = opts.mintAddress;
|
|
737
|
+
if (Object.keys(updates).length === 0) {
|
|
738
|
+
console.error("No configuration options provided. Use --help for available options.");
|
|
739
|
+
process.exit(1);
|
|
740
|
+
}
|
|
741
|
+
try {
|
|
742
|
+
saveConfig(updates);
|
|
743
|
+
console.log("Configuration saved to ~/.thawgate/config.json");
|
|
744
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
745
|
+
console.log(` ${key}: ${value}`);
|
|
746
|
+
}
|
|
747
|
+
} catch (err) {
|
|
748
|
+
console.error(`Failed to save configuration: ${err.message}`);
|
|
749
|
+
process.exit(1);
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
program2.command("status").description("Display stablecoin configuration and status").requiredOption("--mint <pubkey>", "Mint address").action(async (opts) => {
|
|
753
|
+
const { client, logger } = createClient(program2.opts());
|
|
754
|
+
const mint = new import_web3.PublicKey(opts.mint);
|
|
755
|
+
const config2 = await client.getConfig(mint);
|
|
756
|
+
const pauseState = await client.getPauseState(mint);
|
|
757
|
+
logger.output({
|
|
758
|
+
mint: config2.mint.toBase58(),
|
|
759
|
+
name: config2.name,
|
|
760
|
+
symbol: config2.symbol,
|
|
761
|
+
decimals: config2.decimals,
|
|
762
|
+
authority: config2.authority.toBase58(),
|
|
763
|
+
paused: pauseState.paused,
|
|
764
|
+
permanentDelegate: config2.enablePermanentDelegate,
|
|
765
|
+
transferHook: config2.enableTransferHook,
|
|
766
|
+
defaultFrozen: config2.defaultAccountFrozen,
|
|
767
|
+
totalMinted: config2.totalMinted.toString(),
|
|
768
|
+
totalBurned: config2.totalBurned.toString()
|
|
769
|
+
});
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
function parseRoleType(role) {
|
|
773
|
+
const map = {
|
|
774
|
+
minter: import_sdk.RoleType.Minter,
|
|
775
|
+
burner: import_sdk.RoleType.Burner,
|
|
776
|
+
pauser: import_sdk.RoleType.Pauser,
|
|
777
|
+
blacklister: import_sdk.RoleType.Blacklister,
|
|
778
|
+
seizer: import_sdk.RoleType.Seizer
|
|
779
|
+
};
|
|
780
|
+
const result = map[role.toLowerCase()];
|
|
781
|
+
if (result === void 0) {
|
|
782
|
+
throw new Error(
|
|
783
|
+
`Invalid role: ${role}. Valid roles: ${Object.keys(map).join(", ")}`
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
return result;
|
|
787
|
+
}
|
|
788
|
+
function parsePreset(preset) {
|
|
789
|
+
const p = String(preset).toLowerCase();
|
|
790
|
+
if (p !== "sss1" && p !== "sss2") {
|
|
791
|
+
throw new Error(`Invalid preset: ${preset}. Valid presets: sss1, sss2 (for an SSS-ACL mint gated by ThawGate, use create-stablecoin)`);
|
|
792
|
+
}
|
|
793
|
+
return p;
|
|
794
|
+
}
|
|
795
|
+
function parseQuotaPeriod(period) {
|
|
796
|
+
const map = {
|
|
797
|
+
daily: import_sdk.QuotaPeriod.Daily,
|
|
798
|
+
weekly: import_sdk.QuotaPeriod.Weekly,
|
|
799
|
+
monthly: import_sdk.QuotaPeriod.Monthly,
|
|
800
|
+
lifetime: import_sdk.QuotaPeriod.Lifetime
|
|
801
|
+
};
|
|
802
|
+
const result = map[period.toLowerCase()];
|
|
803
|
+
if (result === void 0) {
|
|
804
|
+
throw new Error(`Invalid period: ${period}. Valid periods: ${Object.keys(map).join(", ")}`);
|
|
805
|
+
}
|
|
806
|
+
return result;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// src/gate.ts
|
|
810
|
+
var import_anchor2 = require("@coral-xyz/anchor");
|
|
811
|
+
var import_web32 = require("@solana/web3.js");
|
|
812
|
+
var import_sdk2 = require("@thawgate/sdk");
|
|
813
|
+
var ALLOWLIST_MODES = ["off", "allowOnly", "bypassForPdas"];
|
|
814
|
+
function onOff(v, flag) {
|
|
815
|
+
if (v === "on" || v === "true") return true;
|
|
816
|
+
if (v === "off" || v === "false") return false;
|
|
817
|
+
throw new Error(`${flag} takes on or off, not ${v}`);
|
|
818
|
+
}
|
|
819
|
+
function policyFromFlags(f, current) {
|
|
820
|
+
const p = {};
|
|
821
|
+
if (f.blacklist !== void 0) p.checkBlacklist = onOff(f.blacklist, "--blacklist");
|
|
822
|
+
if (f.allowlist !== void 0) {
|
|
823
|
+
if (!ALLOWLIST_MODES.includes(f.allowlist)) throw new Error(`--allowlist takes ${ALLOWLIST_MODES.join(", ")}, not ${f.allowlist}`);
|
|
824
|
+
p.allowlistMode = f.allowlist;
|
|
825
|
+
}
|
|
826
|
+
const sasGiven = f.sasCredential !== void 0 || f.sasSchema !== void 0 || f.minKyc !== void 0;
|
|
827
|
+
if (f.sas === false) {
|
|
828
|
+
if (sasGiven) throw new Error("--no-sas can't be combined with --sas-credential, --sas-schema or --min-kyc");
|
|
829
|
+
p.sas = null;
|
|
830
|
+
} else if (sasGiven) {
|
|
831
|
+
const stored = current?.requireSas ? current : void 0;
|
|
832
|
+
const credential = f.sasCredential ? new import_web32.PublicKey(f.sasCredential) : stored?.sasCredential;
|
|
833
|
+
const schema = f.sasSchema ? new import_web32.PublicKey(f.sasSchema) : stored?.sasSchema;
|
|
834
|
+
if (!credential || !schema) throw new Error("a SAS policy needs both --sas-credential and --sas-schema");
|
|
835
|
+
const minKycLevel = f.minKyc !== void 0 ? Number(f.minKyc) : stored?.minKycLevel ?? 0;
|
|
836
|
+
if (!Number.isInteger(minKycLevel) || minKycLevel < 0 || minKycLevel > 255) throw new Error("--min-kyc takes 0-255");
|
|
837
|
+
p.sas = { credential, schema, minKycLevel };
|
|
838
|
+
}
|
|
839
|
+
return p;
|
|
840
|
+
}
|
|
841
|
+
function withPolicyFlags(cmd, negatable = false) {
|
|
842
|
+
cmd.option("--blacklist <on|off>", "Deny holders on the issuer blacklist").option("--allowlist <mode>", `Issuer allowlist: ${ALLOWLIST_MODES.join(" | ")}`).option("--sas-credential <pubkey>", "Require a SAS attestation under this credential...").option("--sas-schema <pubkey>", "...and this schema (nonce = the holder's wallet)").option("--min-kyc <n>", "Minimum kyc_level (the attestation's first byte); 0 = any");
|
|
843
|
+
if (negatable) cmd.option("--no-sas", "Turn the SAS requirement off");
|
|
844
|
+
return cmd;
|
|
845
|
+
}
|
|
846
|
+
var pk = (v, flag) => {
|
|
847
|
+
if (typeof v !== "string") throw new Error(`${flag} is required`);
|
|
848
|
+
return new import_web32.PublicKey(v);
|
|
849
|
+
};
|
|
850
|
+
function u64Arg(v, flag) {
|
|
851
|
+
if (typeof v !== "string" || !/^\d+$/.test(v)) throw new Error(`${flag} takes a whole number, not ${String(v)}`);
|
|
852
|
+
const n = new import_anchor2.BN(v);
|
|
853
|
+
if (n.bitLength() > 64) throw new Error(`${flag} is larger than a u64`);
|
|
854
|
+
return n;
|
|
855
|
+
}
|
|
856
|
+
function explanationOut(e, verbose) {
|
|
857
|
+
return {
|
|
858
|
+
mint: e.mint.toBase58(),
|
|
859
|
+
wallet: e.wallet.toBase58(),
|
|
860
|
+
tokenAccount: e.tokenAccount.toBase58(),
|
|
861
|
+
account: e.account,
|
|
862
|
+
status: e.status,
|
|
863
|
+
code: e.code,
|
|
864
|
+
reason: e.reason,
|
|
865
|
+
simulated: e.simulated,
|
|
866
|
+
gatingProgram: e.gatingProgram?.toBase58() ?? null,
|
|
867
|
+
...verbose ? { logs: e.logs } : {}
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
function policyOut(p) {
|
|
871
|
+
return {
|
|
872
|
+
policy: p.address.toBase58(),
|
|
873
|
+
authority: p.authority.toBase58(),
|
|
874
|
+
issuerProgram: p.issuerProgram.toBase58(),
|
|
875
|
+
checkBlacklist: p.checkBlacklist,
|
|
876
|
+
allowlistMode: p.allowlistMode,
|
|
877
|
+
requireSas: p.requireSas,
|
|
878
|
+
sasCredential: p.requireSas ? p.sasCredential.toBase58() : null,
|
|
879
|
+
sasSchema: p.requireSas ? p.sasSchema.toBase58() : null,
|
|
880
|
+
minKycLevel: p.minKycLevel
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
function registerGateCommands(program2) {
|
|
884
|
+
const ctx = () => createClient(program2.opts());
|
|
885
|
+
const verbose = () => Boolean(program2.opts().verbose);
|
|
886
|
+
withPolicyFlags(
|
|
887
|
+
program2.command("create-stablecoin").description("Create an SSS-ACL stablecoin gated by ThawGate: initialize, enable_token_acl, minter role and reserves (3 txs)").requiredOption("--name <name>", "Name (max 32 chars)").requiredOption("--symbol <symbol>", "Symbol (max 10 chars)").option("--uri <uri>", "Metadata URI", "").option("--decimals <n>", "Decimals", "6").requiredOption("--reserves <amount>", "Reserves to post now, in base units (the supply can't exceed them)").requiredOption("--report-uri <uri>", "Link to the reserve report (max 200 bytes)").option("--max-staleness <seconds>", "Minting stops when the last reserve post is older than this", "86400").option("--attestor <pubkey>", "Who posts reserves (default: you, and you post --reserves now)")
|
|
888
|
+
).action(async (opts) => {
|
|
889
|
+
const { client, logger } = ctx();
|
|
890
|
+
const policy2 = policyFromFlags(opts);
|
|
891
|
+
logger.info(`Creating ${opts.symbol} (SSS-ACL, gated by ThawGate)`);
|
|
892
|
+
const created = await client.createStablecoin({
|
|
893
|
+
name: opts.name,
|
|
894
|
+
symbol: opts.symbol,
|
|
895
|
+
uri: opts.uri,
|
|
896
|
+
decimals: Number(opts.decimals),
|
|
897
|
+
policy: policy2,
|
|
898
|
+
reserves: {
|
|
899
|
+
amount: opts.reserves,
|
|
900
|
+
reportUri: opts.reportUri,
|
|
901
|
+
maxStalenessSeconds: Number(opts.maxStaleness),
|
|
902
|
+
attestor: opts.attestor ? pk(opts.attestor, "--attestor") : void 0
|
|
903
|
+
}
|
|
904
|
+
});
|
|
905
|
+
logger.output({ mint: created.mint.toBase58(), ...created.signatures });
|
|
906
|
+
});
|
|
907
|
+
withPolicyFlags(
|
|
908
|
+
program2.command("enable-token-acl").description("sss-token enable_token_acl on an Acl-mode mint: Token ACL config + ThawGate policy (MasterAuthority)").requiredOption("--mint <pubkey>", "Mint address")
|
|
909
|
+
).action(async (opts) => {
|
|
910
|
+
const { client, logger } = ctx();
|
|
911
|
+
const sig = await client.send(await client.enableTokenAcl(pk(opts.mint, "--mint"), policyFromFlags(opts)));
|
|
912
|
+
logger.output({ signature: sig });
|
|
913
|
+
});
|
|
914
|
+
const policy = program2.command("policy").description("A mint's ThawGate policy");
|
|
915
|
+
policy.command("show").description("Show the policy and the mint's Token ACL config").requiredOption("--mint <pubkey>", "Mint address").action(async (opts) => {
|
|
916
|
+
const { client, logger } = ctx();
|
|
917
|
+
const mint = pk(opts.mint, "--mint");
|
|
918
|
+
const [p, config2] = await Promise.all([client.gate.getPolicy(mint), client.gate.getMintConfig(mint)]);
|
|
919
|
+
logger.output({
|
|
920
|
+
mint: mint.toBase58(),
|
|
921
|
+
gatingProgram: config2?.gatingProgram.toBase58() ?? null,
|
|
922
|
+
freezeAuthority: config2?.freezeAuthority.toBase58() ?? null,
|
|
923
|
+
permissionlessThaw: config2?.enablePermissionlessThaw ?? null,
|
|
924
|
+
permissionlessFreeze: config2?.enablePermissionlessFreeze ?? null,
|
|
925
|
+
...p ? policyOut(p) : { policy: null }
|
|
926
|
+
});
|
|
927
|
+
});
|
|
928
|
+
withPolicyFlags(
|
|
929
|
+
policy.command("init").description("Create the policy of a Token ACL mint whose freeze authority is your key (sss-token mints: enable-token-acl)").requiredOption("--mint <pubkey>", "Mint address").option("--authority <pubkey>", "Policy admin (default: you)")
|
|
930
|
+
).action(async (opts) => {
|
|
931
|
+
const { client, logger } = ctx();
|
|
932
|
+
const input = policyFromFlags(opts);
|
|
933
|
+
if (opts.authority) input.authority = pk(opts.authority, "--authority");
|
|
934
|
+
const sig = await client.send(await client.gate.initPolicy(pk(opts.mint, "--mint"), input));
|
|
935
|
+
logger.output({ signature: sig });
|
|
936
|
+
});
|
|
937
|
+
withPolicyFlags(
|
|
938
|
+
policy.command("update").description("Change the policy (only the flags given) and rewrite its extra metas; holders who no longer comply become freezable").requiredOption("--mint <pubkey>", "Mint address"),
|
|
939
|
+
true
|
|
940
|
+
).action(async (opts) => {
|
|
941
|
+
const { client, logger } = ctx();
|
|
942
|
+
const mint = pk(opts.mint, "--mint");
|
|
943
|
+
const changes = policyFromFlags(opts, await client.gate.getPolicy(mint));
|
|
944
|
+
const sig = await client.send(await client.gate.updatePolicy(mint, changes));
|
|
945
|
+
logger.output({ signature: sig, ...policyOut(await client.gate.getPolicy(mint)) });
|
|
946
|
+
});
|
|
947
|
+
policy.command("setup-extra-metas").description("Rewrite both extra-metas lists from the stored policy (idempotent)").requiredOption("--mint <pubkey>", "Mint address").action(async (opts) => {
|
|
948
|
+
const { client, logger } = ctx();
|
|
949
|
+
const sig = await client.send(await client.gate.setupExtraMetas(pk(opts.mint, "--mint")));
|
|
950
|
+
logger.output({ signature: sig });
|
|
951
|
+
});
|
|
952
|
+
withPolicyFlags(
|
|
953
|
+
program2.command("swap-gate").description("Move an existing Token ACL mint to ThawGate in one tx: policy, set_gating_program, toggles, token_acl metadata").requiredOption("--mint <pubkey>", "Mint address (its Token ACL freeze authority is your key)").option("--skip-metadata", "Leave the mint's token_acl metadata field as it is").option("--confirm", "Confirm: holders are judged by the new policy from this transaction on")
|
|
954
|
+
).action(async (opts) => {
|
|
955
|
+
const mint = pk(opts.mint, "--mint");
|
|
956
|
+
if (!opts.confirm) {
|
|
957
|
+
console.error(`ERROR: swap-gate changes who may hold this token.
|
|
958
|
+
Re-run with --confirm:
|
|
959
|
+
thawgate swap-gate --mint ${mint.toBase58()} ... --confirm`);
|
|
960
|
+
process.exit(1);
|
|
961
|
+
}
|
|
962
|
+
const { client, logger } = ctx();
|
|
963
|
+
const ixs = await client.gate.swapGate(mint, policyFromFlags(opts), { skipMetadata: Boolean(opts.skipMetadata) });
|
|
964
|
+
const sig = await client.send(ixs);
|
|
965
|
+
logger.output({ signature: sig, instructions: ixs.length });
|
|
966
|
+
});
|
|
967
|
+
program2.command("explain").description("Why may (or can't) this wallet hold the token? Simulates the thaw (or the freeze, if thawed); sends nothing").requiredOption("--mint <pubkey>", "Mint address").requiredOption("--wallet <pubkey>", "Holder wallet").option("--token-account <pubkey>", "A token account of the wallet (default: its associated token account)").action(async (opts) => {
|
|
968
|
+
const { client, logger } = ctx();
|
|
969
|
+
const tokenAccount = opts.tokenAccount ? pk(opts.tokenAccount, "--token-account") : void 0;
|
|
970
|
+
const e = await client.gate.explain(pk(opts.mint, "--mint"), pk(opts.wallet, "--wallet"), { tokenAccount });
|
|
971
|
+
logger.output(explanationOut(e, verbose()));
|
|
972
|
+
});
|
|
973
|
+
program2.command("unlock").description("Create the owner's associated token account if needed and thaw it through the gate (you pay; the owner signs nothing)").requiredOption("--mint <pubkey>", "Mint address").option("--owner <pubkey>", "Holder wallet (default: you)").action(async (opts) => {
|
|
974
|
+
const { client, keypair, logger } = ctx();
|
|
975
|
+
const mint = pk(opts.mint, "--mint");
|
|
976
|
+
const owner = opts.owner ? pk(opts.owner, "--owner") : keypair.publicKey;
|
|
977
|
+
const e = await client.gate.explain(mint, owner);
|
|
978
|
+
if (e.account === "thawed") {
|
|
979
|
+
logger.output({ status: "already_unlocked", tokenAccount: e.tokenAccount.toBase58(), reason: e.reason });
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
if (e.status !== "can_unlock") {
|
|
983
|
+
logger.output({ status: e.status, code: e.code, reason: e.reason });
|
|
984
|
+
process.exitCode = 1;
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
const sig = await client.send(await client.gate.createAtaAndThaw(mint, owner));
|
|
988
|
+
logger.output({ status: "unlocked", tokenAccount: e.tokenAccount.toBase58(), signature: sig, reason: e.reason });
|
|
989
|
+
});
|
|
990
|
+
program2.command("freeze-if-invalid").description("Freeze a token account permissionlessly if the policy flags its owner; sends nothing if it complies").requiredOption("--token-account <pubkey>", "Token account").action(async (opts) => {
|
|
991
|
+
const { client, logger } = ctx();
|
|
992
|
+
const r = await client.gate.freezeIfInvalid(pk(opts.tokenAccount, "--token-account"));
|
|
993
|
+
logger.output({ ...r, signature: r.signature ?? null });
|
|
994
|
+
});
|
|
995
|
+
const sasCmd = program2.command("sas").description("Solana Attestation Service: credentials, the KYC schema, attestations");
|
|
996
|
+
sasCmd.command("create-credential").description("Create a SAS credential you issue under (a self-issued test credential)").requiredOption("--name <name>", "Credential name (1-32 bytes)").option("--signer <pubkey...>", "Keys that may attest (default: you)").action(async (opts) => {
|
|
997
|
+
const { client, keypair, logger } = ctx();
|
|
998
|
+
const signers = opts.signer?.map((s) => new import_web32.PublicKey(s));
|
|
999
|
+
const { instruction, credential } = import_sdk2.sas.createCredentialIx({ payer: keypair.publicKey, authority: keypair.publicKey, name: opts.name, signers });
|
|
1000
|
+
const sig = await client.send([instruction]);
|
|
1001
|
+
logger.output({ credential: credential.toBase58(), signature: sig });
|
|
1002
|
+
});
|
|
1003
|
+
sasCmd.command("create-schema").description("Create the KYC schema ThawGate reads (kyc_level: u8, country: String) under your credential").requiredOption("--credential <pubkey>", "Credential").requiredOption("--name <name>", "Schema name (1-32 bytes)").option("--description <text>", "Description", "KYC level and country (ThawGate)").action(async (opts) => {
|
|
1004
|
+
const { client, keypair, logger } = ctx();
|
|
1005
|
+
const { instruction, schema } = import_sdk2.sas.createSchemaIx({
|
|
1006
|
+
payer: keypair.publicKey,
|
|
1007
|
+
authority: keypair.publicKey,
|
|
1008
|
+
credential: pk(opts.credential, "--credential"),
|
|
1009
|
+
name: opts.name,
|
|
1010
|
+
description: opts.description,
|
|
1011
|
+
layout: [...import_sdk2.sas.KYC_SCHEMA.layout],
|
|
1012
|
+
fieldNames: [...import_sdk2.sas.KYC_SCHEMA.fieldNames]
|
|
1013
|
+
});
|
|
1014
|
+
const sig = await client.send([instruction]);
|
|
1015
|
+
logger.output({ schema: schema.toBase58(), signature: sig });
|
|
1016
|
+
});
|
|
1017
|
+
sasCmd.command("attest").description("Attest a wallet's KYC (nonce = the wallet), as a signer of the credential").requiredOption("--credential <pubkey>", "Credential").requiredOption("--schema <pubkey>", "Schema (the KYC layout)").requiredOption("--wallet <pubkey>", "Holder wallet").requiredOption("--kyc-level <n>", "kyc_level, 0-255").option("--country <code>", "Country", "IN").option("--expiry-days <days>", "Days until it expires; 0 = never", "365").action(async (opts) => {
|
|
1018
|
+
const { client, keypair, logger } = ctx();
|
|
1019
|
+
const days = Number(opts.expiryDays);
|
|
1020
|
+
const expiry = days === 0 ? 0 : await client.clusterTime() + Math.round(days * 86400);
|
|
1021
|
+
const { instruction, attestation } = import_sdk2.sas.createAttestationIx({
|
|
1022
|
+
payer: keypair.publicKey,
|
|
1023
|
+
authority: keypair.publicKey,
|
|
1024
|
+
credential: pk(opts.credential, "--credential"),
|
|
1025
|
+
schema: pk(opts.schema, "--schema"),
|
|
1026
|
+
nonce: pk(opts.wallet, "--wallet"),
|
|
1027
|
+
data: import_sdk2.sas.encodeKycData({ kycLevel: Number(opts.kycLevel), country: opts.country }),
|
|
1028
|
+
expiry
|
|
1029
|
+
});
|
|
1030
|
+
const sig = await client.send([instruction]);
|
|
1031
|
+
logger.output({ attestation: attestation.toBase58(), expiry, signature: sig });
|
|
1032
|
+
});
|
|
1033
|
+
sasCmd.command("revoke").description("Close a wallet's attestation; under a SAS policy its accounts become freezable").requiredOption("--credential <pubkey>", "Credential").requiredOption("--schema <pubkey>", "Schema").requiredOption("--wallet <pubkey>", "Holder wallet").action(async (opts) => {
|
|
1034
|
+
const { client, keypair, logger } = ctx();
|
|
1035
|
+
const credential = pk(opts.credential, "--credential");
|
|
1036
|
+
const attestation = import_sdk2.sas.findAttestationPda(credential, pk(opts.schema, "--schema"), pk(opts.wallet, "--wallet"));
|
|
1037
|
+
const sig = await client.send([import_sdk2.sas.closeAttestationIx({ payer: keypair.publicKey, authority: keypair.publicKey, credential, attestation })]);
|
|
1038
|
+
logger.output({ attestation: attestation.toBase58(), signature: sig });
|
|
1039
|
+
});
|
|
1040
|
+
const allowlist = program2.command("allowlist").description("The sss-token issuer allowlist (MasterAuthority)");
|
|
1041
|
+
for (const [name, add] of [["add", true], ["remove", false]]) {
|
|
1042
|
+
allowlist.command(name).description(add ? "Add a wallet to the allowlist" : "Deactivate a wallet's allowlist entry").requiredOption("--mint <pubkey>", "Mint address").requiredOption("--wallet <pubkey>", "Wallet").action(async (opts) => {
|
|
1043
|
+
const { client, keypair, logger } = ctx();
|
|
1044
|
+
const compliance = client.compliance(pk(opts.mint, "--mint"));
|
|
1045
|
+
const wallet = pk(opts.wallet, "--wallet");
|
|
1046
|
+
const ixs = add ? await compliance.addToAllowlist(keypair.publicKey, wallet) : await compliance.removeFromAllowlist(keypair.publicKey, wallet);
|
|
1047
|
+
logger.output({ signature: await client.send(ixs) });
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
const reservesCmd = program2.command("reserves").description("A mint's reserve attestation: mint_tokens refuses supply past it");
|
|
1051
|
+
reservesCmd.command("post").description("Post the mint's reserves as its attestor (--keypair). Minting may rely on them until as-of + max staleness").requiredOption("--mint <pubkey>", "Mint address").requiredOption("--amount <base units>", "Reserves in the mint's base units (1.00 of a 6-decimal coin = 1000000)").option("--report-uri <uri>", "Link to the reserve report, max 200 bytes (default: the one posted last)").option("--as-of <unix seconds>", "When the reserves were measured (default: the cluster's time now)").action(async (opts) => {
|
|
1052
|
+
const { client, keypair, logger } = ctx();
|
|
1053
|
+
const mint = pk(opts.mint, "--mint");
|
|
1054
|
+
const amount = u64Arg(opts.amount, "--amount");
|
|
1055
|
+
const reserves = client.reserves(mint);
|
|
1056
|
+
const current = await reserves.fetch();
|
|
1057
|
+
if (!current) throw new Error(`${mint.toBase58()} has no reserve attestation: its MasterAuthority sets the attestor first (set_reserve_attestor)`);
|
|
1058
|
+
if (!current.attestor.equals(keypair.publicKey)) {
|
|
1059
|
+
throw new Error(`only the mint's attestor ${current.attestor.toBase58()} can post its reserves (--keypair is ${keypair.publicKey.toBase58()})`);
|
|
1060
|
+
}
|
|
1061
|
+
const asOf = opts.asOf !== void 0 ? u64Arg(opts.asOf, "--as-of").toNumber() : await client.clusterTime();
|
|
1062
|
+
if (asOf < current.asOf.toNumber()) throw new Error(`--as-of ${asOf} is older than the posted as_of ${current.asOf.toString()}`);
|
|
1063
|
+
const reportUri = opts.reportUri ?? current.reportUri;
|
|
1064
|
+
const signature = await client.send(await reserves.attestReserves(keypair.publicKey, amount, asOf, reportUri));
|
|
1065
|
+
const freshUntil = asOf + current.maxStaleness.toNumber();
|
|
1066
|
+
logger.output({
|
|
1067
|
+
signature,
|
|
1068
|
+
mint: mint.toBase58(),
|
|
1069
|
+
attestor: keypair.publicKey.toBase58(),
|
|
1070
|
+
reserves: amount.toString(),
|
|
1071
|
+
asOf,
|
|
1072
|
+
freshUntil,
|
|
1073
|
+
freshUntilUtc: new Date(freshUntil * 1e3).toISOString(),
|
|
1074
|
+
reportUri
|
|
1075
|
+
});
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// src/index.ts
|
|
1080
|
+
var program = new import_commander.Command();
|
|
1081
|
+
program.name("thawgate").description("ThawGate CLI: Token ACL gate policies, SSS-ACL stablecoins and SAS credentials (devnet IDs by default)").version("0.1.0").option("--rpc-url <url>", "Solana RPC endpoint URL (or SSS_RPC_URL; default devnet)").option("--commitment <level>", "Commitment level", "confirmed").option("--keypair <path>", "Path to keypair file (or SSS_KEYPAIR_PATH; default ~/.config/solana/id.json)").option("--json", "Output in JSON format (progress lines go to stderr)").option("--verbose", "Enable verbose logging");
|
|
1082
|
+
registerGateCommands(program);
|
|
1083
|
+
registerCommands(program);
|
|
1084
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
1085
|
+
console.error("Error:", err.message);
|
|
1086
|
+
const logs = err?.logs ?? err?.transactionLogs;
|
|
1087
|
+
const decision = logs ? (0, import_sdk3.parseGateLogs)(logs).decision : null;
|
|
1088
|
+
if (decision) console.error(`Gate: TG:${decision.kind}:${decision.code}`);
|
|
1089
|
+
if (logs && program.opts().verbose) console.error(logs.join("\n"));
|
|
1090
|
+
process.exit(1);
|
|
1091
|
+
});
|
|
1092
|
+
//# sourceMappingURL=index.js.map
|