genlayer 0.30.0 → 0.32.0
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 +12 -0
- package/CLAUDE.md +55 -0
- package/README.md +121 -8
- package/dist/index.js +8424 -2489
- package/docs/delegator-guide.md +203 -0
- package/docs/validator-guide.md +260 -0
- package/package.json +2 -2
- package/src/commands/account/create.ts +23 -0
- package/src/commands/account/import.ts +81 -0
- package/src/commands/account/index.ts +67 -0
- package/src/commands/{keygen → account}/lock.ts +6 -7
- package/src/commands/account/send.ts +150 -0
- package/src/commands/account/show.ts +60 -0
- package/src/commands/{keygen → account}/unlock.ts +12 -12
- package/src/commands/contracts/code.ts +33 -0
- package/src/commands/contracts/index.ts +10 -0
- package/src/commands/network/setNetwork.ts +5 -4
- package/src/commands/staking/StakingAction.ts +125 -0
- package/src/commands/staking/delegatorClaim.ts +41 -0
- package/src/commands/staking/delegatorExit.ts +50 -0
- package/src/commands/staking/delegatorJoin.ts +42 -0
- package/src/commands/staking/index.ts +224 -0
- package/src/commands/staking/setIdentity.ts +61 -0
- package/src/commands/staking/setOperator.ts +40 -0
- package/src/commands/staking/stakingInfo.ts +292 -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/index.ts +4 -2
- package/src/lib/actions/BaseAction.ts +43 -10
- package/tests/actions/code.test.ts +87 -0
- package/tests/actions/create.test.ts +18 -18
- package/tests/actions/lock.test.ts +7 -7
- package/tests/actions/setNetwork.test.ts +18 -57
- package/tests/actions/staking.test.ts +323 -0
- package/tests/actions/unlock.test.ts +12 -12
- package/tests/commands/account.test.ts +121 -0
- package/tests/commands/code.test.ts +69 -0
- package/tests/commands/staking.test.ts +211 -0
- package/tests/index.test.ts +6 -2
- package/tests/libs/baseAction.test.ts +7 -1
- package/src/commands/keygen/create.ts +0 -23
- package/src/commands/keygen/index.ts +0 -39
- package/tests/commands/keygen.test.ts +0 -123
|
@@ -0,0 +1,150 @@
|
|
|
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
|
+
import {KeystoreData} from "../../lib/interfaces/KeystoreData";
|
|
8
|
+
|
|
9
|
+
export interface SendOptions {
|
|
10
|
+
to: string;
|
|
11
|
+
amount: string;
|
|
12
|
+
rpc?: string;
|
|
13
|
+
network?: 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
|
+
const keypairPath = this.getConfigByKey("keyPairPath");
|
|
52
|
+
|
|
53
|
+
if (!keypairPath || !existsSync(keypairPath)) {
|
|
54
|
+
this.failSpinner("No account found. Run 'genlayer account create' first.");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const keystoreData: KeystoreData = JSON.parse(readFileSync(keypairPath, "utf-8"));
|
|
59
|
+
|
|
60
|
+
if (!this.isValidKeystoreFormat(keystoreData)) {
|
|
61
|
+
this.failSpinner("Invalid keystore format.");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Get private key
|
|
66
|
+
const cachedKey = await this.keychainManager.getPrivateKey();
|
|
67
|
+
let privateKey: string;
|
|
68
|
+
|
|
69
|
+
if (cachedKey) {
|
|
70
|
+
privateKey = cachedKey;
|
|
71
|
+
} else {
|
|
72
|
+
this.stopSpinner();
|
|
73
|
+
const password = await this.promptPassword("Enter password to unlock account:");
|
|
74
|
+
this.startSpinner("Preparing transfer...");
|
|
75
|
+
const wallet = await ethers.Wallet.fromEncryptedJson(keystoreData.encrypted, password);
|
|
76
|
+
privateKey = wallet.privateKey;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const network = this.getNetwork(options.network);
|
|
80
|
+
const account = createAccount(privateKey as Hash);
|
|
81
|
+
const amount = this.parseAmount(options.amount);
|
|
82
|
+
|
|
83
|
+
const client = createClient({
|
|
84
|
+
chain: network,
|
|
85
|
+
account,
|
|
86
|
+
endpoint: options.rpc,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
this.setSpinnerText(`Sending ${formatEther(amount)} GEN to ${options.to}...`);
|
|
90
|
+
|
|
91
|
+
// Get nonce
|
|
92
|
+
const nonce = await client.getCurrentNonce({address: account.address});
|
|
93
|
+
|
|
94
|
+
// Prepare and sign transaction (let prepareTransactionRequest estimate gas)
|
|
95
|
+
const transactionRequest = await client.prepareTransactionRequest({
|
|
96
|
+
account,
|
|
97
|
+
to: options.to as Address,
|
|
98
|
+
value: amount,
|
|
99
|
+
type: "legacy",
|
|
100
|
+
nonce: Number(nonce),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const serializedTransaction = await account.signTransaction(transactionRequest);
|
|
104
|
+
const txHash = await client.sendRawTransaction({serializedTransaction});
|
|
105
|
+
|
|
106
|
+
this.setSpinnerText(`Transaction submitted: ${txHash}\nWaiting for confirmation...`);
|
|
107
|
+
|
|
108
|
+
// Poll for receipt (standard ETH transfer, not GenVM tx)
|
|
109
|
+
let receipt = null;
|
|
110
|
+
for (let i = 0; i < 60; i++) {
|
|
111
|
+
try {
|
|
112
|
+
receipt = await client.getTransactionReceipt({hash: txHash});
|
|
113
|
+
if (receipt) break;
|
|
114
|
+
} catch {
|
|
115
|
+
// Receipt not available yet, continue polling
|
|
116
|
+
}
|
|
117
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!receipt) {
|
|
121
|
+
// Tx submitted but receipt not found yet - still success
|
|
122
|
+
this.succeedSpinner("Transfer submitted (pending confirmation)", {
|
|
123
|
+
transactionHash: txHash,
|
|
124
|
+
from: account.address,
|
|
125
|
+
to: options.to,
|
|
126
|
+
amount: `${formatEther(amount)} GEN`,
|
|
127
|
+
});
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (receipt.status === "reverted") {
|
|
132
|
+
this.failSpinner("Transaction reverted");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const result = {
|
|
137
|
+
transactionHash: txHash,
|
|
138
|
+
from: account.address,
|
|
139
|
+
to: options.to,
|
|
140
|
+
amount: `${formatEther(amount)} GEN`,
|
|
141
|
+
blockNumber: receipt.blockNumber.toString(),
|
|
142
|
+
gasUsed: receipt.gasUsed.toString(),
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
this.succeedSpinner("Transfer successful!", result);
|
|
146
|
+
} catch (error: any) {
|
|
147
|
+
this.failSpinner("Transfer failed", error.message || error);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
import {KeystoreData} from "../../lib/interfaces/KeystoreData";
|
|
7
|
+
|
|
8
|
+
export class ShowAccountAction extends BaseAction {
|
|
9
|
+
constructor() {
|
|
10
|
+
super();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
private getNetwork(): GenLayerChain {
|
|
14
|
+
return resolveNetwork(this.getConfig().network);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async execute(): Promise<void> {
|
|
18
|
+
this.startSpinner("Fetching account info...");
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const keypairPath = this.getConfigByKey("keyPairPath");
|
|
22
|
+
|
|
23
|
+
if (!keypairPath || !existsSync(keypairPath)) {
|
|
24
|
+
this.failSpinner("No account found. Run 'genlayer account create' first.");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const keystoreData: KeystoreData = JSON.parse(readFileSync(keypairPath, "utf-8"));
|
|
29
|
+
|
|
30
|
+
if (!this.isValidKeystoreFormat(keystoreData)) {
|
|
31
|
+
this.failSpinner("Invalid keystore format.");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const address = keystoreData.address as Address;
|
|
36
|
+
const network = this.getNetwork();
|
|
37
|
+
|
|
38
|
+
const client = createClient({
|
|
39
|
+
chain: network,
|
|
40
|
+
account: address,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const balance = await client.getBalance({address});
|
|
44
|
+
const formattedBalance = formatEther(balance);
|
|
45
|
+
|
|
46
|
+
const isUnlocked = await this.keychainManager.getPrivateKey();
|
|
47
|
+
|
|
48
|
+
const result = {
|
|
49
|
+
address,
|
|
50
|
+
balance: `${formattedBalance} GEN`,
|
|
51
|
+
network: network.name || "localnet",
|
|
52
|
+
status: isUnlocked ? "unlocked" : "locked",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
this.succeedSpinner("Account info", result);
|
|
56
|
+
} catch (error: any) {
|
|
57
|
+
this.failSpinner("Failed to get account info", error.message || error);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import {BaseAction} from "../../lib/actions/BaseAction";
|
|
2
|
+
import {readFileSync, existsSync} from "fs";
|
|
3
|
+
import {ethers} from "ethers";
|
|
4
4
|
|
|
5
|
-
export class
|
|
5
|
+
export class UnlockAccountAction extends BaseAction {
|
|
6
6
|
async execute(): Promise<void> {
|
|
7
7
|
this.startSpinner("Checking keychain availability...");
|
|
8
8
|
|
|
@@ -12,30 +12,30 @@ export class UnlockAction extends BaseAction {
|
|
|
12
12
|
return;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
this.setSpinnerText("Checking for existing
|
|
15
|
+
this.setSpinnerText("Checking for existing account...");
|
|
16
16
|
|
|
17
17
|
const keypairPath = this.getConfigByKey("keyPairPath");
|
|
18
18
|
if (!keypairPath || !existsSync(keypairPath)) {
|
|
19
|
-
this.failSpinner("No
|
|
19
|
+
this.failSpinner("No account found. Run 'genlayer account create' first.");
|
|
20
20
|
return;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
const keystoreData = JSON.parse(readFileSync(keypairPath, "utf-8"));
|
|
24
24
|
if (!this.isValidKeystoreFormat(keystoreData)) {
|
|
25
|
-
this.failSpinner("Invalid keystore format.
|
|
25
|
+
this.failSpinner("Invalid keystore format.");
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
this.stopSpinner();
|
|
30
30
|
|
|
31
31
|
try {
|
|
32
|
-
const password = await this.promptPassword("Enter password to
|
|
32
|
+
const password = await this.promptPassword("Enter password to unlock account:");
|
|
33
33
|
const wallet = await ethers.Wallet.fromEncryptedJson(keystoreData.encrypted, password);
|
|
34
|
-
|
|
34
|
+
|
|
35
35
|
await this.keychainManager.storePrivateKey(wallet.privateKey);
|
|
36
|
-
this.succeedSpinner("
|
|
36
|
+
this.succeedSpinner("Account unlocked! Private key cached in OS keychain.");
|
|
37
37
|
} catch (error) {
|
|
38
|
-
this.failSpinner("Failed to unlock
|
|
38
|
+
this.failSpinner("Failed to unlock account.", error);
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
-
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {BaseAction} from "../../lib/actions/BaseAction";
|
|
2
|
+
import type {Address} from "genlayer-js/types";
|
|
3
|
+
|
|
4
|
+
export interface CodeOptions {
|
|
5
|
+
rpc?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class CodeAction extends BaseAction {
|
|
9
|
+
constructor() {
|
|
10
|
+
super();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async code({
|
|
14
|
+
contractAddress,
|
|
15
|
+
rpc,
|
|
16
|
+
}: {
|
|
17
|
+
contractAddress: string;
|
|
18
|
+
rpc?: string;
|
|
19
|
+
}): Promise<void> {
|
|
20
|
+
const client = await this.getClient(rpc, true);
|
|
21
|
+
await client.initializeConsensusSmartContract();
|
|
22
|
+
this.startSpinner(`Getting code for contract at ${contractAddress}...`);
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const result = await client.getContractCode(contractAddress as Address);
|
|
26
|
+
this.succeedSpinner("Contract code retrieved successfully", result);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
this.failSpinner("Error retrieving contract code", error);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
@@ -3,6 +3,7 @@ import {DeployAction, DeployOptions, DeployScriptsOptions} from "./deploy";
|
|
|
3
3
|
import {CallAction, CallOptions} from "./call";
|
|
4
4
|
import {WriteAction, WriteOptions} from "./write";
|
|
5
5
|
import {SchemaAction, SchemaOptions} from "./schema";
|
|
6
|
+
import {CodeAction, CodeOptions} from "./code";
|
|
6
7
|
|
|
7
8
|
function parseArg(value: string, previous: any[] = []): any[] {
|
|
8
9
|
if (value === "true") return [...previous, true];
|
|
@@ -72,5 +73,14 @@ export function initializeContractsCommands(program: Command) {
|
|
|
72
73
|
await schemaAction.schema({contractAddress, ...options});
|
|
73
74
|
});
|
|
74
75
|
|
|
76
|
+
program
|
|
77
|
+
.command("code <contractAddress>")
|
|
78
|
+
.description("Get the source for a deployed contract")
|
|
79
|
+
.option("--rpc <rpcUrl>", "RPC URL for the network")
|
|
80
|
+
.action(async (contractAddress: string, options: CodeOptions) => {
|
|
81
|
+
const codeAction = new CodeAction();
|
|
82
|
+
await codeAction.code({contractAddress, ...options});
|
|
83
|
+
});
|
|
84
|
+
|
|
75
85
|
return program;
|
|
76
86
|
}
|
|
@@ -38,7 +38,7 @@ export class NetworkActions extends BaseAction {
|
|
|
38
38
|
this.failSpinner(`Network ${networkName} not found`);
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
|
-
this.writeConfig("network",
|
|
41
|
+
this.writeConfig("network", selectedNetwork.alias);
|
|
42
42
|
this.succeedSpinner(`Network successfully set to ${selectedNetwork.name}`);
|
|
43
43
|
return;
|
|
44
44
|
}
|
|
@@ -48,13 +48,14 @@ export class NetworkActions extends BaseAction {
|
|
|
48
48
|
type: "list",
|
|
49
49
|
name: "selectedNetwork",
|
|
50
50
|
message: "Select which network do you want to use:",
|
|
51
|
-
choices: networks,
|
|
51
|
+
choices: networks.map(n => ({name: n.name, value: n.alias})),
|
|
52
52
|
},
|
|
53
53
|
];
|
|
54
54
|
const networkAnswer = await inquirer.prompt(networkQuestions);
|
|
55
|
-
const
|
|
55
|
+
const selectedAlias = networkAnswer.selectedNetwork;
|
|
56
|
+
const selectedNetwork = networks.find(n => n.alias === selectedAlias)!;
|
|
56
57
|
|
|
57
|
-
this.writeConfig("network",
|
|
58
|
+
this.writeConfig("network", selectedAlias);
|
|
58
59
|
this.succeedSpinner(`Network successfully set to ${selectedNetwork.name}`);
|
|
59
60
|
}
|
|
60
61
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import {BaseAction, BUILT_IN_NETWORKS, resolveNetwork} from "../../lib/actions/BaseAction";
|
|
2
|
+
import {createClient, createAccount, formatStakingAmount, parseStakingAmount, abi} from "genlayer-js";
|
|
3
|
+
import type {GenLayerClient, GenLayerChain, Address} from "genlayer-js/types";
|
|
4
|
+
import {readFileSync, existsSync} from "fs";
|
|
5
|
+
import {ethers} from "ethers";
|
|
6
|
+
import {KeystoreData} from "../../lib/interfaces/KeystoreData";
|
|
7
|
+
|
|
8
|
+
export interface StakingConfig {
|
|
9
|
+
rpc?: string;
|
|
10
|
+
stakingAddress?: string;
|
|
11
|
+
network?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class StakingAction extends BaseAction {
|
|
15
|
+
private _stakingClient: GenLayerClient<GenLayerChain> | null = null;
|
|
16
|
+
|
|
17
|
+
constructor() {
|
|
18
|
+
super();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private getNetwork(config: StakingConfig): GenLayerChain {
|
|
22
|
+
// Priority: --network option > global config > localnet default
|
|
23
|
+
if (config.network) {
|
|
24
|
+
const network = BUILT_IN_NETWORKS[config.network];
|
|
25
|
+
if (!network) {
|
|
26
|
+
throw new Error(`Unknown network: ${config.network}. Available: ${Object.keys(BUILT_IN_NETWORKS).join(", ")}`);
|
|
27
|
+
}
|
|
28
|
+
return {...network};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return resolveNetwork(this.getConfig().network);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
protected async getStakingClient(config: StakingConfig): Promise<GenLayerClient<GenLayerChain>> {
|
|
35
|
+
if (!this._stakingClient) {
|
|
36
|
+
const network = this.getNetwork(config);
|
|
37
|
+
|
|
38
|
+
// Override staking address if provided
|
|
39
|
+
if (config.stakingAddress) {
|
|
40
|
+
network.stakingContract = {
|
|
41
|
+
address: config.stakingAddress,
|
|
42
|
+
abi: abi.STAKING_ABI,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const privateKey = await this.getPrivateKeyForStaking();
|
|
47
|
+
const account = createAccount(privateKey as `0x${string}`);
|
|
48
|
+
|
|
49
|
+
this._stakingClient = createClient({
|
|
50
|
+
chain: network,
|
|
51
|
+
endpoint: config.rpc,
|
|
52
|
+
account,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return this._stakingClient;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
protected async getReadOnlyStakingClient(config: StakingConfig): Promise<GenLayerClient<GenLayerChain>> {
|
|
59
|
+
const network = this.getNetwork(config);
|
|
60
|
+
|
|
61
|
+
if (config.stakingAddress) {
|
|
62
|
+
network.stakingContract = {
|
|
63
|
+
address: config.stakingAddress,
|
|
64
|
+
abi: abi.STAKING_ABI,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const keypairPath = this.getConfigByKey("keyPairPath");
|
|
69
|
+
if (!keypairPath || !existsSync(keypairPath)) {
|
|
70
|
+
throw new Error("No account found. Run 'genlayer account create' first.");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const keystoreData: KeystoreData = JSON.parse(readFileSync(keypairPath, "utf-8"));
|
|
74
|
+
|
|
75
|
+
return createClient({
|
|
76
|
+
chain: network,
|
|
77
|
+
endpoint: config.rpc,
|
|
78
|
+
account: keystoreData.address as Address,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private async getPrivateKeyForStaking(): Promise<string> {
|
|
83
|
+
const keypairPath = this.getConfigByKey("keyPairPath");
|
|
84
|
+
|
|
85
|
+
if (!keypairPath || !existsSync(keypairPath)) {
|
|
86
|
+
throw new Error("No account found. Run 'genlayer account create' first.");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const keystoreData: KeystoreData = JSON.parse(readFileSync(keypairPath, "utf-8"));
|
|
90
|
+
|
|
91
|
+
if (!this.isValidKeystoreFormat(keystoreData)) {
|
|
92
|
+
throw new Error("Invalid keystore format.");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const cachedKey = await this.keychainManager.getPrivateKey();
|
|
96
|
+
if (cachedKey) {
|
|
97
|
+
return cachedKey;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Stop spinner before prompting for password
|
|
101
|
+
this.stopSpinner();
|
|
102
|
+
const password = await this.promptPassword("Enter password to unlock account:");
|
|
103
|
+
this.startSpinner("Continuing...");
|
|
104
|
+
|
|
105
|
+
const wallet = await ethers.Wallet.fromEncryptedJson(keystoreData.encrypted, password);
|
|
106
|
+
return wallet.privateKey;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
protected parseAmount(amount: string): bigint {
|
|
110
|
+
return parseStakingAmount(amount);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
protected formatAmount(amount: bigint): string {
|
|
114
|
+
return formatStakingAmount(amount);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
protected async getSignerAddress(): Promise<Address> {
|
|
118
|
+
const keypairPath = this.getConfigByKey("keyPairPath");
|
|
119
|
+
if (!keypairPath || !existsSync(keypairPath)) {
|
|
120
|
+
throw new Error("Keypair file not found.");
|
|
121
|
+
}
|
|
122
|
+
const keystoreData: KeystoreData = JSON.parse(readFileSync(keypairPath, "utf-8"));
|
|
123
|
+
return keystoreData.address as Address;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {StakingAction, StakingConfig} from "./StakingAction";
|
|
2
|
+
import type {Address} from "genlayer-js/types";
|
|
3
|
+
|
|
4
|
+
export interface DelegatorClaimOptions extends StakingConfig {
|
|
5
|
+
validator: string;
|
|
6
|
+
delegator?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class DelegatorClaimAction extends StakingAction {
|
|
10
|
+
constructor() {
|
|
11
|
+
super();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async execute(options: DelegatorClaimOptions): Promise<void> {
|
|
15
|
+
this.startSpinner("Claiming delegator withdrawals...");
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const client = await this.getStakingClient(options);
|
|
19
|
+
const delegatorAddress = options.delegator || (await this.getSignerAddress());
|
|
20
|
+
|
|
21
|
+
this.setSpinnerText(`Claiming for delegator ${delegatorAddress} from validator ${options.validator}...`);
|
|
22
|
+
|
|
23
|
+
const result = await client.delegatorClaim({
|
|
24
|
+
validator: options.validator as Address,
|
|
25
|
+
delegator: delegatorAddress as Address,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const output = {
|
|
29
|
+
transactionHash: result.transactionHash,
|
|
30
|
+
delegator: delegatorAddress,
|
|
31
|
+
validator: options.validator,
|
|
32
|
+
blockNumber: result.blockNumber.toString(),
|
|
33
|
+
gasUsed: result.gasUsed.toString(),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
this.succeedSpinner("Claim successful!", output);
|
|
37
|
+
} catch (error: any) {
|
|
38
|
+
this.failSpinner("Failed to claim", error.message || error);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {StakingAction, StakingConfig} from "./StakingAction";
|
|
2
|
+
import type {Address} from "genlayer-js/types";
|
|
3
|
+
|
|
4
|
+
export interface DelegatorExitOptions extends StakingConfig {
|
|
5
|
+
validator: string;
|
|
6
|
+
shares: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class DelegatorExitAction extends StakingAction {
|
|
10
|
+
constructor() {
|
|
11
|
+
super();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async execute(options: DelegatorExitOptions): Promise<void> {
|
|
15
|
+
this.startSpinner("Initiating delegator exit...");
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
let shares: bigint;
|
|
19
|
+
try {
|
|
20
|
+
shares = BigInt(options.shares);
|
|
21
|
+
if (shares <= 0n) throw new Error("must be positive");
|
|
22
|
+
} catch {
|
|
23
|
+
this.failSpinner(`Invalid shares value: "${options.shares}". Must be a positive whole number.`);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const client = await this.getStakingClient(options);
|
|
28
|
+
|
|
29
|
+
this.setSpinnerText(`Exiting ${shares} shares from validator ${options.validator}...`);
|
|
30
|
+
|
|
31
|
+
const result = await client.delegatorExit({
|
|
32
|
+
validator: options.validator as Address,
|
|
33
|
+
shares,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const output = {
|
|
37
|
+
transactionHash: result.transactionHash,
|
|
38
|
+
validator: options.validator,
|
|
39
|
+
sharesWithdrawn: shares.toString(),
|
|
40
|
+
blockNumber: result.blockNumber.toString(),
|
|
41
|
+
gasUsed: result.gasUsed.toString(),
|
|
42
|
+
note: "Withdrawal will be claimable after the unbonding period",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
this.succeedSpinner("Exit initiated successfully!", output);
|
|
46
|
+
} catch (error: any) {
|
|
47
|
+
this.failSpinner("Failed to exit", error.message || error);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {StakingAction, StakingConfig} from "./StakingAction";
|
|
2
|
+
import type {Address} from "genlayer-js/types";
|
|
3
|
+
|
|
4
|
+
export interface DelegatorJoinOptions extends StakingConfig {
|
|
5
|
+
validator: string;
|
|
6
|
+
amount: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class DelegatorJoinAction extends StakingAction {
|
|
10
|
+
constructor() {
|
|
11
|
+
super();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async execute(options: DelegatorJoinOptions): Promise<void> {
|
|
15
|
+
this.startSpinner("Joining as delegator...");
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const client = await this.getStakingClient(options);
|
|
19
|
+
const amount = this.parseAmount(options.amount);
|
|
20
|
+
|
|
21
|
+
this.setSpinnerText(`Delegating ${this.formatAmount(amount)} to validator ${options.validator}...`);
|
|
22
|
+
|
|
23
|
+
const result = await client.delegatorJoin({
|
|
24
|
+
validator: options.validator as Address,
|
|
25
|
+
amount,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const output = {
|
|
29
|
+
transactionHash: result.transactionHash,
|
|
30
|
+
validator: result.validator,
|
|
31
|
+
amount: result.amount,
|
|
32
|
+
delegator: result.delegator,
|
|
33
|
+
blockNumber: result.blockNumber.toString(),
|
|
34
|
+
gasUsed: result.gasUsed.toString(),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
this.succeedSpinner("Successfully joined as delegator!", output);
|
|
38
|
+
} catch (error: any) {
|
|
39
|
+
this.failSpinner("Failed to join as delegator", error.message || error);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|