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,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
|
|
51
|
-
key_alias
|
|
52
|
-
|
|
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 =
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
entries
|
|
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
|
-
|
|
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
|
-
|
|
194
|
-
|
|
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
|
|
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
|
|
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 (
|
|
212
|
-
next.
|
|
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
|
});
|