From e84b9ec106883005da087e3f4910d7bdd9a8f2c3 Mon Sep 17 00:00:00 2001 From: Rishabh Singh Date: Thu, 12 Feb 2026 14:35:38 +0530 Subject: [PATCH 1/5] feat: role transfer function --- .../document-store/grant-role.test.ts | 386 +++++++++++++++++ .../document-store/revoke-role.test.ts | 391 ++++++++++++++++++ src/document-store/grant-role.ts | 132 ++++++ src/document-store/revoke-role.ts | 132 ++++++ src/document-store/transferOwnership.ts | 66 +++ 5 files changed, 1107 insertions(+) create mode 100644 src/__tests__/document-store/grant-role.test.ts create mode 100644 src/__tests__/document-store/revoke-role.test.ts create mode 100644 src/document-store/grant-role.ts create mode 100644 src/document-store/revoke-role.ts create mode 100644 src/document-store/transferOwnership.ts diff --git a/src/__tests__/document-store/grant-role.test.ts b/src/__tests__/document-store/grant-role.test.ts new file mode 100644 index 0000000..fbfcf78 --- /dev/null +++ b/src/__tests__/document-store/grant-role.test.ts @@ -0,0 +1,386 @@ +import './fixtures'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { ethers as ethersV5, Wallet as WalletV5 } from 'ethers'; +import { Wallet as WalletV6, Network, ethers as ethersV6 } from 'ethersV6'; +import * as coreModule from '../../core'; + +import { grantDocumentStoreRole } from '../../document-store/grant-role'; +import { + MOCK_DOCUMENT_STORE_ADDRESS, + MOCK_TRANSFERABLE_DOCUMENT_STORE_ADDRESS, + MOCK_TT_DOCUMENT_STORE_ADDRESS, + mockDocumentStoreContract, + mockTransferableDocumentStoreContract, + mockTTDocumentStoreContract, + PRIVATE_KEY, + providerV5, + providerV6, +} from './fixtures'; +import { getEthersContractFromProvider } from '../../utils/ethers'; +import { CHAIN_ID } from '../../utils'; +import { supportInterfaceIds } from '../../document-store/supportInterfaceIds'; + +interface ProviderInfo { + Provider: any; + ethersVersion: 'v5' | 'v6'; + contractType: 'DocumentStore' | 'TransferableDocumentStore'; +} + +const providers: ProviderInfo[] = [ + { + Provider: providerV5, + ethersVersion: 'v5', + contractType: 'DocumentStore', + }, + { + Provider: providerV5, + ethersVersion: 'v5', + contractType: 'TransferableDocumentStore', + }, + { + Provider: providerV6, + ethersVersion: 'v6', + contractType: 'DocumentStore', + }, + { + Provider: providerV6, + ethersVersion: 'v6', + contractType: 'TransferableDocumentStore', + }, +]; + +describe('Grant Document Store Role', () => { + const mockRole = '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890'; + const mockAccount = '0x1234567890123456789012345678901234567890'; + const mockChainId = CHAIN_ID.local; + + describe.each(providers)( + 'Grant role with $contractType and ethers version $ethersVersion', + async ({ Provider, ethersVersion, contractType }) => { + const isTransferable = contractType === 'TransferableDocumentStore'; + const mockContract = isTransferable + ? mockTransferableDocumentStoreContract + : mockDocumentStoreContract; + const mockTxResponse = isTransferable + ? 'transferable_document_store_grant_role_tx_hash' + : 'document_store_grant_role_tx_hash'; + + let wallet: ethersV5.Wallet | ethersV6.Wallet; + if (ethersVersion === 'v5') { + wallet = new WalletV5(PRIVATE_KEY, Provider as any) as ethersV5.Wallet; + vi.spyOn(wallet, 'getChainId').mockResolvedValue(mockChainId as unknown as number); + } else { + wallet = new WalletV6(PRIVATE_KEY, Provider as any); + vi.spyOn(Provider, 'getNetwork').mockResolvedValue({ + chainId: mockChainId, + } as unknown as Network); + } + + const mockDocumentStoreAddress = isTransferable + ? MOCK_TRANSFERABLE_DOCUMENT_STORE_ADDRESS + : MOCK_DOCUMENT_STORE_ADDRESS; + + beforeAll(() => { + vi.clearAllMocks(); + const mockContractConstructor = (mockContract: any) => vi.fn(() => mockContract); + vi.mocked(getEthersContractFromProvider).mockReturnValue( + mockContractConstructor(mockContract), + ); + }); + + beforeEach(() => { + vi.clearAllMocks(); + vi.spyOn(coreModule, 'checkSupportsInterface').mockImplementation( + async (address, interfaceId) => { + if (isTransferable) { + return interfaceId === supportInterfaceIds.ITransferableDocumentStore; + } + return interfaceId === supportInterfaceIds.IDocumentStore; + }, + ); + mockContract.callStatic.grantRole.mockResolvedValue(true); + mockContract.grantRole.staticCall.mockResolvedValue(true); + }); + + it('should grant role successfully', async () => { + const result = await grantDocumentStoreRole( + mockDocumentStoreAddress, + mockRole, + mockAccount, + wallet, + { + chainId: mockChainId, + }, + ); + expect(result).toEqual(mockTxResponse); + expect(coreModule.checkSupportsInterface).toHaveBeenCalled(); + }); + + it('should grant role with explicit contract type', async () => { + const result = await grantDocumentStoreRole( + mockDocumentStoreAddress, + mockRole, + mockAccount, + wallet, + { + chainId: mockChainId, + isTransferable, + }, + ); + expect(result).toEqual(mockTxResponse); + expect(coreModule.checkSupportsInterface).not.toHaveBeenCalled(); + }); + + it('should grant role without chainId option', async () => { + const result = await grantDocumentStoreRole( + mockDocumentStoreAddress, + mockRole, + mockAccount, + wallet, + { + isTransferable, + }, + ); + expect(result).toEqual(mockTxResponse); + }); + + it('should grant role with gas options', async () => { + const result = await grantDocumentStoreRole( + mockDocumentStoreAddress, + mockRole, + mockAccount, + wallet, + { + chainId: mockChainId, + maxFeePerGas: '1000000000', + maxPriorityFeePerGas: '1000000000', + isTransferable, + }, + ); + expect(result).toEqual(mockTxResponse); + }); + + it('should throw when document store address is missing', async () => { + await expect( + grantDocumentStoreRole('', mockRole, mockAccount, wallet, { chainId: mockChainId }), + ).rejects.toThrow('Document store address is required'); + }); + + it('should throw when provider is missing', async () => { + const signerWithoutProvider = new WalletV5('0x'.padEnd(66, '1')); + await expect( + grantDocumentStoreRole( + mockDocumentStoreAddress, + mockRole, + mockAccount, + signerWithoutProvider, + { + chainId: mockChainId, + }, + ), + ).rejects.toThrow('Provider is required'); + }); + + it('should throw when role is missing', async () => { + await expect( + grantDocumentStoreRole(mockDocumentStoreAddress, '', mockAccount, wallet, { + chainId: mockChainId, + }), + ).rejects.toThrow('Role is required'); + }); + + it('should throw when account is missing', async () => { + await expect( + grantDocumentStoreRole(mockDocumentStoreAddress, mockRole, '', wallet, { + chainId: mockChainId, + }), + ).rejects.toThrow('Account is required'); + }); + + it('should throw when callStatic fails', async () => { + const mockError = new Error('callStatic error'); + mockContract.callStatic.grantRole.mockRejectedValue(mockError); + mockContract.grantRole.staticCall.mockRejectedValue(mockError); + await expect( + grantDocumentStoreRole(mockDocumentStoreAddress, mockRole, mockAccount, wallet, { + chainId: mockChainId, + isTransferable, + }), + ).rejects.toThrow('Pre-check (callStatic) for issue failed'); + }); + + it('should fallback to TT Document Store when ERC-165 interfaces not supported', async () => { + vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); + const mockContractConstructor = (mockContract: any) => vi.fn(() => mockContract); + vi.mocked(getEthersContractFromProvider).mockReturnValue( + mockContractConstructor(mockContract), + ); + const result = await grantDocumentStoreRole( + mockDocumentStoreAddress, + mockRole, + mockAccount, + wallet, + { + chainId: mockChainId, + }, + ); + expect(result).toBeDefined(); + expect(coreModule.checkSupportsInterface).toHaveBeenCalledTimes(2); + }); + + it('should handle invalid role format gracefully', async () => { + const invalidRole = 'invalid-role'; + mockContract.callStatic.grantRole.mockRejectedValue(new Error('Invalid role format')); + mockContract.grantRole.staticCall.mockRejectedValue(new Error('Invalid role format')); + await expect( + grantDocumentStoreRole(mockDocumentStoreAddress, invalidRole, mockAccount, wallet, { + chainId: mockChainId, + isTransferable, + }), + ).rejects.toThrow('Pre-check (callStatic) for issue failed'); + }); + + it('should handle already granted role', async () => { + mockContract.callStatic.grantRole.mockRejectedValue(new Error('Role already granted')); + mockContract.grantRole.staticCall.mockRejectedValue(new Error('Role already granted')); + await expect( + grantDocumentStoreRole(mockDocumentStoreAddress, mockRole, mockAccount, wallet, { + chainId: mockChainId, + isTransferable, + }), + ).rejects.toThrow('Pre-check (callStatic) for issue failed'); + }); + + it('should work with different role and account addresses', async () => { + const differentRole = '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890'; + const differentAccount = '0x9876543210987654321098765432109876543210'; + const result = await grantDocumentStoreRole( + mockDocumentStoreAddress, + differentRole, + differentAccount, + wallet, + { + chainId: mockChainId, + isTransferable, + }, + ); + expect(result).toEqual(mockTxResponse); + }); + }, + ); + + describe('TT Document Store (Fallback)', () => { + let wallet: ethersV5.Wallet; + + beforeEach(() => { + vi.clearAllMocks(); + wallet = new WalletV5(PRIVATE_KEY, providerV5 as any); + vi.spyOn(wallet, 'getChainId').mockResolvedValue(mockChainId as unknown as number); + const mockContractConstructor = (mockContract: any) => vi.fn(() => mockContract); + vi.mocked(getEthersContractFromProvider).mockReturnValue( + mockContractConstructor(mockTTDocumentStoreContract), + ); + mockTTDocumentStoreContract.callStatic.grantRole.mockResolvedValue(true); + mockTTDocumentStoreContract.grantRole.staticCall.mockResolvedValue(true); + }); + + it('should auto-detect TT Document Store as fallback when ERC-165 interfaces not supported', async () => { + vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); + const result = await grantDocumentStoreRole( + MOCK_TT_DOCUMENT_STORE_ADDRESS, + mockRole, + mockAccount, + wallet, + { + chainId: mockChainId, + }, + ); + expect(result).toEqual('tt_document_store_grant_role_tx_hash'); + expect(coreModule.checkSupportsInterface).toHaveBeenCalledWith( + MOCK_TT_DOCUMENT_STORE_ADDRESS, + supportInterfaceIds.IDocumentStore, + wallet.provider, + ); + expect(coreModule.checkSupportsInterface).toHaveBeenCalledWith( + MOCK_TT_DOCUMENT_STORE_ADDRESS, + supportInterfaceIds.ITransferableDocumentStore, + wallet.provider, + ); + }); + + it('should grant role with TT Document Store (ethers v5)', async () => { + vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); + const result = await grantDocumentStoreRole( + MOCK_TT_DOCUMENT_STORE_ADDRESS, + mockRole, + mockAccount, + wallet, + { + chainId: mockChainId, + }, + ); + expect(result).toEqual('tt_document_store_grant_role_tx_hash'); + }); + + it('should grant role with TT Document Store (ethers v6)', async () => { + const walletV6 = new WalletV6(PRIVATE_KEY, providerV6 as any); + vi.spyOn(providerV6, 'getNetwork').mockResolvedValue({ + chainId: mockChainId, + } as unknown as Network); + vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); + const result = await grantDocumentStoreRole( + MOCK_TT_DOCUMENT_STORE_ADDRESS, + mockRole, + mockAccount, + walletV6, + { + chainId: mockChainId, + }, + ); + expect(result).toEqual('tt_document_store_grant_role_tx_hash'); + }); + + it('should handle callStatic failure for TT Document Store', async () => { + vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); + const mockError = new Error('TT callStatic error'); + mockTTDocumentStoreContract.callStatic.grantRole.mockRejectedValue(mockError); + mockTTDocumentStoreContract.grantRole.staticCall.mockRejectedValue(mockError); + await expect( + grantDocumentStoreRole(MOCK_TT_DOCUMENT_STORE_ADDRESS, mockRole, mockAccount, wallet, { + chainId: mockChainId, + }), + ).rejects.toThrow('Pre-check (callStatic) for issue failed'); + }); + + it('should grant role TT Document Store with gas options', async () => { + vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); + const result = await grantDocumentStoreRole( + MOCK_TT_DOCUMENT_STORE_ADDRESS, + mockRole, + mockAccount, + wallet, + { + chainId: mockChainId, + maxFeePerGas: '2000000000', + maxPriorityFeePerGas: '1500000000', + }, + ); + expect(result).toEqual('tt_document_store_grant_role_tx_hash'); + }); + + it('should handle already granted role in TT Document Store', async () => { + vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); + mockTTDocumentStoreContract.callStatic.grantRole.mockRejectedValue( + new Error('Role already granted'), + ); + mockTTDocumentStoreContract.grantRole.staticCall.mockRejectedValue( + new Error('Role already granted'), + ); + await expect( + grantDocumentStoreRole(MOCK_TT_DOCUMENT_STORE_ADDRESS, mockRole, mockAccount, wallet, { + chainId: mockChainId, + }), + ).rejects.toThrow('Pre-check (callStatic) for issue failed'); + }); + }); +}); diff --git a/src/__tests__/document-store/revoke-role.test.ts b/src/__tests__/document-store/revoke-role.test.ts new file mode 100644 index 0000000..6d3793c --- /dev/null +++ b/src/__tests__/document-store/revoke-role.test.ts @@ -0,0 +1,391 @@ +import './fixtures'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { ethers as ethersV5, Wallet as WalletV5 } from 'ethers'; +import { Wallet as WalletV6, Network, ethers as ethersV6 } from 'ethersV6'; +import * as coreModule from '../../core'; + +import { revokeDocumentStoreRole } from '../../document-store/revoke-role'; +import { + MOCK_DOCUMENT_STORE_ADDRESS, + MOCK_TRANSFERABLE_DOCUMENT_STORE_ADDRESS, + MOCK_TT_DOCUMENT_STORE_ADDRESS, + mockDocumentStoreContract, + mockTransferableDocumentStoreContract, + mockTTDocumentStoreContract, + PRIVATE_KEY, + providerV5, + providerV6, +} from './fixtures'; +import { getEthersContractFromProvider } from '../../utils/ethers'; +import { CHAIN_ID } from '../../utils'; +import { supportInterfaceIds } from '../../document-store/supportInterfaceIds'; + +interface ProviderInfo { + Provider: any; + ethersVersion: 'v5' | 'v6'; + contractType: 'DocumentStore' | 'TransferableDocumentStore'; +} + +const providers: ProviderInfo[] = [ + { + Provider: providerV5, + ethersVersion: 'v5', + contractType: 'DocumentStore', + }, + { + Provider: providerV5, + ethersVersion: 'v5', + contractType: 'TransferableDocumentStore', + }, + { + Provider: providerV6, + ethersVersion: 'v6', + contractType: 'DocumentStore', + }, + { + Provider: providerV6, + ethersVersion: 'v6', + contractType: 'TransferableDocumentStore', + }, +]; + +describe('Revoke Document Store Role', () => { + const mockRole = '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890'; + const mockAccount = '0x1234567890123456789012345678901234567890'; + const mockChainId = CHAIN_ID.local; + + describe.each(providers)( + 'Revoke role with $contractType and ethers version $ethersVersion', + async ({ Provider, ethersVersion, contractType }) => { + const isTransferable = contractType === 'TransferableDocumentStore'; + const mockContract = isTransferable + ? mockTransferableDocumentStoreContract + : mockDocumentStoreContract; + const mockTxResponse = isTransferable + ? 'transferable_document_store_revoke_role_tx_hash' + : 'document_store_revoke_role_tx_hash'; + + let wallet: ethersV5.Wallet | ethersV6.Wallet; + if (ethersVersion === 'v5') { + wallet = new WalletV5(PRIVATE_KEY, Provider as any) as ethersV5.Wallet; + vi.spyOn(wallet, 'getChainId').mockResolvedValue(mockChainId as unknown as number); + } else { + wallet = new WalletV6(PRIVATE_KEY, Provider as any); + vi.spyOn(Provider, 'getNetwork').mockResolvedValue({ + chainId: mockChainId, + } as unknown as Network); + } + + const mockDocumentStoreAddress = isTransferable + ? MOCK_TRANSFERABLE_DOCUMENT_STORE_ADDRESS + : MOCK_DOCUMENT_STORE_ADDRESS; + + beforeAll(() => { + vi.clearAllMocks(); + const mockContractConstructor = (mockContract: any) => vi.fn(() => mockContract); + vi.mocked(getEthersContractFromProvider).mockReturnValue( + mockContractConstructor(mockContract), + ); + }); + + beforeEach(() => { + vi.clearAllMocks(); + vi.spyOn(coreModule, 'checkSupportsInterface').mockImplementation( + async (address, interfaceId) => { + if (isTransferable) { + return interfaceId === supportInterfaceIds.ITransferableDocumentStore; + } + return interfaceId === supportInterfaceIds.IDocumentStore; + }, + ); + mockContract.callStatic.revokeRole.mockResolvedValue(true); + mockContract.revokeRole.staticCall.mockResolvedValue(true); + }); + + it('should revoke role successfully', async () => { + const result = await revokeDocumentStoreRole( + mockDocumentStoreAddress, + mockRole, + mockAccount, + wallet, + { + chainId: mockChainId, + }, + ); + expect(result).toEqual(mockTxResponse); + expect(coreModule.checkSupportsInterface).toHaveBeenCalled(); + }); + + it('should revoke role with explicit contract type', async () => { + const result = await revokeDocumentStoreRole( + mockDocumentStoreAddress, + mockRole, + mockAccount, + wallet, + { + chainId: mockChainId, + isTransferable, + }, + ); + expect(result).toEqual(mockTxResponse); + expect(coreModule.checkSupportsInterface).not.toHaveBeenCalled(); + }); + + it('should revoke role without chainId option', async () => { + const result = await revokeDocumentStoreRole( + mockDocumentStoreAddress, + mockRole, + mockAccount, + wallet, + { + isTransferable, + }, + ); + expect(result).toEqual(mockTxResponse); + }); + + it('should revoke role with gas options', async () => { + const result = await revokeDocumentStoreRole( + mockDocumentStoreAddress, + mockRole, + mockAccount, + wallet, + { + chainId: mockChainId, + maxFeePerGas: '1000000000', + maxPriorityFeePerGas: '1000000000', + isTransferable, + }, + ); + expect(result).toEqual(mockTxResponse); + }); + + it('should throw when document store address is missing', async () => { + await expect( + revokeDocumentStoreRole('', mockRole, mockAccount, wallet, { chainId: mockChainId }), + ).rejects.toThrow('Document store address is required'); + }); + + it('should throw when provider is missing', async () => { + const signerWithoutProvider = new (ethersVersion === 'v5' ? WalletV5 : WalletV6)( + '0x'.padEnd(66, '1'), + ); + await expect( + revokeDocumentStoreRole( + mockDocumentStoreAddress, + mockRole, + mockAccount, + signerWithoutProvider, + { + chainId: mockChainId, + }, + ), + ).rejects.toThrow('Provider is required'); + }); + + it('should throw when role is missing', async () => { + await expect( + revokeDocumentStoreRole(mockDocumentStoreAddress, '', mockAccount, wallet, { + chainId: mockChainId, + }), + ).rejects.toThrow('Role is required'); + }); + + it('should throw when account is missing', async () => { + await expect( + revokeDocumentStoreRole(mockDocumentStoreAddress, mockRole, '', wallet, { + chainId: mockChainId, + }), + ).rejects.toThrow('Account is required'); + }); + + it('should throw when callStatic fails', async () => { + const mockError = new Error('callStatic error'); + mockContract.callStatic.revokeRole.mockRejectedValue(mockError); + mockContract.revokeRole.staticCall.mockRejectedValue(mockError); + await expect( + revokeDocumentStoreRole(mockDocumentStoreAddress, mockRole, mockAccount, wallet, { + chainId: mockChainId, + isTransferable, + }), + ).rejects.toThrow('Pre-check (callStatic) for issue failed'); + }); + + it('should fallback to TT Document Store when ERC-165 interfaces not supported', async () => { + vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); + const mockContractConstructor = (mockContract: any) => vi.fn(() => mockContract); + vi.mocked(getEthersContractFromProvider).mockReturnValue( + mockContractConstructor(mockTTDocumentStoreContract), + ); + const result = await revokeDocumentStoreRole( + mockDocumentStoreAddress, + mockRole, + mockAccount, + wallet, + { + chainId: mockChainId, + }, + ); + expect(result).toBeDefined(); + expect(coreModule.checkSupportsInterface).toHaveBeenCalledTimes(2); + vi.mocked(getEthersContractFromProvider).mockReturnValue( + mockContractConstructor(mockContract), + ); + }); + + it('should handle invalid role format gracefully', async () => { + const invalidRole = 'invalid-role'; + mockContract.callStatic.revokeRole.mockRejectedValue(new Error('Invalid role format')); + mockContract.revokeRole.staticCall.mockRejectedValue(new Error('Invalid role format')); + await expect( + revokeDocumentStoreRole(mockDocumentStoreAddress, invalidRole, mockAccount, wallet, { + chainId: mockChainId, + isTransferable, + }), + ).rejects.toThrow('Pre-check (callStatic) for issue failed'); + }); + + it('should handle role not granted error', async () => { + mockContract.callStatic.revokeRole.mockRejectedValue(new Error('Role not granted')); + mockContract.revokeRole.staticCall.mockRejectedValue(new Error('Role not granted')); + await expect( + revokeDocumentStoreRole(mockDocumentStoreAddress, mockRole, mockAccount, wallet, { + chainId: mockChainId, + isTransferable, + }), + ).rejects.toThrow('Pre-check (callStatic) for issue failed'); + }); + + it('should work with different role and account addresses', async () => { + const differentRole = '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890'; + const differentAccount = '0x9876543210987654321098765432109876543210'; + const result = await revokeDocumentStoreRole( + mockDocumentStoreAddress, + differentRole, + differentAccount, + wallet, + { + chainId: mockChainId, + isTransferable, + }, + ); + expect(result).toEqual(mockTxResponse); + }); + }, + ); + + describe('TT Document Store (Fallback)', () => { + let wallet: ethersV5.Wallet; + + beforeEach(() => { + vi.clearAllMocks(); + wallet = new WalletV5(PRIVATE_KEY, providerV5 as any); + vi.spyOn(wallet, 'getChainId').mockResolvedValue(mockChainId as unknown as number); + const mockContractConstructor = (mockContract: any) => vi.fn(() => mockContract); + vi.mocked(getEthersContractFromProvider).mockReturnValue( + mockContractConstructor(mockTTDocumentStoreContract), + ); + mockTTDocumentStoreContract.callStatic.revokeRole.mockResolvedValue(true); + mockTTDocumentStoreContract.revokeRole.staticCall.mockResolvedValue(true); + }); + + it('should auto-detect TT Document Store as fallback when ERC-165 interfaces not supported', async () => { + vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); + const result = await revokeDocumentStoreRole( + MOCK_TT_DOCUMENT_STORE_ADDRESS, + mockRole, + mockAccount, + wallet, + { + chainId: mockChainId, + }, + ); + expect(result).toEqual('tt_document_store_revoke_role_tx_hash'); + expect(coreModule.checkSupportsInterface).toHaveBeenCalledWith( + MOCK_TT_DOCUMENT_STORE_ADDRESS, + supportInterfaceIds.IDocumentStore, + wallet.provider, + ); + expect(coreModule.checkSupportsInterface).toHaveBeenCalledWith( + MOCK_TT_DOCUMENT_STORE_ADDRESS, + supportInterfaceIds.ITransferableDocumentStore, + wallet.provider, + ); + }); + + it('should revoke role with TT Document Store (ethers v5)', async () => { + vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); + const result = await revokeDocumentStoreRole( + MOCK_TT_DOCUMENT_STORE_ADDRESS, + mockRole, + mockAccount, + wallet, + { + chainId: mockChainId, + }, + ); + expect(result).toEqual('tt_document_store_revoke_role_tx_hash'); + }); + + it('should revoke role with TT Document Store (ethers v6)', async () => { + const walletV6 = new WalletV6(PRIVATE_KEY, providerV6 as any); + vi.spyOn(providerV6, 'getNetwork').mockResolvedValue({ + chainId: mockChainId, + } as unknown as Network); + vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); + const result = await revokeDocumentStoreRole( + MOCK_TT_DOCUMENT_STORE_ADDRESS, + mockRole, + mockAccount, + walletV6, + { + chainId: mockChainId, + }, + ); + expect(result).toEqual('tt_document_store_revoke_role_tx_hash'); + }); + + it('should handle callStatic failure for TT Document Store', async () => { + vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); + const mockError = new Error('TT callStatic error'); + mockTTDocumentStoreContract.callStatic.revokeRole.mockRejectedValue(mockError); + mockTTDocumentStoreContract.revokeRole.staticCall.mockRejectedValue(mockError); + await expect( + revokeDocumentStoreRole(MOCK_TT_DOCUMENT_STORE_ADDRESS, mockRole, mockAccount, wallet, { + chainId: mockChainId, + }), + ).rejects.toThrow('Pre-check (callStatic) for issue failed'); + }); + + it('should revoke role TT Document Store with gas options', async () => { + vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); + const result = await revokeDocumentStoreRole( + MOCK_TT_DOCUMENT_STORE_ADDRESS, + mockRole, + mockAccount, + wallet, + { + chainId: mockChainId, + maxFeePerGas: '2000000000', + maxPriorityFeePerGas: '1500000000', + }, + ); + expect(result).toEqual('tt_document_store_revoke_role_tx_hash'); + }); + + it('should handle role not granted error in TT Document Store', async () => { + vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); + mockTTDocumentStoreContract.callStatic.revokeRole.mockRejectedValue( + new Error('Role not granted'), + ); + mockTTDocumentStoreContract.revokeRole.staticCall.mockRejectedValue( + new Error('Role not granted'), + ); + await expect( + revokeDocumentStoreRole(MOCK_TT_DOCUMENT_STORE_ADDRESS, mockRole, mockAccount, wallet, { + chainId: mockChainId, + }), + ).rejects.toThrow('Pre-check (callStatic) for issue failed'); + }); + }); +}); diff --git a/src/document-store/grant-role.ts b/src/document-store/grant-role.ts new file mode 100644 index 0000000..6fd4932 --- /dev/null +++ b/src/document-store/grant-role.ts @@ -0,0 +1,132 @@ +import { + Signer as SignerV6, + Contract as ContractV6, + ContractTransaction as ContractTransactionV6, +} from 'ethersV6'; +import { + Contract as ContractV5, + ContractTransaction as ContractTransactionV5, + Signer as SignerV5, +} from 'ethers'; +import { CHAIN_ID } from '../utils'; +import { GasValue } from '../token-registry-functions/types'; +import { checkSupportsInterface } from '../core'; +import { supportInterfaceIds } from './supportInterfaceIds'; +import { TT_DOCUMENT_STORE_ABI } from './tt-document-store-abi'; +import { getEthersContractFromProvider, isV6EthersProvider } from '../utils/ethers'; +import { + DocumentStore__factory, + TransferableDocumentStore__factory, +} from '@trustvc/document-store'; +import { getTxOptions } from '../token-registry-functions/utils'; + +export interface IssueOptions { + chainId?: CHAIN_ID; + maxFeePerGas?: GasValue; + maxPriorityFeePerGas?: GasValue; + isTransferable?: boolean; +} + +/** + * Grants a role to an account on the DocumentStore contract. + * Supports both Ethers v5 and v6 signers. + * Supports three types of document stores: + * 1. DocumentStore (ERC-165 compliant) + * 2. TransferableDocumentStore (ERC-165 compliant) + * 3. TT Document Store (legacy, no ERC-165 support - used as fallback) + * @param {string} documentStoreAddress - The address of the DocumentStore contract. + * @param {string} role - The role to grant (e.g., 'ISSUER', 'REVOKER', 'ADMIN'). + * @param {string} account - The account to grant the role to. + * @param {SignerV5 | SignerV6} signer - Signer instance (Ethers v5 or v6) that authorizes the grant role transaction. + * @param {IssueOptions} options - Optional transaction metadata including gas values and chain ID. + * @returns {Promise} A promise resolving to the transaction result from the grant role call. + * @throws {Error} If the document store address or signer provider is not provided. + * @throws {Error} If the role is invalid. + * @throws {Error} If the `callStatic.grantRole` fails as a pre-check. + */ +export const grantDocumentStoreRole = async ( + documentStoreAddress: string, + role: string, + account: string, + signer: SignerV5 | SignerV6, + options: IssueOptions = {}, +): Promise => { + if (!documentStoreAddress) throw new Error('Document store address is required'); + if (!signer.provider) throw new Error('Provider is required'); + if (!role) throw new Error('Role is required'); + if (!account) throw new Error('Account is required'); + + const { chainId, maxFeePerGas, maxPriorityFeePerGas, isTransferable } = options; + + let isDocumentStore = !isTransferable; + let isTransferableDocumentStore = isTransferable; + let isTTDocumentStore = false; + + // Detect contract type by checking interface support + if (isTransferable === undefined) { + [isDocumentStore, isTransferableDocumentStore] = await Promise.all([ + checkSupportsInterface( + documentStoreAddress, + supportInterfaceIds.IDocumentStore, + signer.provider, + ), + checkSupportsInterface( + documentStoreAddress, + supportInterfaceIds.ITransferableDocumentStore, + signer.provider, + ), + ]); + + // If neither DocumentStore nor TransferableDocumentStore is supported, + // fallback to TT Document Store (legacy contract without ERC-165) + if (!isDocumentStore && !isTransferableDocumentStore) { + isTTDocumentStore = true; + } + } + + if (!isDocumentStore && !isTransferableDocumentStore && !isTTDocumentStore) { + throw new Error( + 'Contract does not support DocumentStore, TransferableDocumentStore, or TT Document Store interface', + ); + } + + // Get the appropriate Contract class based on provider version + const Contract = getEthersContractFromProvider(signer.provider); + + // Connect to the appropriate DocumentStore contract based on interface detection + let documentStoreAbi; + if (isTTDocumentStore) { + documentStoreAbi = TT_DOCUMENT_STORE_ABI; + } else { + const DocumentStoreFactory = isTransferableDocumentStore + ? TransferableDocumentStore__factory + : DocumentStore__factory; + documentStoreAbi = DocumentStoreFactory.abi; + } + + const documentStoreContract: ContractV5 | ContractV6 = new Contract( + documentStoreAddress, + documentStoreAbi, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, + ); + // Check callStatic (dry run) to ensure transaction will succeed + try { + const isV6 = isV6EthersProvider(signer.provider); + + if (isV6) { + await (documentStoreContract as ContractV6).grantRole.staticCall(role, account); + } else { + await (documentStoreContract as ContractV5).callStatic.grantRole(role, account); + } + } catch (e) { + console.error('callStatic failed:', e); + throw new Error('Pre-check (callStatic) for issue failed'); + } + + // Get transaction options (gas settings) + const txOptions = await getTxOptions(signer, chainId, maxFeePerGas, maxPriorityFeePerGas); + + // Send the actual transaction + return await documentStoreContract.grantRole(role, account, txOptions); +}; diff --git a/src/document-store/revoke-role.ts b/src/document-store/revoke-role.ts new file mode 100644 index 0000000..b0dd6cf --- /dev/null +++ b/src/document-store/revoke-role.ts @@ -0,0 +1,132 @@ +import { + Signer as SignerV6, + Contract as ContractV6, + ContractTransaction as ContractTransactionV6, +} from 'ethersV6'; +import { + Contract as ContractV5, + ContractTransaction as ContractTransactionV5, + Signer as SignerV5, +} from 'ethers'; +import { CHAIN_ID } from '../utils'; +import { GasValue } from '../token-registry-functions/types'; +import { checkSupportsInterface } from '../core'; +import { supportInterfaceIds } from './supportInterfaceIds'; +import { TT_DOCUMENT_STORE_ABI } from './tt-document-store-abi'; +import { getEthersContractFromProvider, isV6EthersProvider } from '../utils/ethers'; +import { + DocumentStore__factory, + TransferableDocumentStore__factory, +} from '@trustvc/document-store'; +import { getTxOptions } from '../token-registry-functions/utils'; + +export interface IssueOptions { + chainId?: CHAIN_ID; + maxFeePerGas?: GasValue; + maxPriorityFeePerGas?: GasValue; + isTransferable?: boolean; +} + +/** + * Revokes a role from an account on the DocumentStore contract. + * Supports both Ethers v5 and v6 signers. + * Supports three types of document stores: + * 1. DocumentStore (ERC-165 compliant) + * 2. TransferableDocumentStore (ERC-165 compliant) + * 3. TT Document Store (legacy, no ERC-165 support - used as fallback) + * @param {string} documentStoreAddress - The address of the DocumentStore contract. + * @param {string} role - The role to revoke (e.g., 'ISSUER', 'REVOKER', 'ADMIN'). + * @param {string} account - The account to revoke the role from. + * @param {SignerV5 | SignerV6} signer - Signer instance (Ethers v5 or v6) that authorizes the revoke role transaction. + * @param {IssueOptions} options - Optional transaction metadata including gas values and chain ID. + * @returns {Promise} A promise resolving to the transaction result from the revoke role call. + * @throws {Error} If the document store address or signer provider is not provided. + * @throws {Error} If the role is invalid. + * @throws {Error} If the `callStatic.revokeRole` fails as a pre-check. + */ +export const revokeDocumentStoreRole = async ( + documentStoreAddress: string, + role: string, + account: string, + signer: SignerV5 | SignerV6, + options: IssueOptions = {}, +): Promise => { + if (!documentStoreAddress) throw new Error('Document store address is required'); + if (!signer.provider) throw new Error('Provider is required'); + if (!role) throw new Error('Role is required'); + if (!account) throw new Error('Account is required'); + + const { chainId, maxFeePerGas, maxPriorityFeePerGas, isTransferable } = options; + + let isDocumentStore = !isTransferable; + let isTransferableDocumentStore = isTransferable; + let isTTDocumentStore = false; + + // Detect contract type by checking interface support + if (isTransferable === undefined) { + [isDocumentStore, isTransferableDocumentStore] = await Promise.all([ + checkSupportsInterface( + documentStoreAddress, + supportInterfaceIds.IDocumentStore, + signer.provider, + ), + checkSupportsInterface( + documentStoreAddress, + supportInterfaceIds.ITransferableDocumentStore, + signer.provider, + ), + ]); + + // If neither DocumentStore nor TransferableDocumentStore is supported, + // fallback to TT Document Store (legacy contract without ERC-165) + if (!isDocumentStore && !isTransferableDocumentStore) { + isTTDocumentStore = true; + } + } + + if (!isDocumentStore && !isTransferableDocumentStore && !isTTDocumentStore) { + throw new Error( + 'Contract does not support DocumentStore, TransferableDocumentStore, or TT Document Store interface', + ); + } + + // Get the appropriate Contract class based on provider version + const Contract = getEthersContractFromProvider(signer.provider); + + // Connect to the appropriate DocumentStore contract based on interface detection + let documentStoreAbi; + if (isTTDocumentStore) { + documentStoreAbi = TT_DOCUMENT_STORE_ABI; + } else { + const DocumentStoreFactory = isTransferableDocumentStore + ? TransferableDocumentStore__factory + : DocumentStore__factory; + documentStoreAbi = DocumentStoreFactory.abi; + } + + const documentStoreContract: ContractV5 | ContractV6 = new Contract( + documentStoreAddress, + documentStoreAbi, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, + ); + // Check callStatic (dry run) to ensure transaction will succeed + try { + const isV6 = isV6EthersProvider(signer.provider); + + if (isV6) { + await (documentStoreContract as ContractV6).revokeRole.staticCall(role, account); + } else { + await (documentStoreContract as ContractV5).callStatic.revokeRole(role, account); + } + } catch (e) { + console.error('callStatic failed:', e); + throw new Error('Pre-check (callStatic) for issue failed'); + } + + // Get transaction options (gas settings) + const txOptions = await getTxOptions(signer, chainId, maxFeePerGas, maxPriorityFeePerGas); + + // Send the actual transaction + return await documentStoreContract.revokeRole(role, account, txOptions); +}; diff --git a/src/document-store/transferOwnership.ts b/src/document-store/transferOwnership.ts new file mode 100644 index 0000000..78d30aa --- /dev/null +++ b/src/document-store/transferOwnership.ts @@ -0,0 +1,66 @@ +import { Signer as SignerV6, ContractTransaction as ContractTransactionV6 } from 'ethersV6'; +import { ContractTransaction as ContractTransactionV5, Signer as SignerV5 } from 'ethers'; +import { CHAIN_ID } from '../utils'; +import { GasValue } from '../token-registry-functions/types'; +import { revokeDocumentStoreRole } from './revoke-role'; + +import { grantDocumentStoreRole } from './grant-role'; +import { getRoleString } from './document-store-roles'; + +export interface IssueOptions { + chainId?: CHAIN_ID; + maxFeePerGas?: GasValue; + maxPriorityFeePerGas?: GasValue; + isTransferable?: boolean; +} + +/** + * Revokes a role from an account on the DocumentStore contract. + * Supports both Ethers v5 and v6 signers. + * Supports three types of document stores: + * 1. DocumentStore (ERC-165 compliant) + * 2. TransferableDocumentStore (ERC-165 compliant) + * 3. TT Document Store (legacy, no ERC-165 support - used as fallback) + * @param {string} documentStoreAddress - The address of the DocumentStore contract. + * @param {string} account - The account to revoke the role from. + * @param {SignerV5 | SignerV6} signer - Signer instance (Ethers v5 or v6) that authorizes the revoke role transaction. + * @param {IssueOptions} options - Optional transaction metadata including gas values and chain ID. + * @returns {Promise<{grantTransaction: Promise; revokeTransaction: Promise}>} A promise resolving to the transaction result from the revoke role call. + * @throws {Error} If the document store address or signer provider is not provided. + * @throws {Error} If the role is invalid. + * @throws {Error} If the `callStatic.revokeRole` fails as a pre-check. + */ +export const transferOwnershipDocumentStore = async ( + documentStoreAddress: string, + account: string, + signer: SignerV5 | SignerV6, + options: IssueOptions = {}, +): Promise<{ + grantTransaction: Promise; + revokeTransaction: Promise; +}> => { + if (!documentStoreAddress) throw new Error('Document store address is required'); + if (!signer.provider) throw new Error('Provider is required'); + if (!account) throw new Error('Account is required'); + + const ownerAddress = await signer.getAddress(); + const roleString = await getRoleString(documentStoreAddress, 'admin'); + //call the transferOwnership function of the document store contract + + //call grant and revoke function here + const grantTransaction = grantDocumentStoreRole( + documentStoreAddress, + roleString, + account, + signer, + options, + ); + const revokeTransaction = revokeDocumentStoreRole( + documentStoreAddress, + roleString, + ownerAddress, + signer, + options, + ); + return { grantTransaction, revokeTransaction }; +}; From 0f2d5bc4348d3e260b126004b35ae938a91e80f6 Mon Sep 17 00:00:00 2001 From: Rishabh Singh Date: Thu, 12 Feb 2026 15:04:12 +0530 Subject: [PATCH 2/5] fix: tests --- .../document-store/grant-role.test.ts | 38 ++++++++++--------- .../document-store/revoke-role.test.ts | 31 +++++++-------- src/document-store/grant-role.ts | 2 +- src/document-store/revoke-role.ts | 2 +- src/document-store/transferOwnership.ts | 16 +++++++- 5 files changed, 53 insertions(+), 36 deletions(-) diff --git a/src/__tests__/document-store/grant-role.test.ts b/src/__tests__/document-store/grant-role.test.ts index fbfcf78..10e995a 100644 --- a/src/__tests__/document-store/grant-role.test.ts +++ b/src/__tests__/document-store/grant-role.test.ts @@ -65,23 +65,13 @@ describe('Grant Document Store Role', () => { ? 'transferable_document_store_grant_role_tx_hash' : 'document_store_grant_role_tx_hash'; - let wallet: ethersV5.Wallet | ethersV6.Wallet; - if (ethersVersion === 'v5') { - wallet = new WalletV5(PRIVATE_KEY, Provider as any) as ethersV5.Wallet; - vi.spyOn(wallet, 'getChainId').mockResolvedValue(mockChainId as unknown as number); - } else { - wallet = new WalletV6(PRIVATE_KEY, Provider as any); - vi.spyOn(Provider, 'getNetwork').mockResolvedValue({ - chainId: mockChainId, - } as unknown as Network); - } - const mockDocumentStoreAddress = isTransferable ? MOCK_TRANSFERABLE_DOCUMENT_STORE_ADDRESS : MOCK_DOCUMENT_STORE_ADDRESS; + let wallet: ethersV5.Wallet | ethersV6.Wallet; + beforeAll(() => { - vi.clearAllMocks(); const mockContractConstructor = (mockContract: any) => vi.fn(() => mockContract); vi.mocked(getEthersContractFromProvider).mockReturnValue( mockContractConstructor(mockContract), @@ -100,6 +90,16 @@ describe('Grant Document Store Role', () => { ); mockContract.callStatic.grantRole.mockResolvedValue(true); mockContract.grantRole.staticCall.mockResolvedValue(true); + + if (ethersVersion === 'v5') { + wallet = new WalletV5(PRIVATE_KEY, Provider as any) as ethersV5.Wallet; + vi.spyOn(wallet, 'getChainId').mockResolvedValue(mockChainId as unknown as number); + } else { + wallet = new WalletV6(PRIVATE_KEY, Provider as any); + vi.spyOn(Provider, 'getNetwork').mockResolvedValue({ + chainId: mockChainId, + } as unknown as Network); + } }); it('should grant role successfully', async () => { @@ -167,7 +167,9 @@ describe('Grant Document Store Role', () => { }); it('should throw when provider is missing', async () => { - const signerWithoutProvider = new WalletV5('0x'.padEnd(66, '1')); + const signerWithoutProvider = new (ethersVersion === 'v5' ? WalletV5 : WalletV6)( + '0x'.padEnd(66, '1'), + ); await expect( grantDocumentStoreRole( mockDocumentStoreAddress, @@ -206,7 +208,7 @@ describe('Grant Document Store Role', () => { chainId: mockChainId, isTransferable, }), - ).rejects.toThrow('Pre-check (callStatic) for issue failed'); + ).rejects.toThrow('Pre-check (callStatic) for grant-role failed'); }); it('should fallback to TT Document Store when ERC-165 interfaces not supported', async () => { @@ -237,7 +239,7 @@ describe('Grant Document Store Role', () => { chainId: mockChainId, isTransferable, }), - ).rejects.toThrow('Pre-check (callStatic) for issue failed'); + ).rejects.toThrow('Pre-check (callStatic) for grant-role failed'); }); it('should handle already granted role', async () => { @@ -248,7 +250,7 @@ describe('Grant Document Store Role', () => { chainId: mockChainId, isTransferable, }), - ).rejects.toThrow('Pre-check (callStatic) for issue failed'); + ).rejects.toThrow('Pre-check (callStatic) for grant-role failed'); }); it('should work with different role and account addresses', async () => { @@ -349,7 +351,7 @@ describe('Grant Document Store Role', () => { grantDocumentStoreRole(MOCK_TT_DOCUMENT_STORE_ADDRESS, mockRole, mockAccount, wallet, { chainId: mockChainId, }), - ).rejects.toThrow('Pre-check (callStatic) for issue failed'); + ).rejects.toThrow('Pre-check (callStatic) for grant-role failed'); }); it('should grant role TT Document Store with gas options', async () => { @@ -380,7 +382,7 @@ describe('Grant Document Store Role', () => { grantDocumentStoreRole(MOCK_TT_DOCUMENT_STORE_ADDRESS, mockRole, mockAccount, wallet, { chainId: mockChainId, }), - ).rejects.toThrow('Pre-check (callStatic) for issue failed'); + ).rejects.toThrow('Pre-check (callStatic) for grant-role failed'); }); }); }); diff --git a/src/__tests__/document-store/revoke-role.test.ts b/src/__tests__/document-store/revoke-role.test.ts index 6d3793c..e870598 100644 --- a/src/__tests__/document-store/revoke-role.test.ts +++ b/src/__tests__/document-store/revoke-role.test.ts @@ -66,15 +66,6 @@ describe('Revoke Document Store Role', () => { : 'document_store_revoke_role_tx_hash'; let wallet: ethersV5.Wallet | ethersV6.Wallet; - if (ethersVersion === 'v5') { - wallet = new WalletV5(PRIVATE_KEY, Provider as any) as ethersV5.Wallet; - vi.spyOn(wallet, 'getChainId').mockResolvedValue(mockChainId as unknown as number); - } else { - wallet = new WalletV6(PRIVATE_KEY, Provider as any); - vi.spyOn(Provider, 'getNetwork').mockResolvedValue({ - chainId: mockChainId, - } as unknown as Network); - } const mockDocumentStoreAddress = isTransferable ? MOCK_TRANSFERABLE_DOCUMENT_STORE_ADDRESS @@ -100,6 +91,16 @@ describe('Revoke Document Store Role', () => { ); mockContract.callStatic.revokeRole.mockResolvedValue(true); mockContract.revokeRole.staticCall.mockResolvedValue(true); + + if (ethersVersion === 'v5') { + wallet = new WalletV5(PRIVATE_KEY, Provider as any) as ethersV5.Wallet; + vi.spyOn(wallet, 'getChainId').mockResolvedValue(mockChainId as unknown as number); + } else { + wallet = new WalletV6(PRIVATE_KEY, Provider as any); + vi.spyOn(Provider, 'getNetwork').mockResolvedValue({ + chainId: mockChainId, + } as unknown as Network); + } }); it('should revoke role successfully', async () => { @@ -208,7 +209,7 @@ describe('Revoke Document Store Role', () => { chainId: mockChainId, isTransferable, }), - ).rejects.toThrow('Pre-check (callStatic) for issue failed'); + ).rejects.toThrow('Pre-check (callStatic) for revoke-role failed'); }); it('should fallback to TT Document Store when ERC-165 interfaces not supported', async () => { @@ -242,7 +243,7 @@ describe('Revoke Document Store Role', () => { chainId: mockChainId, isTransferable, }), - ).rejects.toThrow('Pre-check (callStatic) for issue failed'); + ).rejects.toThrow('Pre-check (callStatic) for revoke-role failed'); }); it('should handle role not granted error', async () => { @@ -253,11 +254,11 @@ describe('Revoke Document Store Role', () => { chainId: mockChainId, isTransferable, }), - ).rejects.toThrow('Pre-check (callStatic) for issue failed'); + ).rejects.toThrow('Pre-check (callStatic) for revoke-role failed'); }); it('should work with different role and account addresses', async () => { - const differentRole = '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890'; + const differentRole = '0x1111111111111111111111111111111111111111111111111111111111111111'; const differentAccount = '0x9876543210987654321098765432109876543210'; const result = await revokeDocumentStoreRole( mockDocumentStoreAddress, @@ -354,7 +355,7 @@ describe('Revoke Document Store Role', () => { revokeDocumentStoreRole(MOCK_TT_DOCUMENT_STORE_ADDRESS, mockRole, mockAccount, wallet, { chainId: mockChainId, }), - ).rejects.toThrow('Pre-check (callStatic) for issue failed'); + ).rejects.toThrow('Pre-check (callStatic) for revoke-role failed'); }); it('should revoke role TT Document Store with gas options', async () => { @@ -385,7 +386,7 @@ describe('Revoke Document Store Role', () => { revokeDocumentStoreRole(MOCK_TT_DOCUMENT_STORE_ADDRESS, mockRole, mockAccount, wallet, { chainId: mockChainId, }), - ).rejects.toThrow('Pre-check (callStatic) for issue failed'); + ).rejects.toThrow('Pre-check (callStatic) for revoke-role failed'); }); }); }); diff --git a/src/document-store/grant-role.ts b/src/document-store/grant-role.ts index 6fd4932..9515764 100644 --- a/src/document-store/grant-role.ts +++ b/src/document-store/grant-role.ts @@ -121,7 +121,7 @@ export const grantDocumentStoreRole = async ( } } catch (e) { console.error('callStatic failed:', e); - throw new Error('Pre-check (callStatic) for issue failed'); + throw new Error('Pre-check (callStatic) for grant-role failed'); } // Get transaction options (gas settings) diff --git a/src/document-store/revoke-role.ts b/src/document-store/revoke-role.ts index b0dd6cf..8690f0e 100644 --- a/src/document-store/revoke-role.ts +++ b/src/document-store/revoke-role.ts @@ -121,7 +121,7 @@ export const revokeDocumentStoreRole = async ( } } catch (e) { console.error('callStatic failed:', e); - throw new Error('Pre-check (callStatic) for issue failed'); + throw new Error('Pre-check (callStatic) for revoke-role failed'); } // Get transaction options (gas settings) diff --git a/src/document-store/transferOwnership.ts b/src/document-store/transferOwnership.ts index 78d30aa..6586592 100644 --- a/src/document-store/transferOwnership.ts +++ b/src/document-store/transferOwnership.ts @@ -15,7 +15,7 @@ export interface IssueOptions { } /** - * Revokes a role from an account on the DocumentStore contract. + * Transfers ownership of a DocumentStore contract to a new owner. * Supports both Ethers v5 and v6 signers. * Supports three types of document stores: * 1. DocumentStore (ERC-165 compliant) @@ -55,6 +55,14 @@ export const transferOwnershipDocumentStore = async ( signer, options, ); + //check if the grant transaction is successful + const grantTransactionResult = await grantTransaction; + if (!grantTransactionResult) { + //add custom error message not proceeding eith the revoke transaction + throw new Error('Grant transaction failed, not proceeding with revoke transaction'); + //return the grant transaction result + } + //call revoke function here const revokeTransaction = revokeDocumentStoreRole( documentStoreAddress, roleString, @@ -62,5 +70,11 @@ export const transferOwnershipDocumentStore = async ( signer, options, ); + //check if the revoke transaction is successful + const revokeTransactionResult = await revokeTransaction; + if (!revokeTransactionResult) { + throw new Error('Revoke transaction failed'); + //return the revoke transaction result + } return { grantTransaction, revokeTransaction }; }; From 37fff639c1b04b993ec900e1a36ce7d9e97518b3 Mon Sep 17 00:00:00 2001 From: Rishabh Singh Date: Thu, 12 Feb 2026 15:49:00 +0530 Subject: [PATCH 3/5] fix: type and import fixes --- src/document-store/grant-role.ts | 20 +++----------------- src/document-store/index.ts | 10 +++++----- src/document-store/issue.ts | 20 +++----------------- src/document-store/revoke-role.ts | 20 +++----------------- src/document-store/revoke.ts | 6 ------ src/document-store/transferOwnership.ts | 14 +++----------- src/document-store/types.ts | 9 +++++++++ src/index.ts | 8 ++++---- 8 files changed, 30 insertions(+), 77 deletions(-) create mode 100644 src/document-store/types.ts diff --git a/src/document-store/grant-role.ts b/src/document-store/grant-role.ts index 9515764..4198bdd 100644 --- a/src/document-store/grant-role.ts +++ b/src/document-store/grant-role.ts @@ -8,8 +8,6 @@ import { ContractTransaction as ContractTransactionV5, Signer as SignerV5, } from 'ethers'; -import { CHAIN_ID } from '../utils'; -import { GasValue } from '../token-registry-functions/types'; import { checkSupportsInterface } from '../core'; import { supportInterfaceIds } from './supportInterfaceIds'; import { TT_DOCUMENT_STORE_ABI } from './tt-document-store-abi'; @@ -19,13 +17,7 @@ import { TransferableDocumentStore__factory, } from '@trustvc/document-store'; import { getTxOptions } from '../token-registry-functions/utils'; - -export interface IssueOptions { - chainId?: CHAIN_ID; - maxFeePerGas?: GasValue; - maxPriorityFeePerGas?: GasValue; - isTransferable?: boolean; -} +import { CommandOptions } from './types'; /** * Grants a role to an account on the DocumentStore contract. @@ -38,7 +30,7 @@ export interface IssueOptions { * @param {string} role - The role to grant (e.g., 'ISSUER', 'REVOKER', 'ADMIN'). * @param {string} account - The account to grant the role to. * @param {SignerV5 | SignerV6} signer - Signer instance (Ethers v5 or v6) that authorizes the grant role transaction. - * @param {IssueOptions} options - Optional transaction metadata including gas values and chain ID. + * @param {CommandOptions} options - Optional transaction metadata including gas values and chain ID. * @returns {Promise} A promise resolving to the transaction result from the grant role call. * @throws {Error} If the document store address or signer provider is not provided. * @throws {Error} If the role is invalid. @@ -49,7 +41,7 @@ export const grantDocumentStoreRole = async ( role: string, account: string, signer: SignerV5 | SignerV6, - options: IssueOptions = {}, + options: CommandOptions = {}, ): Promise => { if (!documentStoreAddress) throw new Error('Document store address is required'); if (!signer.provider) throw new Error('Provider is required'); @@ -84,12 +76,6 @@ export const grantDocumentStoreRole = async ( } } - if (!isDocumentStore && !isTransferableDocumentStore && !isTTDocumentStore) { - throw new Error( - 'Contract does not support DocumentStore, TransferableDocumentStore, or TT Document Store interface', - ); - } - // Get the appropriate Contract class based on provider version const Contract = getEthersContractFromProvider(signer.provider); diff --git a/src/document-store/index.ts b/src/document-store/index.ts index 28ac477..16c20c9 100644 --- a/src/document-store/index.ts +++ b/src/document-store/index.ts @@ -1,8 +1,8 @@ -export { documentStoreIssue, IssueOptions } from './issue'; -export { documentStoreRevoke, RevokeOptions } from './revoke'; -// export { revokeDocumentStoreRole } from './revoke-role'; -// export { grantDocumentStoreRole } from './grant-role'; -export { deployDocumentStore, DeployOptions } from './deploy'; +export { documentStoreIssue } from './issue'; +export { documentStoreRevoke } from './revoke'; +export { revokeDocumentStoreRole } from './revoke-role'; +export { grantDocumentStoreRole } from './grant-role'; +export { deployDocumentStore } from './deploy'; export { supportInterfaceIds } from './supportInterfaceIds'; export { DocumentStore__factory, diff --git a/src/document-store/issue.ts b/src/document-store/issue.ts index c0f6a31..d0a4d6d 100644 --- a/src/document-store/issue.ts +++ b/src/document-store/issue.ts @@ -13,12 +13,11 @@ import { Signer as SignerV5, } from 'ethers'; import { getEthersContractFromProvider, isV6EthersProvider } from '../utils/ethers'; -import { CHAIN_ID } from '../utils'; -import { GasValue } from '../token-registry-functions/types'; import { getTxOptions } from '../token-registry-functions/utils'; import { checkSupportsInterface } from '../core'; import { supportInterfaceIds } from './supportInterfaceIds'; import { TT_DOCUMENT_STORE_ABI } from './tt-document-store-abi'; +import { CommandOptions } from './types'; /** * Issues a document hash to the DocumentStore contract. @@ -30,25 +29,18 @@ import { TT_DOCUMENT_STORE_ABI } from './tt-document-store-abi'; * @param {string} documentStoreAddress - The address of the DocumentStore contract. * @param {string} documentHash - The hash of the document to issue (must be a valid hex string). * @param {SignerV5 | SignerV6} signer - Signer instance (Ethers v5 or v6) that authorizes the issue transaction. - * @param {IssueOptions} options - Optional transaction metadata including gas values and chain ID. + * @param {CommandOptions} options - Optional transaction metadata including gas values and chain ID. * @returns {Promise} A promise resolving to the transaction result from the issue call. * @throws {Error} If the document store address or signer provider is not provided. * @throws {Error} If the document hash is invalid. * @throws {Error} If the `callStatic.issue` fails as a pre-check. */ -export interface IssueOptions { - chainId?: CHAIN_ID; - maxFeePerGas?: GasValue; - maxPriorityFeePerGas?: GasValue; - isTransferable?: boolean; -} - const documentStoreIssue = async ( documentStoreAddress: string, documentHash: string, signer: SignerV5 | SignerV6, - options: IssueOptions = {}, + options: CommandOptions = {}, ): Promise => { if (!documentStoreAddress) throw new Error('Document store address is required'); if (!signer.provider) throw new Error('Provider is required'); @@ -82,12 +74,6 @@ const documentStoreIssue = async ( } } - if (!isDocumentStore && !isTransferableDocumentStore && !isTTDocumentStore) { - throw new Error( - 'Contract does not support DocumentStore, TransferableDocumentStore, or TT Document Store interface', - ); - } - // Get the appropriate Contract class based on provider version const Contract = getEthersContractFromProvider(signer.provider); diff --git a/src/document-store/revoke-role.ts b/src/document-store/revoke-role.ts index 8690f0e..03db6d1 100644 --- a/src/document-store/revoke-role.ts +++ b/src/document-store/revoke-role.ts @@ -8,8 +8,6 @@ import { ContractTransaction as ContractTransactionV5, Signer as SignerV5, } from 'ethers'; -import { CHAIN_ID } from '../utils'; -import { GasValue } from '../token-registry-functions/types'; import { checkSupportsInterface } from '../core'; import { supportInterfaceIds } from './supportInterfaceIds'; import { TT_DOCUMENT_STORE_ABI } from './tt-document-store-abi'; @@ -19,13 +17,7 @@ import { TransferableDocumentStore__factory, } from '@trustvc/document-store'; import { getTxOptions } from '../token-registry-functions/utils'; - -export interface IssueOptions { - chainId?: CHAIN_ID; - maxFeePerGas?: GasValue; - maxPriorityFeePerGas?: GasValue; - isTransferable?: boolean; -} +import { CommandOptions } from './types'; /** * Revokes a role from an account on the DocumentStore contract. @@ -38,7 +30,7 @@ export interface IssueOptions { * @param {string} role - The role to revoke (e.g., 'ISSUER', 'REVOKER', 'ADMIN'). * @param {string} account - The account to revoke the role from. * @param {SignerV5 | SignerV6} signer - Signer instance (Ethers v5 or v6) that authorizes the revoke role transaction. - * @param {IssueOptions} options - Optional transaction metadata including gas values and chain ID. + * @param {CommandOptions} options - Optional transaction metadata including gas values and chain ID. * @returns {Promise} A promise resolving to the transaction result from the revoke role call. * @throws {Error} If the document store address or signer provider is not provided. * @throws {Error} If the role is invalid. @@ -49,7 +41,7 @@ export const revokeDocumentStoreRole = async ( role: string, account: string, signer: SignerV5 | SignerV6, - options: IssueOptions = {}, + options: CommandOptions = {}, ): Promise => { if (!documentStoreAddress) throw new Error('Document store address is required'); if (!signer.provider) throw new Error('Provider is required'); @@ -84,12 +76,6 @@ export const revokeDocumentStoreRole = async ( } } - if (!isDocumentStore && !isTransferableDocumentStore && !isTTDocumentStore) { - throw new Error( - 'Contract does not support DocumentStore, TransferableDocumentStore, or TT Document Store interface', - ); - } - // Get the appropriate Contract class based on provider version const Contract = getEthersContractFromProvider(signer.provider); diff --git a/src/document-store/revoke.ts b/src/document-store/revoke.ts index fa17f1e..225e41f 100644 --- a/src/document-store/revoke.ts +++ b/src/document-store/revoke.ts @@ -82,12 +82,6 @@ const documentStoreRevoke = async ( } } - if (!isDocumentStore && !isTransferableDocumentStore && !isTTDocumentStore) { - throw new Error( - 'Contract does not support DocumentStore, TransferableDocumentStore, or TT Document Store interface', - ); - } - // Get the appropriate Contract class based on provider version const Contract = getEthersContractFromProvider(signer.provider); diff --git a/src/document-store/transferOwnership.ts b/src/document-store/transferOwnership.ts index 6586592..3259c0c 100644 --- a/src/document-store/transferOwnership.ts +++ b/src/document-store/transferOwnership.ts @@ -1,18 +1,10 @@ import { Signer as SignerV6, ContractTransaction as ContractTransactionV6 } from 'ethersV6'; import { ContractTransaction as ContractTransactionV5, Signer as SignerV5 } from 'ethers'; -import { CHAIN_ID } from '../utils'; -import { GasValue } from '../token-registry-functions/types'; import { revokeDocumentStoreRole } from './revoke-role'; import { grantDocumentStoreRole } from './grant-role'; import { getRoleString } from './document-store-roles'; - -export interface IssueOptions { - chainId?: CHAIN_ID; - maxFeePerGas?: GasValue; - maxPriorityFeePerGas?: GasValue; - isTransferable?: boolean; -} +import { CommandOptions } from './types'; /** * Transfers ownership of a DocumentStore contract to a new owner. @@ -24,7 +16,7 @@ export interface IssueOptions { * @param {string} documentStoreAddress - The address of the DocumentStore contract. * @param {string} account - The account to revoke the role from. * @param {SignerV5 | SignerV6} signer - Signer instance (Ethers v5 or v6) that authorizes the revoke role transaction. - * @param {IssueOptions} options - Optional transaction metadata including gas values and chain ID. + * @param {CommandOptions} options - Optional transaction metadata including gas values and chain ID. * @returns {Promise<{grantTransaction: Promise; revokeTransaction: Promise}>} A promise resolving to the transaction result from the revoke role call. * @throws {Error} If the document store address or signer provider is not provided. * @throws {Error} If the role is invalid. @@ -34,7 +26,7 @@ export const transferOwnershipDocumentStore = async ( documentStoreAddress: string, account: string, signer: SignerV5 | SignerV6, - options: IssueOptions = {}, + options: CommandOptions = {}, ): Promise<{ grantTransaction: Promise; revokeTransaction: Promise; diff --git a/src/document-store/types.ts b/src/document-store/types.ts new file mode 100644 index 0000000..e9378c4 --- /dev/null +++ b/src/document-store/types.ts @@ -0,0 +1,9 @@ +import { CHAIN_ID } from 'src/utils'; +import { GasValue } from '../token-registry-functions/types'; + +export interface CommandOptions { + chainId?: CHAIN_ID; + maxFeePerGas?: GasValue; + maxPriorityFeePerGas?: GasValue; + isTransferable?: boolean; +} diff --git a/src/index.ts b/src/index.ts index eac5718..f7e7d75 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,8 +21,8 @@ import { } from './token-registry-v5'; import { deployDocumentStore, - // grantDocumentStoreRole, - // revokeDocumentStoreRole, + grantDocumentStoreRole, + revokeDocumentStoreRole, documentStoreIssue, documentStoreRevoke, DocumentStore__factory, @@ -57,8 +57,8 @@ export { v5GetEventFromReceipt, v5ComputeInterfaceId, deployDocumentStore, - // grantDocumentStoreRole, - // revokeDocumentStoreRole, + grantDocumentStoreRole, + revokeDocumentStoreRole, documentStoreIssue, documentStoreRevoke, DocumentStore__factory, From ea1cf9ce336c3b0b18d550c8b1553637bdf774fb Mon Sep 17 00:00:00 2001 From: Rishabh Singh Date: Fri, 13 Feb 2026 10:33:37 +0530 Subject: [PATCH 4/5] fix: name and import fix --- .../document-store/grant-role.test.ts | 40 +++++++++---------- .../document-store/revoke-role.test.ts | 40 +++++++++---------- src/document-store/grant-role.ts | 2 +- src/document-store/index.ts | 5 ++- src/document-store/revoke-role.ts | 4 +- src/document-store/transferOwnership.ts | 20 +++++----- src/document-store/types.ts | 2 +- src/index.ts | 8 ++-- 8 files changed, 62 insertions(+), 59 deletions(-) diff --git a/src/__tests__/document-store/grant-role.test.ts b/src/__tests__/document-store/grant-role.test.ts index 10e995a..542b059 100644 --- a/src/__tests__/document-store/grant-role.test.ts +++ b/src/__tests__/document-store/grant-role.test.ts @@ -4,7 +4,7 @@ import { ethers as ethersV5, Wallet as WalletV5 } from 'ethers'; import { Wallet as WalletV6, Network, ethers as ethersV6 } from 'ethersV6'; import * as coreModule from '../../core'; -import { grantDocumentStoreRole } from '../../document-store/grant-role'; +import { documentStoreGrantRole } from '../../document-store/grant-role'; import { MOCK_DOCUMENT_STORE_ADDRESS, MOCK_TRANSFERABLE_DOCUMENT_STORE_ADDRESS, @@ -103,7 +103,7 @@ describe('Grant Document Store Role', () => { }); it('should grant role successfully', async () => { - const result = await grantDocumentStoreRole( + const result = await documentStoreGrantRole( mockDocumentStoreAddress, mockRole, mockAccount, @@ -117,7 +117,7 @@ describe('Grant Document Store Role', () => { }); it('should grant role with explicit contract type', async () => { - const result = await grantDocumentStoreRole( + const result = await documentStoreGrantRole( mockDocumentStoreAddress, mockRole, mockAccount, @@ -132,7 +132,7 @@ describe('Grant Document Store Role', () => { }); it('should grant role without chainId option', async () => { - const result = await grantDocumentStoreRole( + const result = await documentStoreGrantRole( mockDocumentStoreAddress, mockRole, mockAccount, @@ -145,7 +145,7 @@ describe('Grant Document Store Role', () => { }); it('should grant role with gas options', async () => { - const result = await grantDocumentStoreRole( + const result = await documentStoreGrantRole( mockDocumentStoreAddress, mockRole, mockAccount, @@ -162,7 +162,7 @@ describe('Grant Document Store Role', () => { it('should throw when document store address is missing', async () => { await expect( - grantDocumentStoreRole('', mockRole, mockAccount, wallet, { chainId: mockChainId }), + documentStoreGrantRole('', mockRole, mockAccount, wallet, { chainId: mockChainId }), ).rejects.toThrow('Document store address is required'); }); @@ -171,7 +171,7 @@ describe('Grant Document Store Role', () => { '0x'.padEnd(66, '1'), ); await expect( - grantDocumentStoreRole( + documentStoreGrantRole( mockDocumentStoreAddress, mockRole, mockAccount, @@ -185,7 +185,7 @@ describe('Grant Document Store Role', () => { it('should throw when role is missing', async () => { await expect( - grantDocumentStoreRole(mockDocumentStoreAddress, '', mockAccount, wallet, { + documentStoreGrantRole(mockDocumentStoreAddress, '', mockAccount, wallet, { chainId: mockChainId, }), ).rejects.toThrow('Role is required'); @@ -193,7 +193,7 @@ describe('Grant Document Store Role', () => { it('should throw when account is missing', async () => { await expect( - grantDocumentStoreRole(mockDocumentStoreAddress, mockRole, '', wallet, { + documentStoreGrantRole(mockDocumentStoreAddress, mockRole, '', wallet, { chainId: mockChainId, }), ).rejects.toThrow('Account is required'); @@ -204,7 +204,7 @@ describe('Grant Document Store Role', () => { mockContract.callStatic.grantRole.mockRejectedValue(mockError); mockContract.grantRole.staticCall.mockRejectedValue(mockError); await expect( - grantDocumentStoreRole(mockDocumentStoreAddress, mockRole, mockAccount, wallet, { + documentStoreGrantRole(mockDocumentStoreAddress, mockRole, mockAccount, wallet, { chainId: mockChainId, isTransferable, }), @@ -217,7 +217,7 @@ describe('Grant Document Store Role', () => { vi.mocked(getEthersContractFromProvider).mockReturnValue( mockContractConstructor(mockContract), ); - const result = await grantDocumentStoreRole( + const result = await documentStoreGrantRole( mockDocumentStoreAddress, mockRole, mockAccount, @@ -235,7 +235,7 @@ describe('Grant Document Store Role', () => { mockContract.callStatic.grantRole.mockRejectedValue(new Error('Invalid role format')); mockContract.grantRole.staticCall.mockRejectedValue(new Error('Invalid role format')); await expect( - grantDocumentStoreRole(mockDocumentStoreAddress, invalidRole, mockAccount, wallet, { + documentStoreGrantRole(mockDocumentStoreAddress, invalidRole, mockAccount, wallet, { chainId: mockChainId, isTransferable, }), @@ -246,7 +246,7 @@ describe('Grant Document Store Role', () => { mockContract.callStatic.grantRole.mockRejectedValue(new Error('Role already granted')); mockContract.grantRole.staticCall.mockRejectedValue(new Error('Role already granted')); await expect( - grantDocumentStoreRole(mockDocumentStoreAddress, mockRole, mockAccount, wallet, { + documentStoreGrantRole(mockDocumentStoreAddress, mockRole, mockAccount, wallet, { chainId: mockChainId, isTransferable, }), @@ -256,7 +256,7 @@ describe('Grant Document Store Role', () => { it('should work with different role and account addresses', async () => { const differentRole = '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890'; const differentAccount = '0x9876543210987654321098765432109876543210'; - const result = await grantDocumentStoreRole( + const result = await documentStoreGrantRole( mockDocumentStoreAddress, differentRole, differentAccount, @@ -288,7 +288,7 @@ describe('Grant Document Store Role', () => { it('should auto-detect TT Document Store as fallback when ERC-165 interfaces not supported', async () => { vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); - const result = await grantDocumentStoreRole( + const result = await documentStoreGrantRole( MOCK_TT_DOCUMENT_STORE_ADDRESS, mockRole, mockAccount, @@ -312,7 +312,7 @@ describe('Grant Document Store Role', () => { it('should grant role with TT Document Store (ethers v5)', async () => { vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); - const result = await grantDocumentStoreRole( + const result = await documentStoreGrantRole( MOCK_TT_DOCUMENT_STORE_ADDRESS, mockRole, mockAccount, @@ -330,7 +330,7 @@ describe('Grant Document Store Role', () => { chainId: mockChainId, } as unknown as Network); vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); - const result = await grantDocumentStoreRole( + const result = await documentStoreGrantRole( MOCK_TT_DOCUMENT_STORE_ADDRESS, mockRole, mockAccount, @@ -348,7 +348,7 @@ describe('Grant Document Store Role', () => { mockTTDocumentStoreContract.callStatic.grantRole.mockRejectedValue(mockError); mockTTDocumentStoreContract.grantRole.staticCall.mockRejectedValue(mockError); await expect( - grantDocumentStoreRole(MOCK_TT_DOCUMENT_STORE_ADDRESS, mockRole, mockAccount, wallet, { + documentStoreGrantRole(MOCK_TT_DOCUMENT_STORE_ADDRESS, mockRole, mockAccount, wallet, { chainId: mockChainId, }), ).rejects.toThrow('Pre-check (callStatic) for grant-role failed'); @@ -356,7 +356,7 @@ describe('Grant Document Store Role', () => { it('should grant role TT Document Store with gas options', async () => { vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); - const result = await grantDocumentStoreRole( + const result = await documentStoreGrantRole( MOCK_TT_DOCUMENT_STORE_ADDRESS, mockRole, mockAccount, @@ -379,7 +379,7 @@ describe('Grant Document Store Role', () => { new Error('Role already granted'), ); await expect( - grantDocumentStoreRole(MOCK_TT_DOCUMENT_STORE_ADDRESS, mockRole, mockAccount, wallet, { + documentStoreGrantRole(MOCK_TT_DOCUMENT_STORE_ADDRESS, mockRole, mockAccount, wallet, { chainId: mockChainId, }), ).rejects.toThrow('Pre-check (callStatic) for grant-role failed'); diff --git a/src/__tests__/document-store/revoke-role.test.ts b/src/__tests__/document-store/revoke-role.test.ts index e870598..cc300f5 100644 --- a/src/__tests__/document-store/revoke-role.test.ts +++ b/src/__tests__/document-store/revoke-role.test.ts @@ -4,7 +4,7 @@ import { ethers as ethersV5, Wallet as WalletV5 } from 'ethers'; import { Wallet as WalletV6, Network, ethers as ethersV6 } from 'ethersV6'; import * as coreModule from '../../core'; -import { revokeDocumentStoreRole } from '../../document-store/revoke-role'; +import { documentStoreRevokeRole } from '../../document-store/revoke-role'; import { MOCK_DOCUMENT_STORE_ADDRESS, MOCK_TRANSFERABLE_DOCUMENT_STORE_ADDRESS, @@ -104,7 +104,7 @@ describe('Revoke Document Store Role', () => { }); it('should revoke role successfully', async () => { - const result = await revokeDocumentStoreRole( + const result = await documentStoreRevokeRole( mockDocumentStoreAddress, mockRole, mockAccount, @@ -118,7 +118,7 @@ describe('Revoke Document Store Role', () => { }); it('should revoke role with explicit contract type', async () => { - const result = await revokeDocumentStoreRole( + const result = await documentStoreRevokeRole( mockDocumentStoreAddress, mockRole, mockAccount, @@ -133,7 +133,7 @@ describe('Revoke Document Store Role', () => { }); it('should revoke role without chainId option', async () => { - const result = await revokeDocumentStoreRole( + const result = await documentStoreRevokeRole( mockDocumentStoreAddress, mockRole, mockAccount, @@ -146,7 +146,7 @@ describe('Revoke Document Store Role', () => { }); it('should revoke role with gas options', async () => { - const result = await revokeDocumentStoreRole( + const result = await documentStoreRevokeRole( mockDocumentStoreAddress, mockRole, mockAccount, @@ -163,7 +163,7 @@ describe('Revoke Document Store Role', () => { it('should throw when document store address is missing', async () => { await expect( - revokeDocumentStoreRole('', mockRole, mockAccount, wallet, { chainId: mockChainId }), + documentStoreRevokeRole('', mockRole, mockAccount, wallet, { chainId: mockChainId }), ).rejects.toThrow('Document store address is required'); }); @@ -172,7 +172,7 @@ describe('Revoke Document Store Role', () => { '0x'.padEnd(66, '1'), ); await expect( - revokeDocumentStoreRole( + documentStoreRevokeRole( mockDocumentStoreAddress, mockRole, mockAccount, @@ -186,7 +186,7 @@ describe('Revoke Document Store Role', () => { it('should throw when role is missing', async () => { await expect( - revokeDocumentStoreRole(mockDocumentStoreAddress, '', mockAccount, wallet, { + documentStoreRevokeRole(mockDocumentStoreAddress, '', mockAccount, wallet, { chainId: mockChainId, }), ).rejects.toThrow('Role is required'); @@ -194,7 +194,7 @@ describe('Revoke Document Store Role', () => { it('should throw when account is missing', async () => { await expect( - revokeDocumentStoreRole(mockDocumentStoreAddress, mockRole, '', wallet, { + documentStoreRevokeRole(mockDocumentStoreAddress, mockRole, '', wallet, { chainId: mockChainId, }), ).rejects.toThrow('Account is required'); @@ -205,7 +205,7 @@ describe('Revoke Document Store Role', () => { mockContract.callStatic.revokeRole.mockRejectedValue(mockError); mockContract.revokeRole.staticCall.mockRejectedValue(mockError); await expect( - revokeDocumentStoreRole(mockDocumentStoreAddress, mockRole, mockAccount, wallet, { + documentStoreRevokeRole(mockDocumentStoreAddress, mockRole, mockAccount, wallet, { chainId: mockChainId, isTransferable, }), @@ -218,7 +218,7 @@ describe('Revoke Document Store Role', () => { vi.mocked(getEthersContractFromProvider).mockReturnValue( mockContractConstructor(mockTTDocumentStoreContract), ); - const result = await revokeDocumentStoreRole( + const result = await documentStoreRevokeRole( mockDocumentStoreAddress, mockRole, mockAccount, @@ -239,7 +239,7 @@ describe('Revoke Document Store Role', () => { mockContract.callStatic.revokeRole.mockRejectedValue(new Error('Invalid role format')); mockContract.revokeRole.staticCall.mockRejectedValue(new Error('Invalid role format')); await expect( - revokeDocumentStoreRole(mockDocumentStoreAddress, invalidRole, mockAccount, wallet, { + documentStoreRevokeRole(mockDocumentStoreAddress, invalidRole, mockAccount, wallet, { chainId: mockChainId, isTransferable, }), @@ -250,7 +250,7 @@ describe('Revoke Document Store Role', () => { mockContract.callStatic.revokeRole.mockRejectedValue(new Error('Role not granted')); mockContract.revokeRole.staticCall.mockRejectedValue(new Error('Role not granted')); await expect( - revokeDocumentStoreRole(mockDocumentStoreAddress, mockRole, mockAccount, wallet, { + documentStoreRevokeRole(mockDocumentStoreAddress, mockRole, mockAccount, wallet, { chainId: mockChainId, isTransferable, }), @@ -260,7 +260,7 @@ describe('Revoke Document Store Role', () => { it('should work with different role and account addresses', async () => { const differentRole = '0x1111111111111111111111111111111111111111111111111111111111111111'; const differentAccount = '0x9876543210987654321098765432109876543210'; - const result = await revokeDocumentStoreRole( + const result = await documentStoreRevokeRole( mockDocumentStoreAddress, differentRole, differentAccount, @@ -292,7 +292,7 @@ describe('Revoke Document Store Role', () => { it('should auto-detect TT Document Store as fallback when ERC-165 interfaces not supported', async () => { vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); - const result = await revokeDocumentStoreRole( + const result = await documentStoreRevokeRole( MOCK_TT_DOCUMENT_STORE_ADDRESS, mockRole, mockAccount, @@ -316,7 +316,7 @@ describe('Revoke Document Store Role', () => { it('should revoke role with TT Document Store (ethers v5)', async () => { vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); - const result = await revokeDocumentStoreRole( + const result = await documentStoreRevokeRole( MOCK_TT_DOCUMENT_STORE_ADDRESS, mockRole, mockAccount, @@ -334,7 +334,7 @@ describe('Revoke Document Store Role', () => { chainId: mockChainId, } as unknown as Network); vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); - const result = await revokeDocumentStoreRole( + const result = await documentStoreRevokeRole( MOCK_TT_DOCUMENT_STORE_ADDRESS, mockRole, mockAccount, @@ -352,7 +352,7 @@ describe('Revoke Document Store Role', () => { mockTTDocumentStoreContract.callStatic.revokeRole.mockRejectedValue(mockError); mockTTDocumentStoreContract.revokeRole.staticCall.mockRejectedValue(mockError); await expect( - revokeDocumentStoreRole(MOCK_TT_DOCUMENT_STORE_ADDRESS, mockRole, mockAccount, wallet, { + documentStoreRevokeRole(MOCK_TT_DOCUMENT_STORE_ADDRESS, mockRole, mockAccount, wallet, { chainId: mockChainId, }), ).rejects.toThrow('Pre-check (callStatic) for revoke-role failed'); @@ -360,7 +360,7 @@ describe('Revoke Document Store Role', () => { it('should revoke role TT Document Store with gas options', async () => { vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); - const result = await revokeDocumentStoreRole( + const result = await documentStoreRevokeRole( MOCK_TT_DOCUMENT_STORE_ADDRESS, mockRole, mockAccount, @@ -383,7 +383,7 @@ describe('Revoke Document Store Role', () => { new Error('Role not granted'), ); await expect( - revokeDocumentStoreRole(MOCK_TT_DOCUMENT_STORE_ADDRESS, mockRole, mockAccount, wallet, { + documentStoreRevokeRole(MOCK_TT_DOCUMENT_STORE_ADDRESS, mockRole, mockAccount, wallet, { chainId: mockChainId, }), ).rejects.toThrow('Pre-check (callStatic) for revoke-role failed'); diff --git a/src/document-store/grant-role.ts b/src/document-store/grant-role.ts index 4198bdd..d09a823 100644 --- a/src/document-store/grant-role.ts +++ b/src/document-store/grant-role.ts @@ -36,7 +36,7 @@ import { CommandOptions } from './types'; * @throws {Error} If the role is invalid. * @throws {Error} If the `callStatic.grantRole` fails as a pre-check. */ -export const grantDocumentStoreRole = async ( +export const documentStoreGrantRole = async ( documentStoreAddress: string, role: string, account: string, diff --git a/src/document-store/index.ts b/src/document-store/index.ts index 16c20c9..c8bd451 100644 --- a/src/document-store/index.ts +++ b/src/document-store/index.ts @@ -1,7 +1,8 @@ export { documentStoreIssue } from './issue'; export { documentStoreRevoke } from './revoke'; -export { revokeDocumentStoreRole } from './revoke-role'; -export { grantDocumentStoreRole } from './grant-role'; +export { documentStoreRevokeRole } from './revoke-role'; +export { documentStoreGrantRole } from './grant-role'; +export { documentStoreTransferOwnership } from './transferOwnership'; export { deployDocumentStore } from './deploy'; export { supportInterfaceIds } from './supportInterfaceIds'; export { diff --git a/src/document-store/revoke-role.ts b/src/document-store/revoke-role.ts index 03db6d1..5c43d93 100644 --- a/src/document-store/revoke-role.ts +++ b/src/document-store/revoke-role.ts @@ -1,7 +1,7 @@ import { Signer as SignerV6, Contract as ContractV6, - ContractTransaction as ContractTransactionV6, + ContractTransactionResponse as ContractTransactionV6, } from 'ethersV6'; import { Contract as ContractV5, @@ -36,7 +36,7 @@ import { CommandOptions } from './types'; * @throws {Error} If the role is invalid. * @throws {Error} If the `callStatic.revokeRole` fails as a pre-check. */ -export const revokeDocumentStoreRole = async ( +export const documentStoreRevokeRole = async ( documentStoreAddress: string, role: string, account: string, diff --git a/src/document-store/transferOwnership.ts b/src/document-store/transferOwnership.ts index 3259c0c..e8464fd 100644 --- a/src/document-store/transferOwnership.ts +++ b/src/document-store/transferOwnership.ts @@ -1,8 +1,8 @@ import { Signer as SignerV6, ContractTransaction as ContractTransactionV6 } from 'ethersV6'; import { ContractTransaction as ContractTransactionV5, Signer as SignerV5 } from 'ethers'; -import { revokeDocumentStoreRole } from './revoke-role'; +import { documentStoreRevokeRole } from './revoke-role'; -import { grantDocumentStoreRole } from './grant-role'; +import { documentStoreGrantRole } from './grant-role'; import { getRoleString } from './document-store-roles'; import { CommandOptions } from './types'; @@ -14,15 +14,15 @@ import { CommandOptions } from './types'; * 2. TransferableDocumentStore (ERC-165 compliant) * 3. TT Document Store (legacy, no ERC-165 support - used as fallback) * @param {string} documentStoreAddress - The address of the DocumentStore contract. - * @param {string} account - The account to revoke the role from. - * @param {SignerV5 | SignerV6} signer - Signer instance (Ethers v5 or v6) that authorizes the revoke role transaction. + * @param {string} account - The account to transfer ownership to. + * @param {SignerV5 | SignerV6} signer - Signer instance (Ethers v5 or v6) that authorizes the transfer ownership transaction. * @param {CommandOptions} options - Optional transaction metadata including gas values and chain ID. - * @returns {Promise<{grantTransaction: Promise; revokeTransaction: Promise}>} A promise resolving to the transaction result from the revoke role call. + * @returns {Promise<{grantTransaction: Promise; revokeTransaction: Promise}>} A promise resolving to the transaction result from the grant and revoke role calls. * @throws {Error} If the document store address or signer provider is not provided. * @throws {Error} If the role is invalid. * @throws {Error} If the `callStatic.revokeRole` fails as a pre-check. */ -export const transferOwnershipDocumentStore = async ( +export const documentStoreTransferOwnership = async ( documentStoreAddress: string, account: string, signer: SignerV5 | SignerV6, @@ -36,11 +36,13 @@ export const transferOwnershipDocumentStore = async ( if (!account) throw new Error('Account is required'); const ownerAddress = await signer.getAddress(); - const roleString = await getRoleString(documentStoreAddress, 'admin'); + const roleString = await getRoleString(documentStoreAddress, 'admin', { + provider: signer.provider, + }); //call the transferOwnership function of the document store contract //call grant and revoke function here - const grantTransaction = grantDocumentStoreRole( + const grantTransaction = documentStoreGrantRole( documentStoreAddress, roleString, account, @@ -55,7 +57,7 @@ export const transferOwnershipDocumentStore = async ( //return the grant transaction result } //call revoke function here - const revokeTransaction = revokeDocumentStoreRole( + const revokeTransaction = documentStoreRevokeRole( documentStoreAddress, roleString, ownerAddress, diff --git a/src/document-store/types.ts b/src/document-store/types.ts index e9378c4..c811512 100644 --- a/src/document-store/types.ts +++ b/src/document-store/types.ts @@ -1,4 +1,4 @@ -import { CHAIN_ID } from 'src/utils'; +import { CHAIN_ID } from '../utils'; import { GasValue } from '../token-registry-functions/types'; export interface CommandOptions { diff --git a/src/index.ts b/src/index.ts index f7e7d75..974c908 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,8 +21,8 @@ import { } from './token-registry-v5'; import { deployDocumentStore, - grantDocumentStoreRole, - revokeDocumentStoreRole, + documentStoreGrantRole, + documentStoreRevokeRole, documentStoreIssue, documentStoreRevoke, DocumentStore__factory, @@ -57,8 +57,8 @@ export { v5GetEventFromReceipt, v5ComputeInterfaceId, deployDocumentStore, - grantDocumentStoreRole, - revokeDocumentStoreRole, + documentStoreGrantRole, + documentStoreRevokeRole, documentStoreIssue, documentStoreRevoke, DocumentStore__factory, From f7a411199e527d46f04f5db41a95cc68aff7d718 Mon Sep 17 00:00:00 2001 From: Rishabh Singh Date: Fri, 13 Feb 2026 10:41:59 +0530 Subject: [PATCH 5/5] fix: tests --- src/__tests__/document-store/grant-role.test.ts | 2 +- src/__tests__/document-store/revoke-role.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/__tests__/document-store/grant-role.test.ts b/src/__tests__/document-store/grant-role.test.ts index 542b059..db02acb 100644 --- a/src/__tests__/document-store/grant-role.test.ts +++ b/src/__tests__/document-store/grant-role.test.ts @@ -56,7 +56,7 @@ describe('Grant Document Store Role', () => { describe.each(providers)( 'Grant role with $contractType and ethers version $ethersVersion', - async ({ Provider, ethersVersion, contractType }) => { + ({ Provider, ethersVersion, contractType }) => { const isTransferable = contractType === 'TransferableDocumentStore'; const mockContract = isTransferable ? mockTransferableDocumentStoreContract diff --git a/src/__tests__/document-store/revoke-role.test.ts b/src/__tests__/document-store/revoke-role.test.ts index cc300f5..dc4addb 100644 --- a/src/__tests__/document-store/revoke-role.test.ts +++ b/src/__tests__/document-store/revoke-role.test.ts @@ -56,7 +56,7 @@ describe('Revoke Document Store Role', () => { describe.each(providers)( 'Revoke role with $contractType and ethers version $ethersVersion', - async ({ Provider, ethersVersion, contractType }) => { + ({ Provider, ethersVersion, contractType }) => { const isTransferable = contractType === 'TransferableDocumentStore'; const mockContract = isTransferable ? mockTransferableDocumentStoreContract