diff --git a/package-lock.json b/package-lock.json index c2dfceca..1139cd58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5683,7 +5683,7 @@ }, "node_modules/genlayer-js": { "version": "1.1.8", - "resolved": "git+ssh://git@github.com/genlayerlabs/genlayer-js.git#57889281db1e34df49abcf6129ffe6f347e4de4d", + "resolved": "git+ssh://git@github.com/genlayerlabs/genlayer-js.git#bf42f13a66a2bb762e5ef1065eb89789b9c45d4a", "dev": true, "license": "MIT", "dependencies": { diff --git a/src/commands/staking/StakingAction.ts b/src/commands/staking/StakingAction.ts index 714f843d..62c762eb 100644 --- a/src/commands/staking/StakingAction.ts +++ b/src/commands/staking/StakingAction.ts @@ -107,6 +107,32 @@ export class StakingAction extends BaseAction { return {...session, stakingAddress}; } + /** + * Build a staking client bound to a browser-wallet session: an Address-only + * account plus the session's EIP-1193 provider. The SDK's `executeWrite` + * branches on `account.type` — for an Address it routes `eth_sendTransaction` + * through the provider (the bridge signs), so browser writes run the exact + * same `client.(...)` calls as the keystore lane. No keystore / + * keychain / password code path is ever touched. Callers should + * `session.setNextLabel(...)` before each write so the bridge shows a + * human-readable label instead of a generic one. + */ + protected getBrowserStakingClient( + config: StakingConfig, + session: BrowserSession, + ): GenLayerClient { + const network = this.getNetwork(config); + if (config.stakingAddress) { + network.stakingContract = {address: config.stakingAddress as Address, abi: abi.STAKING_ABI}; + } + return createClient({ + chain: network, + endpoint: config.rpc, + account: session.signerAddress, + provider: session.eip1193Provider, + } as Parameters[0]); + } + protected async getStakingClient(config: StakingConfig): Promise> { if (!this._stakingClient) { // Set account override if provided diff --git a/src/commands/staking/delegatorClaim.ts b/src/commands/staking/delegatorClaim.ts index 7a78b0f2..dcda92ee 100644 --- a/src/commands/staking/delegatorClaim.ts +++ b/src/commands/staking/delegatorClaim.ts @@ -1,7 +1,5 @@ import {StakingAction, StakingConfig} from "./StakingAction"; import type {Address} from "genlayer-js/types"; -import {abi} from "genlayer-js"; -import {buildTx} from "../../lib/wallet/txBuilders"; export interface DelegatorClaimOptions extends StakingConfig { validator: string; @@ -57,24 +55,21 @@ export class DelegatorClaimAction extends StakingAction { this.startSpinner("Confirm the transaction in your browser wallet..."); try { const delegatorAddress = options.delegator || session.signerAddress; - const {to, data} = buildTx(abi.STAKING_ABI as any, session.stakingAddress, "delegatorClaim", [ - delegatorAddress as Address, - options.validator as Address, - ]); + const client = this.getBrowserStakingClient(options, session); this.log(` From (browser wallet): ${session.signerAddress}`); - const receipt = await session.sendTransaction({ - to, - data, - label: `Claim delegator withdrawals`, + session.setNextLabel(`Claim delegator withdrawals`); + const result = await client.delegatorClaim({ + validator: options.validator as Address, + delegator: delegatorAddress as Address, }); this.succeedSpinner("Claim successful!", { - transactionHash: receipt.transactionHash, + transactionHash: result.transactionHash, delegator: delegatorAddress, validator: options.validator, - blockNumber: receipt.blockNumber.toString(), - gasUsed: receipt.gasUsed.toString(), + blockNumber: result.blockNumber.toString(), + gasUsed: result.gasUsed.toString(), }); } catch (error: any) { this.failSpinner("Failed to claim", error.message || error); diff --git a/src/commands/staking/delegatorExit.ts b/src/commands/staking/delegatorExit.ts index 300a69d1..fdd42be5 100644 --- a/src/commands/staking/delegatorExit.ts +++ b/src/commands/staking/delegatorExit.ts @@ -1,7 +1,5 @@ import {StakingAction, StakingConfig} from "./StakingAction"; import type {Address} from "genlayer-js/types"; -import {abi} from "genlayer-js"; -import {buildTx} from "../../lib/wallet/txBuilders"; export interface DelegatorExitOptions extends StakingConfig { validator: string; @@ -80,29 +78,25 @@ export class DelegatorExitAction extends StakingAction { return; } - const {to, data} = buildTx(abi.STAKING_ABI as any, session.stakingAddress, "delegatorExit", [ - options.validator as Address, - shares, - ]); + const client = this.getBrowserStakingClient(options, session); this.log(` From (browser wallet): ${session.signerAddress}`); - const receipt = await session.sendTransaction({ - to, - data, - label: `Exit ${shares} shares from validator`, + session.setNextLabel(`Exit ${shares} shares from validator`); + const result = await client.delegatorExit({ + validator: options.validator as Address, + shares, }); // Check epoch to determine note - const readClient = await this.getReadOnlyStakingClient(options); - const epochInfo = await readClient.getEpochInfo(); + const epochInfo = await client.getEpochInfo(); const isEpochZero = epochInfo.currentEpoch === 0n; this.succeedSpinner("Exit initiated successfully!", { - transactionHash: receipt.transactionHash, + transactionHash: result.transactionHash, validator: options.validator, sharesWithdrawn: shares.toString(), - blockNumber: receipt.blockNumber.toString(), - gasUsed: receipt.gasUsed.toString(), + blockNumber: result.blockNumber.toString(), + gasUsed: result.gasUsed.toString(), note: isEpochZero ? "Epoch 0: Withdrawal claimable immediately" : "Withdrawal will be claimable after the unbonding period", diff --git a/src/commands/staking/delegatorJoin.ts b/src/commands/staking/delegatorJoin.ts index 3820b838..ac8eedf4 100644 --- a/src/commands/staking/delegatorJoin.ts +++ b/src/commands/staking/delegatorJoin.ts @@ -1,8 +1,6 @@ import {StakingAction, StakingConfig} from "./StakingAction"; import type {Address} from "genlayer-js/types"; -import {abi} from "genlayer-js"; import chalk from "chalk"; -import {buildTx} from "../../lib/wallet/txBuilders"; export interface DelegatorJoinOptions extends StakingConfig { validator: string; @@ -60,25 +58,22 @@ export class DelegatorJoinAction extends StakingAction { this.startSpinner("Confirm the transaction in your browser wallet..."); try { const amount = this.parseAmount(options.amount); - const {to, data} = buildTx(abi.STAKING_ABI as any, session.stakingAddress, "delegatorJoin", [ - options.validator as Address, - ]); + const client = this.getBrowserStakingClient(options, session); this.log(` From (browser wallet): ${session.signerAddress}`); - const receipt = await session.sendTransaction({ - to, - data, - value: amount, - label: `Delegate ${this.formatAmount(amount)} to validator`, + session.setNextLabel(`Delegate ${this.formatAmount(amount)} to validator`); + const result = await client.delegatorJoin({ + validator: options.validator as Address, + amount, }); this.succeedSpinner("Successfully joined as delegator!", { - transactionHash: receipt.transactionHash, - validator: options.validator, - amount: this.formatAmount(amount), - delegator: session.signerAddress, - blockNumber: receipt.blockNumber.toString(), - gasUsed: receipt.gasUsed.toString(), + transactionHash: result.transactionHash, + validator: result.validator, + amount: result.amount, + delegator: result.delegator, + blockNumber: result.blockNumber.toString(), + gasUsed: result.gasUsed.toString(), }); console.log(chalk.dim(`\nTo view your delegation: genlayer staking delegation-info --validator ${options.validator}`)); } catch (error: any) { diff --git a/src/commands/staking/setIdentity.ts b/src/commands/staking/setIdentity.ts index d60058f3..9b35a064 100644 --- a/src/commands/staking/setIdentity.ts +++ b/src/commands/staking/setIdentity.ts @@ -6,7 +6,6 @@ import type { SetIdentityOptions as SdkSetIdentityOptions, StakingTransactionResult, } from "genlayer-js/types"; -import {buildSetIdentityTx} from "../../lib/wallet/txBuilders"; export interface SetIdentityOptions extends StakingConfig { validator: string; @@ -99,7 +98,17 @@ export class SetIdentityAction extends StakingAction { this.startSpinner("Confirm the transaction in your browser wallet..."); try { const validatorWallet = options.validator as Address; - const {to, data} = buildSetIdentityTx(validatorWallet, { + // `setIdentity` exists at runtime but is missing from the installed + // genlayer-js StakingActions .d.ts — cast to bridge that type gap. The + // SDK owns the extraCid encoding (hex passthrough vs UTF-8 -> hex). + const client = this.getBrowserStakingClient(options, session) as GenLayerClient & { + setIdentity(o: SdkSetIdentityOptions): Promise; + }; + + this.log(` From (browser wallet): ${session.signerAddress}`); + session.setNextLabel(`Set identity (${options.moniker})`); + const result = await client.setIdentity({ + validator: validatorWallet, moniker: options.moniker, logoUri: options.logoUri, website: options.website, @@ -111,19 +120,12 @@ export class SetIdentityAction extends StakingAction { extraCid: options.extraCid, }); - this.log(` From (browser wallet): ${session.signerAddress}`); - const receipt = await session.sendTransaction({ - to, - data, - label: `Set identity (${options.moniker})`, - }); - const output: Record = { - transactionHash: receipt.transactionHash, + transactionHash: result.transactionHash, validator: validatorWallet, moniker: options.moniker, - blockNumber: receipt.blockNumber.toString(), - gasUsed: receipt.gasUsed.toString(), + blockNumber: result.blockNumber.toString(), + gasUsed: result.gasUsed.toString(), }; // Add optional fields that were set diff --git a/src/commands/staking/setOperator.ts b/src/commands/staking/setOperator.ts index 7566d3b0..e4221514 100644 --- a/src/commands/staking/setOperator.ts +++ b/src/commands/staking/setOperator.ts @@ -6,8 +6,6 @@ import type { SetOperatorOptions as SdkSetOperatorOptions, StakingTransactionResult, } from "genlayer-js/types"; -import {abi} from "genlayer-js"; -import {buildTx} from "../../lib/wallet/txBuilders"; export interface SetOperatorOptions extends StakingConfig { validator: string; @@ -73,23 +71,25 @@ export class SetOperatorAction extends StakingAction { this.startSpinner("Confirm the transaction in your browser wallet..."); try { const validatorWallet = options.validator as Address; - const {to, data} = buildTx(abi.VALIDATOR_WALLET_ABI as any, validatorWallet, "setOperator", [ - options.operator as Address, - ]); + // `setOperator` exists at runtime but is missing from the installed + // genlayer-js StakingActions .d.ts — cast to bridge that type gap. + const client = this.getBrowserStakingClient(options, session) as GenLayerClient & { + setOperator(o: SdkSetOperatorOptions): Promise; + }; this.log(` From (browser wallet): ${session.signerAddress}`); - const receipt = await session.sendTransaction({ - to, - data, - label: `Set operator to ${options.operator}`, + session.setNextLabel(`Set operator to ${options.operator}`); + const result = await client.setOperator({ + validator: validatorWallet, + operator: options.operator as Address, }); this.succeedSpinner("Operator updated!", { - transactionHash: receipt.transactionHash, + transactionHash: result.transactionHash, validator: validatorWallet, newOperator: options.operator, - blockNumber: receipt.blockNumber.toString(), - gasUsed: receipt.gasUsed.toString(), + blockNumber: result.blockNumber.toString(), + gasUsed: result.gasUsed.toString(), }); } catch (error: any) { this.failSpinner("Failed to set operator", error.message || error); diff --git a/src/commands/staking/validatorClaim.ts b/src/commands/staking/validatorClaim.ts index b3e9afca..4a39d50f 100644 --- a/src/commands/staking/validatorClaim.ts +++ b/src/commands/staking/validatorClaim.ts @@ -1,7 +1,5 @@ import {StakingAction, StakingConfig} from "./StakingAction"; import type {Address} from "genlayer-js/types"; -import {abi} from "genlayer-js"; -import {buildTx} from "../../lib/wallet/txBuilders"; export interface ValidatorClaimOptions extends StakingConfig { validator: string; @@ -62,21 +60,24 @@ export class ValidatorClaimAction extends StakingAction { this.startSpinner("Confirm the transaction in your browser wallet..."); try { const validatorWallet = options.validator as Address; - const {to, data} = buildTx(abi.VALIDATOR_WALLET_ABI as any, validatorWallet, "validatorClaim"); + const client = this.getBrowserStakingClient(options, session); this.log(` From (browser wallet): ${session.signerAddress}`); - const receipt = await session.sendTransaction({ - to, - data, - label: `Claim validator withdrawals`, - }); - - this.succeedSpinner("Claim successful!", { - transactionHash: receipt.transactionHash, + session.setNextLabel(`Claim validator withdrawals`); + const result = await client.validatorClaim({validator: validatorWallet}); + + const output: Record = { + transactionHash: result.transactionHash, validator: validatorWallet, - blockNumber: receipt.blockNumber.toString(), - gasUsed: receipt.gasUsed.toString(), - }); + blockNumber: result.blockNumber.toString(), + gasUsed: result.gasUsed.toString(), + }; + + if (result.claimedAmount !== undefined) { + output.claimedAmount = this.formatAmount(result.claimedAmount); + } + + this.succeedSpinner("Claim successful!", output); } catch (error: any) { this.failSpinner("Failed to claim", error.message || error); } finally { diff --git a/src/commands/staking/validatorDeposit.ts b/src/commands/staking/validatorDeposit.ts index fd2fa73d..56568a45 100644 --- a/src/commands/staking/validatorDeposit.ts +++ b/src/commands/staking/validatorDeposit.ts @@ -1,7 +1,5 @@ import {StakingAction, StakingConfig} from "./StakingAction"; import type {Address} from "genlayer-js/types"; -import {abi} from "genlayer-js"; -import {buildTx} from "../../lib/wallet/txBuilders"; export interface ValidatorDepositOptions extends StakingConfig { amount: string; @@ -67,22 +65,21 @@ export class ValidatorDepositAction extends StakingAction { try { const amount = this.parseAmount(options.amount); const validatorWallet = options.validator as Address; - const {to, data} = buildTx(abi.VALIDATOR_WALLET_ABI as any, validatorWallet, "validatorDeposit"); + const client = this.getBrowserStakingClient(options, session); this.log(` From (browser wallet): ${session.signerAddress}`); - const receipt = await session.sendTransaction({ - to, - data, - value: amount, - label: `Deposit ${this.formatAmount(amount)} to validator`, + session.setNextLabel(`Deposit ${this.formatAmount(amount)} to validator`); + const result = await client.validatorDeposit({ + validator: validatorWallet, + amount, }); this.succeedSpinner("Deposit successful!", { - transactionHash: receipt.transactionHash, + transactionHash: result.transactionHash, validator: validatorWallet, amount: this.formatAmount(amount), - blockNumber: receipt.blockNumber.toString(), - gasUsed: receipt.gasUsed.toString(), + blockNumber: result.blockNumber.toString(), + gasUsed: result.gasUsed.toString(), }); } catch (error: any) { this.failSpinner("Failed to make deposit", error.message || error); diff --git a/src/commands/staking/validatorExit.ts b/src/commands/staking/validatorExit.ts index 44fc2f51..3f4ced2e 100644 --- a/src/commands/staking/validatorExit.ts +++ b/src/commands/staking/validatorExit.ts @@ -1,7 +1,5 @@ import {StakingAction, StakingConfig} from "./StakingAction"; import type {Address} from "genlayer-js/types"; -import {abi} from "genlayer-js"; -import {buildTx} from "../../lib/wallet/txBuilders"; export interface ValidatorExitOptions extends StakingConfig { validator: string; @@ -90,26 +88,25 @@ export class ValidatorExitAction extends StakingAction { } const validatorWallet = options.validator as Address; - const {to, data} = buildTx(abi.VALIDATOR_WALLET_ABI as any, validatorWallet, "validatorExit", [shares]); + const client = this.getBrowserStakingClient(options, session); this.log(` From (browser wallet): ${session.signerAddress}`); - const receipt = await session.sendTransaction({ - to, - data, - label: `Exit validator (${shares} shares)`, + session.setNextLabel(`Exit validator (${shares} shares)`); + const result = await client.validatorExit({ + validator: validatorWallet, + shares, }); // Check epoch to determine note - const readClient = await this.getReadOnlyStakingClient(options); - const epochInfo = await readClient.getEpochInfo(); + const epochInfo = await client.getEpochInfo(); const isEpochZero = epochInfo.currentEpoch === 0n; this.succeedSpinner("Exit initiated successfully!", { - transactionHash: receipt.transactionHash, + transactionHash: result.transactionHash, validator: validatorWallet, sharesWithdrawn: shares.toString(), - blockNumber: receipt.blockNumber.toString(), - gasUsed: receipt.gasUsed.toString(), + blockNumber: result.blockNumber.toString(), + gasUsed: result.gasUsed.toString(), note: isEpochZero ? "Epoch 0: Withdrawal claimable immediately" : "Withdrawal will be claimable after the unbonding period", diff --git a/src/commands/staking/validatorJoin.ts b/src/commands/staking/validatorJoin.ts index d41281be..943bf409 100644 --- a/src/commands/staking/validatorJoin.ts +++ b/src/commands/staking/validatorJoin.ts @@ -1,6 +1,5 @@ import {StakingAction, StakingConfig} from "./StakingAction"; import type {Address} from "genlayer-js/types"; -import {buildValidatorJoinTx, extractValidatorWallet} from "../../lib/wallet/stakingTx"; export interface ValidatorJoinOptions extends StakingConfig { amount: string; @@ -62,29 +61,28 @@ export class ValidatorJoinAction extends StakingAction { this.startSpinner("Confirm the transaction in your browser wallet..."); try { const amount = this.parseAmount(options.amount); - const {to, data} = buildValidatorJoinTx(session.stakingAddress, options.operator); + const client = this.getBrowserStakingClient(options, session); this.log(` From (browser wallet): ${session.signerAddress}`); if (options.operator) { this.log(` Operator: ${options.operator}`); } - const receipt = await session.sendTransaction({ - to, - data, - value: amount, - label: `Join as validator (${this.formatAmount(amount)})`, + // Same SDK call as the keystore lane; the SDK decodes the ValidatorJoin + // event and returns validatorWallet for both lanes. + session.setNextLabel(`Join as validator (${this.formatAmount(amount)})`); + const result = await client.validatorJoin({ + amount, + operator: options.operator as Address | undefined, }); - const validatorWallet = extractValidatorWallet(receipt); - this.succeedSpinner("Validator created successfully!", { - transactionHash: receipt.transactionHash, - validatorWallet, - amount: this.formatAmount(amount), - operator: options.operator ?? session.signerAddress, - blockNumber: receipt.blockNumber.toString(), - gasUsed: receipt.gasUsed.toString(), + transactionHash: result.transactionHash, + validatorWallet: result.validatorWallet, + amount: result.amount, + operator: result.operator, + blockNumber: result.blockNumber.toString(), + gasUsed: result.gasUsed.toString(), }); } catch (error: any) { this.failSpinner("Failed to create validator", error.message || error); diff --git a/src/commands/staking/validatorPrime.ts b/src/commands/staking/validatorPrime.ts index b0f12cf0..42e26903 100644 --- a/src/commands/staking/validatorPrime.ts +++ b/src/commands/staking/validatorPrime.ts @@ -1,8 +1,14 @@ import {StakingAction, StakingConfig} from "./StakingAction"; -import type {Address} from "genlayer-js/types"; -import {abi} from "genlayer-js"; +import type {Address, GenLayerClient, GenLayerChain, StakingTransactionResult} from "genlayer-js/types"; import chalk from "chalk"; -import {buildTx} from "../../lib/wallet/txBuilders"; + +/** + * `validatorPrime` exists on the client at runtime but is missing from the + * installed genlayer-js StakingActions .d.ts — this alias bridges that type gap. + */ +type ClientWithPrime = GenLayerClient & { + validatorPrime(o: {validator: Address}): Promise; +}; export interface ValidatorPrimeOptions extends StakingConfig { validator: string; @@ -51,22 +57,17 @@ export class ValidatorPrimeAction extends StakingAction { this.startSpinner("Confirm the transaction in your browser wallet..."); try { - const {to, data} = buildTx(abi.STAKING_ABI as any, session.stakingAddress, "validatorPrime", [ - options.validator as Address, - ]); + const client = this.getBrowserStakingClient(options, session) as ClientWithPrime; this.log(` From (browser wallet): ${session.signerAddress}`); - const receipt = await session.sendTransaction({ - to, - data, - label: `Prime ${options.validator}`, - }); + session.setNextLabel(`Prime ${options.validator}`); + const result = await client.validatorPrime({validator: options.validator as Address}); this.succeedSpinner("Validator primed for next epoch!", { - transactionHash: receipt.transactionHash, + transactionHash: result.transactionHash, validator: options.validator, - blockNumber: receipt.blockNumber.toString(), - gasUsed: receipt.gasUsed.toString(), + blockNumber: result.blockNumber.toString(), + gasUsed: result.gasUsed.toString(), }); } catch (error: any) { this.failSpinner("Failed to prime validator", error.message || error); @@ -128,6 +129,7 @@ export class ValidatorPrimeAction extends StakingAction { try { this.startSpinner("Fetching validators..."); const allValidators = await this.getAllValidatorsFromTree(options); + const client = this.getBrowserStakingClient(options, session) as ClientWithPrime; this.stopSpinner(); console.log(`\nPriming ${allValidators.length} validators:\n`); @@ -139,9 +141,9 @@ export class ValidatorPrimeAction extends StakingAction { process.stdout.write(` ${addr} ... `); try { - const {to, data} = buildTx(abi.STAKING_ABI as any, session.stakingAddress, "validatorPrime", [addr]); - const receipt = await session.sendTransaction({to, data, label: `Prime ${addr}`}); - console.log(chalk.green(`primed ${receipt.transactionHash}`)); + session.setNextLabel(`Prime ${addr}`); + const result = await client.validatorPrime({validator: addr}); + console.log(chalk.green(`primed ${result.transactionHash}`)); succeeded++; } catch (error: any) { const msg = error.message || String(error); diff --git a/src/commands/staking/wizard.ts b/src/commands/staking/wizard.ts index 16283d95..290f59df 100644 --- a/src/commands/staking/wizard.ts +++ b/src/commands/staking/wizard.ts @@ -3,14 +3,19 @@ import {resolveNetwork} from "../../lib/actions/BaseAction"; import {CreateAccountAction} from "../account/create"; import {ExportAccountAction} from "../account/export"; import inquirer from "inquirer"; -import type {Address} from "genlayer-js/types"; +import type { + Address, + GenLayerClient, + GenLayerChain, + StakingTransactionResult, + SetIdentityOptions as SdkSetIdentityOptions, +} from "genlayer-js/types"; import {formatEther, parseEther} from "viem"; -import {createClient, abi} from "genlayer-js"; +import {createClient} from "genlayer-js"; import {readFileSync, existsSync} from "fs"; import path from "path"; -import {buildValidatorJoinTx, buildSetIdentityTx, extractValidatorWallet} from "../../lib/wallet/stakingTx"; -import {buildTx} from "../../lib/wallet/txBuilders"; import type {VestingClient, VestingValidatorJoinResult} from "../vesting/vestingTypes"; +import type {BrowserSession} from "../../lib/wallet/browserSend"; import {vestingAvailableToStake} from "../../lib/vesting/availableToStake"; const BROWSER_WALLET_CHOICE = "__browser_wallet__"; @@ -371,6 +376,26 @@ export class ValidatorWizardAction extends StakingAction { }) as unknown as VestingClient; } + /** + * Vesting client bound to the browser session (Address-only account + the + * session's EIP-1193 provider). The SDK routes writes through the provider, + * so vesting writes run the same client.(...) calls as the keystore + * path; the same client serves the getValidatorWallets read. + */ + private getWizardVestingBrowserClient( + state: Partial, + options: WizardOptions, + session: BrowserSession, + ): VestingClient { + const network = resolveNetwork(state.networkAlias!, this.getCustomNetworks()); + return createClient({ + chain: network, + account: session.signerAddress, + endpoint: options.rpc, + provider: session.eip1193Provider, + } as Parameters[0]) as unknown as VestingClient; + } + /** * Choose where the self-stake is funded from: the owner's wallet (default, * original flow) or one of the owner's vesting contracts. When vesting is @@ -1149,27 +1174,27 @@ export class ValidatorWizardAction extends StakingAction { private async stepJoinValidatorBrowser(state: Partial, options: WizardOptions): Promise { const session = await this.ensureBrowserSession(state, options); const amount = this.parseAmount(state.stakeAmount!); + const client = this.getBrowserStakingClient({...options, network: state.networkAlias}, session); this.startSpinner("Confirm the transaction in your browser wallet..."); try { - const {to, data} = buildValidatorJoinTx(session.stakingAddress, state.operatorAddress); - const receipt = await session.sendTransaction({ - to, - data, - value: amount, - label: `Join as validator (${this.formatAmount(amount)})`, + // Same SDK call as the keystore lane; the SDK decodes the ValidatorJoin + // event and returns validatorWallet for both lanes. + session.setNextLabel(`Join as validator (${this.formatAmount(amount)})`); + const result = await client.validatorJoin({ + amount, + operator: state.operatorAddress as Address, }); - const validatorWallet = extractValidatorWallet(receipt); - state.validatorWalletAddress = ensureHexPrefix(validatorWallet); + state.validatorWalletAddress = ensureHexPrefix(result.validatorWallet); this.succeedSpinner("Validator created successfully!", { - transactionHash: receipt.transactionHash, + transactionHash: result.transactionHash, validatorWallet: state.validatorWalletAddress, - amount: this.formatAmount(amount), - operator: state.operatorAddress, - blockNumber: receipt.blockNumber.toString(), + amount: result.amount, + operator: result.operator, + blockNumber: result.blockNumber.toString(), }); console.log(""); @@ -1253,27 +1278,23 @@ export class ValidatorWizardAction extends StakingAction { const session = await this.ensureBrowserSession(state, options); const amount = this.parseAmount(state.stakeAmount!); const vesting = state.vestingContract as Address; + const client = this.getWizardVestingBrowserClient(state, options, session); this.startSpinner("Confirm the transaction in your browser wallet..."); try { - const {to, data} = buildTx(abi.VESTING_ABI as any, vesting, "vestingValidatorJoin", [ - state.operatorAddress, + session.setNextLabel(`Create vesting validator (${this.formatAmount(amount)})`); + const result = await client.vestingValidatorJoin({ + vesting, + operator: state.operatorAddress as Address, amount, - ]); - - const receipt = await session.sendTransaction({ - to, - data, - label: `Create vesting validator (${this.formatAmount(amount)})`, }); - // The join receipt does not carry the wallet address; read the newest entry - // the vesting contract tracks. + // The join result does not carry the wallet address; the vesting contract + // tracks its wallets, so the newest entry is the one just created. let validatorWallet: Address | undefined; try { - const readClient = this.getWizardVestingReadClient(state, options); - const wallets = await readClient.getValidatorWallets(vesting); + const wallets = await client.getValidatorWallets(vesting); validatorWallet = wallets[wallets.length - 1]; } catch { // Leave undefined; the summary falls back to the owner address. @@ -1281,12 +1302,12 @@ export class ValidatorWizardAction extends StakingAction { if (validatorWallet) state.validatorWalletAddress = ensureHexPrefix(validatorWallet); this.succeedSpinner("Vesting-backed validator created successfully!", { - transactionHash: receipt.transactionHash, + transactionHash: result.transactionHash, vesting, validatorWallet: state.validatorWalletAddress, amount: this.formatAmount(amount), operator: state.operatorAddress, - blockNumber: receipt.blockNumber.toString(), + blockNumber: result.blockNumber.toString(), }); console.log(""); @@ -1438,8 +1459,8 @@ export class ValidatorWizardAction extends StakingAction { * Send the set-identity transaction from `state.identity`. Routing: * - vesting funding → the vesting contract's vestingValidatorSetIdentity * (keystore via the SDK client, browser via the shared bridge); - * - wallet funding → staking's setIdentity (keystore) / buildSetIdentityTx - * (browser), unchanged. + * - wallet funding → staking's setIdentity, through the SDK client for both + * the keystore and the browser (provider) lanes. * Shared by the interactive and non-interactive identity steps so both behave * identically. A revert (e.g. consensus gaps) is caught: the wizard warns and * continues to the summary rather than crashing or losing the created validator. @@ -1467,7 +1488,18 @@ export class ValidatorWizardAction extends StakingAction { } else if (state.ownerIsBrowserWallet) { const session = await this.ensureBrowserSession(state, options); this.setSpinnerText("Confirm the identity transaction in your browser wallet..."); - const {to, data} = buildSetIdentityTx(validatorAddress, { + // `setIdentity` exists at runtime but is missing from the installed + // genlayer-js StakingActions .d.ts — cast bridges that type gap. The + // SDK owns the extraCid encoding. + const client = this.getBrowserStakingClient( + {...options, network: state.networkAlias}, + session, + ) as GenLayerClient & { + setIdentity(o: SdkSetIdentityOptions): Promise; + }; + session.setNextLabel(`Set validator identity (${identity.moniker})`); + await client.setIdentity({ + validator: validatorAddress as Address, moniker: identity.moniker, logoUri: identity.logoUri, website: identity.website, @@ -1477,7 +1509,6 @@ export class ValidatorWizardAction extends StakingAction { telegram: identity.telegram, github: identity.github, }); - await session.sendTransaction({to, data, label: `Set validator identity (${identity.moniker})`}); } else { const client = await this.getStakingClient({ ...options, @@ -1525,19 +1556,21 @@ export class ValidatorWizardAction extends StakingAction { if (state.ownerIsBrowserWallet) { const session = await this.ensureBrowserSession(state, options); this.setSpinnerText("Confirm the identity transaction in your browser wallet..."); - const {to, data} = buildTx(abi.VESTING_ABI as any, vesting, "vestingValidatorSetIdentity", [ - validatorAddress, - identity.moniker, - identity.logoUri || "", - identity.website || "", - identity.description || "", - identity.email || "", - identity.twitter || "", - identity.telegram || "", - identity.github || "", - "0x", - ]); - await session.sendTransaction({to, data, label: `Set validator identity (${identity.moniker})`}); + const client = this.getWizardVestingBrowserClient(state, options, session); + session.setNextLabel(`Set validator identity (${identity.moniker})`); + await client.vestingValidatorSetIdentity({ + vesting, + wallet: validatorAddress as Address, + moniker: identity.moniker, + logoUri: identity.logoUri || "", + website: identity.website || "", + description: identity.description || "", + email: identity.email || "", + twitter: identity.twitter || "", + telegram: identity.telegram || "", + github: identity.github || "", + extraCid: "0x", + }); return; } diff --git a/src/commands/vesting/VestingAction.ts b/src/commands/vesting/VestingAction.ts index ef1c504b..08800330 100644 --- a/src/commands/vesting/VestingAction.ts +++ b/src/commands/vesting/VestingAction.ts @@ -4,6 +4,7 @@ import type {Address, GenLayerChain} from "genlayer-js/types"; import {existsSync, readFileSync} from "fs"; import {ethers} from "ethers"; import type {VestingClient, VestingFactoryLookupOptions} from "./vestingTypes"; +import type {BrowserSession} from "../../lib/wallet/browserSend"; export {BUILT_IN_NETWORKS}; @@ -149,6 +150,25 @@ export class VestingAction extends BaseAction { return this.getBrowserSession({network: options.network, rpc: options.rpc}); } + /** + * Build a vesting client bound to a browser-wallet session: an Address-only + * account plus the session's EIP-1193 provider. The SDK's `executeWrite` + * routes `eth_sendTransaction` through the provider for an Address account, + * so browser writes run the exact same `client.(...)` calls as the + * keystore lane — and the same client serves the read helpers + * (resolveBeneficiaryVesting / getStakeInfo / getValidatorWallets). Callers + * should `session.setNextLabel(...)` before each write. + */ + protected getBrowserVestingClient(options: VestingConfig, session: BrowserSession): VestingClient { + const network = this.getNetwork(options); + return createClient({ + chain: network, + endpoint: options.rpc, + account: session.signerAddress, + provider: session.eip1193Provider, + } as Parameters[0]) as VestingClient; + } + protected async resolveBeneficiaryVesting(client: VestingClient, options: VestingConfig): Promise
{ if (options.vesting) { return options.vesting as Address; diff --git a/src/commands/vesting/claim.ts b/src/commands/vesting/claim.ts index 3d27dc7f..6fdc89f4 100644 --- a/src/commands/vesting/claim.ts +++ b/src/commands/vesting/claim.ts @@ -1,7 +1,5 @@ import {VestingAction, VestingConfig} from "./VestingAction"; import type {Address} from "genlayer-js/types"; -import {abi} from "genlayer-js"; -import {buildTx} from "../../lib/wallet/txBuilders"; export interface VestingClaimOptions extends VestingConfig { validator: string; @@ -56,23 +54,21 @@ export class VestingClaimAction extends VestingAction { this.startSpinner("Confirm the transaction in your browser wallet..."); try { - const readClient = await this.getReadOnlyVestingClient(options); - const vesting = await this.resolveBeneficiaryVesting(readClient, options); - - const {to, data} = buildTx(abi.VESTING_ABI as any, vesting, "vestingDelegatorClaim", [options.validator]); + const client = this.getBrowserVestingClient(options, session); + const vesting = await this.resolveBeneficiaryVesting(client, options); - const receipt = await session.sendTransaction({ - to, - data, - label: "Claim vesting delegation withdrawal", + session.setNextLabel("Claim vesting delegation withdrawal"); + const result = await client.vestingDelegatorClaim({ + vesting, + validator: options.validator as Address, }); this.succeedSpinner("Vesting claim successful!", { - transactionHash: receipt.transactionHash, + transactionHash: result.transactionHash, vesting, validator: options.validator, - blockNumber: receipt.blockNumber.toString(), - gasUsed: receipt.gasUsed.toString(), + blockNumber: result.blockNumber.toString(), + gasUsed: result.gasUsed.toString(), }); } catch (error: any) { this.failSpinner("Failed to claim vesting withdrawal", error.message || error); diff --git a/src/commands/vesting/delegate.ts b/src/commands/vesting/delegate.ts index 7807e3d1..fdcfb122 100644 --- a/src/commands/vesting/delegate.ts +++ b/src/commands/vesting/delegate.ts @@ -1,7 +1,5 @@ import {VestingAction, VestingConfig} from "./VestingAction"; import type {Address} from "genlayer-js/types"; -import {abi} from "genlayer-js"; -import {buildTx} from "../../lib/wallet/txBuilders"; export interface VestingDelegateOptions extends VestingConfig { validator: string; @@ -61,29 +59,25 @@ export class VestingDelegateAction extends VestingAction { this.startSpinner("Confirm the transaction in your browser wallet..."); try { - const readClient = await this.getReadOnlyVestingClient(options); - const vesting = await this.resolveBeneficiaryVesting(readClient, options); + const client = this.getBrowserVestingClient(options, session); + const vesting = await this.resolveBeneficiaryVesting(client, options); const amount = this.parseAmount(options.amount); - const {to, data} = buildTx(abi.VESTING_ABI as any, vesting, "vestingDelegatorJoin", [ - options.validator, + session.setNextLabel(`Delegate ${this.formatAmount(amount)} to validator`); + const result = await client.vestingDelegatorJoin({ + vesting, + validator: options.validator as Address, amount, - ]); - - const receipt = await session.sendTransaction({ - to, - data, - label: `Delegate ${this.formatAmount(amount)} to validator`, }); this.succeedSpinner("Vesting delegation successful!", { - transactionHash: receipt.transactionHash, - vesting, - validator: options.validator, - beneficiary: session.signerAddress, - amount: this.formatAmount(amount), - blockNumber: receipt.blockNumber.toString(), - gasUsed: receipt.gasUsed.toString(), + transactionHash: result.transactionHash, + vesting: result.vesting, + validator: result.validator, + beneficiary: result.beneficiary, + amount: result.amount, + blockNumber: result.blockNumber.toString(), + gasUsed: result.gasUsed.toString(), }); } catch (error: any) { this.failSpinner("Failed to delegate vesting tokens", error.message || error); diff --git a/src/commands/vesting/undelegate.ts b/src/commands/vesting/undelegate.ts index 93f4557e..89549539 100644 --- a/src/commands/vesting/undelegate.ts +++ b/src/commands/vesting/undelegate.ts @@ -1,7 +1,5 @@ import {VestingAction, VestingConfig} from "./VestingAction"; import type {Address} from "genlayer-js/types"; -import {abi} from "genlayer-js"; -import {buildTx} from "../../lib/wallet/txBuilders"; export interface VestingUndelegateOptions extends VestingConfig { validator: string; @@ -69,10 +67,10 @@ export class VestingUndelegateAction extends VestingAction { this.startSpinner("Confirm the transaction in your browser wallet..."); try { - const readClient = await this.getReadOnlyVestingClient(options); - const vesting = await this.resolveBeneficiaryVesting(readClient, options); + const client = this.getBrowserVestingClient(options, session); + const vesting = await this.resolveBeneficiaryVesting(client, options); - const stakeInfo = await readClient.getStakeInfo(vesting, options.validator as Address); + const stakeInfo = await client.getStakeInfo(vesting, options.validator as Address); const shares = stakeInfo.shares; if (shares <= 0n) { @@ -80,25 +78,21 @@ export class VestingUndelegateAction extends VestingAction { return; } - const {to, data} = buildTx(abi.VESTING_ABI as any, vesting, "vestingDelegatorExit", [ - options.validator, + session.setNextLabel(`Undelegate ${shares.toString()} shares from validator`); + const result = await client.vestingDelegatorExit({ + vesting, + validator: options.validator as Address, shares, - ]); - - const receipt = await session.sendTransaction({ - to, - data, - label: `Undelegate ${shares.toString()} shares from validator`, }); this.succeedSpinner("Vesting undelegation initiated!", { - transactionHash: receipt.transactionHash, + transactionHash: result.transactionHash, vesting, validator: options.validator, sharesWithdrawn: shares.toString(), stake: stakeInfo.stake, - blockNumber: receipt.blockNumber.toString(), - gasUsed: receipt.gasUsed.toString(), + blockNumber: result.blockNumber.toString(), + gasUsed: result.gasUsed.toString(), note: "Withdrawal will be claimable after the unbonding period", }); } catch (error: any) { diff --git a/src/commands/vesting/validatorClaim.ts b/src/commands/vesting/validatorClaim.ts index 2b806323..67050139 100644 --- a/src/commands/vesting/validatorClaim.ts +++ b/src/commands/vesting/validatorClaim.ts @@ -1,7 +1,5 @@ import {VestingAction, VestingConfig} from "./VestingAction"; import type {Address} from "genlayer-js/types"; -import {abi} from "genlayer-js"; -import {buildTx} from "../../lib/wallet/txBuilders"; export interface VestingValidatorClaimOptions extends VestingConfig { walletAddress: string; @@ -56,25 +54,21 @@ export class VestingValidatorClaimAction extends VestingAction { this.startSpinner("Confirm the transaction in your browser wallet..."); try { - const readClient = await this.getReadOnlyVestingClient(options); - const vesting = await this.resolveBeneficiaryVesting(readClient, options); - - const {to, data} = buildTx(abi.VESTING_ABI as any, vesting, "vestingValidatorClaim", [ - options.walletAddress, - ]); + const client = this.getBrowserVestingClient(options, session); + const vesting = await this.resolveBeneficiaryVesting(client, options); - const receipt = await session.sendTransaction({ - to, - data, - label: "Claim vesting validator withdrawal", + session.setNextLabel("Claim vesting validator withdrawal"); + const result = await client.vestingValidatorClaim({ + vesting, + wallet: options.walletAddress as Address, }); this.succeedSpinner("Vesting validator claim successful!", { - transactionHash: receipt.transactionHash, + transactionHash: result.transactionHash, vesting, wallet: options.walletAddress, - blockNumber: receipt.blockNumber.toString(), - gasUsed: receipt.gasUsed.toString(), + blockNumber: result.blockNumber.toString(), + gasUsed: result.gasUsed.toString(), }); } catch (error: any) { this.failSpinner("Failed to claim vesting validator withdrawal", error.message || error); diff --git a/src/commands/vesting/validatorCreate.ts b/src/commands/vesting/validatorCreate.ts index c23fdc71..a62f25dc 100644 --- a/src/commands/vesting/validatorCreate.ts +++ b/src/commands/vesting/validatorCreate.ts @@ -1,7 +1,5 @@ import {VestingAction, VestingConfig} from "./VestingAction"; import type {Address} from "genlayer-js/types"; -import {abi} from "genlayer-js"; -import {buildTx} from "../../lib/wallet/txBuilders"; export interface VestingValidatorCreateOptions extends VestingConfig { operator: string; @@ -73,39 +71,36 @@ export class VestingValidatorCreateAction extends VestingAction { this.startSpinner("Confirm the transaction in your browser wallet..."); try { - const readClient = await this.getReadOnlyVestingClient(options); - const vesting = await this.resolveBeneficiaryVesting(readClient, options); + const client = this.getBrowserVestingClient(options, session); + const vesting = await this.resolveBeneficiaryVesting(client, options); const amount = this.parseAmount(options.amount); - const {to, data} = buildTx(abi.VESTING_ABI as any, vesting, "vestingValidatorJoin", [ - options.operator, + session.setNextLabel("Create vesting validator"); + const result = await client.vestingValidatorJoin({ + vesting, + operator: options.operator as Address, amount, - ]); - - const receipt = await session.sendTransaction({ - to, - data, - label: "Create vesting validator", }); - // The join receipt does not carry the wallet address; the vesting - // contract tracks its wallets, so the newest entry is the one created. - let validatorWallet; + // The vestingValidatorJoin result does not carry the wallet address; the + // vesting contract tracks its wallets, so the newest entry is the one + // just created. + let validatorWallet: string; try { - const wallets = await readClient.getValidatorWallets(vesting); + const wallets = await client.getValidatorWallets(vesting); validatorWallet = wallets[wallets.length - 1]; } catch { validatorWallet = "(read getValidatorWallets to inspect)"; } this.succeedSpinner("Vesting-backed validator created!", { - transactionHash: receipt.transactionHash, + transactionHash: result.transactionHash, vesting, validatorWallet, operator: options.operator, amount: this.formatAmount(amount), - blockNumber: receipt.blockNumber.toString(), - gasUsed: receipt.gasUsed.toString(), + blockNumber: result.blockNumber.toString(), + gasUsed: result.gasUsed.toString(), }); } catch (error: any) { this.failSpinner("Failed to create vesting-backed validator", error.message || error); diff --git a/src/commands/vesting/validatorDeposit.ts b/src/commands/vesting/validatorDeposit.ts index 5f4cedc0..09df920d 100644 --- a/src/commands/vesting/validatorDeposit.ts +++ b/src/commands/vesting/validatorDeposit.ts @@ -1,7 +1,5 @@ import {VestingAction, VestingConfig} from "./VestingAction"; import type {Address} from "genlayer-js/types"; -import {abi} from "genlayer-js"; -import {buildTx} from "../../lib/wallet/txBuilders"; export interface VestingValidatorDepositOptions extends VestingConfig { walletAddress: string; @@ -60,28 +58,24 @@ export class VestingValidatorDepositAction extends VestingAction { this.startSpinner("Confirm the transaction in your browser wallet..."); try { - const readClient = await this.getReadOnlyVestingClient(options); - const vesting = await this.resolveBeneficiaryVesting(readClient, options); + const client = this.getBrowserVestingClient(options, session); + const vesting = await this.resolveBeneficiaryVesting(client, options); const amount = this.parseAmount(options.amount); - const {to, data} = buildTx(abi.VESTING_ABI as any, vesting, "vestingValidatorDeposit", [ - options.walletAddress, + session.setNextLabel(`Deposit ${this.formatAmount(amount)} to validator wallet`); + const result = await client.vestingValidatorDeposit({ + vesting, + wallet: options.walletAddress as Address, amount, - ]); - - const receipt = await session.sendTransaction({ - to, - data, - label: `Deposit ${this.formatAmount(amount)} to validator wallet`, }); this.succeedSpinner("Vesting validator deposit successful!", { - transactionHash: receipt.transactionHash, + transactionHash: result.transactionHash, vesting, wallet: options.walletAddress, amount: this.formatAmount(amount), - blockNumber: receipt.blockNumber.toString(), - gasUsed: receipt.gasUsed.toString(), + blockNumber: result.blockNumber.toString(), + gasUsed: result.gasUsed.toString(), }); } catch (error: any) { this.failSpinner("Failed to deposit vesting validator tokens", error.message || error); diff --git a/src/commands/vesting/validatorExit.ts b/src/commands/vesting/validatorExit.ts index 7c2005d5..d882d30b 100644 --- a/src/commands/vesting/validatorExit.ts +++ b/src/commands/vesting/validatorExit.ts @@ -1,7 +1,5 @@ import {VestingAction, VestingConfig} from "./VestingAction"; import type {Address} from "genlayer-js/types"; -import {abi} from "genlayer-js"; -import {buildTx} from "../../lib/wallet/txBuilders"; export interface VestingValidatorExitOptions extends VestingConfig { walletAddress: string; @@ -78,27 +76,23 @@ export class VestingValidatorExitAction extends VestingAction { return; } - const readClient = await this.getReadOnlyVestingClient(options); - const vesting = await this.resolveBeneficiaryVesting(readClient, options); + const client = this.getBrowserVestingClient(options, session); + const vesting = await this.resolveBeneficiaryVesting(client, options); - const {to, data} = buildTx(abi.VESTING_ABI as any, vesting, "vestingValidatorExit", [ - options.walletAddress, + session.setNextLabel(`Exit ${shares.toString()} validator shares`); + const result = await client.vestingValidatorExit({ + vesting, + wallet: options.walletAddress as Address, shares, - ]); - - const receipt = await session.sendTransaction({ - to, - data, - label: `Exit ${shares.toString()} validator shares`, }); this.succeedSpinner("Vesting validator exit initiated!", { - transactionHash: receipt.transactionHash, + transactionHash: result.transactionHash, vesting, wallet: options.walletAddress, sharesWithdrawn: shares.toString(), - blockNumber: receipt.blockNumber.toString(), - gasUsed: receipt.gasUsed.toString(), + blockNumber: result.blockNumber.toString(), + gasUsed: result.gasUsed.toString(), note: "Withdrawal will be claimable after the unbonding period unless settled immediately in epoch 0", }); } catch (error: any) { diff --git a/src/commands/vesting/validatorOperatorTransfer.ts b/src/commands/vesting/validatorOperatorTransfer.ts index 607fcb82..c0ca265f 100644 --- a/src/commands/vesting/validatorOperatorTransfer.ts +++ b/src/commands/vesting/validatorOperatorTransfer.ts @@ -1,7 +1,5 @@ import {VestingAction, VestingConfig} from "./VestingAction"; import type {Address} from "genlayer-js/types"; -import {abi} from "genlayer-js"; -import {buildTx} from "../../lib/wallet/txBuilders"; export interface VestingValidatorOperatorTransferOptions extends VestingConfig { walletAddress: string; @@ -69,27 +67,23 @@ export class VestingValidatorInitiateOperatorTransferAction extends VestingActio this.startSpinner("Confirm the transaction in your browser wallet..."); try { - const readClient = await this.getReadOnlyVestingClient(options); - const vesting = await this.resolveBeneficiaryVesting(readClient, options); - - const {to, data} = buildTx(abi.VESTING_ABI as any, vesting, "vestingValidatorInitiateOperatorTransfer", [ - options.walletAddress, - options.newOperator, - ]); - - const receipt = await session.sendTransaction({ - to, - data, - label: "Initiate validator operator transfer", + const client = this.getBrowserVestingClient(options, session); + const vesting = await this.resolveBeneficiaryVesting(client, options); + + session.setNextLabel("Initiate validator operator transfer"); + const result = await client.vestingValidatorInitiateOperatorTransfer({ + vesting, + wallet: options.walletAddress as Address, + newOperator: options.newOperator as Address, }); this.succeedSpinner("Vesting validator operator transfer initiated!", { - transactionHash: receipt.transactionHash, + transactionHash: result.transactionHash, vesting, wallet: options.walletAddress, newOperator: options.newOperator, - blockNumber: receipt.blockNumber.toString(), - gasUsed: receipt.gasUsed.toString(), + blockNumber: result.blockNumber.toString(), + gasUsed: result.gasUsed.toString(), }); } catch (error: any) { this.failSpinner("Failed to initiate vesting validator operator transfer", error.message || error); @@ -148,25 +142,21 @@ export class VestingValidatorCompleteOperatorTransferAction extends VestingActio this.startSpinner("Confirm the transaction in your browser wallet..."); try { - const readClient = await this.getReadOnlyVestingClient(options); - const vesting = await this.resolveBeneficiaryVesting(readClient, options); - - const {to, data} = buildTx(abi.VESTING_ABI as any, vesting, "vestingValidatorCompleteOperatorTransfer", [ - options.walletAddress, - ]); + const client = this.getBrowserVestingClient(options, session); + const vesting = await this.resolveBeneficiaryVesting(client, options); - const receipt = await session.sendTransaction({ - to, - data, - label: "Complete validator operator transfer", + session.setNextLabel("Complete validator operator transfer"); + const result = await client.vestingValidatorCompleteOperatorTransfer({ + vesting, + wallet: options.walletAddress as Address, }); this.succeedSpinner("Vesting validator operator transfer completed!", { - transactionHash: receipt.transactionHash, + transactionHash: result.transactionHash, vesting, wallet: options.walletAddress, - blockNumber: receipt.blockNumber.toString(), - gasUsed: receipt.gasUsed.toString(), + blockNumber: result.blockNumber.toString(), + gasUsed: result.gasUsed.toString(), }); } catch (error: any) { this.failSpinner("Failed to complete vesting validator operator transfer", error.message || error); @@ -225,25 +215,21 @@ export class VestingValidatorCancelOperatorTransferAction extends VestingAction this.startSpinner("Confirm the transaction in your browser wallet..."); try { - const readClient = await this.getReadOnlyVestingClient(options); - const vesting = await this.resolveBeneficiaryVesting(readClient, options); - - const {to, data} = buildTx(abi.VESTING_ABI as any, vesting, "vestingValidatorCancelOperatorTransfer", [ - options.walletAddress, - ]); + const client = this.getBrowserVestingClient(options, session); + const vesting = await this.resolveBeneficiaryVesting(client, options); - const receipt = await session.sendTransaction({ - to, - data, - label: "Cancel validator operator transfer", + session.setNextLabel("Cancel validator operator transfer"); + const result = await client.vestingValidatorCancelOperatorTransfer({ + vesting, + wallet: options.walletAddress as Address, }); this.succeedSpinner("Vesting validator operator transfer cancelled!", { - transactionHash: receipt.transactionHash, + transactionHash: result.transactionHash, vesting, wallet: options.walletAddress, - blockNumber: receipt.blockNumber.toString(), - gasUsed: receipt.gasUsed.toString(), + blockNumber: result.blockNumber.toString(), + gasUsed: result.gasUsed.toString(), }); } catch (error: any) { this.failSpinner("Failed to cancel vesting validator operator transfer", error.message || error); diff --git a/src/commands/vesting/validatorSetIdentity.ts b/src/commands/vesting/validatorSetIdentity.ts index 13bb7a40..11a75d81 100644 --- a/src/commands/vesting/validatorSetIdentity.ts +++ b/src/commands/vesting/validatorSetIdentity.ts @@ -1,8 +1,6 @@ import {VestingAction, VestingConfig} from "./VestingAction"; import type {Address} from "genlayer-js/types"; import {toHex} from "viem"; -import {abi} from "genlayer-js"; -import {buildTx, encodeExtraCid} from "../../lib/wallet/txBuilders"; export interface VestingValidatorSetIdentityOptions extends VestingConfig { walletAddress: string; @@ -86,34 +84,31 @@ export class VestingValidatorSetIdentityAction extends VestingAction { this.startSpinner("Confirm the transaction in your browser wallet..."); try { - const readClient = await this.getReadOnlyVestingClient(options); - const vesting = await this.resolveBeneficiaryVesting(readClient, options); - - const {to, data} = buildTx(abi.VESTING_ABI as any, vesting, "vestingValidatorSetIdentity", [ - options.walletAddress, - options.moniker || "", - options.logoUri || "", - options.website || "", - options.description || "", - options.email || "", - options.twitter || "", - options.telegram || "", - options.github || "", - encodeExtraCid(options.extraCid), - ]); - - const receipt = await session.sendTransaction({ - to, - data, - label: "Set vesting validator identity", + const client = this.getBrowserVestingClient(options, session); + const vesting = await this.resolveBeneficiaryVesting(client, options); + const extraCid = options.extraCid ? toHex(new TextEncoder().encode(options.extraCid)) : "0x"; + + session.setNextLabel("Set vesting validator identity"); + const result = await client.vestingValidatorSetIdentity({ + vesting, + wallet: options.walletAddress as Address, + moniker: options.moniker || "", + logoUri: options.logoUri || "", + website: options.website || "", + description: options.description || "", + email: options.email || "", + twitter: options.twitter || "", + telegram: options.telegram || "", + github: options.github || "", + extraCid, }); const output: Record = { - transactionHash: receipt.transactionHash, + transactionHash: result.transactionHash, vesting, wallet: options.walletAddress, - blockNumber: receipt.blockNumber.toString(), - gasUsed: receipt.gasUsed.toString(), + blockNumber: result.blockNumber.toString(), + gasUsed: result.gasUsed.toString(), }; if (options.moniker) output.moniker = options.moniker; diff --git a/src/commands/vesting/withdraw.ts b/src/commands/vesting/withdraw.ts index 0412c938..527346d2 100644 --- a/src/commands/vesting/withdraw.ts +++ b/src/commands/vesting/withdraw.ts @@ -1,6 +1,4 @@ import {VestingAction, VestingConfig} from "./VestingAction"; -import {abi} from "genlayer-js"; -import {buildTx} from "../../lib/wallet/txBuilders"; export interface VestingWithdrawOptions extends VestingConfig { amount: string; @@ -57,25 +55,23 @@ export class VestingWithdrawAction extends VestingAction { this.startSpinner("Confirm the transaction in your browser wallet..."); try { - const readClient = await this.getReadOnlyVestingClient(options); - const vesting = await this.resolveBeneficiaryVesting(readClient, options); + const client = this.getBrowserVestingClient(options, session); + const vesting = await this.resolveBeneficiaryVesting(client, options); const amount = this.parseAmount(options.amount); - const {to, data} = buildTx(abi.VESTING_ABI as any, vesting, "vestingWithdraw", [amount]); - - const receipt = await session.sendTransaction({ - to, - data, - label: `Withdraw ${this.formatAmount(amount)} from vesting`, + session.setNextLabel(`Withdraw ${this.formatAmount(amount)} from vesting`); + const result = await client.vestingWithdraw({ + vesting, + amount, }); this.succeedSpinner("Vesting withdrawal successful!", { - transactionHash: receipt.transactionHash, - vesting, - beneficiary: session.signerAddress, - amount: this.formatAmount(amount), - blockNumber: receipt.blockNumber.toString(), - gasUsed: receipt.gasUsed.toString(), + transactionHash: result.transactionHash, + vesting: result.vesting, + beneficiary: result.beneficiary, + amount: result.amount, + blockNumber: result.blockNumber.toString(), + gasUsed: result.gasUsed.toString(), }); } catch (error: any) { this.failSpinner("Failed to withdraw vested tokens", error.message || error); diff --git a/src/lib/wallet/stakingTx.ts b/src/lib/wallet/stakingTx.ts deleted file mode 100644 index 3465e496..00000000 --- a/src/lib/wallet/stakingTx.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Back-compat shim. The staking tx builders now live in the generalized - * `txBuilders.ts` (shared across staking + vesting). Re-exported here so the - * PR #367 call sites and tests that import from `stakingTx` keep working. - */ -export { - buildValidatorJoinTx, - buildSetIdentityTx, - extractValidatorWallet, - type BuiltTx, - type IdentityFields, -} from "./txBuilders"; diff --git a/src/lib/wallet/txBuilders.ts b/src/lib/wallet/txBuilders.ts deleted file mode 100644 index 70b4e309..00000000 --- a/src/lib/wallet/txBuilders.ts +++ /dev/null @@ -1,114 +0,0 @@ -import {encodeFunctionData, decodeEventLog, toHex, type Abi, type TransactionReceipt} from "viem"; -import {abi} from "genlayer-js"; -import type {Address} from "genlayer-js/types"; - -/** - * Pure transaction-building / event-decoding helpers for the browser-wallet - * signing path (Lane A: staking + vesting). These expose only the calldata so - * the CLI can hand `{to, data}` to a browser wallet that signs-and-broadcasts - * (eth_sendTransaction) instead of the SDK's sign-then-sendRawTransaction path - * (which MetaMask cannot satisfy). - * - * Dependency-free and side-effect-free so they are trivially unit-testable. - */ - -export interface BuiltTx { - to: Address; - data: `0x${string}`; -} - -function normalizeAddress(address: string): Address { - return (address.startsWith("0x") ? address : `0x${address}`) as Address; -} - -/** - * Generic calldata builder: `encodeFunctionData` against any ABI + a target. - * Per-command usage is a one-liner; avoids bespoke builders per function. - */ -export function buildTx(abiDef: Abi, to: string, functionName: string, args?: unknown[]): BuiltTx { - const data = encodeFunctionData( - args && args.length > 0 ? {abi: abiDef, functionName, args} : {abi: abiDef, functionName}, - ); - return {to: normalizeAddress(to), data}; -} - -/** - * Encode an `extraCid` identity field to bytes hex. `0x`-prefixed input passes - * through; anything else is UTF-8 encoded; empty/undefined → "0x". Mirrors the - * genlayer-js encoding used by setIdentity / vestingValidatorSetIdentity. - */ -export function encodeExtraCid(extraCid?: string): `0x${string}` { - if (!extraCid) return "0x"; - return extraCid.startsWith("0x") ? (extraCid as `0x${string}`) : toHex(new TextEncoder().encode(extraCid)); -} - -/** - * Build the calldata for `validatorJoin`. Two payable overloads exist: - * `validatorJoin(address _operator)` and `validatorJoin()`. Stake is msg.value - * (carried separately as the tx `value`, not encoded here). - */ -export function buildValidatorJoinTx(stakingAddress: string, operator?: string): BuiltTx { - return buildTx( - abi.STAKING_ABI as unknown as Abi, - stakingAddress, - "validatorJoin", - operator ? [operator as Address] : undefined, - ); -} - -export interface IdentityFields { - moniker: string; - logoUri?: string; - website?: string; - description?: string; - email?: string; - twitter?: string; - telegram?: string; - github?: string; - extraCid?: string; -} - -/** - * Build the calldata for `setIdentity` on a ValidatorWallet contract. - * `to` is the validator wallet address (not the staking contract). - */ -export function buildSetIdentityTx(validatorWallet: string, identity: IdentityFields): BuiltTx { - return buildTx(abi.VALIDATOR_WALLET_ABI as unknown as Abi, validatorWallet, "setIdentity", [ - identity.moniker, - identity.logoUri || "", - identity.website || "", - identity.description || "", - identity.email || "", - identity.twitter || "", - identity.telegram || "", - identity.github || "", - encodeExtraCid(identity.extraCid), - ]); -} - -/** - * Decode the `ValidatorJoin` event from a receipt's logs and return the new - * ValidatorWallet contract address. Throws with the same "event not found" - * style as genlayer-js if no matching log is present. - */ -export function extractValidatorWallet(receipt: TransactionReceipt): Address { - for (const log of receipt.logs) { - try { - const decoded = decodeEventLog({ - abi: abi.STAKING_ABI, - data: log.data, - topics: log.topics, - }); - if (decoded.eventName === "ValidatorJoin") { - return (decoded.args as unknown as {validator: Address}).validator; - } - } catch { - // Not a ValidatorJoin event - keep searching. - } - } - - throw new Error( - `ValidatorJoin event not found in transaction ${receipt.transactionHash}. ` + - `Transaction succeeded but validator wallet address could not be determined.`, - ); -} diff --git a/tests/actions/staking.test.ts b/tests/actions/staking.test.ts index 25a394e1..fe9a3679 100644 --- a/tests/actions/staking.test.ts +++ b/tests/actions/staking.test.ts @@ -27,13 +27,6 @@ vi.mock("genlayer-js", () => ({ }, })); -// buildTx is used by the browser-wallet paths of ValidatorDeposit/SetOperator/ -// DelegatorClaim. The genlayer-js mock stubs the ABIs with [], so mock the pure -// tx-builder helper too (real behavior covered in tests/libs/txBuilders.test.ts). -vi.mock("../../src/lib/wallet/txBuilders", () => ({ - buildTx: vi.fn(() => ({to: "0xTarget", data: "0xdata"})), -})); - vi.mock("genlayer-js/chains", () => ({ localnet: {id: 1, name: "localnet", rpcUrls: {default: {http: ["http://localhost:8545"]}}}, studionet: {id: 2, name: "studionet", rpcUrls: {default: {http: ["https://studionet.genlayer.com"]}}}, @@ -49,14 +42,6 @@ vi.mock("genlayer-js/chains", () => ({ }, })); -// The genlayer-js mock above stubs `abi` with an empty ABI, so mock the pure -// tx-builder module (its real behavior is covered in tests/libs/stakingTx.test.ts). -vi.mock("../../src/lib/wallet/stakingTx", () => ({ - buildValidatorJoinTx: vi.fn(() => ({to: "0xStaking", data: "0xdata"})), - buildSetIdentityTx: vi.fn(() => ({to: "0xValidatorWallet", data: "0xidentity"})), - extractValidatorWallet: vi.fn(() => "0xValidatorWalletFromEvent"), -})); - const mockTxResult = { transactionHash: "0xMockedHash" as `0x${string}`, blockNumber: 123n, @@ -569,11 +554,13 @@ describe("StakingInfoAction", () => { describe("ValidatorJoinAction --wallet browser", () => { let action: ValidatorJoinAction; - const mockReceipt = { + const mockBrowserJoinResult = { transactionHash: "0xBrowserHash", blockNumber: 456n, gasUsed: 30000n, - status: "success", + validatorWallet: "0xValidatorWalletFromEvent", + operator: "0xBrowserOwner", + amount: "42000 GEN", }; beforeEach(() => { @@ -590,20 +577,17 @@ describe("ValidatorJoinAction --wallet browser", () => { vi.restoreAllMocks(); }); - test("routes through the browser session and never touches the keystore", async () => { + test("routes through the browser SDK client and never touches the keystore", async () => { const getStakingClientSpy = vi.spyOn(action as any, "getStakingClient"); const getSignerAddressSpy = vi.spyOn(action as any, "getSignerAddress"); // The command's finally calls session.close() (no-op for remote daemon // sessions, full close for an own bridge) — not session.bridge.close(). - const close = vi.fn().mockResolvedValue(undefined); - const sendTransaction = vi.fn().mockResolvedValue(mockReceipt); - vi.spyOn(action as any, "getBrowserWalletSession").mockResolvedValue({ - bridge: {close: vi.fn()}, - close, - stakingAddress: "0xStaking", - signerAddress: "0xBrowserOwner", - sendTransaction, - }); + const session = makeBrowserSession(); + vi.spyOn(action as any, "getBrowserWalletSession").mockResolvedValue(session); + // Browser writes run the SAME SDK call as the keystore lane; the client is + // built by getBrowserStakingClient (synchronous — Address account + provider). + const mockBrowserClient = {validatorJoin: vi.fn().mockResolvedValue(mockBrowserJoinResult)}; + vi.spyOn(action as any, "getBrowserStakingClient").mockReturnValue(mockBrowserClient); await action.execute({amount: "42000gen", wallet: "browser", stakingAddress: "0xStaking"}); @@ -611,10 +595,14 @@ describe("ValidatorJoinAction --wallet browser", () => { expect.any(Object), "validator-join", ); - expect(sendTransaction).toHaveBeenCalledOnce(); + expect(mockBrowserClient.validatorJoin).toHaveBeenCalledWith({ + amount: expect.any(BigInt), + operator: undefined, + }); + expect(session.setNextLabel).toHaveBeenCalledWith(expect.stringContaining("Join as validator")); expect(getStakingClientSpy).not.toHaveBeenCalled(); expect(getSignerAddressSpy).not.toHaveBeenCalled(); - expect(close).toHaveBeenCalledOnce(); + expect(session.close).toHaveBeenCalledOnce(); // Output shape matches the keystore path. expect(action["succeedSpinner"]).toHaveBeenCalledWith( @@ -630,13 +618,10 @@ describe("ValidatorJoinAction --wallet browser", () => { }); test("closes the session even when the send fails", async () => { - const close = vi.fn().mockResolvedValue(undefined); - vi.spyOn(action as any, "getBrowserWalletSession").mockResolvedValue({ - bridge: {close: vi.fn()}, - close, - stakingAddress: "0xStaking", - signerAddress: "0xBrowserOwner", - sendTransaction: vi.fn().mockRejectedValue(new Error("Transaction rejected in wallet")), + const session = makeBrowserSession(); + vi.spyOn(action as any, "getBrowserWalletSession").mockResolvedValue(session); + vi.spyOn(action as any, "getBrowserStakingClient").mockReturnValue({ + validatorJoin: vi.fn().mockRejectedValue(new Error("Transaction rejected in wallet")), }); await action.execute({amount: "42000gen", wallet: "browser", stakingAddress: "0xStaking"}); @@ -645,7 +630,7 @@ describe("ValidatorJoinAction --wallet browser", () => { "Failed to create validator", "Transaction rejected in wallet", ); - expect(close).toHaveBeenCalledOnce(); + expect(session.close).toHaveBeenCalledOnce(); }); test("rejects --wallet browser combined with --password", async () => { @@ -667,19 +652,18 @@ describe("ValidatorJoinAction --wallet browser", () => { }); }); -// Shared factory for a staking browser-wallet session. These commands call -// `session.close()` (not session.bridge.close()) in their finally block. +// Shared factory for a staking browser-wallet session. Writes now run through +// the genlayer-js SDK client (getBrowserStakingClient) which routes +// eth_sendTransaction through `eip1193Provider`; `setNextLabel` sets the +// human-readable bridge label. These commands call `session.close()` (not +// session.bridge.close()) in their finally block. function makeBrowserSession(overrides: Record = {}) { return { bridge: {close: vi.fn()}, stakingAddress: "0xStaking", signerAddress: "0xBrowserOwner", - sendTransaction: vi.fn().mockResolvedValue({ - transactionHash: "0xBH", - blockNumber: 5n, - gasUsed: 6n, - status: "success", - }), + setNextLabel: vi.fn(), + eip1193Provider: {request: vi.fn()}, close: vi.fn().mockResolvedValue(undefined), ...overrides, }; @@ -706,16 +690,24 @@ describe("ValidatorDepositAction --wallet browser", () => { vi.restoreAllMocks(); }); - test("routes through the browser session, skips keystore, closes session", async () => { + test("routes through the browser SDK client, skips keystore, closes session", async () => { const getStakingClientSpy = vi.spyOn(action as any, "getStakingClient"); const getReadOnlyStakingClientSpy = vi.spyOn(action as any, "getReadOnlyStakingClient"); const getSignerAddressSpy = vi.spyOn(action as any, "getSignerAddress"); const session = makeBrowserSession(); vi.spyOn(action as any, "getBrowserWalletSession").mockResolvedValue(session); + const mockClient = { + validatorDeposit: vi.fn().mockResolvedValue({transactionHash: "0xBH", blockNumber: 5n, gasUsed: 6n}), + }; + vi.spyOn(action as any, "getBrowserStakingClient").mockReturnValue(mockClient); await action.execute({validator: "0xVW", amount: "1gen", wallet: "browser"}); - expect(session.sendTransaction).toHaveBeenCalledOnce(); + expect(mockClient.validatorDeposit).toHaveBeenCalledWith({ + validator: "0xVW", + amount: expect.any(BigInt), + }); + expect(session.setNextLabel).toHaveBeenCalledWith(expect.stringContaining("Deposit")); expect(getStakingClientSpy).not.toHaveBeenCalled(); expect(getReadOnlyStakingClientSpy).not.toHaveBeenCalled(); expect(getSignerAddressSpy).not.toHaveBeenCalled(); @@ -733,10 +725,11 @@ describe("ValidatorDepositAction --wallet browser", () => { }); test("closes the session even when the send fails", async () => { - const session = makeBrowserSession({ - sendTransaction: vi.fn().mockRejectedValue(new Error("Rejected in wallet")), - }); + const session = makeBrowserSession(); vi.spyOn(action as any, "getBrowserWalletSession").mockResolvedValue(session); + vi.spyOn(action as any, "getBrowserStakingClient").mockReturnValue({ + validatorDeposit: vi.fn().mockRejectedValue(new Error("Rejected in wallet")), + }); await action.execute({validator: "0xVW", amount: "1gen", wallet: "browser"}); @@ -771,16 +764,21 @@ describe("SetOperatorAction --wallet browser", () => { vi.restoreAllMocks(); }); - test("routes through the browser session, skips keystore, closes session", async () => { + test("routes through the browser SDK client, skips keystore, closes session", async () => { const getStakingClientSpy = vi.spyOn(action as any, "getStakingClient"); const getReadOnlyStakingClientSpy = vi.spyOn(action as any, "getReadOnlyStakingClient"); const getSignerAddressSpy = vi.spyOn(action as any, "getSignerAddress"); const session = makeBrowserSession(); vi.spyOn(action as any, "getBrowserWalletSession").mockResolvedValue(session); + const mockClient = { + setOperator: vi.fn().mockResolvedValue({transactionHash: "0xBH", blockNumber: 5n, gasUsed: 6n}), + }; + vi.spyOn(action as any, "getBrowserStakingClient").mockReturnValue(mockClient); await action.execute({validator: "0xVW", operator: "0xOp", wallet: "browser"}); - expect(session.sendTransaction).toHaveBeenCalledOnce(); + expect(mockClient.setOperator).toHaveBeenCalledWith({validator: "0xVW", operator: "0xOp"}); + expect(session.setNextLabel).toHaveBeenCalledWith(expect.stringContaining("Set operator to 0xOp")); expect(getStakingClientSpy).not.toHaveBeenCalled(); expect(getReadOnlyStakingClientSpy).not.toHaveBeenCalled(); expect(getSignerAddressSpy).not.toHaveBeenCalled(); @@ -811,16 +809,25 @@ describe("DelegatorClaimAction --wallet browser", () => { vi.restoreAllMocks(); }); - test("routes through the browser session, defaults delegator to session signer", async () => { + test("routes through the browser SDK client, defaults delegator to session signer", async () => { const getStakingClientSpy = vi.spyOn(action as any, "getStakingClient"); const getReadOnlyStakingClientSpy = vi.spyOn(action as any, "getReadOnlyStakingClient"); const getSignerAddressSpy = vi.spyOn(action as any, "getSignerAddress"); const session = makeBrowserSession(); vi.spyOn(action as any, "getBrowserWalletSession").mockResolvedValue(session); + const mockClient = { + delegatorClaim: vi.fn().mockResolvedValue({transactionHash: "0xBH", blockNumber: 5n, gasUsed: 6n}), + }; + vi.spyOn(action as any, "getBrowserStakingClient").mockReturnValue(mockClient); await action.execute({validator: "0xVal", wallet: "browser"}); - expect(session.sendTransaction).toHaveBeenCalledOnce(); + // Delegator defaults to the connected session signer, never the keystore. + expect(mockClient.delegatorClaim).toHaveBeenCalledWith({ + validator: "0xVal", + delegator: "0xBrowserOwner", + }); + expect(session.setNextLabel).toHaveBeenCalledWith(expect.stringContaining("Claim delegator withdrawals")); expect(getStakingClientSpy).not.toHaveBeenCalled(); expect(getReadOnlyStakingClientSpy).not.toHaveBeenCalled(); // Browser mode reads the connected wallet from the session, never the keystore. diff --git a/tests/actions/stakingWizard.test.ts b/tests/actions/stakingWizard.test.ts index fc1f5af5..e21cd52f 100644 --- a/tests/actions/stakingWizard.test.ts +++ b/tests/actions/stakingWizard.test.ts @@ -3,7 +3,6 @@ import inquirer from "inquirer"; import {ValidatorWizardAction} from "../../src/commands/staking/wizard"; import {CreateAccountAction} from "../../src/commands/account/create"; import {ExportAccountAction} from "../../src/commands/account/export"; -import {buildTx} from "../../src/lib/wallet/txBuilders"; vi.mock("inquirer"); vi.mock("../../src/commands/account/create"); @@ -51,24 +50,21 @@ vi.mock("genlayer-js", () => ({ abi: {STAKING_ABI: [], VESTING_ABI: []}, })); -// Pure tx-builders (real behavior covered in tests/libs/stakingTx.test.ts). -vi.mock("../../src/lib/wallet/stakingTx", () => ({ - buildValidatorJoinTx: vi.fn(() => ({to: "0xStaking", data: "0xjoin"})), - buildSetIdentityTx: vi.fn(() => ({to: "0xValidatorWallet", data: "0xidentity"})), - extractValidatorWallet: vi.fn(() => "0xValidatorWalletFromEvent"), -})); - -// Generic vesting calldata builder (real behavior covered in tests/libs/txBuilders.test.ts). -vi.mock("../../src/lib/wallet/txBuilders", () => ({ - buildTx: vi.fn(() => ({to: "0xVesting", data: "0xvestingjoin"})), -})); - describe("ValidatorWizardAction --wallet browser (owner)", () => { let action: ValidatorWizardAction; let sendTransaction: ReturnType; let bridgeClose: ReturnType; + let setNextLabel: ReturnType; let getBrowserWalletSessionSpy: any; let getStakingClientSpy: any; + // Browser SDK clients. Writes run the SAME client.(...) calls as the + // keystore lane — the wallet-funded lane via getBrowserStakingClient, the + // vesting-funded lane via the private getWizardVestingBrowserClient. + let validatorJoin: ReturnType; + let setIdentity: ReturnType; + let vestingValidatorJoin: ReturnType; + let vestingValidatorSetIdentity: ReturnType; + let getValidatorWallets: ReturnType; beforeEach(() => { vi.clearAllMocks(); @@ -94,13 +90,15 @@ describe("ValidatorWizardAction --wallet browser (owner)", () => { vi.spyOn(action as any, "getConfigByKey").mockReturnValue("testnet-bradbury"); vi.spyOn(action as any, "writeConfig").mockImplementation(() => {}); - // Browser session seam. + // Browser session seam. Writes route through the SDK client's provider, so + // the session only needs setNextLabel + close for these assertions. sendTransaction = vi.fn().mockResolvedValue({ transactionHash: "0xJoinHash", blockNumber: 10n, gasUsed: 1000n, status: "success", }); + setNextLabel = vi.fn(); bridgeClose = vi.fn().mockResolvedValue(undefined); getBrowserWalletSessionSpy = vi.spyOn(action as any, "getBrowserWalletSession").mockResolvedValue({ bridge: {close: bridgeClose}, @@ -108,12 +106,47 @@ describe("ValidatorWizardAction --wallet browser (owner)", () => { sessionUrl: "http://127.0.0.1:1/#s=t", stakingAddress: "0xStaking", signerAddress: "0xBrowserOwner", + setNextLabel, + eip1193Provider: {request: vi.fn()}, sendTransaction, // The wizard finally block now calls session.close() (no-op for remote, // full close for own bridge). Delegate to bridgeClose so the assertion holds. close: bridgeClose, }); + // Wallet-funded browser lane: getBrowserStakingClient(...).validatorJoin / + // .setIdentity. The join result carries the decoded validator wallet. + validatorJoin = vi.fn().mockResolvedValue({ + transactionHash: "0xJoinHash", + blockNumber: 10n, + gasUsed: 1000n, + validatorWallet: "0xValidatorWalletFromEvent", + operator: "0xBrowserOwner", + amount: "42 GEN", + }); + setIdentity = vi.fn().mockResolvedValue({transactionHash: "0xIdHash", blockNumber: 11n, gasUsed: 100n}); + vi.spyOn(action as any, "getBrowserStakingClient").mockReturnValue({validatorJoin, setIdentity}); + + // Vesting-funded browser lane: getWizardVestingBrowserClient(...). + // vestingValidatorJoin / .vestingValidatorSetIdentity, plus getValidatorWallets + // to resolve the just-created wallet (newest entry). + vestingValidatorJoin = vi.fn().mockResolvedValue({ + transactionHash: "0xVJoinHash", + blockNumber: 12n, + gasUsed: 100n, + }); + vestingValidatorSetIdentity = vi.fn().mockResolvedValue({ + transactionHash: "0xVIdHash", + blockNumber: 13n, + gasUsed: 100n, + }); + getValidatorWallets = vi.fn().mockResolvedValue(["0xVWallet"]); + vi.spyOn(action as any, "getWizardVestingBrowserClient").mockReturnValue({ + vestingValidatorJoin, + vestingValidatorSetIdentity, + getValidatorWallets, + }); + // Ensure the keystore staking path is never exercised. getStakingClientSpy = vi.spyOn(action as any, "getStakingClient"); @@ -126,7 +159,7 @@ describe("ValidatorWizardAction --wallet browser (owner)", () => { vi.restoreAllMocks(); }); - test("routes join through the bridge, keeps operator export, never uses keystore", async () => { + test("routes join through the browser SDK client, keeps operator export, never uses keystore", async () => { // Prompt sequence: // funding source -> wallet (default; original flow unchanged) // step4 useOperator -> true; operatorChoice -> create; operatorName -> "op"; @@ -157,14 +190,11 @@ describe("ValidatorWizardAction --wallet browser (owner)", () => { ); expect(bridgeClose).toHaveBeenCalled(); - // Join tx went through the bridge, not the keystore staking client. - expect(sendTransaction).toHaveBeenCalledWith( - expect.objectContaining({ - to: "0xStaking", - data: "0xjoin", - label: expect.stringContaining("Join as validator"), - }), - ); + // Join ran the SAME SDK call as the keystore lane, through the browser + // client (Address account + provider), not the keystore staking client. + // Operator is the created operator account (fsMock address), passed straight through. + expect(validatorJoin).toHaveBeenCalledWith({amount: 42n * 10n ** 18n, operator: "0xOperatorAddr"}); + expect(setNextLabel).toHaveBeenCalledWith(expect.stringContaining("Join as validator")); expect(getStakingClientSpy).not.toHaveBeenCalled(); // Operator keystore export still happened (step 4 unchanged). @@ -200,23 +230,19 @@ describe("ValidatorWizardAction --wallet browser (owner)", () => { // Beneficiary lookup used the connected browser address. expect(mockGlClient.getBeneficiaryVestings).toHaveBeenCalledWith("0xBrowserOwner"); - // Calldata was built for vestingValidatorJoin with the chosen operator + amount. - expect(vi.mocked(buildTx)).toHaveBeenCalledWith([], "0xVesting", "vestingValidatorJoin", [ - "0xOperatorAddr", - 42n * 10n ** 18n, - ]); - - // Join went through the SAME browser session, no msg.value, never the keystore. - expect(sendTransaction).toHaveBeenCalledWith( - expect.objectContaining({ - to: "0xVesting", - data: "0xvestingjoin", - label: expect.stringContaining("Create vesting validator"), - }), - ); - expect(sendTransaction.mock.calls[0][0].value).toBeUndefined(); + // Join ran vestingValidatorJoin on the browser vesting client with the chosen + // operator + amount — the same SDK call as `vesting validator create`. + expect(vestingValidatorJoin).toHaveBeenCalledWith({ + vesting: "0xVesting", + operator: "0xOperatorAddr", + amount: 42n * 10n ** 18n, + }); + expect(setNextLabel).toHaveBeenCalledWith(expect.stringContaining("Create vesting validator")); + // Never the keystore staking client. expect(getStakingClientSpy).not.toHaveBeenCalled(); + // The created wallet is resolved from the vesting contract's newest entry. + expect(getValidatorWallets).toHaveBeenCalledWith("0xVesting"); expect(action["succeedSpinner"]).toHaveBeenCalledWith( "Vesting-backed validator created successfully!", expect.objectContaining({vesting: "0xVesting", validatorWallet: "0xVWallet"}), @@ -248,24 +274,18 @@ describe("ValidatorWizardAction --wallet browser (owner)", () => { await action.execute({amount: "", wallet: "browser", network: "testnet-bradbury"} as any); - // Identity calldata built for vestingValidatorSetIdentity against the created - // wallet (0xVWallet from getValidatorWallets), extraCid empty. - expect(vi.mocked(buildTx)).toHaveBeenCalledWith([], "0xVesting", "vestingValidatorSetIdentity", [ - "0xVWallet", - "MyVesting", - "", - "", - "", - "", - "", - "", - "", - "0x", - ]); - // Sent through the SAME browser session used for the join, never the keystore. - expect(sendTransaction).toHaveBeenCalledWith( - expect.objectContaining({label: expect.stringContaining("Set validator identity")}), + // Identity ran vestingValidatorSetIdentity on the browser vesting client + // against the created wallet (0xVWallet from getValidatorWallets), extraCid empty. + expect(vestingValidatorSetIdentity).toHaveBeenCalledWith( + expect.objectContaining({ + vesting: "0xVesting", + wallet: "0xVWallet", + moniker: "MyVesting", + extraCid: "0x", + }), ); + // Labeled + sent through the SAME browser client lane, never the keystore. + expect(setNextLabel).toHaveBeenCalledWith(expect.stringContaining("Set validator identity")); expect(getStakingClientSpy).not.toHaveBeenCalled(); expect(action["succeedSpinner"]).toHaveBeenCalledWith("Validator identity set!"); }); @@ -807,8 +827,9 @@ describe("ValidatorWizardAction --non-interactive (keystore owner)", () => { describe("ValidatorWizardAction --non-interactive (browser owner)", () => { let action: ValidatorWizardAction; - let sendTransaction: ReturnType; + let setNextLabel: ReturnType; let bridgeClose: ReturnType; + let validatorJoin: ReturnType; let getBrowserWalletSessionSpy: any; let getStakingClientSpy: any; @@ -834,11 +855,7 @@ describe("ValidatorWizardAction --non-interactive (browser owner)", () => { vi.spyOn(action as any, "getConfigByKey").mockReturnValue("testnet-bradbury"); vi.spyOn(action as any, "writeConfig").mockImplementation(() => {}); - sendTransaction = vi.fn().mockResolvedValue({ - transactionHash: "0xJoinHash", - blockNumber: 10n, - status: "success", - }); + setNextLabel = vi.fn(); bridgeClose = vi.fn().mockResolvedValue(undefined); getBrowserWalletSessionSpy = vi.spyOn(action as any, "getBrowserWalletSession").mockResolvedValue({ bridge: {close: bridgeClose}, @@ -846,10 +863,22 @@ describe("ValidatorWizardAction --non-interactive (browser owner)", () => { sessionUrl: "http://127.0.0.1:1/#s=t", stakingAddress: "0xStaking", signerAddress: "0xBrowserOwner", - sendTransaction, + setNextLabel, + eip1193Provider: {request: vi.fn()}, close: bridgeClose, }); + // Wallet-funded browser lane runs the same client.validatorJoin(...) as keystore. + validatorJoin = vi.fn().mockResolvedValue({ + transactionHash: "0xJoinHash", + blockNumber: 10n, + gasUsed: 1000n, + validatorWallet: "0xValidatorWalletFromEvent", + operator: "0x2222222222222222222222222222222222222222", + amount: "50 GEN", + }); + vi.spyOn(action as any, "getBrowserStakingClient").mockReturnValue({validatorJoin}); + getStakingClientSpy = vi.spyOn(action as any, "getStakingClient"); vi.spyOn(action as any, "listAccounts").mockReturnValue([]); vi.mocked(CreateAccountAction.prototype.execute).mockResolvedValue(undefined as any); @@ -860,7 +889,7 @@ describe("ValidatorWizardAction --non-interactive (browser owner)", () => { vi.restoreAllMocks(); }); - test("browser owner runs end-to-end through the bridge with ZERO prompts", async () => { + test("browser owner runs end-to-end through the browser SDK client with ZERO prompts", async () => { await action.execute({ wallet: "browser", network: "testnet-bradbury", @@ -871,10 +900,13 @@ describe("ValidatorWizardAction --non-interactive (browser owner)", () => { } as any); expect(inquirer.prompt).not.toHaveBeenCalled(); - // Join went through the bridge, never the keystore staking client. - expect(sendTransaction).toHaveBeenCalledWith( - expect.objectContaining({data: "0xjoin", label: expect.stringContaining("Join as validator")}), - ); + // Join ran the SAME SDK call as keystore, through the browser client, never + // the keystore staking client. + expect(validatorJoin).toHaveBeenCalledWith({ + amount: 50n * 10n ** 18n, + operator: "0x2222222222222222222222222222222222222222", + }); + expect(setNextLabel).toHaveBeenCalledWith(expect.stringContaining("Join as validator")); expect(getStakingClientSpy).not.toHaveBeenCalled(); expect(bridgeClose).toHaveBeenCalled(); expect(action["succeedSpinner"]).toHaveBeenCalledWith( diff --git a/tests/actions/vesting.test.ts b/tests/actions/vesting.test.ts index b7aa2dd0..9dbb048f 100644 --- a/tests/actions/vesting.test.ts +++ b/tests/actions/vesting.test.ts @@ -2,8 +2,9 @@ import {describe, test, vi, beforeEach, afterEach, expect} from "vitest"; import {VestingDelegateAction} from "../../src/commands/vesting/delegate"; import {VestingWithdrawAction} from "../../src/commands/vesting/withdraw"; -// Mock genlayer-js. The browser-wallet path builds calldata via the mocked -// txBuilders helper below, so stubbed ABIs are enough here. +// Mock genlayer-js. The browser-wallet path routes writes through the SDK +// client built by getBrowserVestingClient, which we spy in each test, so +// stubbed ABIs are enough here. vi.mock("genlayer-js", () => ({ createClient: vi.fn(), createAccount: vi.fn(() => ({address: "0xMockedAddress"})), @@ -34,22 +35,15 @@ vi.mock("genlayer-js/chains", () => ({ }, })); -// The genlayer-js mock stubs `abi` with empty ABIs, so mock the pure tx-builder -// module (real behavior is covered in tests/libs/txBuilders.test.ts). -vi.mock("../../src/lib/wallet/txBuilders", () => ({ - buildTx: vi.fn(() => ({to: "0xVesting", data: "0xdata"})), -})); - -// Shared vesting browser-wallet session factory. Commands call `session.close()` -// in their finally block. +// Shared vesting browser-wallet session factory. Writes now run through the +// genlayer-js SDK client (getBrowserVestingClient), which routes +// eth_sendTransaction through `eip1193Provider`; `setNextLabel` sets the +// bridge label. Commands call `session.close()` in their finally block. function makeVestingSession(overrides: Record = {}) { return { signerAddress: "0xBen", - sendTransaction: vi.fn().mockResolvedValue({ - transactionHash: "0xVH", - blockNumber: 9n, - gasUsed: 8n, - }), + setNextLabel: vi.fn(), + eip1193Provider: {request: vi.fn()}, close: vi.fn().mockResolvedValue(undefined), ...overrides, }; @@ -61,11 +55,8 @@ function setupVestingBrowserMocks(action: any) { vi.spyOn(action, "succeedSpinner").mockImplementation(() => {}); vi.spyOn(action, "failSpinner").mockImplementation(() => {}); vi.spyOn(action, "log").mockImplementation(() => {}); - // Read-only client used to resolve the beneficiary vesting; account-less. - vi.spyOn(action, "getReadOnlyVestingClient").mockResolvedValue({ - getBeneficiaryVestings: vi.fn().mockResolvedValue(["0xVesting"]), - }); - // Simplest resolution: the vesting address is fixed. + // Simplest resolution: the vesting address is fixed. The browser lane calls + // resolveBeneficiaryVesting(client, options) with the browser SDK client. vi.spyOn(action, "resolveBeneficiaryVesting").mockResolvedValue("0xVesting"); } @@ -82,14 +73,31 @@ describe("VestingDelegateAction --wallet browser", () => { vi.restoreAllMocks(); }); - test("routes through the browser session, skips keystore client, closes session", async () => { + test("routes through the browser SDK client, skips keystore client, closes session", async () => { const getVestingClientSpy = vi.spyOn(action as any, "getVestingClient"); const session = makeVestingSession(); vi.spyOn(action as any, "getVestingBrowserSession").mockResolvedValue(session); + const mockClient = { + vestingDelegatorJoin: vi.fn().mockResolvedValue({ + transactionHash: "0xVH", + vesting: "0xVesting", + validator: "0xVal", + beneficiary: "0xBen", + amount: "1 GEN", + blockNumber: 9n, + gasUsed: 8n, + }), + }; + vi.spyOn(action as any, "getBrowserVestingClient").mockReturnValue(mockClient); await action.execute({validator: "0xVal", amount: "1gen", wallet: "browser"}); - expect(session.sendTransaction).toHaveBeenCalledOnce(); + expect(mockClient.vestingDelegatorJoin).toHaveBeenCalledWith({ + vesting: "0xVesting", + validator: "0xVal", + amount: expect.any(BigInt), + }); + expect(session.setNextLabel).toHaveBeenCalledWith(expect.stringContaining("Delegate")); expect(getVestingClientSpy).not.toHaveBeenCalled(); expect(session.close).toHaveBeenCalledOnce(); expect(action["succeedSpinner"]).toHaveBeenCalledWith( @@ -107,10 +115,11 @@ describe("VestingDelegateAction --wallet browser", () => { }); test("closes the session even when the send fails", async () => { - const session = makeVestingSession({ - sendTransaction: vi.fn().mockRejectedValue(new Error("Rejected in wallet")), - }); + const session = makeVestingSession(); vi.spyOn(action as any, "getVestingBrowserSession").mockResolvedValue(session); + vi.spyOn(action as any, "getBrowserVestingClient").mockReturnValue({ + vestingDelegatorJoin: vi.fn().mockRejectedValue(new Error("Rejected in wallet")), + }); await action.execute({validator: "0xVal", amount: "1gen", wallet: "browser"}); @@ -135,14 +144,29 @@ describe("VestingWithdrawAction --wallet browser", () => { vi.restoreAllMocks(); }); - test("routes through the browser session, skips keystore client, closes session", async () => { + test("routes through the browser SDK client, skips keystore client, closes session", async () => { const getVestingClientSpy = vi.spyOn(action as any, "getVestingClient"); const session = makeVestingSession(); vi.spyOn(action as any, "getVestingBrowserSession").mockResolvedValue(session); + const mockClient = { + vestingWithdraw: vi.fn().mockResolvedValue({ + transactionHash: "0xVH", + vesting: "0xVesting", + beneficiary: "0xBen", + amount: "1 GEN", + blockNumber: 9n, + gasUsed: 8n, + }), + }; + vi.spyOn(action as any, "getBrowserVestingClient").mockReturnValue(mockClient); await action.execute({amount: "1gen", wallet: "browser"}); - expect(session.sendTransaction).toHaveBeenCalledOnce(); + expect(mockClient.vestingWithdraw).toHaveBeenCalledWith({ + vesting: "0xVesting", + amount: expect.any(BigInt), + }); + expect(session.setNextLabel).toHaveBeenCalledWith(expect.stringContaining("Withdraw")); expect(getVestingClientSpy).not.toHaveBeenCalled(); expect(session.close).toHaveBeenCalledOnce(); expect(action["succeedSpinner"]).toHaveBeenCalledWith( diff --git a/tests/libs/stakingTx.test.ts b/tests/libs/stakingTx.test.ts deleted file mode 100644 index 0c213d05..00000000 --- a/tests/libs/stakingTx.test.ts +++ /dev/null @@ -1,116 +0,0 @@ -import {describe, test, expect} from "vitest"; -import {encodeFunctionData, encodeEventTopics, encodeAbiParameters, type TransactionReceipt} from "viem"; -import {abi} from "genlayer-js"; -import { - buildValidatorJoinTx, - buildSetIdentityTx, - extractValidatorWallet, -} from "../../src/lib/wallet/stakingTx"; - -const STAKING = "0x4A4449E617F8D10FDeD0b461CadEf83939E821A5"; -const OPERATOR = "0x1111111111111111111111111111111111111111"; -const VALIDATOR_WALLET = "0x2222222222222222222222222222222222222222"; - -describe("buildValidatorJoinTx", () => { - test("encodes the no-operator overload (bare validatorJoin())", () => { - const {to, data} = buildValidatorJoinTx(STAKING); - const expected = encodeFunctionData({abi: abi.STAKING_ABI, functionName: "validatorJoin"}); - expect(to).toBe(STAKING); - expect(data).toBe(expected); - }); - - test("encodes the operator overload with the operator arg", () => { - const {data} = buildValidatorJoinTx(STAKING, OPERATOR); - const expected = encodeFunctionData({ - abi: abi.STAKING_ABI, - functionName: "validatorJoin", - args: [OPERATOR as `0x${string}`], - }); - expect(data).toBe(expected); - }); - - test("selectors differ between the two overloads", () => { - const bare = buildValidatorJoinTx(STAKING).data; - const withOp = buildValidatorJoinTx(STAKING, OPERATOR).data; - expect(bare.slice(0, 10)).not.toBe(withOp.slice(0, 10)); - }); - - test("prefixes a bare (0x-less) staking address", () => { - const {to} = buildValidatorJoinTx(STAKING.slice(2)); - expect(to).toBe(STAKING); - }); -}); - -describe("buildSetIdentityTx", () => { - test("encodes moniker with empty optional fields and 0x extraCid", () => { - const {to, data} = buildSetIdentityTx(VALIDATOR_WALLET, {moniker: "MyValidator"}); - const expected = encodeFunctionData({ - abi: abi.VALIDATOR_WALLET_ABI, - functionName: "setIdentity", - args: ["MyValidator", "", "", "", "", "", "", "", "0x"], - }); - expect(to).toBe(VALIDATOR_WALLET); - expect(data).toBe(expected); - }); - - test("hex extraCid is passed through verbatim", () => { - const {data} = buildSetIdentityTx(VALIDATOR_WALLET, {moniker: "V", extraCid: "0xdeadbeef"}); - expect(data).toBe( - encodeFunctionData({ - abi: abi.VALIDATOR_WALLET_ABI, - functionName: "setIdentity", - args: ["V", "", "", "", "", "", "", "", "0xdeadbeef"], - }), - ); - }); - - test("non-hex extraCid is UTF-8 hex-encoded", () => { - const {data} = buildSetIdentityTx(VALIDATOR_WALLET, {moniker: "V", extraCid: "cid"}); - const cidHex = ("0x" + Buffer.from("cid", "utf-8").toString("hex")) as `0x${string}`; - expect(data).toBe( - encodeFunctionData({ - abi: abi.VALIDATOR_WALLET_ABI, - functionName: "setIdentity", - args: ["V", "", "", "", "", "", "", "", cidHex], - }), - ); - }); -}); - -describe("extractValidatorWallet", () => { - function receiptWithJoinLog(validator: string): TransactionReceipt { - // ValidatorJoin(operator, validator, amount) — all non-indexed. - const topics = encodeEventTopics({abi: abi.STAKING_ABI, eventName: "ValidatorJoin"}); - const data = encodeAbiParameters( - [ - {name: "operator", type: "address"}, - {name: "validator", type: "address"}, - {name: "amount", type: "uint256"}, - ], - [OPERATOR as `0x${string}`, validator as `0x${string}`, 42000n * 10n ** 18n], - ); - return { - transactionHash: "0xabc" as `0x${string}`, - logs: [{data, topics}], - } as unknown as TransactionReceipt; - } - - test("returns the validator wallet address from the ValidatorJoin log", () => { - const receipt = receiptWithJoinLog(VALIDATOR_WALLET); - expect(extractValidatorWallet(receipt).toLowerCase()).toBe(VALIDATOR_WALLET.toLowerCase()); - }); - - test("ignores unrelated / undecodable logs and finds the join event", () => { - const receipt = receiptWithJoinLog(VALIDATOR_WALLET); - (receipt.logs as any).unshift({data: "0x", topics: ["0xdeadbeef"]}); - expect(extractValidatorWallet(receipt).toLowerCase()).toBe(VALIDATOR_WALLET.toLowerCase()); - }); - - test("throws a clear error when no ValidatorJoin event is present", () => { - const receipt = { - transactionHash: "0xdef" as `0x${string}`, - logs: [], - } as unknown as TransactionReceipt; - expect(() => extractValidatorWallet(receipt)).toThrow(/ValidatorJoin event not found/); - }); -}); diff --git a/tests/libs/txBuilders.test.ts b/tests/libs/txBuilders.test.ts deleted file mode 100644 index 862ffb27..00000000 --- a/tests/libs/txBuilders.test.ts +++ /dev/null @@ -1,152 +0,0 @@ -import {describe, test, expect} from "vitest"; -import {encodeFunctionData, type Abi} from "viem"; -import {abi} from "genlayer-js"; -import {buildTx, encodeExtraCid} from "../../src/lib/wallet/txBuilders"; - -const VALIDATOR_WALLET = "0x2222222222222222222222222222222222222222"; -const VESTING = "0x3333333333333333333333333333333333333333"; -const OPERATOR = "0x1111111111111111111111111111111111111111"; -const VALIDATOR = "0x4444444444444444444444444444444444444444"; - -describe("buildTx (generic calldata builder)", () => { - test("prefixes a bare (0x-less) address", () => { - const {to} = buildTx( - abi.VALIDATOR_WALLET_ABI as unknown as Abi, - VALIDATOR_WALLET.slice(2), - "validatorClaim", - ); - expect(to).toBe(VALIDATOR_WALLET); - }); - - test("no-arg call matches encodeFunctionData without args", () => { - const {data} = buildTx(abi.VALIDATOR_WALLET_ABI as unknown as Abi, VALIDATOR_WALLET, "validatorDeposit"); - expect(data).toBe(encodeFunctionData({abi: abi.VALIDATOR_WALLET_ABI, functionName: "validatorDeposit"})); - }); - - // --- Validator-wallet family --- - test("setOperator(address) encodes against VALIDATOR_WALLET_ABI", () => { - const {to, data} = buildTx(abi.VALIDATOR_WALLET_ABI as unknown as Abi, VALIDATOR_WALLET, "setOperator", [ - OPERATOR, - ]); - expect(to).toBe(VALIDATOR_WALLET); - expect(data).toBe( - encodeFunctionData({ - abi: abi.VALIDATOR_WALLET_ABI, - functionName: "setOperator", - args: [OPERATOR as `0x${string}`], - }), - ); - }); - - test("validatorExit(uint256) encodes shares arg", () => { - const {data} = buildTx(abi.VALIDATOR_WALLET_ABI as unknown as Abi, VALIDATOR_WALLET, "validatorExit", [ - 100n, - ]); - expect(data).toBe( - encodeFunctionData({abi: abi.VALIDATOR_WALLET_ABI, functionName: "validatorExit", args: [100n]}), - ); - }); - - // --- Staking-diamond family --- - test("delegatorClaim(delegator, validator) preserves ARG ORDER (delegator first)", () => { - const delegator = "0x5555555555555555555555555555555555555555"; - const {data} = buildTx(abi.STAKING_ABI as unknown as Abi, VESTING, "delegatorClaim", [ - delegator, - VALIDATOR, - ]); - expect(data).toBe( - encodeFunctionData({ - abi: abi.STAKING_ABI, - functionName: "delegatorClaim", - args: [delegator as `0x${string}`, VALIDATOR as `0x${string}`], - }), - ); - }); - - test("delegatorExit(validator, shares) arg order", () => { - const {data} = buildTx(abi.STAKING_ABI as unknown as Abi, VESTING, "delegatorExit", [VALIDATOR, 7n]); - expect(data).toBe( - encodeFunctionData({ - abi: abi.STAKING_ABI, - functionName: "delegatorExit", - args: [VALIDATOR as `0x${string}`, 7n], - }), - ); - }); - - // --- Vesting family --- - test("vestingDelegatorJoin(validator, amount) encodes against VESTING_ABI", () => { - const {to, data} = buildTx(abi.VESTING_ABI as unknown as Abi, VESTING, "vestingDelegatorJoin", [ - VALIDATOR, - 42n * 10n ** 18n, - ]); - expect(to).toBe(VESTING); - expect(data).toBe( - encodeFunctionData({ - abi: abi.VESTING_ABI, - functionName: "vestingDelegatorJoin", - args: [VALIDATOR as `0x${string}`, 42n * 10n ** 18n], - }), - ); - }); - - test("vestingWithdraw(amount) encodes a single uint arg", () => { - const {data} = buildTx(abi.VESTING_ABI as unknown as Abi, VESTING, "vestingWithdraw", [5n]); - expect(data).toBe( - encodeFunctionData({abi: abi.VESTING_ABI, functionName: "vestingWithdraw", args: [5n]}), - ); - }); - - test("vestingValidatorSetIdentity encodes 10 ordered args incl. extraCid hex + utf8", () => { - const utf8Cid = encodeExtraCid("cid"); - const {data} = buildTx(abi.VESTING_ABI as unknown as Abi, VESTING, "vestingValidatorSetIdentity", [ - VALIDATOR_WALLET, - "Moniker", - "", - "", - "", - "", - "", - "", - "", - utf8Cid, - ]); - expect(data).toBe( - encodeFunctionData({ - abi: abi.VESTING_ABI, - functionName: "vestingValidatorSetIdentity", - args: [VALIDATOR_WALLET as `0x${string}`, "Moniker", "", "", "", "", "", "", "", utf8Cid], - }), - ); - - const hexCid = encodeExtraCid("0xdeadbeef"); - const {data: hexData} = buildTx( - abi.VESTING_ABI as unknown as Abi, - VESTING, - "vestingValidatorSetIdentity", - [VALIDATOR_WALLET, "V", "", "", "", "", "", "", "", hexCid], - ); - expect(hexData).toBe( - encodeFunctionData({ - abi: abi.VESTING_ABI, - functionName: "vestingValidatorSetIdentity", - args: [VALIDATOR_WALLET as `0x${string}`, "V", "", "", "", "", "", "", "", "0xdeadbeef"], - }), - ); - }); -}); - -describe("encodeExtraCid", () => { - test("undefined / empty → 0x", () => { - expect(encodeExtraCid()).toBe("0x"); - expect(encodeExtraCid("")).toBe("0x"); - }); - - test("0x-prefixed passes through verbatim", () => { - expect(encodeExtraCid("0xdeadbeef")).toBe("0xdeadbeef"); - }); - - test("non-hex string is UTF-8 hex-encoded", () => { - expect(encodeExtraCid("cid")).toBe(("0x" + Buffer.from("cid", "utf-8").toString("hex")) as `0x${string}`); - }); -});