withub-cli 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.AUTH_TAG_LENGTH = exports.IV_LENGTH = exports.KEY_LENGTH = exports.ALGORITHM = void 0;
7
+ exports.generateSessionKey = generateSessionKey;
8
+ exports.encryptBuffer = encryptBuffer;
9
+ exports.decryptBuffer = decryptBuffer;
10
+ const crypto_1 = __importDefault(require("crypto"));
11
+ exports.ALGORITHM = 'aes-256-gcm';
12
+ exports.KEY_LENGTH = 32; // 256 bits
13
+ exports.IV_LENGTH = 12; // 96 bits for GCM
14
+ exports.AUTH_TAG_LENGTH = 16; // 128 bits
15
+ /**
16
+ * Generates a random 32-byte session key for AES-256-GCM.
17
+ */
18
+ function generateSessionKey() {
19
+ return crypto_1.default.randomBytes(exports.KEY_LENGTH);
20
+ }
21
+ /**
22
+ * Encrypts a buffer using AES-256-GCM.
23
+ * @param data The data to encrypt
24
+ * @param key The 32-byte session key
25
+ * @returns EncryptedData object containing ciphertext, iv, authTag, and algorithm
26
+ */
27
+ function encryptBuffer(data, key) {
28
+ if (key.length !== exports.KEY_LENGTH) {
29
+ throw new Error(`Invalid key length. Expected ${exports.KEY_LENGTH} bytes, got ${key.length}`);
30
+ }
31
+ const iv = crypto_1.default.randomBytes(exports.IV_LENGTH);
32
+ const cipher = crypto_1.default.createCipheriv(exports.ALGORITHM, key, iv);
33
+ const ciphertext = Buffer.concat([
34
+ cipher.update(data),
35
+ cipher.final()
36
+ ]);
37
+ const authTag = cipher.getAuthTag();
38
+ return {
39
+ ciphertext,
40
+ iv,
41
+ authTag,
42
+ algorithm: exports.ALGORITHM,
43
+ };
44
+ }
45
+ /**
46
+ * Decrypts a buffer using AES-256-GCM.
47
+ * @param encryptedData EncryptedData object or parts
48
+ * @param key The 32-byte session key
49
+ * @returns Decrypted buffer
50
+ */
51
+ function decryptBuffer(encryptedData, key) {
52
+ if (key.length !== exports.KEY_LENGTH) {
53
+ throw new Error(`Invalid key length. Expected ${exports.KEY_LENGTH} bytes, got ${key.length}`);
54
+ }
55
+ const decipher = crypto_1.default.createDecipheriv(exports.ALGORITHM, key, encryptedData.iv);
56
+ decipher.setAuthTag(encryptedData.authTag);
57
+ const decrypted = Buffer.concat([
58
+ decipher.update(encryptedData.ciphertext),
59
+ decipher.final()
60
+ ]);
61
+ return decrypted;
62
+ }
@@ -0,0 +1,255 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.cloneFromMantle = cloneFromMantle;
7
+ exports.downloadCommitChainMantle = downloadCommitChainMantle;
8
+ const promises_1 = __importDefault(require("fs/promises"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const ui_1 = require("./ui");
11
+ const evmRepo_1 = require("./evmRepo");
12
+ const evmProvider_1 = require("./evmProvider");
13
+ const lit_1 = require("./lit");
14
+ const lighthouse_1 = require("./lighthouse");
15
+ const schema_1 = require("./schema");
16
+ const crypto_1 = require("./crypto");
17
+ const fs_1 = require("./fs");
18
+ const repo_1 = require("./repo");
19
+ async function cloneFromMantle(repoIdStr, destDir = process.cwd()) {
20
+ // 1. Resolve RepoId
21
+ let repoId;
22
+ let repoIdHex;
23
+ if (repoIdStr.startsWith('mantle:')) {
24
+ repoIdStr = repoIdStr.replace('mantle:', '');
25
+ }
26
+ if (repoIdStr.startsWith('0x')) {
27
+ repoId = BigInt(repoIdStr);
28
+ repoIdHex = repoIdStr;
29
+ }
30
+ else {
31
+ repoId = BigInt(repoIdStr);
32
+ repoIdHex = (0, evmRepo_1.formatRepoId)(repoId);
33
+ }
34
+ // eslint-disable-next-line no-console
35
+ console.log(ui_1.colors.header(`Cloning repository ${repoIdHex} from Mantle Testnet...`));
36
+ // 2. Fetch On-Chain State
37
+ const signerCtx = await (0, evmProvider_1.loadMantleSigner)();
38
+ const repoService = new evmRepo_1.EvmRepoService(signerCtx);
39
+ const head = await repoService.getRepoState(repoId);
40
+ if (!head || !head.headCommit) {
41
+ throw new Error(`Repository ${repoIdHex} has no head commit on-chain.`);
42
+ }
43
+ // eslint-disable-next-line no-console
44
+ console.log(ui_1.colors.cyan(`Fetching head commit ${head.headCommit}...`));
45
+ // 3. Download Commit & Manifest
46
+ const commitBuf = await downloadBuffer(head.headCommit);
47
+ const commit = JSON.parse(commitBuf.toString('utf8'));
48
+ // Support both fields
49
+ const manifestCid = commit.tree.manifest_cid || commit.tree.manifest_id;
50
+ if (!manifestCid) {
51
+ throw new Error('Connect object missing manifest_cid');
52
+ }
53
+ // eslint-disable-next-line no-console
54
+ console.log(ui_1.colors.cyan(`Fetching manifest ${manifestCid}...`));
55
+ const manifestBuf = await downloadBuffer(manifestCid);
56
+ const manifestValues = JSON.parse(manifestBuf.toString('utf8'));
57
+ const manifest = schema_1.ManifestSchema.parse(manifestValues);
58
+ // 4. Prepare Layout
59
+ const witPath = await ensureEvmLayout(destDir, repoIdHex);
60
+ // Persist Head
61
+ await promises_1.default.writeFile(path_1.default.join(witPath, 'HEAD'), 'refs/heads/main\n', 'utf8');
62
+ const headRefPath = path_1.default.join(witPath, 'refs', 'heads', 'main');
63
+ await promises_1.default.mkdir(path_1.default.dirname(headRefPath), { recursive: true });
64
+ await promises_1.default.writeFile(headRefPath, `${head.headCommit}\n`, 'utf8');
65
+ // 5. Restore Files (Decrypt loop)
66
+ const entries = Object.entries(manifest.files);
67
+ // eslint-disable-next-line no-console
68
+ console.log(ui_1.colors.cyan(`Restoring ${entries.length} files...`));
69
+ const litService = new lit_1.LitService();
70
+ let authSig = null;
71
+ let decryptionCount = 0;
72
+ const index = {};
73
+ for (const [rel, meta] of entries) {
74
+ const fileCid = meta.cid; // Use CID stored in manifest, not the content hash
75
+ // Note: If using Walrus, meta.hash is blob ID. For IPFS/Lighthouse, it is CID.
76
+ // Our Push logic uploaded to Lighthouse and stored CID in meta.hash?
77
+ // Let's verify Push logic:
78
+ // "const uploadRes = await uploadBufferToLighthouse(contentToUpload);"
79
+ // "manifestFiles[rel] = { hash: uploadRes.cid, ... }"
80
+ // Yes, meta.hash is CID.
81
+ const fileBuf = await downloadBuffer(fileCid);
82
+ let plain = Buffer.from(fileBuf);
83
+ if (meta.enc) {
84
+ decryptionCount++;
85
+ // Lazy init AuthSig just once
86
+ if (!authSig) {
87
+ // eslint-disable-next-line no-console
88
+ console.log(ui_1.colors.gray(` Generating SIWE AuthSig for decryption...`));
89
+ authSig = await litService.getAuthSig(signerCtx.signer);
90
+ }
91
+ const encMeta = meta.enc;
92
+ // Expected structure from push.ts:
93
+ // { alg: 'lit-aes-256-gcm', lit_encrypted_key, access_control_conditions, iv, tag, lit_hash }
94
+ if (encMeta.alg === 'lit-aes-256-gcm') {
95
+ // eslint-disable-next-line no-console
96
+ console.log(ui_1.colors.gray(` Decrypting ${rel} (Lit Protocol)...`));
97
+ try {
98
+ const acc = encMeta.unified_access_control_conditions || encMeta.access_control_conditions;
99
+ const sessionKey = await litService.decryptSessionKey(encMeta.lit_encrypted_key, encMeta.lit_hash, acc, authSig);
100
+ // 2. Decrypt Content
101
+ plain = (0, crypto_1.decryptBuffer)({
102
+ ciphertext: plain,
103
+ iv: Buffer.from(encMeta.iv, 'hex'),
104
+ authTag: Buffer.from(encMeta.tag, 'hex'),
105
+ }, sessionKey);
106
+ }
107
+ catch (err) {
108
+ // Check for Lit Access Denied
109
+ const msg = err.message || '';
110
+ if (msg.includes('NodeAccessControlConditionsReturnedNotAuthorized') ||
111
+ msg.includes('not authorized') ||
112
+ (err.errorKind === 'Validation' && err.errorCode === 'NodeAccessControlConditionsReturnedNotAuthorized')) {
113
+ // eslint-disable-next-line no-console
114
+ console.log(ui_1.colors.red(`❌ Access Denied: You do not have permission to decrypt this file.`));
115
+ // eslint-disable-next-line no-console
116
+ console.log(ui_1.colors.yellow(` Please contact the repository owner to add your address to the allowlist.`));
117
+ // eslint-disable-next-line no-console
118
+ console.log(ui_1.colors.gray(` Repository ID: ${repoIdHex}`));
119
+ process.exit(1);
120
+ }
121
+ // eslint-disable-next-line no-console
122
+ console.error(ui_1.colors.red(` ❌ Failed to decrypt ${rel}: ${msg}`));
123
+ process.exit(1);
124
+ }
125
+ }
126
+ }
127
+ // Write to disk
128
+ const absPath = path_1.default.join(destDir, rel);
129
+ await (0, fs_1.ensureDirForFile)(absPath);
130
+ await promises_1.default.writeFile(absPath, plain);
131
+ const mode = parseInt(meta.mode, 8) & 0o777;
132
+ await promises_1.default.chmod(absPath, mode);
133
+ index[rel] = { hash: meta.hash, size: meta.size, mode: meta.mode, mtime: meta.mtime };
134
+ }
135
+ // Write Index
136
+ await (0, fs_1.writeIndex)(path_1.default.join(witPath, 'index'), index);
137
+ // Write Remote State partial
138
+ const remoteState = {
139
+ repo_id: repoIdHex,
140
+ head_commit: head.headCommit,
141
+ head_manifest: manifestCid,
142
+ head_quilt: '', // Not used for EVM currently
143
+ version: Number(head.version),
144
+ };
145
+ await (0, repo_1.writeRemoteState)(witPath, remoteState);
146
+ await (0, repo_1.writeRemoteRef)(witPath, head.headCommit);
147
+ // eslint-disable-next-line no-console
148
+ console.log(ui_1.colors.green(`Clone complete.`));
149
+ if (decryptionCount > 0) {
150
+ // eslint-disable-next-line no-console
151
+ console.log(ui_1.colors.green(`Successfully decrypted ${decryptionCount} private files.`));
152
+ }
153
+ await litService.disconnect();
154
+ // 6. Download History (Commit Chain)
155
+ // We need to initialize the commit map first
156
+ const map = {}; // fresh clone
157
+ await downloadCommitChainMantle(head.headCommit, witPath, map);
158
+ // Write map
159
+ const mapFile = path_1.default.join(witPath, 'objects', 'maps', 'commit_id_map.json');
160
+ await (0, fs_1.ensureDirForFile)(mapFile);
161
+ await promises_1.default.writeFile(mapFile, JSON.stringify(map, null, 2) + '\n', 'utf8');
162
+ }
163
+ async function downloadBuffer(cid) {
164
+ const res = await (0, lighthouse_1.downloadFromLighthouseGateway)(cid, { verify: false });
165
+ // Check if it's an HTML directory listing (Lighthouse/IPFS gateway behavior)
166
+ // We sniff the first few bytes or check strings.
167
+ const preview = Buffer.from(res.bytes.slice(0, 500)).toString('utf8');
168
+ if (preview.trim().startsWith('<!DOCTYPE html') || preview.includes('Index of /ipfs/')) {
169
+ // Parse for the first file link
170
+ // Format: <a href="/ipfs/{cid}/{filename}">
171
+ // We look for the specific CID followed by a slash and a filename.
172
+ // Regex: href="/ipfs/CID/([^"]+)"
173
+ const regex = new RegExp(`href="/ipfs/${cid}/([^"]+)"`);
174
+ const match = preview.match(regex) || Buffer.from(res.bytes).toString('utf8').match(regex);
175
+ if (match && match[1]) {
176
+ const filename = match[1];
177
+ // Recurse with the path
178
+ // Note: We bypass verification because we are modifying the CID/path
179
+ return downloadBuffer(`${cid}/${filename}`);
180
+ }
181
+ }
182
+ return Buffer.from(res.bytes);
183
+ }
184
+ async function ensureEvmLayout(cwd, repoId) {
185
+ const witPath = path_1.default.join(cwd, '.wit');
186
+ await promises_1.default.mkdir(witPath, { recursive: true });
187
+ // Create Config
188
+ const cfg = {
189
+ repo_name: repoId,
190
+ repo_id: repoId,
191
+ chain: 'mantle',
192
+ chains: {
193
+ mantle: {
194
+ storage_backend: 'ipfs',
195
+ author: 'unknown',
196
+ }
197
+ },
198
+ network: 'testnet',
199
+ created_at: new Date().toISOString()
200
+ };
201
+ await (0, repo_1.writeRepoConfig)(witPath, cfg);
202
+ return witPath;
203
+ }
204
+ async function downloadCommitChainMantle(startId, witPath, map) {
205
+ const seen = new Set();
206
+ let current = startId;
207
+ while (current && !seen.has(current)) {
208
+ seen.add(current);
209
+ // Check if we already have it locally
210
+ if (map[current]) {
211
+ break;
212
+ }
213
+ let commitBuf;
214
+ const cachedPath = path_1.default.join(witPath, 'objects', 'commits', `${idToFileName(current)}.json`);
215
+ try {
216
+ const raw = await promises_1.default.readFile(cachedPath, 'utf8');
217
+ commitBuf = Buffer.from(raw, 'utf8');
218
+ }
219
+ catch (e) {
220
+ // eslint-disable-next-line no-console
221
+ console.log(ui_1.colors.gray(`Downloading commit ${current}...`));
222
+ commitBuf = await downloadBuffer(current);
223
+ await (0, fs_1.ensureDirForFile)(cachedPath);
224
+ await promises_1.default.writeFile(cachedPath, commitBuf.toString('utf8'), 'utf8');
225
+ }
226
+ let commit;
227
+ try {
228
+ commit = JSON.parse(commitBuf.toString('utf8'));
229
+ }
230
+ catch {
231
+ throw new Error(`Invalid commit JSON for ${current}`);
232
+ }
233
+ map[current] = current;
234
+ // Also ensure manifest is present
235
+ const manifestCid = commit.tree.manifest_cid || commit.tree.manifest_id;
236
+ if (manifestCid) {
237
+ const mPath = path_1.default.join(witPath, 'objects', 'manifests', `${idToFileName(manifestCid)}.json`);
238
+ try {
239
+ await promises_1.default.access(mPath);
240
+ }
241
+ catch {
242
+ // eslint-disable-next-line no-console
243
+ console.log(ui_1.colors.gray(`Downloading manifest ${manifestCid}...`));
244
+ const mBuf = await downloadBuffer(manifestCid);
245
+ await (0, fs_1.ensureDirForFile)(mPath);
246
+ await promises_1.default.writeFile(mPath, mBuf.toString('utf8'), 'utf8');
247
+ }
248
+ }
249
+ current = commit.parent;
250
+ }
251
+ }
252
+ // Helper needed for ID mapping locally if not imported
253
+ function idToFileName(id) {
254
+ return id.replace(/\//g, '_').replace(/\+/g, '-');
255
+ }
@@ -0,0 +1,218 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.EVM_KEY_HOME = void 0;
7
+ exports.createEvmKey = createEvmKey;
8
+ exports.importEvmKey = importEvmKey;
9
+ exports.listEvmKeys = listEvmKeys;
10
+ exports.loadEvmKey = loadEvmKey;
11
+ exports.readActiveEvmAddress = readActiveEvmAddress;
12
+ exports.setActiveEvmAddress = setActiveEvmAddress;
13
+ exports.normalizeEvmAddress = normalizeEvmAddress;
14
+ exports.normalizeEvmAddressMaybe = normalizeEvmAddressMaybe;
15
+ const promises_1 = __importDefault(require("fs/promises"));
16
+ const os_1 = __importDefault(require("os"));
17
+ const path_1 = __importDefault(require("path"));
18
+ const ethers_1 = require("ethers");
19
+ exports.EVM_KEY_HOME = process.env.WIT_EVM_KEY_HOME || path_1.default.join(os_1.default.homedir(), '.wit', 'keys-evm');
20
+ const GLOBAL_CONFIG = path_1.default.join(os_1.default.homedir(), '.witconfig');
21
+ const EVM_CHAIN_ID = 'mantle';
22
+ async function createEvmKey(alias = 'default') {
23
+ const wallet = ethers_1.Wallet.createRandom();
24
+ const privateKey = wallet.privateKey;
25
+ const publicKey = wallet.signingKey.publicKey;
26
+ const address = normalizeEvmAddress(wallet.address);
27
+ const payload = {
28
+ scheme: 'SECP256K1',
29
+ chain: 'mantle',
30
+ privateKey,
31
+ publicKey,
32
+ address,
33
+ createdAt: new Date().toISOString(),
34
+ alias,
35
+ };
36
+ const file = keyPathFor(address);
37
+ await promises_1.default.mkdir(path_1.default.dirname(file), { recursive: true });
38
+ await promises_1.default.writeFile(file, JSON.stringify(payload, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 });
39
+ await setActiveEvmAddress(address, { alias, updateAuthorIfUnknown: true });
40
+ return { address, privateKey: payload.privateKey, publicKey: payload.publicKey, file };
41
+ }
42
+ async function importEvmKey(privateKeyHex, alias = 'default') {
43
+ const privateKey = normalizePrivateKey(privateKeyHex);
44
+ const wallet = new ethers_1.Wallet(privateKey);
45
+ const publicKey = wallet.signingKey.publicKey;
46
+ const address = normalizeEvmAddress(wallet.address);
47
+ const payload = {
48
+ scheme: 'SECP256K1',
49
+ chain: 'mantle',
50
+ privateKey,
51
+ publicKey,
52
+ address,
53
+ createdAt: new Date().toISOString(),
54
+ alias,
55
+ };
56
+ const file = keyPathFor(address);
57
+ await promises_1.default.mkdir(path_1.default.dirname(file), { recursive: true });
58
+ await promises_1.default.writeFile(file, JSON.stringify(payload, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 });
59
+ await setActiveEvmAddress(address, { alias, updateAuthorIfUnknown: true });
60
+ return { address, privateKey: payload.privateKey, publicKey: payload.publicKey, file };
61
+ }
62
+ async function listEvmKeys() {
63
+ let entries;
64
+ try {
65
+ entries = await promises_1.default.readdir(exports.EVM_KEY_HOME, { withFileTypes: true });
66
+ }
67
+ catch (err) {
68
+ if (err?.code === 'ENOENT')
69
+ return [];
70
+ throw err;
71
+ }
72
+ const keys = [];
73
+ for (const entry of entries) {
74
+ if (!entry.isFile() || !entry.name.endsWith('.key'))
75
+ continue;
76
+ const file = path_1.default.join(exports.EVM_KEY_HOME, entry.name);
77
+ try {
78
+ const parsed = await readKeyFile(file);
79
+ if (parsed.chain && parsed.chain !== 'mantle')
80
+ continue;
81
+ if (parsed.scheme !== 'SECP256K1')
82
+ continue;
83
+ keys.push({
84
+ address: normalizeEvmAddress(parsed.address),
85
+ alias: parsed.alias,
86
+ file,
87
+ createdAt: parsed.createdAt,
88
+ });
89
+ }
90
+ catch (err) {
91
+ // eslint-disable-next-line no-console
92
+ console.warn(`Skipping key ${file}: ${err.message}`);
93
+ }
94
+ }
95
+ return keys.sort((a, b) => (a.createdAt || '').localeCompare(b.createdAt || ''));
96
+ }
97
+ async function loadEvmKey(address) {
98
+ const resolvedAddress = address ? normalizeEvmAddress(address) : await readActiveEvmAddress();
99
+ if (!resolvedAddress) {
100
+ throw new Error('No active EVM address configured. Generate a key with `wit account generate` or set one with `wit account use`.');
101
+ }
102
+ const file = await findKeyFile(resolvedAddress);
103
+ if (!file) {
104
+ throw new Error(`Key file not found for address ${resolvedAddress}. Generate one with \`wit account generate\`.`);
105
+ }
106
+ const parsed = await readKeyFile(file);
107
+ return {
108
+ address: normalizeEvmAddress(parsed.address),
109
+ privateKey: parsed.privateKey,
110
+ publicKey: parsed.publicKey,
111
+ file,
112
+ };
113
+ }
114
+ async function readActiveEvmAddress() {
115
+ const cfg = await readGlobalConfig();
116
+ const fromMap = cfg.active_addresses?.[EVM_CHAIN_ID];
117
+ const normalized = normalizeEvmAddressMaybe(fromMap);
118
+ if (normalized)
119
+ return normalized;
120
+ const fromAuthor = cfg.authors?.[EVM_CHAIN_ID];
121
+ return normalizeEvmAddressMaybe(fromAuthor);
122
+ }
123
+ async function setActiveEvmAddress(address, opts) {
124
+ const normalized = normalizeEvmAddress(address);
125
+ await upsertGlobalConfig((cfg) => {
126
+ const next = { ...cfg };
127
+ if (!next.active_addresses)
128
+ next.active_addresses = {};
129
+ next.active_addresses[EVM_CHAIN_ID] = normalized;
130
+ if (opts?.alias) {
131
+ if (!next.key_aliases)
132
+ next.key_aliases = {};
133
+ next.key_aliases[EVM_CHAIN_ID] = opts.alias;
134
+ }
135
+ if (opts?.updateAuthorIfUnknown) {
136
+ if (!next.authors)
137
+ next.authors = {};
138
+ if (!next.authors[EVM_CHAIN_ID] || next.authors[EVM_CHAIN_ID] === 'unknown') {
139
+ next.authors[EVM_CHAIN_ID] = normalized;
140
+ }
141
+ }
142
+ return next;
143
+ });
144
+ }
145
+ function normalizeEvmAddress(address) {
146
+ if (!address) {
147
+ throw new Error('Invalid EVM address.');
148
+ }
149
+ try {
150
+ return (0, ethers_1.getAddress)(withHexPrefix(address));
151
+ }
152
+ catch {
153
+ throw new Error(`Invalid EVM address "${address}".`);
154
+ }
155
+ }
156
+ function normalizeEvmAddressMaybe(address) {
157
+ if (!address)
158
+ return null;
159
+ try {
160
+ return (0, ethers_1.getAddress)(withHexPrefix(address));
161
+ }
162
+ catch {
163
+ return null;
164
+ }
165
+ }
166
+ function keyPathFor(address) {
167
+ return path_1.default.join(exports.EVM_KEY_HOME, `${normalizeEvmAddress(address)}.key`);
168
+ }
169
+ async function findKeyFile(address) {
170
+ const file = keyPathFor(address);
171
+ try {
172
+ await promises_1.default.access(file);
173
+ return file;
174
+ }
175
+ catch (err) {
176
+ if (err?.code !== 'ENOENT')
177
+ throw err;
178
+ }
179
+ return null;
180
+ }
181
+ function normalizePrivateKey(input) {
182
+ const trimmed = input.trim();
183
+ const hex = trimmed.startsWith('0x') ? trimmed : `0x${trimmed}`;
184
+ if (!(0, ethers_1.isHexString)(hex, 32)) {
185
+ throw new Error('Invalid private key. Expected 32-byte hex.');
186
+ }
187
+ return hex;
188
+ }
189
+ function withHexPrefix(value) {
190
+ const trimmed = value.trim();
191
+ return trimmed.startsWith('0x') ? trimmed : `0x${trimmed}`;
192
+ }
193
+ async function readKeyFile(file) {
194
+ const raw = await promises_1.default.readFile(file, 'utf8');
195
+ const parsed = JSON.parse(raw);
196
+ if (!parsed.privateKey) {
197
+ throw new Error(`Missing privateKey in ${file}`);
198
+ }
199
+ return parsed;
200
+ }
201
+ async function readGlobalConfig() {
202
+ try {
203
+ const raw = await promises_1.default.readFile(GLOBAL_CONFIG, 'utf8');
204
+ return JSON.parse(raw);
205
+ }
206
+ catch (err) {
207
+ if (err?.code === 'ENOENT')
208
+ return {};
209
+ // eslint-disable-next-line no-console
210
+ console.warn(`Warning: could not read ${GLOBAL_CONFIG}: ${err.message}`);
211
+ return {};
212
+ }
213
+ }
214
+ async function upsertGlobalConfig(mutator) {
215
+ const cfg = await readGlobalConfig();
216
+ const next = mutator(cfg);
217
+ await promises_1.default.writeFile(GLOBAL_CONFIG, JSON.stringify(next, null, 2) + '\n', 'utf8');
218
+ }
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveMantleNetwork = resolveMantleNetwork;
4
+ exports.resolveMantleRpcUrl = resolveMantleRpcUrl;
5
+ exports.resolveMantleChainId = resolveMantleChainId;
6
+ exports.resolveMantleConfig = resolveMantleConfig;
7
+ exports.createMantleProvider = createMantleProvider;
8
+ exports.loadMantleSigner = loadMantleSigner;
9
+ exports.resolveEvmTxContext = resolveEvmTxContext;
10
+ const ethers_1 = require("ethers");
11
+ const evmKeys_1 = require("./evmKeys");
12
+ const DEFAULT_RPC_URLS = {
13
+ testnet: 'https://rpc.sepolia.mantle.xyz',
14
+ mainnet: 'https://rpc.mantle.xyz',
15
+ };
16
+ const DEFAULT_CHAIN_IDS = {
17
+ testnet: 5003,
18
+ mainnet: 5000,
19
+ };
20
+ const DEFAULT_CHAIN_NAMES = {
21
+ testnet: 'mantle-sepolia',
22
+ mainnet: 'mantle',
23
+ };
24
+ function resolveMantleNetwork(input) {
25
+ return input === 'testnet' ? 'testnet' : 'mainnet';
26
+ }
27
+ function resolveMantleRpcUrl(network) {
28
+ return (process.env.WIT_MANTLE_RPC_URL ||
29
+ process.env.MANTLE_RPC_URL ||
30
+ DEFAULT_RPC_URLS[network]);
31
+ }
32
+ function resolveMantleChainId(network) {
33
+ const override = process.env.WIT_MANTLE_CHAIN_ID || process.env.MANTLE_CHAIN_ID;
34
+ if (override) {
35
+ const parsed = Number(override);
36
+ if (!Number.isNaN(parsed) && parsed > 0)
37
+ return parsed;
38
+ }
39
+ return DEFAULT_CHAIN_IDS[network];
40
+ }
41
+ function resolveMantleConfig(networkInput) {
42
+ const network = resolveMantleNetwork(networkInput);
43
+ return {
44
+ network,
45
+ chainId: resolveMantleChainId(network),
46
+ chainName: DEFAULT_CHAIN_NAMES[network],
47
+ rpcUrl: resolveMantleRpcUrl(network),
48
+ };
49
+ }
50
+ function createMantleProvider(networkInput) {
51
+ const config = resolveMantleConfig(networkInput);
52
+ return new ethers_1.JsonRpcProvider(config.rpcUrl, {
53
+ name: config.chainName,
54
+ chainId: config.chainId,
55
+ });
56
+ }
57
+ async function loadMantleSigner(networkInput, address) {
58
+ const config = resolveMantleConfig(networkInput);
59
+ const provider = new ethers_1.JsonRpcProvider(config.rpcUrl, {
60
+ name: config.chainName,
61
+ chainId: config.chainId,
62
+ });
63
+ const key = await (0, evmKeys_1.loadEvmKey)(address);
64
+ const signer = new ethers_1.Wallet(key.privateKey, provider);
65
+ return {
66
+ provider,
67
+ signer,
68
+ address: signer.address,
69
+ config,
70
+ };
71
+ }
72
+ async function resolveEvmTxContext(provider, address, tx) {
73
+ const [network, nonce, feeData] = await Promise.all([
74
+ provider.getNetwork(),
75
+ provider.getTransactionCount(address, 'pending'),
76
+ provider.getFeeData(),
77
+ ]);
78
+ let gasLimit;
79
+ if (tx) {
80
+ gasLimit = await provider.estimateGas({ ...tx, from: address });
81
+ }
82
+ return {
83
+ chainId: Number(network.chainId),
84
+ nonce,
85
+ feeData,
86
+ gasLimit,
87
+ };
88
+ }