genlayer 0.38.8 → 0.38.10

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.
Files changed (205) hide show
  1. package/.eslintignore +2 -0
  2. package/.github/workflows/cli-docs.yml +124 -0
  3. package/.github/workflows/publish.yml +55 -0
  4. package/.github/workflows/smoke.yml +27 -0
  5. package/.github/workflows/validate-code.yml +51 -0
  6. package/.prettierignore +19 -0
  7. package/.prettierrc +12 -0
  8. package/.release-it.json +66 -0
  9. package/CHANGELOG.md +545 -0
  10. package/CLAUDE.md +55 -0
  11. package/CONTRIBUTING.md +117 -0
  12. package/dist/index.js +221 -62
  13. package/docs/api-references/_meta.json +9 -0
  14. package/docs/api-references/accounts/_meta.json +3 -0
  15. package/docs/api-references/accounts/account/create.mdx +19 -0
  16. package/docs/api-references/accounts/account/export.mdx +19 -0
  17. package/docs/api-references/accounts/account/import.mdx +22 -0
  18. package/docs/api-references/accounts/account/list.mdx +15 -0
  19. package/docs/api-references/accounts/account/lock.mdx +16 -0
  20. package/docs/api-references/accounts/account/remove.mdx +20 -0
  21. package/docs/api-references/accounts/account/send.mdx +24 -0
  22. package/docs/api-references/accounts/account/show.mdx +17 -0
  23. package/docs/api-references/accounts/account/unlock.mdx +17 -0
  24. package/docs/api-references/accounts/account/use.mdx +19 -0
  25. package/docs/api-references/accounts/account.mdx +32 -0
  26. package/docs/api-references/configuration/_meta.json +4 -0
  27. package/docs/api-references/configuration/config/get.mdx +21 -0
  28. package/docs/api-references/configuration/config/reset.mdx +21 -0
  29. package/docs/api-references/configuration/config/set.mdx +21 -0
  30. package/docs/api-references/configuration/config.mdx +25 -0
  31. package/docs/api-references/configuration/network/info.mdx +15 -0
  32. package/docs/api-references/configuration/network/list.mdx +15 -0
  33. package/docs/api-references/configuration/network/set.mdx +21 -0
  34. package/docs/api-references/configuration/network.mdx +25 -0
  35. package/docs/api-references/contracts/_meta.json +7 -0
  36. package/docs/api-references/contracts/call.mdx +21 -0
  37. package/docs/api-references/contracts/code.mdx +20 -0
  38. package/docs/api-references/contracts/deploy.mdx +17 -0
  39. package/docs/api-references/contracts/schema.mdx +20 -0
  40. package/docs/api-references/contracts/write.mdx +21 -0
  41. package/docs/api-references/environment/_meta.json +7 -0
  42. package/docs/api-references/environment/init.mdx +20 -0
  43. package/docs/api-references/environment/new.mdx +21 -0
  44. package/docs/api-references/environment/stop.mdx +15 -0
  45. package/docs/api-references/environment/up.mdx +20 -0
  46. package/docs/api-references/environment/update/ollama.mdx +16 -0
  47. package/docs/api-references/environment/update.mdx +23 -0
  48. package/docs/api-references/index.mdx +35 -0
  49. package/docs/api-references/localnet/_meta.json +3 -0
  50. package/docs/api-references/localnet/localnet/validators/count.mdx +15 -0
  51. package/docs/api-references/localnet/localnet/validators/create-random.mdx +16 -0
  52. package/docs/api-references/localnet/localnet/validators/create.mdx +19 -0
  53. package/docs/api-references/localnet/localnet/validators/delete.mdx +16 -0
  54. package/docs/api-references/localnet/localnet/validators/get.mdx +16 -0
  55. package/docs/api-references/localnet/localnet/validators/update.mdx +23 -0
  56. package/docs/api-references/localnet/localnet/validators.mdx +28 -0
  57. package/docs/api-references/localnet/localnet.mdx +23 -0
  58. package/docs/api-references/staking/_meta.json +3 -0
  59. package/docs/api-references/staking/staking/active-validators.mdx +18 -0
  60. package/docs/api-references/staking/staking/banned-validators.mdx +18 -0
  61. package/docs/api-references/staking/staking/delegation-info.mdx +25 -0
  62. package/docs/api-references/staking/staking/delegator-claim.mdx +26 -0
  63. package/docs/api-references/staking/staking/delegator-exit.mdx +26 -0
  64. package/docs/api-references/staking/staking/delegator-join.mdx +26 -0
  65. package/docs/api-references/staking/staking/epoch-info.mdx +19 -0
  66. package/docs/api-references/staking/staking/prime-all.mdx +20 -0
  67. package/docs/api-references/staking/staking/quarantined-validators.mdx +18 -0
  68. package/docs/api-references/staking/staking/set-identity.mdx +33 -0
  69. package/docs/api-references/staking/staking/set-operator.mdx +26 -0
  70. package/docs/api-references/staking/staking/validator-claim.mdx +24 -0
  71. package/docs/api-references/staking/staking/validator-deposit.mdx +25 -0
  72. package/docs/api-references/staking/staking/validator-exit.mdx +25 -0
  73. package/docs/api-references/staking/staking/validator-history.mdx +29 -0
  74. package/docs/api-references/staking/staking/validator-info.mdx +25 -0
  75. package/docs/api-references/staking/staking/validator-join.mdx +22 -0
  76. package/docs/api-references/staking/staking/validator-prime.mdx +25 -0
  77. package/docs/api-references/staking/staking/validators.mdx +19 -0
  78. package/docs/api-references/staking/staking/wizard.mdx +20 -0
  79. package/docs/api-references/staking/staking.mdx +42 -0
  80. package/docs/api-references/transactions/_meta.json +6 -0
  81. package/docs/api-references/transactions/appeal-bond.mdx +20 -0
  82. package/docs/api-references/transactions/appeal.mdx +21 -0
  83. package/docs/api-references/transactions/receipt.mdx +25 -0
  84. package/docs/api-references/transactions/trace.mdx +21 -0
  85. package/docs/delegator-guide.md +203 -0
  86. package/docs/validator-guide.md +329 -0
  87. package/esbuild.config.dev.js +17 -0
  88. package/esbuild.config.js +22 -0
  89. package/esbuild.config.prod.js +17 -0
  90. package/eslint.config.js +60 -0
  91. package/package.json +2 -11
  92. package/renovate.json +22 -0
  93. package/scripts/generate-cli-docs.mjs +68 -5
  94. package/src/commands/account/create.ts +30 -0
  95. package/src/commands/account/export.ts +106 -0
  96. package/src/commands/account/import.ts +135 -0
  97. package/src/commands/account/index.ts +129 -0
  98. package/src/commands/account/list.ts +34 -0
  99. package/src/commands/account/lock.ts +39 -0
  100. package/src/commands/account/remove.ts +30 -0
  101. package/src/commands/account/send.ts +162 -0
  102. package/src/commands/account/show.ts +74 -0
  103. package/src/commands/account/unlock.ts +56 -0
  104. package/src/commands/account/use.ts +21 -0
  105. package/src/commands/config/getSetReset.ts +51 -0
  106. package/src/commands/config/index.ts +30 -0
  107. package/src/commands/contracts/call.ts +39 -0
  108. package/src/commands/contracts/code.ts +33 -0
  109. package/src/commands/contracts/deploy.ts +161 -0
  110. package/src/commands/contracts/index.ts +150 -0
  111. package/src/commands/contracts/schema.ts +31 -0
  112. package/src/commands/contracts/write.ts +49 -0
  113. package/src/commands/general/index.ts +45 -0
  114. package/src/commands/general/init.ts +180 -0
  115. package/src/commands/general/start.ts +128 -0
  116. package/src/commands/general/stop.ts +26 -0
  117. package/src/commands/localnet/index.ts +100 -0
  118. package/src/commands/localnet/validators.ts +269 -0
  119. package/src/commands/network/index.ts +29 -0
  120. package/src/commands/network/setNetwork.ts +77 -0
  121. package/src/commands/scaffold/index.ts +16 -0
  122. package/src/commands/scaffold/new.ts +34 -0
  123. package/src/commands/staking/StakingAction.ts +279 -0
  124. package/src/commands/staking/delegatorClaim.ts +41 -0
  125. package/src/commands/staking/delegatorExit.ts +56 -0
  126. package/src/commands/staking/delegatorJoin.ts +44 -0
  127. package/src/commands/staking/index.ts +357 -0
  128. package/src/commands/staking/setIdentity.ts +78 -0
  129. package/src/commands/staking/setOperator.ts +46 -0
  130. package/src/commands/staking/stakingInfo.ts +584 -0
  131. package/src/commands/staking/validatorClaim.ts +43 -0
  132. package/src/commands/staking/validatorDeposit.ts +48 -0
  133. package/src/commands/staking/validatorExit.ts +63 -0
  134. package/src/commands/staking/validatorHistory.ts +300 -0
  135. package/src/commands/staking/validatorJoin.ts +47 -0
  136. package/src/commands/staking/validatorPrime.ts +73 -0
  137. package/src/commands/staking/wizard.ts +809 -0
  138. package/src/commands/transactions/appeal.ts +83 -0
  139. package/src/commands/transactions/index.ts +60 -0
  140. package/src/commands/transactions/receipt.ts +90 -0
  141. package/src/commands/transactions/trace.ts +42 -0
  142. package/src/commands/update/index.ts +25 -0
  143. package/src/commands/update/ollama.ts +103 -0
  144. package/src/lib/actions/BaseAction.ts +301 -0
  145. package/src/lib/clients/jsonRpcClient.ts +41 -0
  146. package/src/lib/clients/system.ts +73 -0
  147. package/src/lib/config/ConfigFileManager.ts +194 -0
  148. package/src/lib/config/KeychainManager.ts +89 -0
  149. package/src/lib/config/simulator.ts +68 -0
  150. package/src/lib/config/text.ts +2 -0
  151. package/src/lib/errors/missingRequirement.ts +9 -0
  152. package/src/lib/errors/versionRequired.ts +9 -0
  153. package/src/lib/interfaces/ISimulatorService.ts +39 -0
  154. package/src/lib/services/simulator.ts +386 -0
  155. package/src/types/node-fetch.d.ts +1 -0
  156. package/tests/actions/appeal.test.ts +141 -0
  157. package/tests/actions/call.test.ts +94 -0
  158. package/tests/actions/code.test.ts +87 -0
  159. package/tests/actions/create.test.ts +65 -0
  160. package/tests/actions/deploy.test.ts +420 -0
  161. package/tests/actions/getSetReset.test.ts +88 -0
  162. package/tests/actions/init.test.ts +483 -0
  163. package/tests/actions/lock.test.ts +86 -0
  164. package/tests/actions/new.test.ts +80 -0
  165. package/tests/actions/ollama.test.ts +193 -0
  166. package/tests/actions/receipt.test.ts +261 -0
  167. package/tests/actions/schema.test.ts +94 -0
  168. package/tests/actions/setNetwork.test.ts +161 -0
  169. package/tests/actions/staking.test.ts +280 -0
  170. package/tests/actions/start.test.ts +257 -0
  171. package/tests/actions/stop.test.ts +77 -0
  172. package/tests/actions/unlock.test.ts +139 -0
  173. package/tests/actions/validators.test.ts +750 -0
  174. package/tests/actions/write.test.ts +102 -0
  175. package/tests/commands/account.test.ts +146 -0
  176. package/tests/commands/appeal.test.ts +97 -0
  177. package/tests/commands/call.test.ts +78 -0
  178. package/tests/commands/code.test.ts +69 -0
  179. package/tests/commands/config.test.ts +54 -0
  180. package/tests/commands/deploy.test.ts +83 -0
  181. package/tests/commands/init.test.ts +101 -0
  182. package/tests/commands/localnet.test.ts +131 -0
  183. package/tests/commands/network.test.ts +60 -0
  184. package/tests/commands/new.test.ts +68 -0
  185. package/tests/commands/parseArg.test.ts +156 -0
  186. package/tests/commands/receipt.test.ts +142 -0
  187. package/tests/commands/schema.test.ts +67 -0
  188. package/tests/commands/staking.test.ts +329 -0
  189. package/tests/commands/stop.test.ts +27 -0
  190. package/tests/commands/up.test.ts +105 -0
  191. package/tests/commands/update.test.ts +45 -0
  192. package/tests/commands/write.test.ts +76 -0
  193. package/tests/index.test.ts +56 -0
  194. package/tests/libs/baseAction.test.ts +535 -0
  195. package/tests/libs/configFileManager.test.ts +118 -0
  196. package/tests/libs/jsonRpcClient.test.ts +59 -0
  197. package/tests/libs/keychainManager.test.ts +156 -0
  198. package/tests/libs/platformCommands.test.ts +78 -0
  199. package/tests/libs/system.test.ts +148 -0
  200. package/tests/services/simulator.test.ts +789 -0
  201. package/tests/smoke.test.ts +134 -0
  202. package/tests/utils.ts +13 -0
  203. package/tsconfig.json +120 -0
  204. package/vitest.config.ts +13 -0
  205. package/vitest.smoke.config.ts +17 -0
