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.
- package/README.md +74 -26
- package/dist/commands/account.js +262 -63
- package/dist/commands/chain.js +35 -0
- package/dist/commands/checkout.js +137 -16
- package/dist/commands/clone.js +64 -7
- package/dist/commands/commit.js +4 -16
- package/dist/commands/fetch.js +76 -4
- package/dist/commands/init.js +75 -13
- package/dist/commands/invite.js +68 -53
- package/dist/commands/ipfsCar.js +98 -0
- package/dist/commands/lighthouse.js +97 -0
- package/dist/commands/lighthouseDownload.js +58 -0
- package/dist/commands/lighthousePin.js +62 -0
- package/dist/commands/pull.js +2 -1
- package/dist/commands/push.js +224 -8
- package/dist/commands/registerCommands.js +108 -2
- package/dist/commands/remove-user.js +46 -0
- package/dist/commands/removeUser.js +30 -1
- package/dist/index.js +15 -0
- package/dist/lib/chain.js +72 -0
- package/dist/lib/crypto.js +62 -0
- package/dist/lib/evmClone.js +255 -0
- package/dist/lib/evmKeys.js +218 -0
- package/dist/lib/evmProvider.js +88 -0
- package/dist/lib/evmRepo.js +192 -0
- package/dist/lib/ipfsCar.js +132 -0
- package/dist/lib/keccak.js +125 -0
- package/dist/lib/keys.js +102 -37
- package/dist/lib/lighthouse.js +661 -0
- package/dist/lib/lit.js +165 -0
- package/dist/lib/manifest.js +22 -4
- package/dist/lib/repo.js +94 -0
- package/dist/lib/schema.js +26 -6
- package/dist/lib/walrus.js +11 -1
- package/package.json +18 -2
|
@@ -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
|
+
}
|