From b827c528f94b44f87dc48771c13591c9ad61ae12 Mon Sep 17 00:00:00 2001 From: Sreeraj S Date: Sun, 1 Feb 2026 12:41:25 +0530 Subject: [PATCH] feat(abstract-cosmos,sdk-coin-hash): add fee granter support Ticket: WIN-8811 - Add feeGranter field to FeeData and method to TransactionBuilder - Support group policy addresses (58 chars) in Hash coin validation - Add tests for both features --- modules/abstract-cosmos/src/lib/iface.ts | 1 + .../src/lib/transactionBuilder.ts | 15 +++++++++++++++ modules/abstract-cosmos/src/lib/utils.ts | 8 ++++++-- modules/sdk-coin-hash/src/lib/constants.ts | 4 ++-- modules/sdk-coin-hash/test/resources/hash.ts | 2 ++ modules/sdk-coin-hash/test/unit/hash.ts | 2 ++ .../unit/transactionBuilder/transferBuilder.ts | 18 ++++++++++++++++++ 7 files changed, 46 insertions(+), 4 deletions(-) diff --git a/modules/abstract-cosmos/src/lib/iface.ts b/modules/abstract-cosmos/src/lib/iface.ts index e42089cd95..d0154b1bde 100644 --- a/modules/abstract-cosmos/src/lib/iface.ts +++ b/modules/abstract-cosmos/src/lib/iface.ts @@ -84,6 +84,7 @@ export interface MessageData { export interface FeeData { amount: Coin[]; gasLimit: number; + feeGranter?: string; } export interface GasAmountDetails { diff --git a/modules/abstract-cosmos/src/lib/transactionBuilder.ts b/modules/abstract-cosmos/src/lib/transactionBuilder.ts index 0fda1877ef..109a49231e 100644 --- a/modules/abstract-cosmos/src/lib/transactionBuilder.ts +++ b/modules/abstract-cosmos/src/lib/transactionBuilder.ts @@ -143,6 +143,21 @@ export abstract class CosmosTransactionBuilder extends Ba return this; } + /** + * Sets the fee granter address for this transaction. + * The fee granter is the address that will pay the transaction fees on behalf of the signer. + * @param {string} address - The address that will pay the transaction fees + * @returns {TransactionBuilder} this transaction builder + */ + feeGranter(address: string): this { + this.validateAddress({ address }); + if (!this._gasBudget) { + throw new BuildTransactionError('Gas budget must be set before setting fee granter'); + } + this._gasBudget.feeGranter = address; + return this; + } + /** * Initialize the transaction builder fields using the decoded transaction data * @param {CosmosTransaction} tx the transaction data diff --git a/modules/abstract-cosmos/src/lib/utils.ts b/modules/abstract-cosmos/src/lib/utils.ts index 2ac2abf1ef..7c5c5f1818 100644 --- a/modules/abstract-cosmos/src/lib/utils.ts +++ b/modules/abstract-cosmos/src/lib/utils.ts @@ -179,10 +179,14 @@ export class CosmosUtils implements BaseUtils { * @returns {FeeData} fee data */ getGasBudgetFromDecodedTx(decodedTx: DecodedTxRaw): FeeData { - return { + const feeData: FeeData = { amount: decodedTx.authInfo.fee?.amount as Coin[], gasLimit: Number(decodedTx.authInfo.fee?.gasLimit), }; + if (decodedTx.authInfo.fee?.granter) { + feeData.feeGranter = decodedTx.authInfo.fee.granter; + } + return feeData; } /** @@ -419,7 +423,7 @@ export class CosmosUtils implements BaseUtils { [{ pubkey: encodedPublicKey, sequence }], cosmosLikeTransaction.gasBudget.amount, cosmosLikeTransaction.gasBudget.gasLimit, - undefined, + cosmosLikeTransaction.gasBudget.feeGranter || undefined, undefined, undefined ); diff --git a/modules/sdk-coin-hash/src/lib/constants.ts b/modules/sdk-coin-hash/src/lib/constants.ts index 0cc6531c35..ffc19cbd50 100644 --- a/modules/sdk-coin-hash/src/lib/constants.ts +++ b/modules/sdk-coin-hash/src/lib/constants.ts @@ -2,12 +2,12 @@ import { CosmosUtils } from '@bitgo/abstract-cosmos'; const cosmosUtils = new CosmosUtils(); export const validDenoms = ['nhash', 'uhash', 'mhash', 'hash', ...cosmosUtils.getTokenDenomsUsingCoinFamily('hash')]; -export const mainnetAccountAddressRegex = /^(pb)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l]{38})$/; +export const mainnetAccountAddressRegex = /^(pb)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l]{38,58})$/; export const mainnetValidatorAddressRegex = /^(pbvaloper)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l]{38})$/; export const mainnetContractAddressRegex = /^(pb)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l]+)$/; export const MAINNET_ADDRESS_PREFIX = 'pb'; -export const testnetAccountAddressRegex = /^(tp)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l]{38})$/; +export const testnetAccountAddressRegex = /^(tp)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l]{38,58})$/; export const testnetValidatorAddressRegex = /^(tpvaloper)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l]{38})$/; export const testnetContractAddressRegex = /^(tp)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l]+)$/; export const TESTNET_ADDRESS_PREFIX = 'tp'; diff --git a/modules/sdk-coin-hash/test/resources/hash.ts b/modules/sdk-coin-hash/test/resources/hash.ts index 41e1e13265..f7cc21cdb4 100644 --- a/modules/sdk-coin-hash/test/resources/hash.ts +++ b/modules/sdk-coin-hash/test/resources/hash.ts @@ -271,6 +271,7 @@ export const mainnetAddress = { address2: 'pb16vmp7sz28pnvgz6f3zm6q93y39jsd33aazwg4u', address3: 'pb2xvd4k9jg5h0d4dhzr4z0txtwe9p5zxf58xcmxd', address4: 'pb1fmxzuzx5c4ja50vu94nt0aessnuedzmppde8qq', + groupPolicyAddress: 'pb1tazefwk2e372fy2jq08w6lztg9yrrvc490r2gp4vt8d0fchlrfqqt58ltq', validatorAddress1: 'pbvaloper13905qnf0mc8f8h3dawsq0894ded0ct83f66l56', validatorAddress2: 'pbvaloper1d7yum2cxwkhmmuxa096prlv5gawjxw0gc2sykq', validatorAddress3: 'pbvaloder17yx96jtu0r24jp8gyxc8y8pj0lgvcz964w2gyg', @@ -286,6 +287,7 @@ export const testnetAddress = { address2: 'tp1ytxha7lg002rzd4jxmahrdjzkd62mx99klgjcc', address3: 'txp1x96r8u4a48k6khknrhzd6c8cm3c64ewxy5prj', address4: 'tp1umned7wx7le70ttvrcem3fsyhn343asr2k2pww', + groupPolicyAddress: 'tp1tazefwk2e372fy2jq08w6lztg9yrrvc490r2gp4vt8d0fchlrfqqyahg0u', validatorAddress1: 'tpvaloper1tgq6cpu6hmsrvkvdu82j99tsxxw7qqajn843fe', validatorAddress2: 'tpvaloper1tgq6cpu6hmsrvkvdu82j99tsxxw7qqajn843fe', validatorAddress3: 'txvaloper1xxxxcpu6hmsrvkvdu82j99tsxxw7qqajn843fe', diff --git a/modules/sdk-coin-hash/test/unit/hash.ts b/modules/sdk-coin-hash/test/unit/hash.ts index b40e5287ad..a32bde2469 100644 --- a/modules/sdk-coin-hash/test/unit/hash.ts +++ b/modules/sdk-coin-hash/test/unit/hash.ts @@ -104,6 +104,7 @@ describe('HASH', function () { should.equal(mainnetUtils.isValidAddress(''), false); should.equal(mainnetUtils.isValidAddress(mainnetAddress.validMemoIdAddress), true); should.equal(mainnetUtils.isValidAddress(mainnetAddress.multipleMemoIdAddress), false); + should.equal(mainnetUtils.isValidAddress(mainnetAddress.groupPolicyAddress), true); should.equal(testnetUtils.isValidAddress(testnetAddress.address1), true); should.equal(testnetUtils.isValidAddress(testnetAddress.address2), true); @@ -114,6 +115,7 @@ describe('HASH', function () { should.equal(testnetUtils.isValidAddress(''), false); should.equal(testnetUtils.isValidAddress(testnetAddress.validMemoIdAddress), true); should.equal(testnetUtils.isValidAddress(testnetAddress.multipleMemoIdAddress), false); + should.equal(testnetUtils.isValidAddress(testnetAddress.groupPolicyAddress), true); }); it('should validate validator addresses correctly', () => { diff --git a/modules/sdk-coin-hash/test/unit/transactionBuilder/transferBuilder.ts b/modules/sdk-coin-hash/test/unit/transactionBuilder/transferBuilder.ts index c4686e8672..203b47cb67 100644 --- a/modules/sdk-coin-hash/test/unit/transactionBuilder/transferBuilder.ts +++ b/modules/sdk-coin-hash/test/unit/transactionBuilder/transferBuilder.ts @@ -310,4 +310,22 @@ describe('Hash Token Transfer Builder', () => { }, ]); }); + + it('should build a Transfer tx with fee granter', async function () { + const feeGranterAddress = testData.testnetAddress.address2; + const txBuilder = factory.getTransferBuilder(); + txBuilder.sequence(testTx.sequence); + txBuilder.gasBudget(testTx.gasBudget); + txBuilder.feeGranter(feeGranterAddress); + txBuilder.messages([testTx.sendMessage.value]); + txBuilder.publicKey(toHex(fromBase64(testTx.pubKey))); + + const tx = await txBuilder.build(); + const json = await (await txBuilder.build()).toJson(); + should.equal(tx.type, TransactionType.Send); + should.deepEqual(json.gasBudget.amount, testTx.gasBudget.amount); + should.deepEqual(json.gasBudget.gasLimit, testTx.gasBudget.gasLimit); + should.equal(json.gasBudget.feeGranter, feeGranterAddress); + should.deepEqual(json.sendMessages, [testTx.sendMessage]); + }); });