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,192 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EvmRepoService = void 0;
4
+ exports.formatRepoId = formatRepoId;
5
+ const ethers_1 = require("ethers");
6
+ const ui_1 = require("./ui");
7
+ const WIT_POLY_REPO_ABI = [
8
+ "function createRepo(string name, string description, bool isPrivate) public returns (uint256)",
9
+ "event RepositoryCreated(uint256 indexed repoId, address indexed owner, string name)",
10
+ "function updateHead(uint256 repoId, string newCommitCid, string newManifestCid, string newQuiltId, string newRootHash, uint64 expectedVersion, string parentCommitCid) public",
11
+ "function hasAccess(uint256 repoId, address user) public view returns (bool)",
12
+ "function addCollaborator(uint256 repoId, address user) public",
13
+ "function removeCollaborator(uint256 repoId, address user) public",
14
+ "event CollaboratorAdded(uint256 indexed repoId, address indexed user)",
15
+ "event CollaboratorRemoved(uint256 indexed repoId, address indexed user)",
16
+ "function repositories(uint256) public view returns (uint256 id, string name, string description, bool isPrivate, address owner, uint64 version, string headCommitCid, string headManifestCid, string headQuiltId, string rootHash, string parentCommitCid)"
17
+ ];
18
+ // Deployed Proxy Address on Mantle Sepolia (5003)
19
+ const WIT_CONTRACT_ADDRESS_MANTLE_SEPOLIA = '0x5996bDBfF0818F948781A27fdEfABa3e608d1506';
20
+ // Deployed Proxy Address on Mantle Mainnet (5000)
21
+ const WIT_CONTRACT_ADDRESS_MANTLE_MAINNET = '0xbc89b2F377386A46c20E09E02d83A8479bFDc203';
22
+ class EvmRepoService {
23
+ constructor(signerCtx) {
24
+ this.signerCtx = signerCtx;
25
+ const address = resolveContractAddress(signerCtx.config.chainId);
26
+ this.contract = new ethers_1.Contract(address, WIT_POLY_REPO_ABI, signerCtx.signer);
27
+ }
28
+ async createRepo(name, description, isPrivate) {
29
+ console.log(ui_1.colors.gray(`Creating repo "${name}" on contract ${this.contract.target}...`));
30
+ try {
31
+ const tx = await this.contract.createRepo(name, description, isPrivate);
32
+ console.log(ui_1.colors.gray(`Tx sent: ${getExplorerLink(tx.hash)}`));
33
+ const receipt = await tx.wait();
34
+ // Parse logs to find RepositoryCreated
35
+ const event = receipt.logs
36
+ .map(log => {
37
+ try {
38
+ return this.contract.interface.parseLog(log);
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ })
44
+ .find(parsed => parsed?.name === 'RepositoryCreated');
45
+ if (!event) {
46
+ throw new Error(`RepositoryCreated event not found in tx ${tx.hash}`);
47
+ }
48
+ return event.args[0];
49
+ }
50
+ catch (err) {
51
+ await this.handleTxError(err);
52
+ throw err;
53
+ }
54
+ }
55
+ async updateHead(repoId, commitCid, manifestCid, snapshotId, rootHash, expectedVersion, parentCommitCid) {
56
+ const parent = parentCommitCid || "";
57
+ // Ensure rootHash is bytes32?? Contract says string.
58
+ // Wait, Contract definition: rootHash is string (based on earlier ABI thought, but let's check solidity)
59
+ // Checking WitPolyRepo.sol: `string rootHash` (line 19 struct).
60
+ // Yes, updateHead arg 5 is string newRootHash.
61
+ console.log(ui_1.colors.gray(`Updating head for repo ${formatRepoId(repoId)} to version ${expectedVersion + 1n}...`));
62
+ try {
63
+ const tx = await this.contract.updateHead(repoId, commitCid, manifestCid, snapshotId, rootHash, expectedVersion, parent);
64
+ console.log(ui_1.colors.gray(`Tx sent: ${getExplorerLink(tx.hash)}`));
65
+ await tx.wait();
66
+ }
67
+ catch (err) {
68
+ await this.handleTxError(err);
69
+ throw err;
70
+ }
71
+ }
72
+ async addCollaborator(repoId, user) {
73
+ console.log(ui_1.colors.gray(`Adding collaborator ${user} to repo ${formatRepoId(repoId)}...`));
74
+ try {
75
+ const tx = await this.contract.addCollaborator(repoId, user);
76
+ console.log(ui_1.colors.gray(`Tx sent: ${getExplorerLink(tx.hash)}`));
77
+ await tx.wait();
78
+ // eslint-disable-next-line no-console
79
+ console.log(ui_1.colors.green(`✅ Collaborator ${user} added successfully.`));
80
+ }
81
+ catch (err) {
82
+ await this.handleTxError(err);
83
+ throw err;
84
+ }
85
+ }
86
+ async removeCollaborator(repoId, user) {
87
+ console.log(ui_1.colors.gray(`Removing collaborator ${user} from repo ${formatRepoId(repoId)}...`));
88
+ try {
89
+ const tx = await this.contract.removeCollaborator(repoId, user);
90
+ console.log(ui_1.colors.gray(`Tx sent: ${getExplorerLink(tx.hash)}`));
91
+ await tx.wait();
92
+ // eslint-disable-next-line no-console
93
+ console.log(ui_1.colors.green(`✅ Collaborator ${user} removed successfully.`));
94
+ }
95
+ catch (err) {
96
+ await this.handleTxError(err);
97
+ throw err;
98
+ }
99
+ }
100
+ async handleTxError(err) {
101
+ const msg = (err?.message || '').toLowerCase();
102
+ // Check for common out of gas / insufficient funds errors
103
+ if (msg.includes('insufficient funds') || msg.includes('gas') || msg.includes('exceeds balance')) {
104
+ try {
105
+ const bal = await this.signerCtx.provider.getBalance(this.signerCtx.address);
106
+ const isMantleMainnet = this.signerCtx.config.chainId === 5000;
107
+ const symbol = 'MNT';
108
+ // eslint-disable-next-line no-console
109
+ console.log(ui_1.colors.red(`\n❌ Transaction failed: Insufficient funds.`));
110
+ // eslint-disable-next-line no-console
111
+ console.log(ui_1.colors.red(` Wallet: ${this.signerCtx.address}`));
112
+ // eslint-disable-next-line no-console
113
+ console.log(ui_1.colors.red(` Balance: ${(0, ethers_1.formatEther)(bal)} ${symbol}`));
114
+ if (isMantleMainnet && bal === 0n) {
115
+ // eslint-disable-next-line no-console
116
+ console.log(ui_1.colors.yellow(`\n👉 This is Mantle Mainnet. You need real ${symbol} tokens to pay for gas.`));
117
+ // eslint-disable-next-line no-console
118
+ console.log(ui_1.colors.yellow(` Please fund your wallet: ${this.signerCtx.address}`));
119
+ }
120
+ else {
121
+ // eslint-disable-next-line no-console
122
+ console.log(ui_1.colors.yellow(`\n👉 Please ensure you have enough ${symbol} for gas fees.`));
123
+ }
124
+ process.exit(1);
125
+ }
126
+ catch (inner) {
127
+ // If checking balance fails, just let original error propagate
128
+ }
129
+ }
130
+ }
131
+ async getRepoState(repoId) {
132
+ const r = await this.contract.repositories(repoId);
133
+ // r is Result array: [id, name, description, isPrivate, owner, version, headCommitCid, headManifestCid, headQuiltId, rootHash, parentCommitCid]
134
+ // The ABI definition above maps names to indices if structured, or we access by index/name.
135
+ // Ethers v6 Result object supports name access.
136
+ // Safety check: if id is 0, it doesn't exist
137
+ if (r.id === 0n) {
138
+ throw new Error(`Repository ${repoId} not found on chain.`);
139
+ }
140
+ return {
141
+ id: r.id,
142
+ name: r.name,
143
+ description: r.description,
144
+ isPrivate: r.isPrivate,
145
+ owner: r.owner,
146
+ version: r.version,
147
+ headCommit: r.headCommitCid,
148
+ headManifest: r.headManifestCid,
149
+ headSnapshot: r.headQuiltId,
150
+ rootHash: r.rootHash,
151
+ parentCommit: r.parentCommitCid
152
+ };
153
+ }
154
+ getAddress() {
155
+ return this.contract.target;
156
+ }
157
+ }
158
+ exports.EvmRepoService = EvmRepoService;
159
+ function resolveContractAddress(chainId) {
160
+ if (chainId === 5003) {
161
+ return WIT_CONTRACT_ADDRESS_MANTLE_SEPOLIA;
162
+ }
163
+ if (chainId === 5000) {
164
+ return WIT_CONTRACT_ADDRESS_MANTLE_MAINNET;
165
+ }
166
+ throw new Error(`No contract address known for chain ID ${chainId}`);
167
+ }
168
+ function getExplorerLink(txHash) {
169
+ // Mantle Sepolia Explorer
170
+ // Mantle Explorer
171
+ if (txHash.startsWith('0x')) {
172
+ // Simple check, theoretically we should pass chainId to this function or check global context
173
+ // For now, let's just make it generic or use mainnet if configured?
174
+ // Actually, let's assume if it calls this, we want the link.
175
+ // But we don't know the chain here easily without passing it.
176
+ // Let's modify it to be generic or default to mainnet if we are switching?
177
+ // Or check a global?
178
+ // Let's just return a generic msg or try to guess.
179
+ // Better: assume Mantle Mainnet is the target now.
180
+ return `https://mantlescan.xyz/tx/${txHash}`;
181
+ }
182
+ return `https://sepolia.mantlescan.xyz/tx/${txHash}`;
183
+ }
184
+ function formatRepoId(id) {
185
+ // Format as 20-byte hex string (similar to address)
186
+ // 20 bytes = 40 hex chars
187
+ let hex = id.toString(16);
188
+ while (hex.length < 40) {
189
+ hex = '0' + hex;
190
+ }
191
+ return '0x' + hex;
192
+ }
@@ -0,0 +1,132 @@
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.packCar = packCar;
7
+ exports.unpackCar = unpackCar;
8
+ exports.mapCarPaths = mapCarPaths;
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const promises_1 = require("stream/promises");
12
+ const stream_1 = require("stream");
13
+ const PLACEHOLDER_ROOT = 'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi';
14
+ async function packCar(inputPath, outputPath, opts) {
15
+ const absInput = path_1.default.resolve(inputPath);
16
+ const absOutput = path_1.default.resolve(outputPath);
17
+ const wrapWithDirectory = opts?.wrap !== false;
18
+ const [{ CarWriter }, { filesFromPaths }, ipfsCar, { CID }] = await Promise.all([
19
+ import('@ipld/car/writer'),
20
+ import('files-from-path'),
21
+ import('ipfs-car'),
22
+ import('multiformats/cid'),
23
+ ]);
24
+ const { CAREncoderStream, createDirectoryEncoderStream, createFileEncoderStream } = ipfsCar;
25
+ const placeholderCid = CID.parse(PLACEHOLDER_ROOT);
26
+ const stat = await fs_1.default.promises.stat(absInput);
27
+ const files = await filesFromPaths([absInput], { hidden: false, sort: true });
28
+ if (files.length === 0) {
29
+ throw new Error(`No files found at ${absInput}`);
30
+ }
31
+ const encoder = stat.isFile() && !wrapWithDirectory ? createFileEncoderStream(files[0]) : createDirectoryEncoderStream(files);
32
+ let rootCid;
33
+ const carStream = encoder
34
+ .pipeThrough(new TransformStream({
35
+ transform(block, controller) {
36
+ rootCid = CID.asCID(block.cid) ?? CID.decode(block.cid.bytes);
37
+ controller.enqueue(block);
38
+ },
39
+ }))
40
+ .pipeThrough(new CAREncoderStream([placeholderCid]));
41
+ await (0, promises_1.pipeline)(stream_1.Readable.fromWeb(carStream), fs_1.default.createWriteStream(absOutput));
42
+ if (!rootCid) {
43
+ throw new Error('Failed to resolve CAR root CID.');
44
+ }
45
+ const fd = await fs_1.default.promises.open(absOutput, 'r+');
46
+ try {
47
+ await CarWriter.updateRootsInFile(fd, [rootCid]);
48
+ }
49
+ finally {
50
+ await fd.close();
51
+ }
52
+ return { root: rootCid.toString(), output: absOutput };
53
+ }
54
+ async function unpackCar(inputPath, outputDir) {
55
+ const absInput = path_1.default.resolve(inputPath);
56
+ const absOutput = path_1.default.resolve(outputDir);
57
+ const [{ CarIndexedReader }, { recursive: exporter }] = await Promise.all([
58
+ import('@ipld/car/indexed-reader'),
59
+ import('ipfs-unixfs-exporter'),
60
+ ]);
61
+ const reader = await CarIndexedReader.fromFile(absInput);
62
+ const roots = await reader.getRoots();
63
+ if (!roots.length) {
64
+ throw new Error('CAR file does not include roots.');
65
+ }
66
+ const entries = exporter(roots[0], {
67
+ async get(cid) {
68
+ const block = await reader.get(cid);
69
+ if (!block) {
70
+ throw new Error(`Missing block: ${cid}`);
71
+ }
72
+ return block.bytes;
73
+ },
74
+ });
75
+ for await (const entry of entries) {
76
+ const entryPath = mapEntryPath(absOutput, entry.path);
77
+ if (entry.type === 'directory') {
78
+ await fs_1.default.promises.mkdir(entryPath, { recursive: true });
79
+ continue;
80
+ }
81
+ if (entry.type === 'file' || entry.type === 'raw' || entry.type === 'identity') {
82
+ await fs_1.default.promises.mkdir(path_1.default.dirname(entryPath), { recursive: true });
83
+ await (0, promises_1.pipeline)(() => entry.content(), fs_1.default.createWriteStream(entryPath));
84
+ continue;
85
+ }
86
+ throw new Error(`Unsupported CAR entry type "${entry.type}" for path: ${entry.path}`);
87
+ }
88
+ }
89
+ async function mapCarPaths(inputPath, opts = {}) {
90
+ const absInput = path_1.default.resolve(inputPath);
91
+ const [{ CarIndexedReader }, { recursive: exporter }] = await Promise.all([
92
+ import('@ipld/car/indexed-reader'),
93
+ import('ipfs-unixfs-exporter'),
94
+ ]);
95
+ const reader = await CarIndexedReader.fromFile(absInput);
96
+ const roots = await reader.getRoots();
97
+ if (!roots.length) {
98
+ throw new Error('CAR file does not include roots.');
99
+ }
100
+ const root = roots[0].toString();
101
+ const entries = exporter(roots[0], {
102
+ async get(cid) {
103
+ const block = await reader.get(cid);
104
+ if (!block) {
105
+ throw new Error(`Missing block: ${cid}`);
106
+ }
107
+ return block.bytes;
108
+ },
109
+ });
110
+ const map = {};
111
+ for await (const entry of entries) {
112
+ if (entry.type === 'file' || entry.type === 'raw' || entry.type === 'identity') {
113
+ const rel = opts.stripRoot === false ? entry.path : stripCarRoot(entry.path);
114
+ map[rel] = entry.cid.toString();
115
+ }
116
+ }
117
+ return { root, map };
118
+ }
119
+ function mapEntryPath(outputRoot, entryPath) {
120
+ const parts = entryPath.split('/');
121
+ if (parts.length === 0) {
122
+ return outputRoot;
123
+ }
124
+ parts[0] = outputRoot;
125
+ return path_1.default.join(...parts);
126
+ }
127
+ function stripCarRoot(entryPath) {
128
+ const parts = entryPath.split('/').filter((part) => part.length > 0);
129
+ if (parts.length <= 1)
130
+ return entryPath;
131
+ return parts.slice(1).join('/');
132
+ }
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.keccak256 = keccak256;
4
+ const MASK_64 = (1n << 64n) - 1n;
5
+ const ROT = [
6
+ [0, 36, 3, 41, 18],
7
+ [1, 44, 10, 45, 2],
8
+ [62, 6, 43, 15, 61],
9
+ [28, 55, 25, 21, 56],
10
+ [27, 20, 39, 8, 14],
11
+ ];
12
+ const RC = [
13
+ 0x0000000000000001n,
14
+ 0x0000000000008082n,
15
+ 0x800000000000808an,
16
+ 0x8000000080008000n,
17
+ 0x000000000000808bn,
18
+ 0x0000000080000001n,
19
+ 0x8000000080008081n,
20
+ 0x8000000000008009n,
21
+ 0x000000000000008an,
22
+ 0x0000000000000088n,
23
+ 0x0000000080008009n,
24
+ 0x000000008000000an,
25
+ 0x000000008000808bn,
26
+ 0x800000000000008bn,
27
+ 0x8000000000008089n,
28
+ 0x8000000000008003n,
29
+ 0x8000000000008002n,
30
+ 0x8000000000000080n,
31
+ 0x000000000000800an,
32
+ 0x800000008000000an,
33
+ 0x8000000080008081n,
34
+ 0x8000000000008080n,
35
+ 0x0000000080000001n,
36
+ 0x8000000080008008n,
37
+ ];
38
+ const RATE_BYTES = 136; // 1088-bit rate for keccak-256
39
+ const OUTPUT_BYTES = 32;
40
+ function keccak256(data) {
41
+ const state = new Array(25).fill(0n);
42
+ let offset = 0;
43
+ while (offset + RATE_BYTES <= data.length) {
44
+ absorbBlock(state, data.subarray(offset, offset + RATE_BYTES));
45
+ keccakF(state);
46
+ offset += RATE_BYTES;
47
+ }
48
+ const block = new Uint8Array(RATE_BYTES);
49
+ block.set(data.subarray(offset));
50
+ block[data.length - offset] = 0x01;
51
+ block[RATE_BYTES - 1] |= 0x80;
52
+ absorbBlock(state, block);
53
+ keccakF(state);
54
+ const out = new Uint8Array(OUTPUT_BYTES);
55
+ for (let i = 0; i < OUTPUT_BYTES / 8; i += 1) {
56
+ store64(out, i * 8, state[i]);
57
+ }
58
+ return out;
59
+ }
60
+ function absorbBlock(state, block) {
61
+ for (let i = 0; i < RATE_BYTES / 8; i += 1) {
62
+ state[i] ^= load64(block, i * 8);
63
+ }
64
+ }
65
+ function load64(bytes, offset) {
66
+ let value = 0n;
67
+ for (let i = 0; i < 8; i += 1) {
68
+ value |= BigInt(bytes[offset + i] ?? 0) << BigInt(8 * i);
69
+ }
70
+ return value;
71
+ }
72
+ function store64(out, offset, value) {
73
+ for (let i = 0; i < 8; i += 1) {
74
+ out[offset + i] = Number((value >> BigInt(8 * i)) & 0xffn);
75
+ }
76
+ }
77
+ function rotl64(value, shift) {
78
+ if (shift === 0)
79
+ return value & MASK_64;
80
+ const s = BigInt(shift);
81
+ return ((value << s) | (value >> (64n - s))) & MASK_64;
82
+ }
83
+ function keccakF(state) {
84
+ const B = new Array(25).fill(0n);
85
+ const C = new Array(5).fill(0n);
86
+ const D = new Array(5).fill(0n);
87
+ for (let round = 0; round < 24; round += 1) {
88
+ for (let x = 0; x < 5; x += 1) {
89
+ C[x] =
90
+ state[x] ^
91
+ state[x + 5] ^
92
+ state[x + 10] ^
93
+ state[x + 15] ^
94
+ state[x + 20];
95
+ }
96
+ for (let x = 0; x < 5; x += 1) {
97
+ D[x] = C[(x + 4) % 5] ^ rotl64(C[(x + 1) % 5], 1);
98
+ }
99
+ for (let x = 0; x < 5; x += 1) {
100
+ for (let y = 0; y < 5; y += 1) {
101
+ state[x + 5 * y] ^= D[x];
102
+ }
103
+ }
104
+ for (let x = 0; x < 5; x += 1) {
105
+ for (let y = 0; y < 5; y += 1) {
106
+ const idx = x + 5 * y;
107
+ const rot = ROT[x][y];
108
+ const nx = y;
109
+ const ny = (2 * x + 3 * y) % 5;
110
+ B[nx + 5 * ny] = rotl64(state[idx], rot);
111
+ }
112
+ }
113
+ for (let y = 0; y < 5; y += 1) {
114
+ const row = y * 5;
115
+ for (let x = 0; x < 5; x += 1) {
116
+ const idx = row + x;
117
+ const b0 = B[idx];
118
+ const b1 = B[row + ((x + 1) % 5)];
119
+ const b2 = B[row + ((x + 2) % 5)];
120
+ state[idx] = (b0 ^ ((~b1) & b2)) & MASK_64;
121
+ }
122
+ }
123
+ state[0] = (state[0] ^ RC[round]) & MASK_64;
124
+ }
125
+ }
package/dist/lib/keys.js CHANGED
@@ -18,7 +18,8 @@ const path_1 = __importDefault(require("path"));
18
18
  const ed25519_1 = require("@mysten/sui/keypairs/ed25519");
19
19
  const client_1 = require("@mysten/sui/client");
20
20
  const walrus_1 = require("./walrus");
21
- exports.KEY_HOME = process.env.WIT_KEY_HOME || path_1.default.join(os_1.default.homedir(), '.wit', 'keys');
21
+ exports.KEY_HOME = process.env.WIT_KEY_HOME || path_1.default.join(os_1.default.homedir(), '.wit', 'keys-sui');
22
+ const LEGACY_KEY_HOME = path_1.default.join(os_1.default.homedir(), '.wit', 'keys');
22
23
  const GLOBAL_CONFIG = path_1.default.join(os_1.default.homedir(), '.witconfig');
23
24
  const SUI_COIN = '0x2::sui::SUI';
24
25
  // WAL CoinType map (9 decimals). Default to testnet when unknown.
@@ -31,6 +32,26 @@ const MIN_WAL_BALANCE = 1000000000n; // 1 WAL (assuming 9 decimals)
31
32
  function keyPathFor(address) {
32
33
  return path_1.default.join(exports.KEY_HOME, `${normalizeAddress(address)}.key`);
33
34
  }
35
+ function keyHomes() {
36
+ if (process.env.WIT_KEY_HOME)
37
+ return [exports.KEY_HOME];
38
+ return [exports.KEY_HOME, LEGACY_KEY_HOME];
39
+ }
40
+ async function findKeyFile(address) {
41
+ const filename = `${normalizeAddress(address)}.key`;
42
+ for (const dir of keyHomes()) {
43
+ const file = path_1.default.join(dir, filename);
44
+ try {
45
+ await promises_1.default.access(file);
46
+ return file;
47
+ }
48
+ catch (err) {
49
+ if (err?.code !== 'ENOENT')
50
+ throw err;
51
+ }
52
+ }
53
+ return null;
54
+ }
34
55
  async function createSigner(alias = 'default') {
35
56
  const keypair = ed25519_1.Ed25519Keypair.generate();
36
57
  const address = keypair.getPublicKey().toSuiAddress();
@@ -38,6 +59,7 @@ async function createSigner(alias = 'default') {
38
59
  await promises_1.default.mkdir(path_1.default.dirname(file), { recursive: true });
39
60
  const payload = {
40
61
  scheme: 'ED25519',
62
+ chain: 'sui',
41
63
  privateKey: keypair.getSecretKey(),
42
64
  address,
43
65
  publicKey: Buffer.from(keypair.getPublicKey().toRawBytes()).toString('base64'),
@@ -45,12 +67,26 @@ async function createSigner(alias = 'default') {
45
67
  alias,
46
68
  };
47
69
  await promises_1.default.writeFile(file, JSON.stringify(payload, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 });
48
- await upsertGlobalConfig((cfg) => ({
49
- ...cfg,
50
- active_address: address,
51
- key_alias: alias || cfg.key_alias || 'default',
52
- author: cfg.author && cfg.author !== 'unknown' ? cfg.author : address,
53
- }));
70
+ await upsertGlobalConfig((cfg) => {
71
+ const next = { ...cfg };
72
+ next.active_address = address;
73
+ next.key_alias = alias || cfg.key_alias || 'default';
74
+ if (!next.active_addresses)
75
+ next.active_addresses = {};
76
+ next.active_addresses.sui = address;
77
+ if (!next.key_aliases)
78
+ next.key_aliases = {};
79
+ next.key_aliases.sui = alias || cfg.key_alias || 'default';
80
+ if (!next.authors)
81
+ next.authors = {};
82
+ if (!next.authors.sui || next.authors.sui === 'unknown') {
83
+ next.authors.sui = address;
84
+ }
85
+ if (!cfg.author || cfg.author === 'unknown') {
86
+ next.author = address;
87
+ }
88
+ return next;
89
+ });
54
90
  return { signer: keypair, address, file };
55
91
  }
56
92
  async function loadSigner(address) {
@@ -59,7 +95,10 @@ async function loadSigner(address) {
59
95
  if (!resolvedAddress) {
60
96
  throw new Error('No active address configured. Generate a key with createSigner() or set active_address in ~/.witconfig.');
61
97
  }
62
- const file = keyPathFor(resolvedAddress);
98
+ const file = await findKeyFile(resolvedAddress);
99
+ if (!file) {
100
+ throw new Error(`Key file not found for address ${resolvedAddress}. Generate a key with createSigner().`);
101
+ }
63
102
  const stored = await readKeyFile(file);
64
103
  const signer = ed25519_1.Ed25519Keypair.fromSecretKey(stored.privateKey);
65
104
  const derived = signer.getPublicKey().toSuiAddress();
@@ -165,51 +204,77 @@ function guessAddress(author) {
165
204
  return undefined;
166
205
  }
167
206
  async function listStoredKeys() {
168
- let entries;
169
- try {
170
- entries = await promises_1.default.readdir(exports.KEY_HOME, { withFileTypes: true });
171
- }
172
- catch (err) {
173
- if (err?.code === 'ENOENT')
174
- return [];
175
- throw err;
176
- }
177
- const keys = [];
178
- for (const entry of entries) {
179
- if (!entry.isFile() || !entry.name.endsWith('.key'))
180
- continue;
181
- const file = path_1.default.join(exports.KEY_HOME, entry.name);
207
+ const keysByAddress = new Map();
208
+ for (const dir of keyHomes()) {
209
+ let entries;
182
210
  try {
183
- const parsed = await readKeyFile(file);
184
- const derived = parsed.address || ed25519_1.Ed25519Keypair.fromSecretKey(parsed.privateKey).getPublicKey().toSuiAddress();
185
- keys.push({
186
- address: normalizeAddress(derived),
187
- alias: parsed.alias,
188
- file,
189
- createdAt: parsed.createdAt,
190
- });
211
+ entries = await promises_1.default.readdir(dir, { withFileTypes: true });
191
212
  }
192
213
  catch (err) {
193
- // eslint-disable-next-line no-console
194
- console.warn(`Skipping key ${file}: ${err.message}`);
214
+ if (err?.code === 'ENOENT')
215
+ continue;
216
+ throw err;
217
+ }
218
+ for (const entry of entries) {
219
+ if (!entry.isFile() || !entry.name.endsWith('.key'))
220
+ continue;
221
+ const file = path_1.default.join(dir, entry.name);
222
+ try {
223
+ const parsed = await readKeyFile(file);
224
+ if (parsed.chain && parsed.chain !== 'sui')
225
+ continue;
226
+ const derived = parsed.address || ed25519_1.Ed25519Keypair.fromSecretKey(parsed.privateKey).getPublicKey().toSuiAddress();
227
+ const address = normalizeAddress(derived);
228
+ if (keysByAddress.has(address))
229
+ continue;
230
+ keysByAddress.set(address, {
231
+ address,
232
+ alias: parsed.alias,
233
+ file,
234
+ createdAt: parsed.createdAt,
235
+ });
236
+ }
237
+ catch (err) {
238
+ // eslint-disable-next-line no-console
239
+ console.warn(`Skipping key ${file}: ${err.message}`);
240
+ }
195
241
  }
196
242
  }
197
- return keys.sort((a, b) => (a.createdAt || '').localeCompare(b.createdAt || ''));
243
+ return Array.from(keysByAddress.values()).sort((a, b) => (a.createdAt || '').localeCompare(b.createdAt || ''));
198
244
  }
199
245
  async function readActiveAddress() {
200
246
  const cfg = await readGlobalConfig();
247
+ const byChain = cfg.active_addresses?.sui;
248
+ if (byChain)
249
+ return normalizeAddress(byChain);
201
250
  if (cfg.active_address)
202
251
  return normalizeAddress(cfg.active_address);
203
- const guessed = guessAddress(cfg.author);
252
+ const guessed = guessAddress(cfg.authors?.sui || cfg.author);
204
253
  return guessed ? normalizeAddress(guessed) : null;
205
254
  }
206
255
  async function setActiveAddress(address, opts) {
207
256
  await upsertGlobalConfig((cfg) => {
208
- const next = { ...cfg, active_address: normalizeAddress(address) };
257
+ const normalized = normalizeAddress(address);
258
+ const next = { ...cfg, active_address: normalized };
209
259
  if (opts?.alias)
210
260
  next.key_alias = opts.alias;
211
- if (opts?.updateAuthorIfUnknown && (!cfg.author || cfg.author === 'unknown')) {
212
- next.author = normalizeAddress(address);
261
+ if (!next.active_addresses)
262
+ next.active_addresses = {};
263
+ next.active_addresses.sui = normalized;
264
+ if (opts?.alias) {
265
+ if (!next.key_aliases)
266
+ next.key_aliases = {};
267
+ next.key_aliases.sui = opts.alias;
268
+ }
269
+ if (opts?.updateAuthorIfUnknown) {
270
+ if (!next.authors)
271
+ next.authors = {};
272
+ if (!next.authors.sui || next.authors.sui === 'unknown') {
273
+ next.authors.sui = normalized;
274
+ }
275
+ if (!cfg.author || cfg.author === 'unknown') {
276
+ next.author = normalized;
277
+ }
213
278
  }
214
279
  return next;
215
280
  });