From e1996a3a212f7d070c0d1c3f9b20e91e0d349f3c Mon Sep 17 00:00:00 2001 From: Gautam Adatra Date: Mon, 13 Apr 2026 08:04:45 +0000 Subject: [PATCH] fix(sdk-coin-vet): improve error messages for delegation/validator mutual exclusivity When a user attempts to delegate from a wallet with an active validator registration (or vice versa), the previous error message reported an address mismatch without explaining the underlying constraint. The user would see a cryptic message like "Invalid staking contract address. Expected 0x03c... got 0x000..." with no indication of why. The fix detects when the provided contract address belongs to the opposing operation's contract, and surfaces a clear human-readable error explaining that the two operations are mutually exclusive for a given wallet. Generic address-mismatch errors are preserved for all other invalid addresses. Ticket: SI-305 --- modules/sdk-coin-vet/src/lib/utils.ts | 16 ++++++++++++++ .../delegateClauseTxnBuilder.ts | 21 ++++++++++++++++++- .../validatorRegistrationTxnBuilder.ts | 17 +++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/modules/sdk-coin-vet/src/lib/utils.ts b/modules/sdk-coin-vet/src/lib/utils.ts index 448961d795..8b797cff69 100644 --- a/modules/sdk-coin-vet/src/lib/utils.ts +++ b/modules/sdk-coin-vet/src/lib/utils.ts @@ -461,6 +461,14 @@ export class Utils implements BaseUtils { validateStakingContractAddress(address: string, coinConfig: Readonly): void { const expectedAddress = this.getDefaultStakingAddress(coinConfig); if (address.toLowerCase() !== expectedAddress.toLowerCase()) { + const validatorRegistrationAddress = this.getContractAddressForValidatorRegistration(coinConfig); + if (address.toLowerCase() === validatorRegistrationAddress.toLowerCase()) { + throw new Error( + 'Delegation is not supported for wallets with an active validator registration. ' + + 'A wallet can either delegate or register as a validator, but not both. ' + + 'Please use a wallet without an existing validator registration to perform delegation.' + ); + } throw new Error( `Invalid staking contract address. Expected ${expectedAddress} for ${coinConfig.network.type}, got ${address}` ); @@ -476,6 +484,14 @@ export class Utils implements BaseUtils { validateContractAddressForValidatorRegistration(address: string, coinConfig: Readonly): void { const expectedAddress = this.getContractAddressForValidatorRegistration(coinConfig); if (address.toLowerCase() !== expectedAddress.toLowerCase()) { + const stakingContractAddress = this.getDefaultStakingAddress(coinConfig); + if (address.toLowerCase() === stakingContractAddress.toLowerCase()) { + throw new Error( + 'Validator registration is not supported for wallets with an active delegation. ' + + 'A wallet can either register as a validator or delegate, but not both. ' + + 'Please use a wallet without an existing delegation to perform validator registration.' + ); + } throw new Error( `Invalid contract address for validator registration. Expected ${expectedAddress} for ${coinConfig.network.type}, got ${address}` ); diff --git a/modules/sdk-coin-vet/test/transactionBuilder/delegateClauseTxnBuilder.ts b/modules/sdk-coin-vet/test/transactionBuilder/delegateClauseTxnBuilder.ts index 7d46aecfd1..0063f6b10e 100644 --- a/modules/sdk-coin-vet/test/transactionBuilder/delegateClauseTxnBuilder.ts +++ b/modules/sdk-coin-vet/test/transactionBuilder/delegateClauseTxnBuilder.ts @@ -1,7 +1,11 @@ import { coins } from '@bitgo/statics'; import { TransactionBuilderFactory, Transaction, DelegateClauseTransaction } from '../../src/lib'; import should from 'should'; -import { DELEGATE_CLAUSE_METHOD_ID, STARGATE_CONTRACT_ADDRESS_TESTNET } from '../../src/lib/constants'; +import { + DELEGATE_CLAUSE_METHOD_ID, + STARGATE_CONTRACT_ADDRESS_TESTNET, + VALIDATOR_REGISTRATION_STAKER_CONTRACT_ADDRESS_TESTNET, +} from '../../src/lib/constants'; import EthereumAbi from 'ethereumjs-abi'; import * as testData from '../resources/vet'; import { BN } from 'ethereumjs-util'; @@ -107,6 +111,21 @@ describe('VET Delegation Transaction', function () { }).throw(/Invalid address/); }); + it('should throw descriptive error when validator registration contract is used for delegation', async function () { + const txBuilder = createBasicTxBuilder(); + txBuilder.stakingContractAddress(VALIDATOR_REGISTRATION_STAKER_CONTRACT_ADDRESS_TESTNET); + txBuilder.tokenId(tokenId); + txBuilder.validator(validatorAddress); + + await txBuilder + .build() + .should.be.rejectedWith( + 'Delegation is not supported for wallets with an active validator registration. ' + + 'A wallet can either delegate or register as a validator, but not both. ' + + 'Please use a wallet without an existing validator registration to perform delegation.' + ); + }); + it('should build transaction with undefined sender but include it in inputs', async function () { const txBuilder = factory.getStakingDelegateBuilder(); txBuilder.stakingContractAddress(STARGATE_CONTRACT_ADDRESS_TESTNET); diff --git a/modules/sdk-coin-vet/test/transactionBuilder/validatorRegistrationTxnBuilder.ts b/modules/sdk-coin-vet/test/transactionBuilder/validatorRegistrationTxnBuilder.ts index e1789fc0ef..e0147b3554 100644 --- a/modules/sdk-coin-vet/test/transactionBuilder/validatorRegistrationTxnBuilder.ts +++ b/modules/sdk-coin-vet/test/transactionBuilder/validatorRegistrationTxnBuilder.ts @@ -3,6 +3,7 @@ import { TransactionBuilderFactory, Transaction, ValidatorRegistrationTransactio import should from 'should'; import { ADD_VALIDATION_METHOD_ID, + STARGATE_CONTRACT_ADDRESS_TESTNET, VALIDATOR_REGISTRATION_STAKER_CONTRACT_ADDRESS_TESTNET, } from '../../src/lib/constants'; import EthereumAbi from 'ethereumjs-abi'; @@ -156,6 +157,22 @@ describe('VET Validator Registration Transaction', function () { }).throw(/Invalid address/); }); + it('should throw descriptive error when delegation contract is used for validator registration', async function () { + const txBuilder = createBasicTxBuilder(); + txBuilder.stakingContractAddress(STARGATE_CONTRACT_ADDRESS_TESTNET); + txBuilder.stakingPeriod(stakingPeriod); + txBuilder.validator(validatorAddress); + txBuilder.amountToStake(amountToStake); + + await txBuilder + .build() + .should.be.rejectedWith( + 'Validator registration is not supported for wallets with an active delegation. ' + + 'A wallet can either register as a validator or delegate, but not both. ' + + 'Please use a wallet without an existing delegation to perform validator registration.' + ); + }); + it('should build transaction with undefined sender but include it in inputs', async function () { const txBuilder = factory.getValidatorRegistrationBuilder(); txBuilder.stakingContractAddress(VALIDATOR_REGISTRATION_STAKER_CONTRACT_ADDRESS_TESTNET);