genlayer 0.31.0 → 0.32.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/CHANGELOG.md +8 -0
- package/CLAUDE.md +55 -0
- package/README.md +121 -8
- package/dist/index.js +7161 -3706
- package/docs/delegator-guide.md +203 -0
- package/docs/validator-guide.md +291 -0
- package/package.json +2 -2
- package/src/commands/account/create.ts +29 -0
- package/src/commands/account/export.ts +106 -0
- package/src/commands/account/import.ts +135 -0
- package/src/commands/account/index.ts +126 -0
- package/src/commands/account/list.ts +34 -0
- package/src/commands/account/lock.ts +39 -0
- package/src/commands/account/remove.ts +30 -0
- package/src/commands/account/send.ts +156 -0
- package/src/commands/account/show.ts +74 -0
- package/src/commands/account/unlock.ts +51 -0
- package/src/commands/account/use.ts +21 -0
- package/src/commands/network/index.ts +18 -3
- package/src/commands/network/setNetwork.ts +43 -26
- package/src/commands/staking/StakingAction.ts +157 -0
- package/src/commands/staking/delegatorClaim.ts +41 -0
- package/src/commands/staking/delegatorExit.ts +50 -0
- package/src/commands/staking/delegatorJoin.ts +44 -0
- package/src/commands/staking/index.ts +251 -0
- package/src/commands/staking/setIdentity.ts +66 -0
- package/src/commands/staking/setOperator.ts +40 -0
- package/src/commands/staking/stakingInfo.ts +300 -0
- package/src/commands/staking/validatorClaim.ts +38 -0
- package/src/commands/staking/validatorDeposit.ts +35 -0
- package/src/commands/staking/validatorExit.ts +44 -0
- package/src/commands/staking/validatorJoin.ts +47 -0
- package/src/commands/staking/validatorPrime.ts +35 -0
- package/src/commands/staking/wizard.ts +802 -0
- package/src/index.ts +4 -2
- package/src/lib/actions/BaseAction.ts +114 -55
- package/src/lib/config/ConfigFileManager.ts +143 -0
- package/src/lib/config/KeychainManager.ts +23 -7
- package/tests/actions/create.test.ts +41 -21
- package/tests/actions/deploy.test.ts +7 -0
- package/tests/actions/lock.test.ts +33 -13
- package/tests/actions/setNetwork.test.ts +18 -57
- package/tests/actions/staking.test.ts +323 -0
- package/tests/actions/unlock.test.ts +51 -33
- package/tests/commands/account.test.ts +146 -0
- package/tests/commands/network.test.ts +10 -10
- package/tests/commands/staking.test.ts +333 -0
- package/tests/index.test.ts +6 -2
- package/tests/libs/baseAction.test.ts +71 -42
- package/tests/libs/configFileManager.test.ts +8 -1
- package/tests/libs/keychainManager.test.ts +56 -16
- package/src/commands/keygen/create.ts +0 -23
- package/src/commands/keygen/index.ts +0 -39
- package/src/commands/keygen/lock.ts +0 -31
- package/src/commands/keygen/unlock.ts +0 -41
- package/src/lib/interfaces/KeystoreData.ts +0 -5
- package/tests/commands/keygen.test.ts +0 -123
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import {BaseAction} from "../../lib/actions/BaseAction";
|
|
2
|
+
import {ethers} from "ethers";
|
|
3
|
+
import {writeFileSync, existsSync, readFileSync} from "fs";
|
|
4
|
+
|
|
5
|
+
export interface ImportAccountOptions {
|
|
6
|
+
privateKey?: string;
|
|
7
|
+
keystore?: string;
|
|
8
|
+
name: string;
|
|
9
|
+
overwrite: boolean;
|
|
10
|
+
setActive?: boolean;
|
|
11
|
+
password?: string;
|
|
12
|
+
sourcePassword?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class ImportAccountAction extends BaseAction {
|
|
16
|
+
constructor() {
|
|
17
|
+
super();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async execute(options: ImportAccountOptions): Promise<void> {
|
|
21
|
+
try {
|
|
22
|
+
const keystorePath = this.getKeystorePath(options.name);
|
|
23
|
+
|
|
24
|
+
if (existsSync(keystorePath) && !options.overwrite) {
|
|
25
|
+
this.failSpinner(`Account '${options.name}' already exists. Use '--overwrite' to replace.`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let privateKey: string;
|
|
29
|
+
|
|
30
|
+
if (options.keystore) {
|
|
31
|
+
privateKey = await this.importFromKeystore(options.keystore, options.sourcePassword);
|
|
32
|
+
} else if (options.privateKey) {
|
|
33
|
+
const normalizedKey = this.normalizePrivateKey(options.privateKey);
|
|
34
|
+
this.validatePrivateKey(normalizedKey);
|
|
35
|
+
privateKey = normalizedKey;
|
|
36
|
+
} else {
|
|
37
|
+
const inputKey = await this.promptPrivateKey();
|
|
38
|
+
const normalizedKey = this.normalizePrivateKey(inputKey);
|
|
39
|
+
this.validatePrivateKey(normalizedKey);
|
|
40
|
+
privateKey = normalizedKey;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const wallet = new ethers.Wallet(privateKey);
|
|
44
|
+
|
|
45
|
+
let password: string;
|
|
46
|
+
if (options.password) {
|
|
47
|
+
password = options.password;
|
|
48
|
+
} else {
|
|
49
|
+
password = await this.promptPassword("Enter a password to encrypt your keystore (minimum 8 characters):");
|
|
50
|
+
const confirmPassword = await this.promptPassword("Confirm password:");
|
|
51
|
+
if (password !== confirmPassword) {
|
|
52
|
+
this.failSpinner("Passwords do not match");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (password.length < BaseAction.MIN_PASSWORD_LENGTH) {
|
|
57
|
+
this.failSpinner(`Password must be at least ${BaseAction.MIN_PASSWORD_LENGTH} characters long`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this.startSpinner(`Importing account '${options.name}'...`);
|
|
61
|
+
|
|
62
|
+
const encryptedJson = await wallet.encrypt(password);
|
|
63
|
+
|
|
64
|
+
// Write standard web3 keystore format directly
|
|
65
|
+
writeFileSync(keystorePath, encryptedJson);
|
|
66
|
+
|
|
67
|
+
if (options.setActive !== false) {
|
|
68
|
+
this.setActiveAccount(options.name);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
await this.keychainManager.removePrivateKey(options.name);
|
|
72
|
+
|
|
73
|
+
this.succeedSpinner(`Account '${options.name}' imported to: ${keystorePath}`);
|
|
74
|
+
this.logInfo(`Address: ${wallet.address}`);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
this.failSpinner("Failed to import account", error);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private async importFromKeystore(keystorePath: string, sourcePassword?: string): Promise<string> {
|
|
81
|
+
if (!existsSync(keystorePath)) {
|
|
82
|
+
this.failSpinner(`Keystore file not found: ${keystorePath}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const fileContent = readFileSync(keystorePath, "utf-8");
|
|
86
|
+
let encryptedJson: string;
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const parsed = JSON.parse(fileContent);
|
|
90
|
+
|
|
91
|
+
// Check if it's our format (with 'encrypted' field) or standard web3 keystore
|
|
92
|
+
if (parsed.encrypted) {
|
|
93
|
+
// Our format
|
|
94
|
+
encryptedJson = parsed.encrypted;
|
|
95
|
+
} else if (parsed.crypto || parsed.Crypto) {
|
|
96
|
+
// Standard web3 keystore format (geth, foundry, etc.)
|
|
97
|
+
encryptedJson = fileContent;
|
|
98
|
+
} else {
|
|
99
|
+
this.failSpinner("Invalid keystore format. Expected encrypted keystore file.");
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
this.failSpinner("Invalid keystore file. Could not parse JSON.");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const password = sourcePassword || await this.promptPassword("Enter password to decrypt keystore:");
|
|
106
|
+
|
|
107
|
+
this.startSpinner("Decrypting keystore...");
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const wallet = await ethers.Wallet.fromEncryptedJson(encryptedJson!, password);
|
|
111
|
+
this.stopSpinner();
|
|
112
|
+
return wallet.privateKey;
|
|
113
|
+
} catch {
|
|
114
|
+
this.failSpinner("Failed to decrypt keystore. Wrong password?");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// This line is unreachable but TypeScript needs it
|
|
118
|
+
throw new Error("Unreachable");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private async promptPrivateKey(): Promise<string> {
|
|
122
|
+
return this.promptPassword("Enter private key to import:");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private normalizePrivateKey(key: string): string {
|
|
126
|
+
const trimmed = key.trim();
|
|
127
|
+
return trimmed.startsWith("0x") ? trimmed : `0x${trimmed}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private validatePrivateKey(key: string): void {
|
|
131
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(key)) {
|
|
132
|
+
this.failSpinner("Invalid private key format. Expected 64 hex characters (with or without 0x prefix).");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import {Command} from "commander";
|
|
2
|
+
import {ShowAccountAction, ShowAccountOptions} from "./show";
|
|
3
|
+
import {CreateAccountAction, CreateAccountOptions} from "./create";
|
|
4
|
+
import {ImportAccountAction, ImportAccountOptions} from "./import";
|
|
5
|
+
import {ExportAccountAction, ExportAccountOptions} from "./export";
|
|
6
|
+
import {UnlockAccountAction, UnlockAccountOptions} from "./unlock";
|
|
7
|
+
import {LockAccountAction, LockAccountOptions} from "./lock";
|
|
8
|
+
import {SendAction, SendOptions} from "./send";
|
|
9
|
+
import {ListAccountsAction} from "./list";
|
|
10
|
+
import {UseAccountAction} from "./use";
|
|
11
|
+
import {RemoveAccountAction} from "./remove";
|
|
12
|
+
|
|
13
|
+
export function initializeAccountCommands(program: Command) {
|
|
14
|
+
const accountCommand = program
|
|
15
|
+
.command("account")
|
|
16
|
+
.description("Manage your accounts (address, balance, keys)")
|
|
17
|
+
.action(async () => {
|
|
18
|
+
// Default action: show account info (use 'account show' for options)
|
|
19
|
+
const showAction = new ShowAccountAction();
|
|
20
|
+
await showAction.execute({});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
accountCommand
|
|
24
|
+
.command("list")
|
|
25
|
+
.description("List all accounts")
|
|
26
|
+
.action(async () => {
|
|
27
|
+
const listAction = new ListAccountsAction();
|
|
28
|
+
await listAction.execute();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
accountCommand
|
|
32
|
+
.command("show")
|
|
33
|
+
.description("Show account details (address, balance)")
|
|
34
|
+
.option("--rpc <rpcUrl>", "RPC URL for the network")
|
|
35
|
+
.option("--account <name>", "Account to show")
|
|
36
|
+
.action(async (options: ShowAccountOptions) => {
|
|
37
|
+
const showAction = new ShowAccountAction();
|
|
38
|
+
await showAction.execute(options);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
accountCommand
|
|
42
|
+
.command("create")
|
|
43
|
+
.description("Create a new account with encrypted keystore")
|
|
44
|
+
.requiredOption("--name <name>", "Name for the account")
|
|
45
|
+
.option("--overwrite", "Overwrite existing account", false)
|
|
46
|
+
.option("--no-set-active", "Do not set as active account")
|
|
47
|
+
.action(async (options: CreateAccountOptions) => {
|
|
48
|
+
const createAction = new CreateAccountAction();
|
|
49
|
+
await createAction.execute(options);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
accountCommand
|
|
53
|
+
.command("import")
|
|
54
|
+
.description("Import an account from a private key or keystore file")
|
|
55
|
+
.requiredOption("--name <name>", "Name for the account")
|
|
56
|
+
.option("--private-key <key>", "Private key to import")
|
|
57
|
+
.option("--keystore <path>", "Path to keystore file to import (geth, foundry, etc.)")
|
|
58
|
+
.option("--password <password>", "Password for the new keystore (skips confirmation prompt)")
|
|
59
|
+
.option("--source-password <password>", "Password to decrypt source keystore (with --keystore)")
|
|
60
|
+
.option("--overwrite", "Overwrite existing account", false)
|
|
61
|
+
.option("--no-set-active", "Do not set as active account")
|
|
62
|
+
.action(async (options: ImportAccountOptions) => {
|
|
63
|
+
const importAction = new ImportAccountAction();
|
|
64
|
+
await importAction.execute(options);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
accountCommand
|
|
68
|
+
.command("export")
|
|
69
|
+
.description("Export an account to a keystore file (web3/geth/foundry compatible)")
|
|
70
|
+
.requiredOption("--output <path>", "Output path for the keystore file")
|
|
71
|
+
.option("--account <name>", "Account to export (defaults to active account)")
|
|
72
|
+
.option("--password <password>", "Password for exported keystore (skips confirmation)")
|
|
73
|
+
.option("--source-password <password>", "Password to decrypt account (if not unlocked)")
|
|
74
|
+
.action(async (options: ExportAccountOptions) => {
|
|
75
|
+
const exportAction = new ExportAccountAction();
|
|
76
|
+
await exportAction.execute(options);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
accountCommand
|
|
80
|
+
.command("use <name>")
|
|
81
|
+
.description("Set the active account")
|
|
82
|
+
.action(async (name: string) => {
|
|
83
|
+
const useAction = new UseAccountAction();
|
|
84
|
+
await useAction.execute(name);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
accountCommand
|
|
88
|
+
.command("remove <name>")
|
|
89
|
+
.description("Remove an account")
|
|
90
|
+
.option("--force", "Skip confirmation prompt", false)
|
|
91
|
+
.action(async (name: string, options: {force?: boolean}) => {
|
|
92
|
+
const removeAction = new RemoveAccountAction();
|
|
93
|
+
await removeAction.execute(name, options);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
accountCommand
|
|
97
|
+
.command("send <to> <amount>")
|
|
98
|
+
.description("Send GEN to an address")
|
|
99
|
+
.option("--rpc <rpcUrl>", "RPC URL for the network")
|
|
100
|
+
.option("--network <network>", "Network to use (localnet, testnet-asimov)")
|
|
101
|
+
.option("--account <name>", "Account to send from")
|
|
102
|
+
.action(async (to: string, amount: string, options: {rpc?: string; network?: string; account?: string}) => {
|
|
103
|
+
const sendAction = new SendAction();
|
|
104
|
+
await sendAction.execute({to, amount, rpc: options.rpc, network: options.network, account: options.account});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
accountCommand
|
|
108
|
+
.command("unlock")
|
|
109
|
+
.description("Unlock account by caching private key in OS keychain")
|
|
110
|
+
.option("--account <name>", "Account to unlock")
|
|
111
|
+
.action(async (options: UnlockAccountOptions) => {
|
|
112
|
+
const unlockAction = new UnlockAccountAction();
|
|
113
|
+
await unlockAction.execute(options);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
accountCommand
|
|
117
|
+
.command("lock")
|
|
118
|
+
.description("Lock account by removing private key from OS keychain")
|
|
119
|
+
.option("--account <name>", "Account to lock")
|
|
120
|
+
.action(async (options: LockAccountOptions) => {
|
|
121
|
+
const lockAction = new LockAccountAction();
|
|
122
|
+
await lockAction.execute(options);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return program;
|
|
126
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {BaseAction} from "../../lib/actions/BaseAction";
|
|
2
|
+
|
|
3
|
+
export class ListAccountsAction extends BaseAction {
|
|
4
|
+
constructor() {
|
|
5
|
+
super();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async execute(): Promise<void> {
|
|
9
|
+
try {
|
|
10
|
+
const accounts = this.listAccounts();
|
|
11
|
+
const activeAccount = this.getActiveAccount();
|
|
12
|
+
const unlockedAccounts = await this.keychainManager.listUnlockedAccounts();
|
|
13
|
+
|
|
14
|
+
if (accounts.length === 0) {
|
|
15
|
+
this.logInfo("No accounts found. Run 'genlayer account create --name <name>' to create one.");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
console.log("");
|
|
20
|
+
for (const account of accounts) {
|
|
21
|
+
const isActive = account.name === activeAccount;
|
|
22
|
+
const isUnlocked = unlockedAccounts.includes(account.name);
|
|
23
|
+
const marker = isActive ? "*" : " ";
|
|
24
|
+
const status = isUnlocked ? "(unlocked)" : "";
|
|
25
|
+
const activeLabel = isActive ? "(active)" : "";
|
|
26
|
+
|
|
27
|
+
console.log(`${marker} ${account.name.padEnd(16)} ${account.address} ${activeLabel} ${status}`.trim());
|
|
28
|
+
}
|
|
29
|
+
console.log("");
|
|
30
|
+
} catch (error) {
|
|
31
|
+
this.failSpinner("Failed to list accounts", error);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {BaseAction} from "../../lib/actions/BaseAction";
|
|
2
|
+
|
|
3
|
+
export interface LockAccountOptions {
|
|
4
|
+
account?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class LockAccountAction extends BaseAction {
|
|
8
|
+
async execute(options?: LockAccountOptions): Promise<void> {
|
|
9
|
+
this.startSpinner("Checking keychain availability...");
|
|
10
|
+
|
|
11
|
+
const keychainAvailable = await this.keychainManager.isKeychainAvailable();
|
|
12
|
+
if (!keychainAvailable) {
|
|
13
|
+
this.failSpinner("OS keychain is not available. This command requires a supported keychain (e.g. macOS Keychain, Windows Credential Manager, or GNOME Keyring).");
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (options?.account) {
|
|
18
|
+
this.accountOverride = options.account;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const accountName = this.resolveAccountName();
|
|
22
|
+
this.setSpinnerText(`Checking for cached private key for '${accountName}'...`);
|
|
23
|
+
|
|
24
|
+
const hasCachedKey = await this.keychainManager.getPrivateKey(accountName);
|
|
25
|
+
if (!hasCachedKey) {
|
|
26
|
+
this.succeedSpinner(`Account '${accountName}' is already locked.`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
this.setSpinnerText(`Removing private key for '${accountName}' from OS keychain...`);
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
await this.keychainManager.removePrivateKey(accountName);
|
|
34
|
+
this.succeedSpinner(`Account '${accountName}' locked! Private key removed from OS keychain.`);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
this.failSpinner("Failed to lock account.", error);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {BaseAction} from "../../lib/actions/BaseAction";
|
|
2
|
+
|
|
3
|
+
export class RemoveAccountAction extends BaseAction {
|
|
4
|
+
constructor() {
|
|
5
|
+
super();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async execute(name: string, options: {force?: boolean}): Promise<void> {
|
|
9
|
+
try {
|
|
10
|
+
if (!this.accountExists(name)) {
|
|
11
|
+
this.failSpinner(`Account '${name}' does not exist.`);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (!options.force) {
|
|
16
|
+
await this.confirmPrompt(`Are you sure you want to remove account '${name}'? This cannot be undone.`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Remove from keychain if unlocked
|
|
20
|
+
await this.keychainManager.removePrivateKey(name);
|
|
21
|
+
|
|
22
|
+
// Remove keystore file
|
|
23
|
+
this.removeAccount(name);
|
|
24
|
+
|
|
25
|
+
this.logSuccess(`Account '${name}' removed`);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
this.failSpinner("Failed to remove account", error);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import {BaseAction, BUILT_IN_NETWORKS, resolveNetwork} from "../../lib/actions/BaseAction";
|
|
2
|
+
import {parseEther, formatEther} from "viem";
|
|
3
|
+
import {createClient, createAccount} from "genlayer-js";
|
|
4
|
+
import type {GenLayerChain, Address, Hash} from "genlayer-js/types";
|
|
5
|
+
import {readFileSync, existsSync} from "fs";
|
|
6
|
+
import {ethers} from "ethers";
|
|
7
|
+
|
|
8
|
+
export interface SendOptions {
|
|
9
|
+
to: string;
|
|
10
|
+
amount: string;
|
|
11
|
+
rpc?: string;
|
|
12
|
+
network?: string;
|
|
13
|
+
account?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class SendAction extends BaseAction {
|
|
17
|
+
constructor() {
|
|
18
|
+
super();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private getNetwork(networkOption?: string): GenLayerChain {
|
|
22
|
+
if (networkOption) {
|
|
23
|
+
const network = BUILT_IN_NETWORKS[networkOption];
|
|
24
|
+
if (!network) {
|
|
25
|
+
throw new Error(`Unknown network: ${networkOption}. Available: ${Object.keys(BUILT_IN_NETWORKS).join(", ")}`);
|
|
26
|
+
}
|
|
27
|
+
return network;
|
|
28
|
+
}
|
|
29
|
+
return resolveNetwork(this.getConfig().network);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private parseAmount(amount: string): bigint {
|
|
33
|
+
// Support "10gen" or "10" (assumes gen) or wei values
|
|
34
|
+
const lowerAmount = amount.toLowerCase();
|
|
35
|
+
if (lowerAmount.endsWith("gen")) {
|
|
36
|
+
const value = lowerAmount.slice(0, -3);
|
|
37
|
+
return parseEther(value);
|
|
38
|
+
}
|
|
39
|
+
// If it's a large number (likely wei), use as-is
|
|
40
|
+
if (BigInt(amount) > 1_000_000_000_000n) {
|
|
41
|
+
return BigInt(amount);
|
|
42
|
+
}
|
|
43
|
+
// Otherwise assume it's in GEN
|
|
44
|
+
return parseEther(amount);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async execute(options: SendOptions): Promise<void> {
|
|
48
|
+
this.startSpinner("Preparing transfer...");
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
if (options.account) {
|
|
52
|
+
this.accountOverride = options.account;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const accountName = this.resolveAccountName();
|
|
56
|
+
const keystorePath = this.getKeystorePath(accountName);
|
|
57
|
+
|
|
58
|
+
if (!existsSync(keystorePath)) {
|
|
59
|
+
this.failSpinner(`Account '${accountName}' not found. Run 'genlayer account create --name ${accountName}' first.`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const keystoreJson = readFileSync(keystorePath, "utf-8");
|
|
64
|
+
const keystoreData = JSON.parse(keystoreJson);
|
|
65
|
+
|
|
66
|
+
if (!this.isValidKeystoreFormat(keystoreData)) {
|
|
67
|
+
this.failSpinner("Invalid keystore format.");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Get private key
|
|
72
|
+
const cachedKey = await this.keychainManager.getPrivateKey(accountName);
|
|
73
|
+
let privateKey: string;
|
|
74
|
+
|
|
75
|
+
if (cachedKey) {
|
|
76
|
+
privateKey = cachedKey;
|
|
77
|
+
} else {
|
|
78
|
+
this.stopSpinner();
|
|
79
|
+
const password = await this.promptPassword(`Enter password to unlock account '${accountName}':`);
|
|
80
|
+
this.startSpinner("Preparing transfer...");
|
|
81
|
+
const wallet = await ethers.Wallet.fromEncryptedJson(keystoreJson, password);
|
|
82
|
+
privateKey = wallet.privateKey;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const network = this.getNetwork(options.network);
|
|
86
|
+
const account = createAccount(privateKey as Hash);
|
|
87
|
+
const amount = this.parseAmount(options.amount);
|
|
88
|
+
|
|
89
|
+
const client = createClient({
|
|
90
|
+
chain: network,
|
|
91
|
+
account,
|
|
92
|
+
endpoint: options.rpc,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
this.setSpinnerText(`Sending ${formatEther(amount)} GEN to ${options.to}...`);
|
|
96
|
+
|
|
97
|
+
// Get nonce
|
|
98
|
+
const nonce = await client.getCurrentNonce({address: account.address});
|
|
99
|
+
|
|
100
|
+
// Prepare and sign transaction (let prepareTransactionRequest estimate gas)
|
|
101
|
+
const transactionRequest = await client.prepareTransactionRequest({
|
|
102
|
+
account,
|
|
103
|
+
to: options.to as Address,
|
|
104
|
+
value: amount,
|
|
105
|
+
type: "legacy",
|
|
106
|
+
nonce: Number(nonce),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const serializedTransaction = await account.signTransaction(transactionRequest);
|
|
110
|
+
const txHash = await client.sendRawTransaction({serializedTransaction});
|
|
111
|
+
|
|
112
|
+
this.setSpinnerText(`Transaction submitted: ${txHash}\nWaiting for confirmation...`);
|
|
113
|
+
|
|
114
|
+
// Poll for receipt (standard ETH transfer, not GenVM tx)
|
|
115
|
+
let receipt = null;
|
|
116
|
+
for (let i = 0; i < 60; i++) {
|
|
117
|
+
try {
|
|
118
|
+
receipt = await client.getTransactionReceipt({hash: txHash});
|
|
119
|
+
if (receipt) break;
|
|
120
|
+
} catch {
|
|
121
|
+
// Receipt not available yet, continue polling
|
|
122
|
+
}
|
|
123
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!receipt) {
|
|
127
|
+
// Tx submitted but receipt not found yet - still success
|
|
128
|
+
this.succeedSpinner("Transfer submitted (pending confirmation)", {
|
|
129
|
+
transactionHash: txHash,
|
|
130
|
+
from: account.address,
|
|
131
|
+
to: options.to,
|
|
132
|
+
amount: `${formatEther(amount)} GEN`,
|
|
133
|
+
});
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (receipt.status === "reverted") {
|
|
138
|
+
this.failSpinner("Transaction reverted");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const result = {
|
|
143
|
+
transactionHash: txHash,
|
|
144
|
+
from: account.address,
|
|
145
|
+
to: options.to,
|
|
146
|
+
amount: `${formatEther(amount)} GEN`,
|
|
147
|
+
blockNumber: receipt.blockNumber.toString(),
|
|
148
|
+
gasUsed: receipt.gasUsed.toString(),
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
this.succeedSpinner("Transfer successful!", result);
|
|
152
|
+
} catch (error: any) {
|
|
153
|
+
this.failSpinner("Transfer failed", error.message || error);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {BaseAction, resolveNetwork} from "../../lib/actions/BaseAction";
|
|
2
|
+
import {formatEther} from "viem";
|
|
3
|
+
import {createClient} from "genlayer-js";
|
|
4
|
+
import type {GenLayerChain, Address} from "genlayer-js/types";
|
|
5
|
+
import {readFileSync, existsSync} from "fs";
|
|
6
|
+
|
|
7
|
+
export interface ShowAccountOptions {
|
|
8
|
+
rpc?: string;
|
|
9
|
+
account?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class ShowAccountAction extends BaseAction {
|
|
13
|
+
constructor() {
|
|
14
|
+
super();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private getNetwork(): GenLayerChain {
|
|
18
|
+
return resolveNetwork(this.getConfig().network);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async execute(options?: ShowAccountOptions): Promise<void> {
|
|
22
|
+
this.startSpinner("Fetching account info...");
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
if (options?.account) {
|
|
26
|
+
this.accountOverride = options.account;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const accountName = this.resolveAccountName();
|
|
30
|
+
const keystorePath = this.getKeystorePath(accountName);
|
|
31
|
+
|
|
32
|
+
if (!existsSync(keystorePath)) {
|
|
33
|
+
this.failSpinner(`Account '${accountName}' not found. Run 'genlayer account create --name ${accountName}' first.`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const keystoreData = JSON.parse(readFileSync(keystorePath, "utf-8"));
|
|
38
|
+
|
|
39
|
+
if (!this.isValidKeystoreFormat(keystoreData)) {
|
|
40
|
+
this.failSpinner("Invalid keystore format.");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const rawAddr = keystoreData.address;
|
|
45
|
+
const address = (rawAddr.startsWith("0x") ? rawAddr : `0x${rawAddr}`) as Address;
|
|
46
|
+
const network = this.getNetwork();
|
|
47
|
+
|
|
48
|
+
const client = createClient({
|
|
49
|
+
chain: network,
|
|
50
|
+
account: address,
|
|
51
|
+
endpoint: options?.rpc,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const balance = await client.getBalance({address});
|
|
55
|
+
const formattedBalance = formatEther(balance);
|
|
56
|
+
|
|
57
|
+
const isUnlocked = await this.keychainManager.isAccountUnlocked(accountName);
|
|
58
|
+
const isActive = this.getActiveAccount() === accountName;
|
|
59
|
+
|
|
60
|
+
const result = {
|
|
61
|
+
name: accountName,
|
|
62
|
+
address,
|
|
63
|
+
balance: `${formattedBalance} GEN`,
|
|
64
|
+
network: network.name || "localnet",
|
|
65
|
+
status: isUnlocked ? "unlocked" : "locked",
|
|
66
|
+
active: isActive,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
this.succeedSpinner("Account info", result);
|
|
70
|
+
} catch (error: any) {
|
|
71
|
+
this.failSpinner("Failed to get account info", error.message || error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {BaseAction} from "../../lib/actions/BaseAction";
|
|
2
|
+
import {readFileSync, existsSync} from "fs";
|
|
3
|
+
import {ethers} from "ethers";
|
|
4
|
+
|
|
5
|
+
export interface UnlockAccountOptions {
|
|
6
|
+
account?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class UnlockAccountAction extends BaseAction {
|
|
10
|
+
async execute(options?: UnlockAccountOptions): Promise<void> {
|
|
11
|
+
this.startSpinner("Checking keychain availability...");
|
|
12
|
+
|
|
13
|
+
const keychainAvailable = await this.keychainManager.isKeychainAvailable();
|
|
14
|
+
if (!keychainAvailable) {
|
|
15
|
+
this.failSpinner("OS keychain is not available. This command requires a supported keychain (e.g. macOS Keychain, Windows Credential Manager, or GNOME Keyring).");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (options?.account) {
|
|
20
|
+
this.accountOverride = options.account;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const accountName = this.resolveAccountName();
|
|
24
|
+
this.setSpinnerText(`Checking for account '${accountName}'...`);
|
|
25
|
+
|
|
26
|
+
const keystorePath = this.getKeystorePath(accountName);
|
|
27
|
+
if (!existsSync(keystorePath)) {
|
|
28
|
+
this.failSpinner(`Account '${accountName}' not found. Run 'genlayer account create --name ${accountName}' first.`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const keystoreJson = readFileSync(keystorePath, "utf-8");
|
|
33
|
+
const keystoreData = JSON.parse(keystoreJson);
|
|
34
|
+
if (!this.isValidKeystoreFormat(keystoreData)) {
|
|
35
|
+
this.failSpinner("Invalid keystore format.");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.stopSpinner();
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const password = await this.promptPassword(`Enter password to unlock '${accountName}':`);
|
|
43
|
+
const wallet = await ethers.Wallet.fromEncryptedJson(keystoreJson, password);
|
|
44
|
+
|
|
45
|
+
await this.keychainManager.storePrivateKey(accountName, wallet.privateKey);
|
|
46
|
+
this.succeedSpinner(`Account '${accountName}' unlocked! Private key cached in OS keychain.`);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
this.failSpinner("Failed to unlock account.", error);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {BaseAction} from "../../lib/actions/BaseAction";
|
|
2
|
+
|
|
3
|
+
export class UseAccountAction extends BaseAction {
|
|
4
|
+
constructor() {
|
|
5
|
+
super();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async execute(name: string): Promise<void> {
|
|
9
|
+
try {
|
|
10
|
+
if (!this.accountExists(name)) {
|
|
11
|
+
this.failSpinner(`Account '${name}' does not exist. Run 'genlayer account list' to see available accounts.`);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
this.setActiveAccount(name);
|
|
16
|
+
this.logSuccess(`Active account set to '${name}'`);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
this.failSpinner("Failed to set active account", error);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|