@@ -0,0 +1,584 @@
1
+ import {StakingAction, StakingConfig} from "./StakingAction";
2
+ import type {Address, ValidatorInfo} from "genlayer-js/types";
3
+ import Table from "cli-table3";
4
+ import chalk from "chalk";
5
+
6
+ // Epoch-related constants
7
+ const ACTIVATION_DELAY_EPOCHS = 2n;
8
+ const UNBONDING_PERIOD_EPOCHS = 7n;
9
+
10
+ export interface StakingInfoOptions extends StakingConfig {
11
+ validator?: string;
12
+ debug?: boolean;
13
+ }
14
+
15
+ export class StakingInfoAction extends StakingAction {
16
+ constructor() {
17
+ super();
18
+ }
19
+
20
+ async getValidatorInfo(options: StakingInfoOptions): Promise<void> {
21
+ this.startSpinner("Fetching validator info...");
22
+
23
+ try {
24
+ const client = await this.getReadOnlyStakingClient(options);
25
+ const validatorAddress = options.validator || (await this.getSignerAddress());
26
+
27
+ const isValidator = await client.isValidator(validatorAddress as Address);
28
+
29
+ if (!isValidator) {
30
+ this.failSpinner(`Address ${validatorAddress} is not a validator`);
31
+ return;
32
+ }
33
+
34
+ const [info, epochInfo] = await Promise.all([
35
+ client.getValidatorInfo(validatorAddress as Address),
36
+ client.getEpochInfo(),
37
+ ]);
38
+
39
+ const currentEpoch = epochInfo.currentEpoch;
40
+
41
+ const result: Record<string, any> = {
42
+ ...(options.debug && {currentEpoch: currentEpoch.toString()}),
43
+ validator: info.address,
44
+ owner: info.owner,
45
+ operator: info.operator,
46
+ vStake: info.vStake,
47
+ vShares: info.vShares.toString(),
48
+ dStake: info.dStake,
49
+ dShares: info.dShares.toString(),
50
+ vDeposit: info.vDeposit,
51
+ vWithdrawal: info.vWithdrawal,
52
+ ePrimed: info.ePrimed.toString(),
53
+ needsPriming: info.needsPriming,
54
+ live: info.live,
55
+ banned: info.banned ? info.bannedEpoch?.toString() : "Not banned",
56
+ selfStakePendingDeposits: (() => {
57
+ // In debug mode, show all deposits; otherwise filter to truly pending only
58
+ const deposits = options.debug
59
+ ? info.pendingDeposits
60
+ : info.pendingDeposits.filter(d => d.epoch + ACTIVATION_DELAY_EPOCHS > currentEpoch);
61
+ return deposits.length > 0
62
+ ? deposits.map(d => {
63
+ const depositEpoch = d.epoch;
64
+ const activationEpoch = depositEpoch + ACTIVATION_DELAY_EPOCHS;
65
+ const epochsUntilActive = activationEpoch - currentEpoch;
66
+ const isActivated = epochsUntilActive <= 0n;
67
+ return {
68
+ epoch: depositEpoch.toString(),
69
+ stake: d.stake,
70
+ shares: d.shares.toString(),
71
+ activatesAtEpoch: activationEpoch.toString(),
72
+ ...(options.debug
73
+ ? {status: isActivated ? "ACTIVATED" : `pending (${epochsUntilActive} epochs)`}
74
+ : {epochsRemaining: epochsUntilActive.toString()}),
75
+ };
76
+ })
77
+ : options.debug ? `None (raw count: ${info.pendingDeposits.length})` : "None";
78
+ })(),
79
+ selfStakePendingWithdrawals:
80
+ info.pendingWithdrawals.length > 0
81
+ ? info.pendingWithdrawals.map(w => {
82
+ const exitEpoch = w.epoch;
83
+ const claimableEpoch = exitEpoch + UNBONDING_PERIOD_EPOCHS;
84
+ const epochsUntilClaimable = claimableEpoch - currentEpoch;
85
+ return {
86
+ epoch: exitEpoch.toString(),
87
+ shares: w.shares.toString(),
88
+ stake: w.stake,
89
+ claimableAtEpoch: claimableEpoch.toString(),
90
+ status:
91
+ epochsUntilClaimable <= 0n
92
+ ? "Claimable now"
93
+ : `Unbonding (${epochsUntilClaimable} epoch${epochsUntilClaimable > 1n ? "s" : ""} remaining)`,
94
+ };
95
+ })
96
+ : "None",
97
+ };
98
+
99
+ // Add identity if set
100
+ if (info.identity?.moniker) {
101
+ result.identity = {
102
+ moniker: info.identity.moniker,
103
+ ...(info.identity.website && {website: info.identity.website}),
104
+ ...(info.identity.description && {description: info.identity.description}),
105
+ ...(info.identity.twitter && {twitter: info.identity.twitter}),
106
+ ...(info.identity.telegram && {telegram: info.identity.telegram}),
107
+ ...(info.identity.github && {github: info.identity.github}),
108
+ ...(info.identity.email && {email: info.identity.email}),
109
+ ...(info.identity.logoUri && {logoUri: info.identity.logoUri}),
110
+ };
111
+ }
112
+
113
+ this.succeedSpinner("Validator info retrieved", result);
114
+ } catch (error: any) {
115
+ this.failSpinner("Failed to get validator info", error.message || error);
116
+ }
117
+ }
118
+
119
+ async getStakeInfo(options: StakingInfoOptions & {delegator?: string}): Promise<void> {
120
+ this.startSpinner("Fetching stake info...");
121
+
122
+ try {
123
+ const client = await this.getReadOnlyStakingClient(options);
124
+ const delegatorAddress = options.delegator || (await this.getSignerAddress());
125
+ const isOwnDelegation = !options.delegator;
126
+
127
+ this.setSpinnerText(`Fetching delegation info for ${delegatorAddress}...`);
128
+
129
+ if (!options.validator) {
130
+ this.failSpinner("Validator address is required");
131
+ return;
132
+ }
133
+
134
+ const [info, epochInfo] = await Promise.all([
135
+ client.getStakeInfo(delegatorAddress as Address, options.validator as Address),
136
+ client.getEpochInfo(),
137
+ ]);
138
+
139
+ const currentEpoch = epochInfo.currentEpoch;
140
+
141
+ // Calculate projected rewards
142
+ let projectedReward = "N/A";
143
+ if (epochInfo.totalWeight > 0n && epochInfo.inflationRaw > 0n && info.stakeRaw > 0n) {
144
+ const rewardRaw = (info.stakeRaw * epochInfo.inflationRaw) / epochInfo.totalWeight;
145
+ projectedReward = client.formatStakingAmount(rewardRaw) + " per epoch";
146
+ } else if (epochInfo.inflationRaw === 0n) {
147
+ projectedReward = "0 GEN (no inflation this epoch)";
148
+ }
149
+
150
+ const result = {
151
+ delegator: info.delegator,
152
+ validator: info.validator,
153
+ shares: info.shares.toString(),
154
+ stake: info.stake,
155
+ projectedReward,
156
+ pendingDeposits: (() => {
157
+ // Filter to only truly pending deposits (not yet active)
158
+ const pending = info.pendingDeposits.filter(d => d.epoch + ACTIVATION_DELAY_EPOCHS > currentEpoch);
159
+ return pending.length > 0
160
+ ? pending.map(d => {
161
+ const depositEpoch = d.epoch;
162
+ const activationEpoch = depositEpoch + ACTIVATION_DELAY_EPOCHS;
163
+ const epochsUntilActive = activationEpoch - currentEpoch;
164
+ return {
165
+ epoch: depositEpoch.toString(),
166
+ stake: d.stake,
167
+ shares: d.shares.toString(),
168
+ activatesAtEpoch: activationEpoch.toString(),
169
+ epochsRemaining: epochsUntilActive.toString(),
170
+ };
171
+ })
172
+ : "None";
173
+ })(),
174
+ pendingWithdrawals:
175
+ info.pendingWithdrawals.length > 0
176
+ ? info.pendingWithdrawals.map(w => {
177
+ const exitEpoch = w.epoch;
178
+ const claimableEpoch = exitEpoch + UNBONDING_PERIOD_EPOCHS; // Must wait 7 full epochs
179
+ const epochsUntilClaimable = claimableEpoch - currentEpoch;
180
+ return {
181
+ epoch: exitEpoch.toString(),
182
+ shares: w.shares.toString(),
183
+ stake: w.stake,
184
+ claimableAtEpoch: claimableEpoch.toString(),
185
+ status:
186
+ epochsUntilClaimable <= 0n
187
+ ? "Claimable now"
188
+ : `Unbonding (${epochsUntilClaimable} epoch${epochsUntilClaimable > 1n ? "s" : ""} remaining)`,
189
+ };
190
+ })
191
+ : "None",
192
+ };
193
+
194
+ const msg = isOwnDelegation ? "Your delegation info" : `Delegation info for ${delegatorAddress}`;
195
+ this.succeedSpinner(msg, result);
196
+ } catch (error: any) {
197
+ this.failSpinner("Failed to get stake info", error.message || error);
198
+ }
199
+ }
200
+
201
+ async getEpochInfo(options: StakingConfig & {epoch?: string}): Promise<void> {
202
+ this.startSpinner("Fetching epoch info...");
203
+
204
+ try {
205
+ const client = await this.getReadOnlyStakingClient(options);
206
+ const info = await client.getEpochInfo();
207
+
208
+ const formatDuration = (ms: number): string => {
209
+ const hours = Math.floor(ms / (1000 * 60 * 60));
210
+ const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60));
211
+ if (hours > 24) {
212
+ const days = Math.floor(hours / 24);
213
+ const remainingHours = hours % 24;
214
+ return `${days}d ${remainingHours}h ${minutes}m`;
215
+ }
216
+ return `${hours}h ${minutes}m`;
217
+ };
218
+
219
+ const formatAmount = client.formatStakingAmount;
220
+
221
+ // If specific epoch requested, show just that epoch's data
222
+ if (options.epoch !== undefined) {
223
+ const epochNum = BigInt(options.epoch);
224
+ const epochData = await client.getEpochData(epochNum);
225
+ const isFinalized = info.lastFinalizedEpoch >= epochNum;
226
+ const startDate = new Date(Number(epochData.start) * 1000);
227
+ const endDate = epochData.end > 0n ? new Date(Number(epochData.end) * 1000) : null;
228
+
229
+ this.succeedSpinner(`Epoch ${epochNum}`);
230
+ console.log(`\n Epoch: ${epochNum}`);
231
+ console.log(` Started: ${startDate.toISOString()}`);
232
+ console.log(` Ended: ${endDate?.toISOString() || "Not yet"}`);
233
+ console.log(` Finalized: ${isFinalized ? "Yes" : "No"}`);
234
+ console.log(` Validators: ${epochData.vcount}`);
235
+ console.log(` Weight: ${epochData.weight}`);
236
+ console.log(` Inflation: ${formatAmount(epochData.inflation)}`);
237
+ console.log(` Claimed: ${formatAmount(epochData.claimed)}`);
238
+ console.log(` Slashed: ${formatAmount(epochData.slashed)}`);
239
+ console.log();
240
+ return;
241
+ }
242
+
243
+ // Default: show current + previous epoch
244
+ const currentEpochData = await client.getEpochData(info.currentEpoch);
245
+ const currentStart = new Date(Number(currentEpochData.start) * 1000);
246
+ const now = Date.now();
247
+ const timeSinceStart = now - currentStart.getTime();
248
+ const timeUntilNext = info.nextEpochEstimate ? info.nextEpochEstimate.getTime() - now : null;
249
+
250
+ this.succeedSpinner("Epoch info");
251
+
252
+ const nextEstimate = timeUntilNext && timeUntilNext > 0
253
+ ? `in ${formatDuration(timeUntilNext)}`
254
+ : currentEpochData.end > 0n ? "Next epoch started" : "N/A";
255
+
256
+ console.log(`\n Current Epoch: ${info.currentEpoch} (started ${formatDuration(timeSinceStart)} ago)`);
257
+ console.log(` Next Epoch: ${nextEstimate}`);
258
+ console.log(` Validators: ${info.activeValidatorsCount}`);
259
+ console.log(` Weight: ${currentEpochData.weight}`);
260
+ console.log(` Slashed: ${formatAmount(currentEpochData.slashed)}`);
261
+
262
+ // Previous epoch (has the actual inflation/rewards data)
263
+ if (info.currentEpoch > 0n) {
264
+ const prevEpoch = info.currentEpoch - 1n;
265
+ const prevData = await client.getEpochData(prevEpoch);
266
+ const isFinalized = info.lastFinalizedEpoch >= prevEpoch;
267
+ const prevEnd = prevData.end > 0n;
268
+
269
+ let status: string;
270
+ if (!prevEnd) {
271
+ status = "still active";
272
+ } else if (isFinalized) {
273
+ status = "finalized";
274
+ } else {
275
+ status = "finalizing txs...";
276
+ }
277
+
278
+ console.log(`\n Previous Epoch: ${prevEpoch} (${status})`);
279
+ console.log(` Inflation: ${formatAmount(prevData.inflation)}`);
280
+ console.log(` Claimed: ${formatAmount(prevData.claimed)}`);
281
+ console.log(` Unclaimed: ${formatAmount(prevData.inflation - prevData.claimed)}`);
282
+ console.log(` Slashed: ${formatAmount(prevData.slashed)}`);
283
+ }
284
+
285
+ console.log(`\n Min Epoch Duration: ${formatDuration(Number(info.epochMinDuration) * 1000)}`);
286
+ console.log(` Validator Min Stake: ${info.validatorMinStake}`);
287
+ console.log(` Delegator Min Stake: ${info.delegatorMinStake}\n`);
288
+ } catch (error: any) {
289
+ this.failSpinner("Failed to get epoch info", error.message || error);
290
+ }
291
+ }
292
+
293
+ async listActiveValidators(options: StakingConfig): Promise<void> {
294
+ this.startSpinner("Fetching active validators...");
295
+
296
+ try {
297
+ const client = await this.getReadOnlyStakingClient(options);
298
+
299
+ const activeValidators = await client.getActiveValidators();
300
+
301
+ const result = {
302
+ count: activeValidators.length,
303
+ validators: activeValidators,
304
+ };
305
+
306
+ this.succeedSpinner("Active validators retrieved", result);
307
+ } catch (error: any) {
308
+ this.failSpinner("Failed to get active validators", error.message || error);
309
+ }
310
+ }
311
+
312
+ async listQuarantinedValidators(options: StakingConfig): Promise<void> {
313
+ this.startSpinner("Fetching quarantined validators...");
314
+
315
+ try {
316
+ const client = await this.getReadOnlyStakingClient(options);
317
+
318
+ const validators = await client.getQuarantinedValidatorsDetailed();
319
+
320
+ const result = {
321
+ count: validators.length,
322
+ validators: validators.map(v => ({
323
+ validator: v.validator,
324
+ untilEpoch: v.untilEpoch.toString(),
325
+ permanentlyBanned: v.permanentlyBanned,
326
+ })),
327
+ };
328
+
329
+ this.succeedSpinner("Quarantined validators retrieved", result);
330
+ } catch (error: any) {
331
+ this.failSpinner("Failed to get quarantined validators", error.message || error);
332
+ }
333
+ }
334
+
335
+ async listBannedValidators(options: StakingConfig): Promise<void> {
336
+ this.startSpinner("Fetching banned validators...");
337
+
338
+ try {
339
+ const client = await this.getReadOnlyStakingClient(options);
340
+
341
+ const validators = await client.getBannedValidators();
342
+
343
+ const result = {
344
+ count: validators.length,
345
+ validators: validators.map(v => ({
346
+ validator: v.validator,
347
+ untilEpoch: v.permanentlyBanned ? "permanent" : v.untilEpoch.toString(),
348
+ permanentlyBanned: v.permanentlyBanned,
349
+ })),
350
+ };
351
+
352
+ this.succeedSpinner("Banned validators retrieved", result);
353
+ } catch (error: any) {
354
+ this.failSpinner("Failed to get banned validators", error.message || error);
355
+ }
356
+ }
357
+
358
+ async listValidators(options: StakingConfig & {all?: boolean}): Promise<void> {
359
+ this.startSpinner("Fetching validator set...");
360
+
361
+ try {
362
+ const client = await this.getReadOnlyStakingClient(options);
363
+
364
+ // Get current user's address to mark "mine"
365
+ let myAddress: Address | null = null;
366
+ try {
367
+ myAddress = await this.getSignerAddress();
368
+ } catch {
369
+ // No account configured, that's fine
370
+ }
371
+
372
+ // Use tree traversal to get ALL validators (including not-yet-primed)
373
+ const allTreeAddresses = await this.getAllValidatorsFromTree(options);
374
+
375
+ // Also fetch status lists in parallel
376
+ const [activeAddresses, quarantinedList, bannedList, epochInfo] = await Promise.all([
377
+ client.getActiveValidators(),
378
+ client.getQuarantinedValidatorsDetailed(),
379
+ options.all ? client.getBannedValidators() : Promise.resolve([]),
380
+ client.getEpochInfo(),
381
+ ]);
382
+
383
+ // Build set of quarantined/banned for status lookup
384
+ const quarantinedSet = new Map(quarantinedList.map(v => [v.validator.toLowerCase(), v]));
385
+ const bannedSet = new Map(bannedList.map(v => [v.validator.toLowerCase(), v]));
386
+ const activeSet = new Set(activeAddresses.map(a => a.toLowerCase()));
387
+
388
+ // Filter out banned if not --all
389
+ const allAddresses = options.all
390
+ ? allTreeAddresses
391
+ : allTreeAddresses.filter(addr => !bannedSet.has(addr.toLowerCase()));
392
+
393
+ this.setSpinnerText(`Fetching details for ${allAddresses.length} validators...`);
394
+
395
+ // Fetch detailed info in batches to avoid rate limiting
396
+ const BATCH_SIZE = 5;
397
+ const addressArray = Array.from(allAddresses);
398
+ const validatorInfos: ValidatorInfo[] = [];
399
+
400
+ for (let i = 0; i < addressArray.length; i += BATCH_SIZE) {
401
+ const batch = addressArray.slice(i, i + BATCH_SIZE);
402
+ const batchResults = await Promise.all(
403
+ batch.map(addr => client.getValidatorInfo(addr as Address))
404
+ );
405
+ validatorInfos.push(...batchResults);
406
+ if (i + BATCH_SIZE < addressArray.length) {
407
+ this.setSpinnerText(`Fetching details... ${Math.min(i + BATCH_SIZE, addressArray.length)}/${addressArray.length}`);
408
+ }
409
+ }
410
+
411
+ // Build table rows
412
+ type ValidatorRow = {
413
+ info: ValidatorInfo;
414
+ status: string;
415
+ isMine: boolean;
416
+ totalStakeRaw: bigint;
417
+ };
418
+
419
+ const rows: ValidatorRow[] = validatorInfos.map(info => {
420
+ const addrLower = info.address.toLowerCase();
421
+ const isQuarantined = quarantinedSet.has(addrLower);
422
+ const isBanned = bannedSet.has(addrLower);
423
+ const isActive = activeSet.has(addrLower);
424
+
425
+ let status = "";
426
+ if (isBanned) {
427
+ const banInfo = bannedSet.get(addrLower)!;
428
+ status = banInfo.permanentlyBanned ? "BANNED" : `banned(e${banInfo.untilEpoch})`;
429
+ } else if (isQuarantined) {
430
+ const qInfo = quarantinedSet.get(addrLower)!;
431
+ status = `quarant(e${qInfo.untilEpoch})`;
432
+ } else if (isActive) {
433
+ status = "active";
434
+ } else {
435
+ status = "pending";
436
+ }
437
+
438
+ const isMine = myAddress
439
+ ? info.owner.toLowerCase() === myAddress.toLowerCase() ||
440
+ info.operator.toLowerCase() === myAddress.toLowerCase()
441
+ : false;
442
+
443
+ return {
444
+ info,
445
+ status,
446
+ isMine,
447
+ totalStakeRaw: info.vStakeRaw + info.dStakeRaw,
448
+ };
449
+ });
450
+
451
+ // Calculate validator weight using the contract formula:
452
+ // weight = (vStake * alpha + dStake * (1 - alpha)) ^ beta
453
+ // Default: alpha = 0.6, beta = 0.5 (square root)
454
+ const ALPHA = 0.6;
455
+ const BETA = 0.5;
456
+ const calcWeight = (vStakeRaw: bigint, dStakeRaw: bigint): number => {
457
+ const vStake = Number(vStakeRaw) / 1e18;
458
+ const dStake = Number(dStakeRaw) / 1e18;
459
+ const util = vStake * ALPHA + dStake * (1 - ALPHA);
460
+ return Math.pow(util, BETA);
461
+ };
462
+
463
+ // Add weight to rows and sort by weight descending
464
+ const rowsWithWeight = rows.map(r => ({
465
+ ...r,
466
+ weight: calcWeight(r.info.vStakeRaw, r.info.dStakeRaw),
467
+ }));
468
+ rowsWithWeight.sort((a, b) => b.weight - a.weight);
469
+
470
+ // Calculate total weight for active validators only (for power %)
471
+ const totalWeight = rowsWithWeight
472
+ .filter(r => r.status === "active")
473
+ .reduce((sum, r) => sum + r.weight, 0);
474
+
475
+ this.stopSpinner();
476
+
477
+ // Format stake - shorten large numbers
478
+ const formatStake = (s: string) => {
479
+ const num = parseFloat(s.replace(" GEN", ""));
480
+ if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`;
481
+ if (num >= 1000) return `${(num / 1000).toFixed(1)}K`;
482
+ if (num >= 1) return num.toFixed(1);
483
+ if (num > 0) return num.toPrecision(2);
484
+ return "0";
485
+ };
486
+
487
+ // Create table (no fixed widths - let it auto-size)
488
+ const table = new Table({
489
+ head: [
490
+ chalk.cyan("#"),
491
+ chalk.cyan("Validator"),
492
+ chalk.cyan("Self"),
493
+ chalk.cyan("Deleg"),
494
+ chalk.cyan("Pending"),
495
+ chalk.cyan("Primed"),
496
+ chalk.cyan("Weight"),
497
+ chalk.cyan("Status"),
498
+ ],
499
+ style: {head: [], border: []},
500
+ });
501
+
502
+ rowsWithWeight.forEach((row, idx) => {
503
+ const {info, status, isMine, weight} = row;
504
+
505
+ // Weight percentage (share of active set weight)
506
+ const weightPct = totalWeight > 0 ? (weight / totalWeight) * 100 : 0;
507
+ const weightStr = status === "active" ? `${weightPct.toFixed(1)}%` : chalk.gray("-");
508
+
509
+ // Pending deposits/withdrawals - sum amounts (filter to truly pending only)
510
+ const currentEpoch = epochInfo.currentEpoch;
511
+ const trulyPendingDeposits = info.pendingDeposits.filter(d => d.epoch + ACTIVATION_DELAY_EPOCHS > currentEpoch);
512
+ const trulyPendingWithdrawals = info.pendingWithdrawals.filter(w => w.epoch + UNBONDING_PERIOD_EPOCHS > currentEpoch);
513
+ const pendingDepositSum = trulyPendingDeposits.reduce((sum, d) => sum + d.stakeRaw, 0n);
514
+ const pendingWithdrawSum = trulyPendingWithdrawals.reduce((sum, w) => sum + w.stakeRaw, 0n);
515
+ let pendingStr = "-";
516
+ if (pendingDepositSum > 0n && pendingWithdrawSum > 0n) {
517
+ pendingStr = chalk.green(`+${formatStake(`${Number(pendingDepositSum) / 1e18} GEN`)}`) +
518
+ " " + chalk.red(`-${formatStake(`${Number(pendingWithdrawSum) / 1e18} GEN`)}`);
519
+ } else if (pendingDepositSum > 0n) {
520
+ pendingStr = chalk.green(`+${formatStake(`${Number(pendingDepositSum) / 1e18} GEN`)}`);
521
+ } else if (pendingWithdrawSum > 0n) {
522
+ pendingStr = chalk.red(`-${formatStake(`${Number(pendingWithdrawSum) / 1e18} GEN`)}`)
523
+ }
524
+
525
+ // Role indicator (colored)
526
+ let roleTag = "";
527
+ if (isMine) {
528
+ if (myAddress && info.owner.toLowerCase() === myAddress.toLowerCase()) {
529
+ roleTag = info.operator.toLowerCase() === myAddress.toLowerCase()
530
+ ? chalk.cyan(" [own+op]")
531
+ : chalk.cyan(" [owner]");
532
+ } else {
533
+ roleTag = chalk.cyan(" [operator]");
534
+ }
535
+ }
536
+
537
+ // Moniker + role + full address on second line
538
+ let moniker = info.identity?.moniker || "";
539
+ if (moniker.length > 20) moniker = moniker.slice(0, 19) + "…";
540
+ const validatorCell = moniker
541
+ ? `${moniker}${roleTag}\n${chalk.gray(info.address)}`
542
+ : `${chalk.gray(info.address)}${roleTag}`;
543
+
544
+ // Primed status - color based on how current it is
545
+ let primedStr: string;
546
+ if (info.ePrimed >= currentEpoch) {
547
+ primedStr = chalk.green(`e${info.ePrimed}`);
548
+ } else if (info.ePrimed === currentEpoch - 1n) {
549
+ primedStr = chalk.yellow(`e${info.ePrimed}`);
550
+ } else {
551
+ primedStr = chalk.red(`e${info.ePrimed}!`);
552
+ }
553
+
554
+ // Status coloring
555
+ let statusStr = status;
556
+ if (status === "active") statusStr = chalk.green(status);
557
+ else if (status === "BANNED") statusStr = chalk.red(status);
558
+ else if (status.startsWith("quarant")) statusStr = chalk.yellow(status);
559
+ else if (status.startsWith("banned")) statusStr = chalk.red(status);
560
+ else if (status === "pending") statusStr = chalk.gray(status);
561
+
562
+ table.push([
563
+ (idx + 1).toString(),
564
+ validatorCell,
565
+ formatStake(info.vStake),
566
+ formatStake(info.dStake),
567
+ pendingStr,
568
+ primedStr,
569
+ weightStr,
570
+ statusStr,
571
+ ]);
572
+ });
573
+
574
+ console.log("");
575
+ console.log(table.toString());
576
+ console.log("");
577
+ const activeCount = rowsWithWeight.filter(r => r.status === "active").length;
578
+ console.log(chalk.gray(`Total: ${rowsWithWeight.length} validators (${activeCount} active)`));
579
+ console.log("");
580
+ } catch (error: any) {
581
+ this.failSpinner("Failed to list validators", error.message || error);
582
+ }
583
+ }
584
+ }
@@ -0,0 +1,43 @@
1
+ import {StakingAction, StakingConfig} from "./StakingAction";
2
+ import type {Address} from "genlayer-js/types";
3
+ import {abi} from "genlayer-js";
4
+
5
+ export interface ValidatorClaimOptions extends StakingConfig {
6
+ validator: string;
7
+ }
8
+
9
+ export class ValidatorClaimAction extends StakingAction {
10
+ constructor() {
11
+ super();
12
+ }
13
+
14
+ async execute(options: ValidatorClaimOptions): Promise<void> {
15
+ this.startSpinner("Claiming validator withdrawals...");
16
+
17
+ try {
18
+ const validatorWallet = options.validator as Address;
19
+ const {walletClient, publicClient} = await this.getViemClients(options);
20
+
21
+ this.setSpinnerText(`Claiming for validator ${validatorWallet}...`);
22
+
23
+ const hash = await walletClient.writeContract({
24
+ address: validatorWallet,
25
+ abi: abi.VALIDATOR_WALLET_ABI,
26
+ functionName: "validatorClaim",
27
+ });
28
+
29
+ const receipt = await publicClient.waitForTransactionReceipt({hash});
30
+
31
+ const output = {
32
+ transactionHash: receipt.transactionHash,
33
+ validator: validatorWallet,
34
+ blockNumber: receipt.blockNumber.toString(),
35
+ gasUsed: receipt.gasUsed.toString(),
36
+ };
37
+
38
+ this.succeedSpinner("Claim successful!", output);
39
+ } catch (error: any) {
40
+ this.failSpinner("Failed to claim", error.message || error);
41
+ }
42
+ }
43
+ }
@@ -0,0 +1,48 @@
1
+ import {StakingAction, StakingConfig} from "./StakingAction";
2
+ import type {Address} from "genlayer-js/types";
3
+ import {abi} from "genlayer-js";
4
+
5
+ export interface ValidatorDepositOptions extends StakingConfig {
6
+ amount: string;
7
+ validator: string;
8
+ }
9
+
10
+ export class ValidatorDepositAction extends StakingAction {
11
+ constructor() {
12
+ super();
13
+ }
14
+
15
+ async execute(options: ValidatorDepositOptions): Promise<void> {
16
+ this.startSpinner("Making validator deposit...");
17
+
18
+ try {
19
+ const amount = this.parseAmount(options.amount);
20
+ const validatorWallet = options.validator as Address;
21
+
22
+ const {walletClient, publicClient} = await this.getViemClients(options);
23
+
24
+ this.setSpinnerText(`Depositing ${this.formatAmount(amount)} to validator ${validatorWallet}...`);
25
+
26
+ const hash = await walletClient.writeContract({
27
+ address: validatorWallet,
28
+ abi: abi.VALIDATOR_WALLET_ABI,
29
+ functionName: "validatorDeposit",
30
+ value: amount,
31
+ });
32
+
33
+ const receipt = await publicClient.waitForTransactionReceipt({hash});
34
+
35
+ const output = {
36
+ transactionHash: receipt.transactionHash,
37
+ validator: validatorWallet,
38
+ amount: this.formatAmount(amount),
39
+ blockNumber: receipt.blockNumber.toString(),
40
+ gasUsed: receipt.gasUsed.toString(),
41
+ };
42
+
43
+ this.succeedSpinner("Deposit successful!", output);
44
+ } catch (error: any) {
45
+ this.failSpinner("Failed to make deposit", error.message || error);
46
+ }
47
+ }
48
+ }