From cda27b37c70bfd2111367b9e3384ebedd573e176 Mon Sep 17 00:00:00 2001 From: Rishabh Singh Date: Wed, 11 Feb 2026 09:47:11 +0530 Subject: [PATCH 01/11] feat: deploy issue revoke functions --- package-lock.json | 7 + package.json | 1 + src/__tests__/document-store/deploy.test.ts | 117 +++++ src/__tests__/document-store/fixtures.ts | 266 +++++++++++ src/__tests__/document-store/issue.test.ts | 496 ++++++++++++++++++++ src/__tests__/document-store/revoke.test.ts | 353 ++++++++++++++ src/document-store/contract-interfaces.ts | 39 ++ src/document-store/deploy.ts | 99 ++++ src/document-store/document-store-roles.ts | 44 ++ src/document-store/index.ts | 10 + src/document-store/issue.ts | 133 ++++++ src/document-store/revoke.ts | 133 ++++++ src/document-store/supportInterfaceIds.ts | 24 + src/document-store/tt-document-store-abi.ts | 233 +++++++++ src/index.ts | 17 + 15 files changed, 1972 insertions(+) create mode 100644 src/__tests__/document-store/deploy.test.ts create mode 100644 src/__tests__/document-store/fixtures.ts create mode 100644 src/__tests__/document-store/issue.test.ts create mode 100644 src/__tests__/document-store/revoke.test.ts create mode 100644 src/document-store/contract-interfaces.ts create mode 100644 src/document-store/deploy.ts create mode 100644 src/document-store/document-store-roles.ts create mode 100644 src/document-store/index.ts create mode 100644 src/document-store/issue.ts create mode 100644 src/document-store/revoke.ts create mode 100644 src/document-store/supportInterfaceIds.ts create mode 100644 src/document-store/tt-document-store-abi.ts diff --git a/package-lock.json b/package-lock.json index f5b78b3..11bf4df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@tradetrust-tt/token-registry-v5": "npm:@tradetrust-tt/token-registry@^5.5.0", "@tradetrust-tt/tradetrust": "^6.10.2", "@tradetrust-tt/tt-verify": "^9.6.0", + "@trustvc/document-store": "^1.0.2", "@trustvc/w3c": "^2.0.0", "@trustvc/w3c-context": "^2.0.0", "@trustvc/w3c-credential-status": "^2.0.0", @@ -6725,6 +6726,12 @@ "ethers": "^5.7.2" } }, + "node_modules/@trustvc/document-store": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@trustvc/document-store/-/document-store-1.0.2.tgz", + "integrity": "sha512-KxGds1jfFhgGXMoFgo+sPv/QOGHTYLmm8Q+ZHQTlN5sTuMgQc7yf8JvIttjamaRSNtbIl5B9LTB86w+dIRFRBA==", + "license": "Apache-2.0" + }, "node_modules/@trustvc/w3c": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@trustvc/w3c/-/w3c-2.0.0.tgz", diff --git a/package.json b/package.json index 68b2397..a8c75ff 100644 --- a/package.json +++ b/package.json @@ -121,6 +121,7 @@ "@tradetrust-tt/token-registry-v5": "npm:@tradetrust-tt/token-registry@^5.5.0", "@tradetrust-tt/tradetrust": "^6.10.2", "@tradetrust-tt/tt-verify": "^9.6.0", + "@trustvc/document-store": "^1.0.2", "@trustvc/w3c": "^2.0.0", "@trustvc/w3c-context": "^2.0.0", "@trustvc/w3c-credential-status": "^2.0.0", diff --git a/src/__tests__/document-store/deploy.test.ts b/src/__tests__/document-store/deploy.test.ts new file mode 100644 index 0000000..c7d0935 --- /dev/null +++ b/src/__tests__/document-store/deploy.test.ts @@ -0,0 +1,117 @@ +import './fixtures'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { Wallet as WalletV5 } from 'ethers'; +import { Wallet as WalletV6, Network } from 'ethersV6'; +import { deployDocumentStore } from '../../document-store/deploy'; +import { PRIVATE_KEY, providerV5, providerV6 } from './fixtures'; +import { CHAIN_ID } from '../../utils'; + +describe('Deploy Document Store', () => { + const mockStoreName = 'Test Document Store'; + const mockOwnerAddress = '0x1234567890123456789012345678901234567890'; + const mockChainId = CHAIN_ID.sepolia; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('DocumentStore deployment', () => { + it('should deploy DocumentStore successfully with ethers v6', async () => { + const providerV6: any = { + getNetwork: vi.fn().mockResolvedValue({ chainId: mockChainId }), + provider: {}, + }; + + const wallet: any = { + provider: providerV6, // must exist + }; + const result = await deployDocumentStore(mockStoreName, mockOwnerAddress, wallet, { + chainId: mockChainId, + isTransferable: false, + }); + console.log('result', result); + + expect(result).toEqual({ + address: '0xDeployedDocumentStoreAddress', + transactionHash: 'deploy_tx_hash', + }); + }); + + it('should deploy TransferableDocumentStore successfully with ethers v6', async () => { + const providerV6: any = { + getNetwork: vi.fn().mockResolvedValue({ chainId: mockChainId }), + provider: {}, + }; + + const wallet: any = { + provider: providerV6, // must exist + }; + vi.spyOn(providerV6, 'getNetwork').mockResolvedValue({ + chainId: mockChainId, + } as unknown as Network); + + const result = await deployDocumentStore(mockStoreName, mockOwnerAddress, wallet, { + chainId: mockChainId, + isTransferable: true, + }); + + expect(result).toEqual({ + address: '0xDeployedDocumentStoreAddress', + transactionHash: 'deploy_tx_hash', + }); + }); + + it('should deploy DocumentStore successfully with ethers v5', async () => { + const wallet = new WalletV5(PRIVATE_KEY, providerV5); + vi.spyOn(wallet, 'getChainId').mockResolvedValue(mockChainId as unknown as number); + + const result = await deployDocumentStore(mockStoreName, mockOwnerAddress, wallet, { + chainId: mockChainId, + isTransferable: false, + }); + + expect(result).toEqual({ + address: '0xDeployedDocumentStoreAddress', + transactionHash: 'deploy_tx_hash', + }); + }); + + it('should deploy TransferableDocumentStore successfully with ethers v5', async () => { + const wallet = new WalletV5(PRIVATE_KEY, providerV5); + vi.spyOn(wallet, 'getChainId').mockResolvedValue(mockChainId as unknown as number); + + const result = await deployDocumentStore(mockStoreName, mockOwnerAddress, wallet, { + chainId: mockChainId, + isTransferable: true, + }); + + expect(result).toEqual({ + address: '0xDeployedDocumentStoreAddress', + transactionHash: 'deploy_tx_hash', + }); + }); + + it('should throw when store name is missing', async () => { + const wallet = new WalletV6(PRIVATE_KEY, providerV6); + await expect( + deployDocumentStore('', mockOwnerAddress, wallet, { chainId: mockChainId }), + ).rejects.toThrow('Store name is required'); + }); + + it('should throw when owner address is missing', async () => { + const wallet = new WalletV6(PRIVATE_KEY, providerV6); + await expect( + deployDocumentStore(mockStoreName, '', wallet, { chainId: mockChainId }), + ).rejects.toThrow('Owner address is required'); + }); + + it('should throw when provider is missing', async () => { + const signerWithoutProvider = new WalletV5('0x'.padEnd(66, '1')); + await expect( + deployDocumentStore(mockStoreName, mockOwnerAddress, signerWithoutProvider, { + chainId: mockChainId, + }), + ).rejects.toThrow('Provider is required'); + }); + }); +}); diff --git a/src/__tests__/document-store/fixtures.ts b/src/__tests__/document-store/fixtures.ts new file mode 100644 index 0000000..c81f42e --- /dev/null +++ b/src/__tests__/document-store/fixtures.ts @@ -0,0 +1,266 @@ +import { vi } from 'vitest'; +import { ethers as ethersV5 } from 'ethers'; +import { JsonRpcProvider as JsonRpcProviderV6, ethers as ethersV6 } from 'ethersV6'; +import * as originalModule from '../../utils/ethers'; + +export const MOCK_DOCUMENT_STORE_ADDRESS = '0xDocumentStoreContract'; +export const MOCK_TRANSFERABLE_DOCUMENT_STORE_ADDRESS = '0xTransferableDocumentStoreContract'; +export const MOCK_TT_DOCUMENT_STORE_ADDRESS = '0xTTDocumentStoreContract'; + +vi.mock('ethersV6', async () => { + const actual = await vi.importActual('ethersV6'); + + return { + ...actual, + ContractFactory: vi.fn().mockImplementation(() => ({ + deploy: vi.fn().mockResolvedValue({ + waitForDeployment: vi.fn().mockResolvedValue(undefined), + deploymentTransaction: vi.fn(() => ({ + wait: vi.fn().mockResolvedValue({ + address: '0xDeployedDocumentStoreAddress', + transactionHash: 'deploy_tx_hash', + }), + hash: 'deploy_tx_hash', + })), + }), + })), + }; +}); + +vi.mock('ethers', async () => { + const actual = await vi.importActual('ethers'); + return { + ...actual, + + ContractFactory: vi.fn().mockImplementation(() => ({ + deploy: vi.fn().mockResolvedValue({ + waitForDeployment: vi.fn().mockResolvedValue(undefined), + target: '0xDeployedDocumentStoreAddress', + deployTransaction: { + wait: vi.fn().mockResolvedValue({ + address: '0xDeployedDocumentStoreAddress', + transactionHash: 'deploy_tx_hash', + }), + hash: 'deploy_tx_hash', + }, + }), + })), + }; +}); + +vi.mock('../../utils/ethers', async (importOriginal) => { + const original = (await importOriginal()) as typeof originalModule; + + return { + ...original, // Keep all original exports + getEthersContractFromProvider: vi.fn(() => vi.fn()), // Only mock this function + }; +}); + +vi.mock('../../core', () => ({ + checkSupportsInterface: vi.fn(), +})); + +vi.mock('@trustvc/document-store', () => ({ + DocumentStore__factory: class { + static abi = ['constructor(string storeName, address owner)']; + static bytecode = '0x60006000'; + constructor() { + return { + deploy: vi.fn(() => ({ + waitForDeployment: vi.fn().mockResolvedValue(undefined), + target: '0xDeployedDocumentStoreAddress', + deploymentTransaction: vi.fn(() => ({ hash: 'deploy_tx_hash' })), + deployTransaction: { + wait: vi.fn().mockResolvedValue({}), + hash: 'deploy_tx_hash', + }, + address: '0xDeployedDocumentStoreAddress', + })), + }; + } + }, + TransferableDocumentStore__factory: class { + static abi = 'TransferableDocumentStoreABI'; + static bytecode = 'TransferableDocumentStoreBytecode'; + constructor() { + return { + deploy: vi.fn(() => ({ + waitForDeployment: vi.fn().mockResolvedValue(undefined), + target: '0xDeployedTransferableDocumentStoreAddress', + deploymentTransaction: vi.fn(() => ({ hash: 'transferable_deploy_tx_hash' })), + deployTransaction: { + wait: vi.fn().mockResolvedValue({}), + hash: 'transferable_deploy_tx_hash', + }, + address: '0xDeployedTransferableDocumentStoreAddress', + })), + }; + } + }, + IDocumentStore__factory: { + createInterface: vi.fn(() => ({ + fragments: [ + { type: 'function', format: () => 'isActive(bytes32)' }, + { type: 'function', format: () => 'isIssued(bytes32)' }, + { type: 'function', format: () => 'isRevoked(bytes32)' }, + { type: 'function', format: () => 'name()' }, + { type: 'function', format: () => 'revoke(bytes32)' }, + ], + })), + }, + ITransferableDocumentStore__factory: { + createInterface: vi.fn(() => ({ + fragments: [ + { type: 'function', format: () => 'isActive(bytes32)' }, + { type: 'function', format: () => 'isIssued(bytes32)' }, + { type: 'function', format: () => 'isRevoked(bytes32)' }, + { type: 'function', format: () => 'issue(bytes32)' }, + { type: 'function', format: () => 'name()' }, + { type: 'function', format: () => 'revoke(bytes32)' }, + ], + })), + }, +})); + +export const mockDocumentStoreContract = { + callStatic: { + issue: vi.fn(), + revoke: vi.fn(), + grantRole: vi.fn(), + revokeRole: vi.fn(), + }, + issue: Object.assign( + // Direct call returns transaction response + vi.fn(() => Promise.resolve('document_store_issue_tx_hash')), + { + // Static call returns boolean + staticCall: vi.fn(() => Promise.resolve(true)), + }, + ), + revoke: Object.assign( + // Direct call returns transaction response + vi.fn(() => Promise.resolve('document_store_revoke_tx_hash')), + { + // Static call returns boolean + staticCall: vi.fn(() => Promise.resolve(true)), + }, + ), + grantRole: Object.assign( + // Direct call returns transaction response + vi.fn(() => Promise.resolve('document_store_grant_role_tx_hash')), + { + // Static call returns boolean + staticCall: vi.fn(() => Promise.resolve(true)), + }, + ), + revokeRole: Object.assign( + // Direct call returns transaction response + vi.fn(() => Promise.resolve('document_store_revoke_role_tx_hash')), + { + // Static call returns boolean + staticCall: vi.fn(() => Promise.resolve(true)), + }, + ), + isIssued: vi.fn(() => Promise.resolve(true)), + isRevoked: vi.fn(() => Promise.resolve(false)), + isActive: vi.fn(() => Promise.resolve(true)), + name: vi.fn(() => Promise.resolve('Test Document Store')), +}; + +export const mockTransferableDocumentStoreContract = { + callStatic: { + issue: vi.fn(), + revoke: vi.fn(), + grantRole: vi.fn(), + revokeRole: vi.fn(), + }, + issue: Object.assign( + // Direct call returns transaction response + vi.fn(() => Promise.resolve('transferable_document_store_issue_tx_hash')), + { + // Static call returns boolean + staticCall: vi.fn(() => Promise.resolve(true)), + }, + ), + revoke: Object.assign( + // Direct call returns transaction response + vi.fn(() => Promise.resolve('transferable_document_store_revoke_tx_hash')), + { + // Static call returns boolean + staticCall: vi.fn(() => Promise.resolve(true)), + }, + ), + grantRole: Object.assign( + // Direct call returns transaction response + vi.fn(() => Promise.resolve('transferable_document_store_grant_role_tx_hash')), + { + // Static call returns boolean + staticCall: vi.fn(() => Promise.resolve(true)), + }, + ), + revokeRole: Object.assign( + // Direct call returns transaction response + vi.fn(() => Promise.resolve('transferable_document_store_revoke_role_tx_hash')), + { + // Static call returns boolean + staticCall: vi.fn(() => Promise.resolve(true)), + }, + ), + isIssued: vi.fn(() => Promise.resolve(true)), + isRevoked: vi.fn(() => Promise.resolve(false)), + isActive: vi.fn(() => Promise.resolve(true)), + name: vi.fn(() => Promise.resolve('Test Transferable Document Store')), +}; + +export const mockTTDocumentStoreContract = { + callStatic: { + issue: vi.fn(), + revoke: vi.fn(), + grantRole: vi.fn(), + revokeRole: vi.fn(), + }, + issue: Object.assign( + // Direct call returns transaction response + vi.fn(() => Promise.resolve('tt_document_store_issue_tx_hash')), + { + // Static call returns boolean + staticCall: vi.fn(() => Promise.resolve(true)), + }, + ), + revoke: Object.assign( + // Direct call returns transaction response + vi.fn(() => Promise.resolve('tt_document_store_revoke_tx_hash')), + { + // Static call returns boolean + staticCall: vi.fn(() => Promise.resolve(true)), + }, + ), + grantRole: Object.assign( + // Direct call returns transaction response + vi.fn(() => Promise.resolve('tt_document_store_grant_role_tx_hash')), + { + // Static call returns boolean + staticCall: vi.fn(() => Promise.resolve(true)), + }, + ), + revokeRole: Object.assign( + // Direct call returns transaction response + vi.fn(() => Promise.resolve('tt_document_store_revoke_role_tx_hash')), + { + // Static call returns boolean + staticCall: vi.fn(() => Promise.resolve(true)), + }, + ), + isIssued: vi.fn(() => Promise.resolve(true)), + isRevoked: vi.fn(() => Promise.resolve(false)), + isActive: vi.fn(() => Promise.resolve(true)), + name: vi.fn(() => Promise.resolve('Test TT Document Store')), +}; + +// Note: Dummy test wallets — private keys for local development and CI/CD only. +// These wallets are not for production and hold no funds or value on any network. +export const PRIVATE_KEY = '0x59c6995e998f97a5a004497e5f1ebce0c16828d44b3f8d0bfa3a89d271d5b6b9'; + +export const providerV5 = new ethersV5.providers.JsonRpcProvider(); +export const providerV6 = new JsonRpcProviderV6(); diff --git a/src/__tests__/document-store/issue.test.ts b/src/__tests__/document-store/issue.test.ts new file mode 100644 index 0000000..e0bfb6a --- /dev/null +++ b/src/__tests__/document-store/issue.test.ts @@ -0,0 +1,496 @@ +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 { documentStoreIssue } from '../../document-store/issue'; +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('Issue Document', () => { + const mockDocumentHash = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'; + const mockChainId = CHAIN_ID.local; + + describe.each(providers)( + 'Issue Document 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_issue_tx_hash' + : 'document_store_issue_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(() => { + // Clear any existing mocks first + vi.clearAllMocks(); + const mockContractConstructor = (mockContract: any) => vi.fn(() => mockContract); + // Only set up the mock if it hasn't been set up yet + 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.issue.mockResolvedValue(true); + mockContract.issue.staticCall.mockResolvedValue(true); + }); + + it('should issue document hash successfully', async () => { + const result = await documentStoreIssue( + mockDocumentStoreAddress, + + mockDocumentHash, + wallet, + { + chainId: mockChainId, + }, + ); + + expect(result).toEqual(mockTxResponse); + expect(coreModule.checkSupportsInterface).toHaveBeenCalled(); + }); + + it('should issue document with explicit contract type', async () => { + const result = await documentStoreIssue( + mockDocumentStoreAddress, + + mockDocumentHash, + wallet, + { + chainId: mockChainId, + isTransferable, + }, + ); + + expect(result).toEqual(mockTxResponse); + // Should not check interface when explicitly provided + expect(coreModule.checkSupportsInterface).not.toHaveBeenCalled(); + }); + + it('should issue document without chainId option', async () => { + const result = await documentStoreIssue( + mockDocumentStoreAddress, + + mockDocumentHash, + wallet, + { + isTransferable, + }, + ); + + expect(result).toEqual(mockTxResponse); + }); + + it('should issue document with gas options', async () => { + const result = await documentStoreIssue( + mockDocumentStoreAddress, + + mockDocumentHash, + wallet, + { + chainId: mockChainId, + maxFeePerGas: '1000000000', + maxPriorityFeePerGas: '1000000000', + isTransferable, + }, + ); + + expect(result).toEqual(mockTxResponse); + }); + + it('should throw when document store address is missing', async () => { + await expect( + documentStoreIssue('', mockDocumentHash, 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( + documentStoreIssue(mockDocumentStoreAddress, mockDocumentHash, signerWithoutProvider, { + chainId: mockChainId, + }), + ).rejects.toThrow('Provider is required'); + }); + + it('should throw when document hash is missing', async () => { + await expect( + documentStoreIssue(mockDocumentStoreAddress, '', wallet, { chainId: mockChainId }), + ).rejects.toThrow('Document hash is required'); + }); + + it('should throw when callStatic fails', async () => { + const mockError = new Error('callStatic error'); + mockContract.callStatic.issue.mockRejectedValue(mockError); + mockContract.issue.staticCall.mockRejectedValue(mockError); + + await expect( + documentStoreIssue(mockDocumentStoreAddress, mockDocumentHash, 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 () => { + // When checkSupportsInterface returns false, it should fallback to TT Document Store + vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); + + // Mock the contract to use TT Document Store + const mockContractConstructor = (mockContract: any) => vi.fn(() => mockContract); + vi.mocked(getEthersContractFromProvider).mockReturnValue( + mockContractConstructor(mockContract), + ); + + const result = await documentStoreIssue( + mockDocumentStoreAddress, + + mockDocumentHash, + wallet, + { + chainId: mockChainId, + }, + ); + + // Should successfully issue using TT Document Store as fallback + expect(result).toBeDefined(); + expect(coreModule.checkSupportsInterface).toHaveBeenCalledTimes(2); + }); + + it('should handle invalid document hash format gracefully', async () => { + const invalidHash = '0xinvalid'; + mockContract.callStatic.issue.mockRejectedValue(new Error('Invalid hash format')); + mockContract.issue.staticCall.mockRejectedValue(new Error('Invalid hash format')); + + await expect( + documentStoreIssue(mockDocumentStoreAddress, invalidHash, wallet, { + chainId: mockChainId, + isTransferable, + }), + ).rejects.toThrow('Pre-check (callStatic) for issue failed'); + }); + + it('should handle already issued document', async () => { + mockContract.callStatic.issue.mockRejectedValue(new Error('Document already issued')); + mockContract.issue.staticCall.mockRejectedValue(new Error('Document already issued')); + + await expect( + documentStoreIssue(mockDocumentStoreAddress, mockDocumentHash, wallet, { + chainId: mockChainId, + isTransferable, + }), + ).rejects.toThrow('Pre-check (callStatic) for issue failed'); + }); + + it('should work with different document hash formats', async () => { + const differentHash = '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890'; + const result = await documentStoreIssue(mockDocumentStoreAddress, differentHash, wallet, { + chainId: mockChainId, + isTransferable, + }); + + expect(result).toEqual(mockTxResponse); + }); + }, + ); + + describe('Contract Type Detection', () => { + 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(mockDocumentStoreContract), + ); + + mockDocumentStoreContract.callStatic.issue.mockResolvedValue(true); + mockDocumentStoreContract.issue.staticCall.mockResolvedValue(true); + }); + + it('should auto-detect DocumentStore interface', async () => { + vi.spyOn(coreModule, 'checkSupportsInterface').mockImplementation( + async (address, interfaceId) => { + return interfaceId === supportInterfaceIds.IDocumentStore; + }, + ); + + const result = await documentStoreIssue( + MOCK_DOCUMENT_STORE_ADDRESS, + + mockDocumentHash, + wallet, + { + chainId: mockChainId, + }, + ); + + expect(result).toEqual('document_store_issue_tx_hash'); + expect(coreModule.checkSupportsInterface).toHaveBeenCalledWith( + MOCK_DOCUMENT_STORE_ADDRESS, + supportInterfaceIds.IDocumentStore, + wallet.provider, + ); + }); + + it('should auto-detect TransferableDocumentStore interface', async () => { + const mockContractConstructor = (mockContract: any) => vi.fn(() => mockContract); + vi.mocked(getEthersContractFromProvider).mockReturnValue( + mockContractConstructor(mockTransferableDocumentStoreContract), + ); + + mockTransferableDocumentStoreContract.callStatic.issue.mockResolvedValue(true); + mockTransferableDocumentStoreContract.issue.staticCall.mockResolvedValue(true); + + vi.spyOn(coreModule, 'checkSupportsInterface').mockImplementation( + async (address, interfaceId) => { + return interfaceId === supportInterfaceIds.ITransferableDocumentStore; + }, + ); + + const result = await documentStoreIssue( + MOCK_TRANSFERABLE_DOCUMENT_STORE_ADDRESS, + + mockDocumentHash, + wallet, + { + chainId: mockChainId, + }, + ); + + expect(result).toEqual('transferable_document_store_issue_tx_hash'); + expect(coreModule.checkSupportsInterface).toHaveBeenCalledWith( + MOCK_TRANSFERABLE_DOCUMENT_STORE_ADDRESS, + supportInterfaceIds.ITransferableDocumentStore, + wallet.provider, + ); + }); + + it('should prioritize TransferableDocumentStore when both interfaces are supported', async () => { + const mockContractConstructor = (mockContract: any) => vi.fn(() => mockContract); + vi.mocked(getEthersContractFromProvider).mockReturnValue( + mockContractConstructor(mockTransferableDocumentStoreContract), + ); + + mockTransferableDocumentStoreContract.callStatic.issue.mockResolvedValue(true); + mockTransferableDocumentStoreContract.issue.staticCall.mockResolvedValue(true); + + vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(true); + + const result = await documentStoreIssue( + MOCK_TRANSFERABLE_DOCUMENT_STORE_ADDRESS, + + mockDocumentHash, + wallet, + { + chainId: mockChainId, + }, + ); + + expect(result).toEqual('transferable_document_store_issue_tx_hash'); + }); + }); + + 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.issue.mockResolvedValue(true); + mockTTDocumentStoreContract.issue.staticCall.mockResolvedValue(true); + }); + + it('should auto-detect TT Document Store as fallback when ERC-165 interfaces not supported', async () => { + // Mock checkSupportsInterface to return false for both DocumentStore and TransferableDocumentStore + vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); + + const result = await documentStoreIssue( + MOCK_TT_DOCUMENT_STORE_ADDRESS, + + mockDocumentHash, + wallet, + { + chainId: mockChainId, + }, + ); + + expect(result).toEqual('tt_document_store_issue_tx_hash'); + // Should check both interfaces before falling back + 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 issue document with TT Document Store (ethers v5)', async () => { + vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); + + const result = await documentStoreIssue( + MOCK_TT_DOCUMENT_STORE_ADDRESS, + + mockDocumentHash, + wallet, + { + chainId: mockChainId, + }, + ); + + expect(result).toEqual('tt_document_store_issue_tx_hash'); + }); + + it('should issue document 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 documentStoreIssue( + MOCK_TT_DOCUMENT_STORE_ADDRESS, + + mockDocumentHash, + walletV6, + { + chainId: mockChainId, + }, + ); + + expect(result).toEqual('tt_document_store_issue_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.issue.mockRejectedValue(mockError); + mockTTDocumentStoreContract.issue.staticCall.mockRejectedValue(mockError); + + await expect( + documentStoreIssue(MOCK_TT_DOCUMENT_STORE_ADDRESS, mockDocumentHash, wallet, { + chainId: mockChainId, + }), + ).rejects.toThrow('Pre-check (callStatic) for issue failed'); + }); + + it('should issue TT Document Store with gas options', async () => { + vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); + + const result = await documentStoreIssue( + MOCK_TT_DOCUMENT_STORE_ADDRESS, + mockDocumentHash, + wallet, + { + chainId: mockChainId, + maxFeePerGas: '2000000000', + maxPriorityFeePerGas: '1500000000', + }, + ); + + expect(result).toEqual('tt_document_store_issue_tx_hash'); + }); + + it('should handle already issued document in TT Document Store', async () => { + vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); + + mockTTDocumentStoreContract.callStatic.issue.mockRejectedValue( + new Error('Document already issued'), + ); + mockTTDocumentStoreContract.issue.staticCall.mockRejectedValue( + new Error('Document already issued'), + ); + + await expect( + documentStoreIssue(MOCK_TT_DOCUMENT_STORE_ADDRESS, mockDocumentHash, wallet, { + chainId: mockChainId, + }), + ).rejects.toThrow('Pre-check (callStatic) for issue failed'); + }); + }); +}); diff --git a/src/__tests__/document-store/revoke.test.ts b/src/__tests__/document-store/revoke.test.ts new file mode 100644 index 0000000..6ece9f6 --- /dev/null +++ b/src/__tests__/document-store/revoke.test.ts @@ -0,0 +1,353 @@ +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 { documentStoreRevoke } from '../../document-store/revoke'; +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', () => { + const mockDocumentHash = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd'; + const mockChainId = CHAIN_ID.local; + + describe.each(providers)( + 'Revoke Document 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_tx_hash' + : 'document_store_revoke_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.revoke.mockResolvedValue(true); + mockContract.revoke.staticCall.mockResolvedValue(true); + }); + + it('should revoke document hash successfully', async () => { + const result = await documentStoreRevoke( + mockDocumentStoreAddress, + wallet, + mockDocumentHash, + { + chainId: mockChainId, + }, + ); + expect(result).toEqual(mockTxResponse); + expect(coreModule.checkSupportsInterface).toHaveBeenCalled(); + }); + + it('should revoke document with explicit contract type', async () => { + const result = await documentStoreRevoke( + mockDocumentStoreAddress, + wallet, + mockDocumentHash, + { + chainId: mockChainId, + isTransferable, + }, + ); + expect(result).toEqual(mockTxResponse); + expect(coreModule.checkSupportsInterface).not.toHaveBeenCalled(); + }); + + it('should revoke document without chainId option', async () => { + const result = await documentStoreRevoke( + mockDocumentStoreAddress, + wallet, + mockDocumentHash, + { + isTransferable, + }, + ); + expect(result).toEqual(mockTxResponse); + }); + + it('should revoke document with gas options', async () => { + const result = await documentStoreRevoke( + mockDocumentStoreAddress, + wallet, + mockDocumentHash, + { + chainId: mockChainId, + maxFeePerGas: '1000000000', + maxPriorityFeePerGas: '1000000000', + isTransferable, + }, + ); + expect(result).toEqual(mockTxResponse); + }); + + it('should throw when document store address is missing', async () => { + await expect( + documentStoreRevoke('', wallet, mockDocumentHash, { 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( + documentStoreRevoke(mockDocumentStoreAddress, signerWithoutProvider, mockDocumentHash, { + chainId: mockChainId, + }), + ).rejects.toThrow('Provider is required'); + }); + + it('should throw when document hash is missing', async () => { + await expect( + documentStoreRevoke(mockDocumentStoreAddress, wallet, '', { chainId: mockChainId }), + ).rejects.toThrow('Document hash is required'); + }); + + it('should throw when callStatic fails', async () => { + const mockError = new Error('callStatic error'); + mockContract.callStatic.revoke.mockRejectedValue(mockError); + mockContract.revoke.staticCall.mockRejectedValue(mockError); + await expect( + documentStoreRevoke(mockDocumentStoreAddress, wallet, mockDocumentHash, { + chainId: mockChainId, + isTransferable, + }), + ).rejects.toThrow('Pre-check (callStatic) for revoke 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 documentStoreRevoke( + mockDocumentStoreAddress, + wallet, + mockDocumentHash, + { + chainId: mockChainId, + }, + ); + expect(result).toBeDefined(); + expect(coreModule.checkSupportsInterface).toHaveBeenCalledTimes(2); + }); + + it('should handle invalid document hash format gracefully', async () => { + const invalidHash = '0xinvalid'; + mockContract.callStatic.revoke.mockRejectedValue(new Error('Invalid hash format')); + mockContract.revoke.staticCall.mockRejectedValue(new Error('Invalid hash format')); + await expect( + documentStoreRevoke(mockDocumentStoreAddress, wallet, invalidHash, { + chainId: mockChainId, + isTransferable, + }), + ).rejects.toThrow('Pre-check (callStatic) for revoke failed'); + }); + + it('should handle already revoked document', async () => { + mockContract.callStatic.revoke.mockRejectedValue(new Error('Document already revoked')); + mockContract.revoke.staticCall.mockRejectedValue(new Error('Document already revoked')); + await expect( + documentStoreRevoke(mockDocumentStoreAddress, wallet, mockDocumentHash, { + chainId: mockChainId, + isTransferable, + }), + ).rejects.toThrow('Pre-check (callStatic) for revoke failed'); + }); + + it('should work with different document hash formats', async () => { + const differentHash = '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890'; + const result = await documentStoreRevoke(mockDocumentStoreAddress, wallet, differentHash, { + 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.revoke.mockResolvedValue(true); + mockTTDocumentStoreContract.revoke.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 documentStoreRevoke( + MOCK_TT_DOCUMENT_STORE_ADDRESS, + wallet, + mockDocumentHash, + { + chainId: mockChainId, + }, + ); + expect(result).toEqual('tt_document_store_revoke_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 document with TT Document Store (ethers v5)', async () => { + vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); + const result = await documentStoreRevoke( + MOCK_TT_DOCUMENT_STORE_ADDRESS, + wallet, + mockDocumentHash, + { + chainId: mockChainId, + }, + ); + expect(result).toEqual('tt_document_store_revoke_tx_hash'); + }); + + it('should revoke document 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 documentStoreRevoke( + MOCK_TT_DOCUMENT_STORE_ADDRESS, + walletV6, + mockDocumentHash, + { + chainId: mockChainId, + }, + ); + expect(result).toEqual('tt_document_store_revoke_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.revoke.mockRejectedValue(mockError); + mockTTDocumentStoreContract.revoke.staticCall.mockRejectedValue(mockError); + await expect( + documentStoreRevoke(MOCK_TT_DOCUMENT_STORE_ADDRESS, wallet, mockDocumentHash, { + chainId: mockChainId, + }), + ).rejects.toThrow('Pre-check (callStatic) for revoke failed'); + }); + + it('should revoke TT Document Store with gas options', async () => { + vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); + const result = await documentStoreRevoke( + MOCK_TT_DOCUMENT_STORE_ADDRESS, + wallet, + mockDocumentHash, + { + chainId: mockChainId, + maxFeePerGas: '2000000000', + maxPriorityFeePerGas: '1500000000', + }, + ); + expect(result).toEqual('tt_document_store_revoke_tx_hash'); + }); + + it('should handle already revoked document in TT Document Store', async () => { + vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); + mockTTDocumentStoreContract.callStatic.revoke.mockRejectedValue( + new Error('Document already revoked'), + ); + mockTTDocumentStoreContract.revoke.staticCall.mockRejectedValue( + new Error('Document already revoked'), + ); + await expect( + documentStoreRevoke(MOCK_TT_DOCUMENT_STORE_ADDRESS, wallet, mockDocumentHash, { + chainId: mockChainId, + }), + ).rejects.toThrow('Pre-check (callStatic) for revoke failed'); + }); + }); +}); diff --git a/src/document-store/contract-interfaces.ts b/src/document-store/contract-interfaces.ts new file mode 100644 index 0000000..69b7a1b --- /dev/null +++ b/src/document-store/contract-interfaces.ts @@ -0,0 +1,39 @@ +import { utils as ethersUtils } from 'ethers'; +import { + IDocumentStore__factory, + ITransferableDocumentStore__factory, +} from '@trustvc/document-store'; + +/** + * Contract interface function signatures for DocumentStore contracts + * These are used for ERC-165 interface detection and other purposes + */ + +const IDocumentStoreInterface = IDocumentStore__factory.createInterface() as ethersUtils.Interface; +const ITransferableDocumentStoreInterface = + ITransferableDocumentStore__factory.createInterface() as ethersUtils.Interface; + +// Extract function signatures from interfaces +const IDocumentStoreFunctions = Object.values(IDocumentStoreInterface.fragments) + .filter((f) => f.type === 'function') + .map((f) => f.format('sighash')); + +const ITransferableDocumentStoreFunctions = Object.values( + ITransferableDocumentStoreInterface.fragments, +) + .filter((f) => f.type === 'function') + .map((f) => f.format('sighash')); + +export const contractInterfaces = { + /** + * IDocumentStore interface functions + * Functions: isActive, isIssued, isRevoked, name, revoke + */ + DocumentStore: IDocumentStoreFunctions, + + /** + * ITransferableDocumentStore interface functions + * Functions: isActive, isIssued, isRevoked, issue, name, revoke + */ + TransferableDocumentStore: ITransferableDocumentStoreFunctions, +}; diff --git a/src/document-store/deploy.ts b/src/document-store/deploy.ts new file mode 100644 index 0000000..51914b8 --- /dev/null +++ b/src/document-store/deploy.ts @@ -0,0 +1,99 @@ +import { + DocumentStore__factory, + TransferableDocumentStore__factory, +} from '@trustvc/document-store'; +import { + Signer as SignerV6, + ContractFactory as ContractFactoryV6, + ContractTransactionReceipt as ContractReceiptV6, +} from 'ethersV6'; +import { + Signer as SignerV5, + ContractFactory as ContractFactoryV5, + ContractReceipt as ContractReceiptV5, +} from 'ethers'; +import { isV6EthersProvider } from '../utils/ethers'; +import { CHAIN_ID } from '../utils'; +import { GasValue } from '../token-registry-functions/types'; +import { getTxOptions } from '../token-registry-functions/utils'; + +/** + * Deploys a new DocumentStore contract. + * Supports both Ethers v5 and v6 signers. + * @param {string} storeName - The name of the document store. + * @param {string} owner - The owner address of the document store. + * @param {SignerV5 | SignerV6} signer - Signer instance (Ethers v5 or v6) that authorizes the deployment. + * @param {DeployOptions} options - Optional transaction metadata including gas values and chain ID. + * @returns {Promise} A promise resolving to the deployed contract address and transaction hash. + * @throws {Error} If the signer provider is not provided. + * @throws {Error} If the store name or owner address is not provided. + * @throws {Error} If deployment fails. + */ + +export interface DeployOptions { + chainId?: CHAIN_ID; + maxFeePerGas?: GasValue; + maxPriorityFeePerGas?: GasValue; + isTransferable?: boolean; +} + +export type TransactionReceipt = ContractReceiptV5 | ContractReceiptV6; + +const deployDocumentStore = async ( + storeName: string, + owner: string, + signer: SignerV5 | SignerV6, + options: DeployOptions = {}, +): Promise => { + if (!storeName) throw new Error('Store name is required'); + if (!owner) throw new Error('Owner address is required'); + if (!signer.provider) throw new Error('Provider is required'); + + const { chainId, maxFeePerGas, maxPriorityFeePerGas } = options; + + // Get transaction options (gas settings) + const txOptions = await getTxOptions(signer, chainId, maxFeePerGas, maxPriorityFeePerGas); + + const isV6 = isV6EthersProvider(signer.provider); + console.log('isv6', isV6); + const DocumentStoreFactory = options.isTransferable + ? TransferableDocumentStore__factory + : DocumentStore__factory; + + try { + if (isV6) { + // Ethers v6 deployment + const ContractFactory = new ContractFactoryV6( + DocumentStoreFactory.abi, + DocumentStoreFactory.bytecode, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, + ); + console.log('before ContractFactory'); + const contract = await ContractFactory.deploy(storeName, owner, txOptions); + console.log('after ContractFactory'); + const receipt = await contract.deploymentTransaction().wait(); + + return receipt; + } else { + // Ethers v5 deployment + const ContractFactory = new ContractFactoryV5( + DocumentStoreFactory.abi, + DocumentStoreFactory.bytecode, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, + ); + const contract = await ContractFactory.deploy(storeName, owner, txOptions); + const receipt = await contract.deployTransaction.wait(); + + return receipt; + } + } catch (e) { + console.error('Deployment failed:', e); + throw new Error( + `Failed to deploy DocumentStore: ${e instanceof Error ? e.message : String(e)}`, + ); + } +}; + +export { deployDocumentStore }; diff --git a/src/document-store/document-store-roles.ts b/src/document-store/document-store-roles.ts new file mode 100644 index 0000000..66a66d5 --- /dev/null +++ b/src/document-store/document-store-roles.ts @@ -0,0 +1,44 @@ +import { utils } from '@tradetrust-tt/tt-verify'; +import { Provider, Contract as ContractV6 } from 'ethersV6'; +import { providers, Contract as ContractV5 } from 'ethers'; +import { CHAIN_ID, SUPPORTED_CHAINS } from '../utils'; +import { getEthersContractFromProvider } from '../utils/ethers'; +import { TT_DOCUMENT_STORE_ABI } from './tt-document-store-abi'; + +interface CallOptions { + chainId?: CHAIN_ID; + provider?: Provider | providers.Provider; +} + +export const getRoleString = async ( + documentStoreAddress: string, + role: string, + options?: CallOptions, +): Promise => { + const { chainId } = options || {}; + let provider = options?.provider; + + if (!provider) { + provider = utils.getProvider({ network: SUPPORTED_CHAINS[chainId].name }); + } + + const Contract = getEthersContractFromProvider(provider); + const documentStore: ContractV5 | ContractV6 = new Contract( + documentStoreAddress, + TT_DOCUMENT_STORE_ABI, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + provider as any, + ); + switch (role) { + case 'admin': + return await documentStore.DEFAULT_ADMIN_ROLE(); + case 'issuer': + return await documentStore.ISSUER_ROLE(); + case 'revoker': + return await documentStore.REVOKER_ROLE(); + default: + throw new Error('Invalid role'); + } +}; + +export const rolesList = ['admin', 'issuer', 'revoker']; diff --git a/src/document-store/index.ts b/src/document-store/index.ts new file mode 100644 index 0000000..bb04ec2 --- /dev/null +++ b/src/document-store/index.ts @@ -0,0 +1,10 @@ +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 { supportInterfaceIds } from './supportInterfaceIds'; +export { + DocumentStore__factory, + TransferableDocumentStore__factory, +} from '@trustvc/document-store'; diff --git a/src/document-store/issue.ts b/src/document-store/issue.ts new file mode 100644 index 0000000..c0f6a31 --- /dev/null +++ b/src/document-store/issue.ts @@ -0,0 +1,133 @@ +import { + DocumentStore__factory, + TransferableDocumentStore__factory, +} from '@trustvc/document-store'; +import { + Signer as SignerV6, + Contract as ContractV6, + ContractTransactionResponse as ContractTransactionV6, +} from 'ethersV6'; +import { + Contract as ContractV5, + ContractTransaction as ContractTransactionV5, + 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'; + +/** + * Issues a document hash to 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} 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. + * @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 = {}, +): Promise => { + if (!documentStoreAddress) throw new Error('Document store address is required'); + if (!signer.provider) throw new Error('Provider is required'); + if (!documentHash) throw new Error('Document hash 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).issue.staticCall(documentHash); + } else { + await (documentStoreContract as ContractV5).callStatic.issue(documentHash); + } + } 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.issue(documentHash, txOptions); +}; + +export { documentStoreIssue }; diff --git a/src/document-store/revoke.ts b/src/document-store/revoke.ts new file mode 100644 index 0000000..e94f9ea --- /dev/null +++ b/src/document-store/revoke.ts @@ -0,0 +1,133 @@ +import { + DocumentStore__factory, + TransferableDocumentStore__factory, +} from '@trustvc/document-store'; +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 { 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'; + +/** + * Revokes a document hash from 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 {SignerV5 | SignerV6} signer - Signer instance (Ethers v5 or v6) that authorizes the revoke transaction. + * @param {string} documentHash - The hash of the document to revoke (must be a valid hex string). + * @param {RevokeOptions} options - Optional transaction metadata including gas values and chain ID. + * @returns {Promise} A promise resolving to the transaction result from the revoke 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.revoke` fails as a pre-check. + */ + +export interface RevokeOptions { + chainId?: CHAIN_ID; + maxFeePerGas?: GasValue; + maxPriorityFeePerGas?: GasValue; + isTransferable?: boolean; +} + +const documentStoreRevoke = async ( + documentStoreAddress: string, + signer: SignerV5 | SignerV6, + documentHash: string, + options: RevokeOptions = {}, +): Promise => { + if (!documentStoreAddress) throw new Error('Document store address is required'); + if (!signer.provider) throw new Error('Provider is required'); + if (!documentHash) throw new Error('Document hash 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).revoke.staticCall(documentHash); + } else { + await (documentStoreContract as ContractV5).callStatic.revoke(documentHash); + } + } catch (e) { + console.error('callStatic failed:', e); + throw new Error('Pre-check (callStatic) for revoke failed'); + } + + // Get transaction options (gas settings) + const txOptions = await getTxOptions(signer, chainId, maxFeePerGas, maxPriorityFeePerGas); + + // Send the actual transaction + return await documentStoreContract.revoke(documentHash, txOptions); +}; + +export { documentStoreRevoke }; diff --git a/src/document-store/supportInterfaceIds.ts b/src/document-store/supportInterfaceIds.ts new file mode 100644 index 0000000..3784bf1 --- /dev/null +++ b/src/document-store/supportInterfaceIds.ts @@ -0,0 +1,24 @@ +import { utils } from '@tradetrust-tt/token-registry-v5'; +import { contractInterfaces } from './contract-interfaces'; + +/** + * ERC-165 Interface IDs for DocumentStore contracts + * These IDs are computed from the function selectors of each interface + */ +const { computeInterfaceId } = utils; + +export const supportInterfaceIds = { + /** + * IDocumentStore interface ID + * Functions: isActive, isIssued, isRevoked, name, revoke + * Computed: 0xb9391097 + */ + IDocumentStore: computeInterfaceId(contractInterfaces.DocumentStore), + + /** + * ITransferableDocumentStore interface ID + * Functions: isActive, isIssued, isRevoked, issue, name, revoke + * Computed: 0xc2cb4227 + */ + ITransferableDocumentStore: computeInterfaceId(contractInterfaces.TransferableDocumentStore), +}; diff --git a/src/document-store/tt-document-store-abi.ts b/src/document-store/tt-document-store-abi.ts new file mode 100644 index 0000000..1933a0e --- /dev/null +++ b/src/document-store/tt-document-store-abi.ts @@ -0,0 +1,233 @@ +/** + * TT Document Store ABI + * This is a legacy document store contract that doesn't support ERC-165 supportsInterface. + * It will be detected as a fallback when other document store interfaces are not supported. + */ + +export const TT_DOCUMENT_STORE_ABI = [ + // TODO: Replace with the actual full ABI + // Minimal ABI for issue function (0x0f75e81f) + { + inputs: [ + { internalType: 'string', name: '_name', type: 'string' }, + { internalType: 'address', name: 'owner', type: 'address' }, + ], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { + anonymous: false, + inputs: [{ indexed: true, internalType: 'bytes32', name: 'document', type: 'bytes32' }], + name: 'DocumentIssued', + type: 'event', + }, + { + anonymous: false, + inputs: [{ indexed: true, internalType: 'bytes32', name: 'document', type: 'bytes32' }], + name: 'DocumentRevoked', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'bytes32', name: 'role', type: 'bytes32' }, + { indexed: true, internalType: 'bytes32', name: 'previousAdminRole', type: 'bytes32' }, + { indexed: true, internalType: 'bytes32', name: 'newAdminRole', type: 'bytes32' }, + ], + name: 'RoleAdminChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'bytes32', name: 'role', type: 'bytes32' }, + { indexed: true, internalType: 'address', name: 'account', type: 'address' }, + { indexed: true, internalType: 'address', name: 'sender', type: 'address' }, + ], + name: 'RoleGranted', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'bytes32', name: 'role', type: 'bytes32' }, + { indexed: true, internalType: 'address', name: 'account', type: 'address' }, + { indexed: true, internalType: 'address', name: 'sender', type: 'address' }, + ], + name: 'RoleRevoked', + type: 'event', + }, + { + inputs: [], + name: 'DEFAULT_ADMIN_ROLE', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'ISSUER_ROLE', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'REVOKER_ROLE', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes32[]', name: 'documents', type: 'bytes32[]' }], + name: 'bulkIssue', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes32[]', name: 'documents', type: 'bytes32[]' }], + name: 'bulkRevoke', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + name: 'documentIssued', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + name: 'documentRevoked', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes32', name: 'document', type: 'bytes32' }], + name: 'getIssuedBlock', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes32', name: 'role', type: 'bytes32' }], + name: 'getRoleAdmin', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'role', type: 'bytes32' }, + { internalType: 'address', name: 'account', type: 'address' }, + ], + name: 'grantRole', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'role', type: 'bytes32' }, + { internalType: 'address', name: 'account', type: 'address' }, + ], + name: 'hasRole', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes32', name: 'document', type: 'bytes32' }], + name: 'isIssued', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'document', type: 'bytes32' }, + { internalType: 'uint256', name: 'blockNumber', type: 'uint256' }, + ], + name: 'isIssuedBefore', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes32', name: 'document', type: 'bytes32' }], + name: 'isRevoked', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'document', type: 'bytes32' }, + { internalType: 'uint256', name: 'blockNumber', type: 'uint256' }, + ], + name: 'isRevokedBefore', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes32', name: 'document', type: 'bytes32' }], + name: 'issue', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'name', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'role', type: 'bytes32' }, + { internalType: 'address', name: 'account', type: 'address' }, + ], + name: 'renounceRole', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes32', name: 'document', type: 'bytes32' }], + name: 'revoke', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'role', type: 'bytes32' }, + { internalType: 'address', name: 'account', type: 'address' }, + ], + name: 'revokeRole', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes4', name: 'interfaceId', type: 'bytes4' }], + name: 'supportsInterface', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'version', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, + // Add other functions from the ABI here (name, isIssued, isRevoked, etc.) +] as const; diff --git a/src/index.ts b/src/index.ts index 9bc3637..f7e7d75 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,6 +19,15 @@ import { v5GetEventFromReceipt, v5ComputeInterfaceId, } from './token-registry-v5'; +import { + deployDocumentStore, + grantDocumentStoreRole, + revokeDocumentStoreRole, + documentStoreIssue, + documentStoreRevoke, + DocumentStore__factory, + TransferableDocumentStore__factory, +} from './document-store'; export type { TypedContractMethod } from './token-registry-v5/typedContractMethod'; export * from './token-registry-functions'; export * from './core'; @@ -28,6 +37,7 @@ export * from './open-cert'; export * from './w3c'; export * from './utils'; export * from './dnsprove'; + export { v4SupportInterfaceIds, v4ContractAddress, @@ -46,4 +56,11 @@ export { v5EncodeInitParams, v5GetEventFromReceipt, v5ComputeInterfaceId, + deployDocumentStore, + grantDocumentStoreRole, + revokeDocumentStoreRole, + documentStoreIssue, + documentStoreRevoke, + DocumentStore__factory, + TransferableDocumentStore__factory, }; From 83498ec325512f2ddc209d18bd9db5ea90d955c0 Mon Sep 17 00:00:00 2001 From: Rishabh Singh Date: Wed, 11 Feb 2026 09:54:29 +0530 Subject: [PATCH 02/11] fix: comment imports --- src/document-store/index.ts | 4 ++-- src/index.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/document-store/index.ts b/src/document-store/index.ts index bb04ec2..28ac477 100644 --- a/src/document-store/index.ts +++ b/src/document-store/index.ts @@ -1,7 +1,7 @@ export { documentStoreIssue, IssueOptions } from './issue'; export { documentStoreRevoke, RevokeOptions } from './revoke'; -export { revokeDocumentStoreRole } from './revoke-role'; -export { grantDocumentStoreRole } from './grant-role'; +// export { revokeDocumentStoreRole } from './revoke-role'; +// export { grantDocumentStoreRole } from './grant-role'; export { deployDocumentStore, DeployOptions } from './deploy'; export { supportInterfaceIds } from './supportInterfaceIds'; export { diff --git a/src/index.ts b/src/index.ts index f7e7d75..eac5718 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 e09933fa3fcf35d49debe00a873bde8d9dc93956 Mon Sep 17 00:00:00 2001 From: Rishabh Singh Date: Wed, 11 Feb 2026 10:13:11 +0530 Subject: [PATCH 03/11] fix: fixtures --- src/__tests__/document-store/fixtures.ts | 100 ++++++++++------------- 1 file changed, 45 insertions(+), 55 deletions(-) diff --git a/src/__tests__/document-store/fixtures.ts b/src/__tests__/document-store/fixtures.ts index c81f42e..6561d3a 100644 --- a/src/__tests__/document-store/fixtures.ts +++ b/src/__tests__/document-store/fixtures.ts @@ -61,6 +61,17 @@ vi.mock('../../core', () => ({ checkSupportsInterface: vi.fn(), })); +const documentStoreFxnFragments = [ + { type: 'function', format: () => 'isActive(bytes32)' }, + { type: 'function', format: () => 'isIssued(bytes32)' }, + { type: 'function', format: () => 'isRevoked(bytes32)' }, + { type: 'function', format: () => 'issue(bytes32)' }, + { type: 'function', format: () => 'name()' }, + { type: 'function', format: () => 'revoke(bytes32)' }, + { type: 'function', format: () => 'grantRole(address, bytes32)' }, + { type: 'function', format: () => 'revokeRole(address, bytes32)' }, +]; + vi.mock('@trustvc/document-store', () => ({ DocumentStore__factory: class { static abi = ['constructor(string storeName, address owner)']; @@ -100,42 +111,31 @@ vi.mock('@trustvc/document-store', () => ({ }, IDocumentStore__factory: { createInterface: vi.fn(() => ({ - fragments: [ - { type: 'function', format: () => 'isActive(bytes32)' }, - { type: 'function', format: () => 'isIssued(bytes32)' }, - { type: 'function', format: () => 'isRevoked(bytes32)' }, - { type: 'function', format: () => 'name()' }, - { type: 'function', format: () => 'revoke(bytes32)' }, - ], + fragments: documentStoreFxnFragments, })), }, ITransferableDocumentStore__factory: { createInterface: vi.fn(() => ({ - fragments: [ - { type: 'function', format: () => 'isActive(bytes32)' }, - { type: 'function', format: () => 'isIssued(bytes32)' }, - { type: 'function', format: () => 'isRevoked(bytes32)' }, - { type: 'function', format: () => 'issue(bytes32)' }, - { type: 'function', format: () => 'name()' }, - { type: 'function', format: () => 'revoke(bytes32)' }, - ], + fragments: documentStoreFxnFragments, })), }, })); - +const promiseResolveTrue = vi.fn(() => Promise.resolve(true)); +const promiseResolveFalse = vi.fn(() => Promise.resolve(false)); +const callStaticFxn = { + issue: vi.fn(), + revoke: vi.fn(), + grantRole: vi.fn(), + revokeRole: vi.fn(), +}; export const mockDocumentStoreContract = { - callStatic: { - issue: vi.fn(), - revoke: vi.fn(), - grantRole: vi.fn(), - revokeRole: vi.fn(), - }, + callStatic: callStaticFxn, issue: Object.assign( // Direct call returns transaction response vi.fn(() => Promise.resolve('document_store_issue_tx_hash')), { // Static call returns boolean - staticCall: vi.fn(() => Promise.resolve(true)), + staticCall: promiseResolveTrue, }, ), revoke: Object.assign( @@ -143,7 +143,7 @@ export const mockDocumentStoreContract = { vi.fn(() => Promise.resolve('document_store_revoke_tx_hash')), { // Static call returns boolean - staticCall: vi.fn(() => Promise.resolve(true)), + staticCall: promiseResolveTrue, }, ), grantRole: Object.assign( @@ -151,7 +151,7 @@ export const mockDocumentStoreContract = { vi.fn(() => Promise.resolve('document_store_grant_role_tx_hash')), { // Static call returns boolean - staticCall: vi.fn(() => Promise.resolve(true)), + staticCall: promiseResolveTrue, }, ), revokeRole: Object.assign( @@ -159,28 +159,23 @@ export const mockDocumentStoreContract = { vi.fn(() => Promise.resolve('document_store_revoke_role_tx_hash')), { // Static call returns boolean - staticCall: vi.fn(() => Promise.resolve(true)), + staticCall: promiseResolveTrue, }, ), - isIssued: vi.fn(() => Promise.resolve(true)), - isRevoked: vi.fn(() => Promise.resolve(false)), - isActive: vi.fn(() => Promise.resolve(true)), + isIssued: promiseResolveTrue, + isRevoked: promiseResolveFalse, + isActive: promiseResolveTrue, name: vi.fn(() => Promise.resolve('Test Document Store')), }; export const mockTransferableDocumentStoreContract = { - callStatic: { - issue: vi.fn(), - revoke: vi.fn(), - grantRole: vi.fn(), - revokeRole: vi.fn(), - }, + callStatic: callStaticFxn, issue: Object.assign( // Direct call returns transaction response vi.fn(() => Promise.resolve('transferable_document_store_issue_tx_hash')), { // Static call returns boolean - staticCall: vi.fn(() => Promise.resolve(true)), + staticCall: promiseResolveTrue, }, ), revoke: Object.assign( @@ -188,7 +183,7 @@ export const mockTransferableDocumentStoreContract = { vi.fn(() => Promise.resolve('transferable_document_store_revoke_tx_hash')), { // Static call returns boolean - staticCall: vi.fn(() => Promise.resolve(true)), + staticCall: promiseResolveTrue, }, ), grantRole: Object.assign( @@ -196,7 +191,7 @@ export const mockTransferableDocumentStoreContract = { vi.fn(() => Promise.resolve('transferable_document_store_grant_role_tx_hash')), { // Static call returns boolean - staticCall: vi.fn(() => Promise.resolve(true)), + staticCall: promiseResolveTrue, }, ), revokeRole: Object.assign( @@ -204,28 +199,23 @@ export const mockTransferableDocumentStoreContract = { vi.fn(() => Promise.resolve('transferable_document_store_revoke_role_tx_hash')), { // Static call returns boolean - staticCall: vi.fn(() => Promise.resolve(true)), + staticCall: promiseResolveTrue, }, ), - isIssued: vi.fn(() => Promise.resolve(true)), - isRevoked: vi.fn(() => Promise.resolve(false)), - isActive: vi.fn(() => Promise.resolve(true)), + isIssued: promiseResolveTrue, + isRevoked: promiseResolveFalse, + isActive: promiseResolveTrue, name: vi.fn(() => Promise.resolve('Test Transferable Document Store')), }; export const mockTTDocumentStoreContract = { - callStatic: { - issue: vi.fn(), - revoke: vi.fn(), - grantRole: vi.fn(), - revokeRole: vi.fn(), - }, + callStatic: callStaticFxn, issue: Object.assign( // Direct call returns transaction response vi.fn(() => Promise.resolve('tt_document_store_issue_tx_hash')), { // Static call returns boolean - staticCall: vi.fn(() => Promise.resolve(true)), + staticCall: promiseResolveFalse, }, ), revoke: Object.assign( @@ -233,7 +223,7 @@ export const mockTTDocumentStoreContract = { vi.fn(() => Promise.resolve('tt_document_store_revoke_tx_hash')), { // Static call returns boolean - staticCall: vi.fn(() => Promise.resolve(true)), + staticCall: promiseResolveFalse, }, ), grantRole: Object.assign( @@ -241,7 +231,7 @@ export const mockTTDocumentStoreContract = { vi.fn(() => Promise.resolve('tt_document_store_grant_role_tx_hash')), { // Static call returns boolean - staticCall: vi.fn(() => Promise.resolve(true)), + staticCall: promiseResolveFalse, }, ), revokeRole: Object.assign( @@ -249,12 +239,12 @@ export const mockTTDocumentStoreContract = { vi.fn(() => Promise.resolve('tt_document_store_revoke_role_tx_hash')), { // Static call returns boolean - staticCall: vi.fn(() => Promise.resolve(true)), + staticCall: promiseResolveFalse, }, ), - isIssued: vi.fn(() => Promise.resolve(true)), - isRevoked: vi.fn(() => Promise.resolve(false)), - isActive: vi.fn(() => Promise.resolve(true)), + isIssued: promiseResolveFalse, + isRevoked: promiseResolveFalse, + isActive: promiseResolveTrue, name: vi.fn(() => Promise.resolve('Test TT Document Store')), }; From f09804dce90796cf2e417fc0fe708f6d5343cdde Mon Sep 17 00:00:00 2001 From: Rishabh Singh Date: Wed, 11 Feb 2026 11:20:29 +0530 Subject: [PATCH 04/11] fix: update readme --- README.md | 217 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) diff --git a/README.md b/README.md index 9cde147..53173b6 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ TrustVC is a comprehensive wrapper library designed to simplify the signing and - [a) Token Registry v4](#a-token-registry-v4) - [b) Token Registry V5](#b-token-registry-v5) - [8. **Document Builder**](#8-document-builder) + - [9. **Document Store**](#9-document-store) ## Installation @@ -890,3 +891,219 @@ To get the current state of the document as a JSON string: const documentJson = builder.toString(); console.log(documentJson); ``` + +## 9. Document Store + +> TrustVC provides comprehensive Document Store functionality for managing blockchain-based document storage and verification. The Document Store module supports both standard DocumentStore and TransferableDocumentStore contracts, enabling secure document issuance, revocation, and role management on various blockchain networks. + +### Key Features + +- **Dual Contract Support**: Works with both DocumentStore and TransferableDocumentStore contracts +- **Automatic Contract Detection**: Uses ERC-165 interface checking to automatically identify contract types +- **Ethers Compatibility**: Full support for both ethers v5 and v6 +- **Type Safety**: Full TypeScript support with comprehensive type definitions + +### Functions + +#### deployDocumentStore + +Deploys a new DocumentStore contract to the blockchain. + +**Parameters:** +- `storeName` (string): The name of the document store +- `owner` (string): The owner address of the document store +- `signer` (Signer): Ethers v5 or v6 signer instance +- `options` (DeployOptions, optional): Deployment configuration + +**DeployOptions:** +- `chainId` (CHAIN_ID, optional): Target blockchain network +- `maxFeePerGas` (GasValue, optional): Maximum fee per gas +- `maxPriorityFeePerGas` (GasValue, optional): Maximum priority fee per gas +- `isTransferable` (boolean, optional): Whether to deploy TransferableDocumentStore + +**Returns:** `Promise` - The deployment transaction receipt + +**Example:** + +```ts +import { deployDocumentStore } from '@trustvc/trustvc'; + +const receipt = await deployDocumentStore( + 'My Document Store', + '0x1234567890123456789012345678901234567890', + signer, + { + chainId: CHAIN_ID.sepolia, + isTransferable: true, + } +); + +console.log('Contract deployed at:', receipt.contractAddress); +``` + +#### documentStoreIssue + +Issues a document to the Document Store. + +**Parameters:** +- `documentStoreAddress` (string): Address of the Document Store contract +- `documentHash` (string): Hash of the document to issue +- `signer` (Signer): Ethers v5 or v6 signer instance +- `options` (IssueOptions, optional): Issuance configuration + +**IssueOptions:** +- `chainId` (CHAIN_ID, optional): Target blockchain network +- `maxFeePerGas` (GasValue, optional): Maximum fee per gas +- `maxPriorityFeePerGas` (GasValue, optional): Maximum priority fee per gas +- `isTransferable` (boolean, optional): Whether the contract is TransferableDocumentStore + +**Returns:** `Promise` - Transaction hash of the issuance + +**Example:** + +```ts +import { documentStoreIssue } from '@trustvc/trustvc'; + +const txHash = await documentStoreIssue( + '0x1234567890123456789012345678901234567890', + '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890', + signer, + { + chainId: CHAIN_ID.sepolia, + isTransferable: false, + } +); + +console.log('Document issued, transaction:', txHash); +``` + +#### documentStoreRevoke + +Revokes a document from the Document Store. + +**Parameters:** +- `documentStoreAddress` (string): Address of the Document Store contract +- `documentHash` (string): Hash of the document to revoke +- `signer` (Signer): Ethers v5 or v6 signer instance +- `options` (RevokeOptions, optional): Revocation configuration + +**RevokeOptions:** +- `chainId` (CHAIN_ID, optional): Target blockchain network +- `maxFeePerGas` (GasValue, optional): Maximum fee per gas +- `maxPriorityFeePerGas` (GasValue, optional): Maximum priority fee per gas +- `isTransferable` (boolean, optional): Whether the contract is TransferableDocumentStore + +**Returns:** `Promise` - Transaction hash of the revocation + +**Example:** + +```ts +import { documentStoreRevoke } from '@trustvc/trustvc'; + +const txHash = await documentStoreRevoke( + '0x1234567890123456789012345678901234567890', + '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890', + signer, + { + chainId: CHAIN_ID.sepolia, + isTransferable: false, + } +); + +console.log('Document revoked, transaction:', txHash); +``` + +### Contract Types + +#### DocumentStore +- Basic document store functionality +- Supports document issuance and revocation +- ERC-165 interface: `0x01ffc9a7` (IDocumentStore) + +#### TransferableDocumentStore +- Extended document store with transfer capabilities +- All DocumentStore functionality plus transfer features +- ERC-165 interface: `0x8c5a6b8a` (ITransferableDocumentStore) + +### Error Handling + +All functions include comprehensive error handling: + +- **Validation Errors**: Invalid addresses, missing parameters, provider issues +- **Contract Errors**: Pre-check failures, insufficient permissions, contract reverts +- **Network Errors**: Connection issues, transaction failures +- **Interface Detection**: Automatic fallback when ERC-165 interfaces are not supported + +### Gas Optimization + +The Document Store functions support gas optimization through configurable transaction options: + +```ts +const options = { + maxFeePerGas: '50000000000', // 50 gwei + maxPriorityFeePerGas: '2000000000', // 2 gwei + chainId: CHAIN_ID.mainnet, +}; +``` + +### Usage Patterns + +#### Basic Usage + +```ts +import { deployDocumentStore, documentStoreIssue, documentStoreRevoke } from '@trustvc/trustvc'; + +// Deploy a new document store +const receipt = await deployDocumentStore('My Store', ownerAddress, signer); + +// Issue a document +await documentStoreIssue(receipt.contractAddress, documentHash, signer); + +// Revoke if needed +await documentStoreRevoke(receipt.contractAddress, documentHash, signer); +``` + +#### Advanced Usage with Options + +```ts +import { CHAIN_ID } from '@trustvc/trustvc'; + +// Deploy with specific network and gas settings +const receipt = await deployDocumentStore( + 'Production Store', + ownerAddress, + signer, + { + chainId: CHAIN_ID.mainnet, + maxFeePerGas: '100000000000', // 100 gwei + maxPriorityFeePerGas: '5000000000', // 5 gwei + isTransferable: true, + } +); + +// Issue with explicit contract type detection +await documentStoreIssue( + receipt.contractAddress, + documentHash, + signer, + { + chainId: CHAIN_ID.mainnet, + isTransferable: true, // Explicit type detection + } +); +``` + +#### Batch Operations + +```ts +// Issue multiple documents +const documentHashes = [ + '0xhash1...', + '0xhash2...', + '0xhash3...', +]; + +for (const hash of documentHashes) { + await documentStoreIssue(storeAddress, hash, signer); +} +``` From b1362631e33b38877e11446bb5efca3c3f96fa06 Mon Sep 17 00:00:00 2001 From: Rishabh Singh Date: Wed, 11 Feb 2026 12:13:14 +0530 Subject: [PATCH 05/11] fix: remove consoles --- src/document-store/deploy.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/document-store/deploy.ts b/src/document-store/deploy.ts index 51914b8..5912a74 100644 --- a/src/document-store/deploy.ts +++ b/src/document-store/deploy.ts @@ -55,7 +55,6 @@ const deployDocumentStore = async ( const txOptions = await getTxOptions(signer, chainId, maxFeePerGas, maxPriorityFeePerGas); const isV6 = isV6EthersProvider(signer.provider); - console.log('isv6', isV6); const DocumentStoreFactory = options.isTransferable ? TransferableDocumentStore__factory : DocumentStore__factory; @@ -69,9 +68,7 @@ const deployDocumentStore = async ( // eslint-disable-next-line @typescript-eslint/no-explicit-any signer as any, ); - console.log('before ContractFactory'); const contract = await ContractFactory.deploy(storeName, owner, txOptions); - console.log('after ContractFactory'); const receipt = await contract.deploymentTransaction().wait(); return receipt; From a3e06fde16775c905ea9fd3652f8cf181cafd4e0 Mon Sep 17 00:00:00 2001 From: Rishabh Singh Date: Wed, 11 Feb 2026 15:55:56 +0530 Subject: [PATCH 06/11] fix: code rabit suggestion fixes --- src/__tests__/document-store/deploy.test.ts | 1 - src/__tests__/document-store/fixtures.ts | 4 +- src/__tests__/document-store/issue.test.ts | 9 +--- src/__tests__/document-store/revoke.test.ts | 54 ++++++++++++--------- src/document-store/deploy.ts | 2 +- src/document-store/document-store-roles.ts | 1 + src/document-store/revoke.ts | 6 +-- 7 files changed, 39 insertions(+), 38 deletions(-) diff --git a/src/__tests__/document-store/deploy.test.ts b/src/__tests__/document-store/deploy.test.ts index c7d0935..8b870b2 100644 --- a/src/__tests__/document-store/deploy.test.ts +++ b/src/__tests__/document-store/deploy.test.ts @@ -29,7 +29,6 @@ describe('Deploy Document Store', () => { chainId: mockChainId, isTransferable: false, }); - console.log('result', result); expect(result).toEqual({ address: '0xDeployedDocumentStoreAddress', diff --git a/src/__tests__/document-store/fixtures.ts b/src/__tests__/document-store/fixtures.ts index 6561d3a..e29e321 100644 --- a/src/__tests__/document-store/fixtures.ts +++ b/src/__tests__/document-store/fixtures.ts @@ -68,8 +68,8 @@ const documentStoreFxnFragments = [ { type: 'function', format: () => 'issue(bytes32)' }, { type: 'function', format: () => 'name()' }, { type: 'function', format: () => 'revoke(bytes32)' }, - { type: 'function', format: () => 'grantRole(address, bytes32)' }, - { type: 'function', format: () => 'revokeRole(address, bytes32)' }, + { type: 'function', format: () => 'grantRole(bytes32, address)' }, + { type: 'function', format: () => 'revokeRole(bytes32, address)' }, ]; vi.mock('@trustvc/document-store', () => ({ diff --git a/src/__tests__/document-store/issue.test.ts b/src/__tests__/document-store/issue.test.ts index e0bfb6a..57f2741 100644 --- a/src/__tests__/document-store/issue.test.ts +++ b/src/__tests__/document-store/issue.test.ts @@ -1,5 +1,5 @@ import './fixtures'; -import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { describe, it, expect, beforeEach, vi, beforeAll } 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'; @@ -55,7 +55,7 @@ describe('Issue Document', () => { describe.each(providers)( 'Issue Document with $contractType and ethers version $ethersVersion', - async ({ Provider, ethersVersion, contractType }) => { + ({ Provider, ethersVersion, contractType }) => { const isTransferable = contractType === 'TransferableDocumentStore'; const mockContract = isTransferable ? mockTransferableDocumentStoreContract @@ -82,11 +82,6 @@ describe('Issue Document', () => { beforeAll(() => { // Clear any existing mocks first vi.clearAllMocks(); - const mockContractConstructor = (mockContract: any) => vi.fn(() => mockContract); - // Only set up the mock if it hasn't been set up yet - vi.mocked(getEthersContractFromProvider).mockReturnValue( - mockContractConstructor(mockContract), - ); }); beforeEach(() => { diff --git a/src/__tests__/document-store/revoke.test.ts b/src/__tests__/document-store/revoke.test.ts index 6ece9f6..c351307 100644 --- a/src/__tests__/document-store/revoke.test.ts +++ b/src/__tests__/document-store/revoke.test.ts @@ -55,7 +55,7 @@ describe('Revoke Document', () => { describe.each(providers)( 'Revoke Document with $contractType and ethers version $ethersVersion', - async ({ Provider, ethersVersion, contractType }) => { + ({ Provider, ethersVersion, contractType }) => { const isTransferable = contractType === 'TransferableDocumentStore'; const mockContract = isTransferable ? mockTransferableDocumentStoreContract @@ -79,16 +79,20 @@ describe('Revoke Document', () => { ? MOCK_TRANSFERABLE_DOCUMENT_STORE_ADDRESS : MOCK_DOCUMENT_STORE_ADDRESS; - beforeAll(() => { + // beforeAll(() => { + // vi.clearAllMocks(); + // const mockContractConstructor = (mockContract: any) => vi.fn(() => mockContract); + // vi.mocked(getEthersContractFromProvider).mockReturnValue( + // mockContractConstructor(mockContract), + // ); + // }); + + beforeEach(() => { 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) { @@ -104,8 +108,9 @@ describe('Revoke Document', () => { it('should revoke document hash successfully', async () => { const result = await documentStoreRevoke( mockDocumentStoreAddress, - wallet, mockDocumentHash, + wallet, + { chainId: mockChainId, }, @@ -117,8 +122,8 @@ describe('Revoke Document', () => { it('should revoke document with explicit contract type', async () => { const result = await documentStoreRevoke( mockDocumentStoreAddress, - wallet, mockDocumentHash, + wallet, { chainId: mockChainId, isTransferable, @@ -131,8 +136,8 @@ describe('Revoke Document', () => { it('should revoke document without chainId option', async () => { const result = await documentStoreRevoke( mockDocumentStoreAddress, - wallet, mockDocumentHash, + wallet, { isTransferable, }, @@ -143,8 +148,8 @@ describe('Revoke Document', () => { it('should revoke document with gas options', async () => { const result = await documentStoreRevoke( mockDocumentStoreAddress, - wallet, mockDocumentHash, + wallet, { chainId: mockChainId, maxFeePerGas: '1000000000', @@ -157,14 +162,14 @@ describe('Revoke Document', () => { it('should throw when document store address is missing', async () => { await expect( - documentStoreRevoke('', wallet, mockDocumentHash, { chainId: mockChainId }), + documentStoreRevoke('', mockDocumentHash, 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( - documentStoreRevoke(mockDocumentStoreAddress, signerWithoutProvider, mockDocumentHash, { + documentStoreRevoke(mockDocumentStoreAddress, mockDocumentHash, signerWithoutProvider, { chainId: mockChainId, }), ).rejects.toThrow('Provider is required'); @@ -172,7 +177,7 @@ describe('Revoke Document', () => { it('should throw when document hash is missing', async () => { await expect( - documentStoreRevoke(mockDocumentStoreAddress, wallet, '', { chainId: mockChainId }), + documentStoreRevoke(mockDocumentStoreAddress, '', wallet, { chainId: mockChainId }), ).rejects.toThrow('Document hash is required'); }); @@ -181,7 +186,7 @@ describe('Revoke Document', () => { mockContract.callStatic.revoke.mockRejectedValue(mockError); mockContract.revoke.staticCall.mockRejectedValue(mockError); await expect( - documentStoreRevoke(mockDocumentStoreAddress, wallet, mockDocumentHash, { + documentStoreRevoke(mockDocumentStoreAddress, mockDocumentHash, wallet, { chainId: mockChainId, isTransferable, }), @@ -196,8 +201,9 @@ describe('Revoke Document', () => { ); const result = await documentStoreRevoke( mockDocumentStoreAddress, - wallet, mockDocumentHash, + wallet, + { chainId: mockChainId, }, @@ -211,7 +217,7 @@ describe('Revoke Document', () => { mockContract.callStatic.revoke.mockRejectedValue(new Error('Invalid hash format')); mockContract.revoke.staticCall.mockRejectedValue(new Error('Invalid hash format')); await expect( - documentStoreRevoke(mockDocumentStoreAddress, wallet, invalidHash, { + documentStoreRevoke(mockDocumentStoreAddress, invalidHash, wallet, { chainId: mockChainId, isTransferable, }), @@ -222,7 +228,7 @@ describe('Revoke Document', () => { mockContract.callStatic.revoke.mockRejectedValue(new Error('Document already revoked')); mockContract.revoke.staticCall.mockRejectedValue(new Error('Document already revoked')); await expect( - documentStoreRevoke(mockDocumentStoreAddress, wallet, mockDocumentHash, { + documentStoreRevoke(mockDocumentStoreAddress, mockDocumentHash, wallet, { chainId: mockChainId, isTransferable, }), @@ -231,7 +237,7 @@ describe('Revoke Document', () => { it('should work with different document hash formats', async () => { const differentHash = '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890'; - const result = await documentStoreRevoke(mockDocumentStoreAddress, wallet, differentHash, { + const result = await documentStoreRevoke(mockDocumentStoreAddress, differentHash, wallet, { chainId: mockChainId, isTransferable, }); @@ -259,8 +265,8 @@ describe('Revoke Document', () => { vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); const result = await documentStoreRevoke( MOCK_TT_DOCUMENT_STORE_ADDRESS, - wallet, mockDocumentHash, + wallet, { chainId: mockChainId, }, @@ -282,8 +288,8 @@ describe('Revoke Document', () => { vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); const result = await documentStoreRevoke( MOCK_TT_DOCUMENT_STORE_ADDRESS, - wallet, mockDocumentHash, + wallet, { chainId: mockChainId, }, @@ -299,8 +305,8 @@ describe('Revoke Document', () => { vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); const result = await documentStoreRevoke( MOCK_TT_DOCUMENT_STORE_ADDRESS, - walletV6, mockDocumentHash, + walletV6, { chainId: mockChainId, }, @@ -314,7 +320,7 @@ describe('Revoke Document', () => { mockTTDocumentStoreContract.callStatic.revoke.mockRejectedValue(mockError); mockTTDocumentStoreContract.revoke.staticCall.mockRejectedValue(mockError); await expect( - documentStoreRevoke(MOCK_TT_DOCUMENT_STORE_ADDRESS, wallet, mockDocumentHash, { + documentStoreRevoke(MOCK_TT_DOCUMENT_STORE_ADDRESS, mockDocumentHash, wallet, { chainId: mockChainId, }), ).rejects.toThrow('Pre-check (callStatic) for revoke failed'); @@ -324,8 +330,8 @@ describe('Revoke Document', () => { vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); const result = await documentStoreRevoke( MOCK_TT_DOCUMENT_STORE_ADDRESS, - wallet, mockDocumentHash, + wallet, { chainId: mockChainId, maxFeePerGas: '2000000000', @@ -344,7 +350,7 @@ describe('Revoke Document', () => { new Error('Document already revoked'), ); await expect( - documentStoreRevoke(MOCK_TT_DOCUMENT_STORE_ADDRESS, wallet, mockDocumentHash, { + documentStoreRevoke(MOCK_TT_DOCUMENT_STORE_ADDRESS, mockDocumentHash, wallet, { chainId: mockChainId, }), ).rejects.toThrow('Pre-check (callStatic) for revoke failed'); diff --git a/src/document-store/deploy.ts b/src/document-store/deploy.ts index 5912a74..4294d52 100644 --- a/src/document-store/deploy.ts +++ b/src/document-store/deploy.ts @@ -69,7 +69,7 @@ const deployDocumentStore = async ( signer as any, ); const contract = await ContractFactory.deploy(storeName, owner, txOptions); - const receipt = await contract.deploymentTransaction().wait(); + const receipt = await contract.deploymentTransaction()?.wait(); return receipt; } else { diff --git a/src/document-store/document-store-roles.ts b/src/document-store/document-store-roles.ts index 66a66d5..35eca0c 100644 --- a/src/document-store/document-store-roles.ts +++ b/src/document-store/document-store-roles.ts @@ -19,6 +19,7 @@ export const getRoleString = async ( let provider = options?.provider; if (!provider) { + if (!chainId) throw new Error('Either provider or chainId must be provided'); provider = utils.getProvider({ network: SUPPORTED_CHAINS[chainId].name }); } diff --git a/src/document-store/revoke.ts b/src/document-store/revoke.ts index e94f9ea..359f71d 100644 --- a/src/document-store/revoke.ts +++ b/src/document-store/revoke.ts @@ -5,7 +5,7 @@ import { import { Signer as SignerV6, Contract as ContractV6, - ContractTransaction as ContractTransactionV6, + ContractTransactionResponse as ContractTransactionV6, } from 'ethersV6'; import { Contract as ContractV5, @@ -28,8 +28,8 @@ import { TT_DOCUMENT_STORE_ABI } from './tt-document-store-abi'; * 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 {SignerV5 | SignerV6} signer - Signer instance (Ethers v5 or v6) that authorizes the revoke transaction. * @param {string} documentHash - The hash of the document to revoke (must be a valid hex string). + * @param {SignerV5 | SignerV6} signer - Signer instance (Ethers v5 or v6) that authorizes the revoke transaction. * @param {RevokeOptions} options - Optional transaction metadata including gas values and chain ID. * @returns {Promise} A promise resolving to the transaction result from the revoke call. * @throws {Error} If the document store address or signer provider is not provided. @@ -46,8 +46,8 @@ export interface RevokeOptions { const documentStoreRevoke = async ( documentStoreAddress: string, - signer: SignerV5 | SignerV6, documentHash: string, + signer: SignerV5 | SignerV6, options: RevokeOptions = {}, ): Promise => { if (!documentStoreAddress) throw new Error('Document store address is required'); From c466b794609b91f9e67f2316f6ef6457089b3965 Mon Sep 17 00:00:00 2001 From: Rishabh Singh Date: Wed, 11 Feb 2026 16:33:48 +0530 Subject: [PATCH 07/11] fix: tests --- src/__tests__/document-store/issue.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/__tests__/document-store/issue.test.ts b/src/__tests__/document-store/issue.test.ts index 57f2741..2c57523 100644 --- a/src/__tests__/document-store/issue.test.ts +++ b/src/__tests__/document-store/issue.test.ts @@ -82,6 +82,11 @@ describe('Issue Document', () => { beforeAll(() => { // Clear any existing mocks first vi.clearAllMocks(); + const mockContractConstructor = (mockContract: any) => vi.fn(() => mockContract); + // Only set up the mock if it hasn't been set up yet + vi.mocked(getEthersContractFromProvider).mockReturnValue( + mockContractConstructor(mockContract), + ); }); beforeEach(() => { From e1bca3e9afc662626f90cdd867b30a4abd33a996 Mon Sep 17 00:00:00 2001 From: Rishabh Singh Date: Thu, 12 Feb 2026 13:27:32 +0530 Subject: [PATCH 08/11] fix: update document store version --- package-lock.json | 54 +++++++++++++----------------------- package.json | 2 +- src/document-store/revoke.ts | 7 ++--- 3 files changed, 24 insertions(+), 39 deletions(-) diff --git a/package-lock.json b/package-lock.json index 11bf4df..286a0a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@tradetrust-tt/token-registry-v5": "npm:@tradetrust-tt/token-registry@^5.5.0", "@tradetrust-tt/tradetrust": "^6.10.2", "@tradetrust-tt/tt-verify": "^9.6.0", - "@trustvc/document-store": "^1.0.2", + "@trustvc/document-store": "^1.0.3", "@trustvc/w3c": "^2.0.0", "@trustvc/w3c-context": "^2.0.0", "@trustvc/w3c-credential-status": "^2.0.0", @@ -1266,7 +1266,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -1290,7 +1289,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -2207,7 +2205,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@ethersproject/address": "^5.8.0", "@ethersproject/bignumber": "^5.8.0", @@ -2620,7 +2617,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@ethersproject/abstract-provider": "^5.8.0", "@ethersproject/abstract-signer": "^5.8.0", @@ -3489,7 +3485,6 @@ "integrity": "sha512-GFhTcJpB+MI6FhvXEI9b2K0snulNLWHqC/BbcJtyNYcKUiw7l3Lgis5ApsYncJ0leALX7/of4XfmXk+maT111w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@microsoft/api-extractor-model": "7.28.13", "@microsoft/tsdoc": "0.14.2", @@ -3911,7 +3906,6 @@ "integrity": "sha512-YhzPdzb612X591FOe68q+qXVXGG2ANZRvDo0RRUtimev85rCrAlv/TLMEZw5c+kq9AbzocLTVX/h2jVIFPL9Xg==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "ethers": "^5.0.0", "hardhat": "^2.0.0" @@ -3933,7 +3927,6 @@ "integrity": "sha512-rYKilwgzQ7/imScn3M9/pFfUf4I1AZEH3KhyJmtPdE2zfaXAn2mFfUy4FbKewzc2We5y/LlKLj36fWJLKC2SIQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^3.0.0", "@octokit/graphql": "^5.0.0", @@ -6727,9 +6720,9 @@ } }, "node_modules/@trustvc/document-store": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@trustvc/document-store/-/document-store-1.0.2.tgz", - "integrity": "sha512-KxGds1jfFhgGXMoFgo+sPv/QOGHTYLmm8Q+ZHQTlN5sTuMgQc7yf8JvIttjamaRSNtbIl5B9LTB86w+dIRFRBA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@trustvc/document-store/-/document-store-1.0.3.tgz", + "integrity": "sha512-YIECQwcoreIfyTbol1/5u9CGK6mbg0Q0bSN2/Ks388zLus1IXELWK5EHYuyrFPbb9d8ajsvz7m+ySsMIY9574w==", "license": "Apache-2.0" }, "node_modules/@trustvc/w3c": { @@ -6990,7 +6983,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~5.26.4" } @@ -7017,7 +7009,8 @@ "version": "2.7.3", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.48.0", @@ -7065,7 +7058,6 @@ "integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.48.0", "@typescript-eslint/types": "8.48.0", @@ -7578,7 +7570,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -8504,7 +8495,6 @@ "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", @@ -9440,7 +9430,6 @@ "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -10590,7 +10579,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -10655,7 +10643,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -10716,7 +10703,6 @@ "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -11265,7 +11251,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@ethersproject/abi": "5.8.0", "@ethersproject/abstract-provider": "5.8.0", @@ -12563,7 +12548,6 @@ "integrity": "sha512-du7ecjx1/ueAUjvtZhVkJvWytPCjlagG3ZktYTphfzAbc1Flc6sRolw5mhKL/Loub1EIFRaflutM4bdB/YsUUw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ethereumjs/util": "^9.1.0", "@ethersproject/abi": "^5.1.2", @@ -14596,7 +14580,6 @@ "resolved": "https://registry.npmjs.org/ky/-/ky-0.33.3.tgz", "integrity": "sha512-CasD9OCEQSFIam2U8efFK81Yeg8vNMTBUqtMOHlrcWQHqUX3HeCl9Dr31u4toV7emlH8Mymk5+9p0lL6mKb/Xw==", "license": "MIT", - "peer": true, "engines": { "node": ">=14.16" }, @@ -15463,7 +15446,6 @@ "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", "dev": true, "license": "MIT", - "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -20018,7 +20000,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -20087,7 +20068,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -21056,7 +21036,6 @@ "integrity": "sha512-sMIK9IaOdLP9hxzTxdTVHxINsazlDgv2gjZ1yeyRZXpIT3xAnuQUDEez8k+AC+lFUtGnfzA2Ct3V5lDyiMestw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^9.0.2", "@semantic-release/error": "^3.0.0", @@ -22128,7 +22107,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/string-format/-/string-format-2.0.0.tgz", "integrity": "sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==", - "license": "WTFPL OR MIT" + "license": "WTFPL OR MIT", + "peer": true }, "node_modules/string-width": { "version": "4.2.3", @@ -22773,7 +22753,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -22920,6 +22899,7 @@ "resolved": "https://registry.npmjs.org/ts-command-line-args/-/ts-command-line-args-2.5.1.tgz", "integrity": "sha512-H69ZwTw3rFHb5WYpQya40YAX2/w7Ut75uUECbgBIsLmM+BNuYnxsltfyyLMxy6sEeKxgijLTnQtLd0nKd6+IYw==", "license": "ISC", + "peer": true, "dependencies": { "chalk": "^4.1.0", "command-line-args": "^5.1.1", @@ -22935,6 +22915,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", + "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -22950,6 +22931,7 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", + "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -22966,6 +22948,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "license": "MIT", + "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -22995,7 +22978,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -23208,6 +23190,7 @@ "resolved": "https://registry.npmjs.org/typechain/-/typechain-8.3.2.tgz", "integrity": "sha512-x/sQYr5w9K7yv3es7jo4KTX05CLxOf7TRWwoHlrjRh8H82G64g+k7VuWPJlgMo6qrjfCulOdfBjiaDtmhFYD/Q==", "license": "MIT", + "peer": true, "dependencies": { "@types/prettier": "^2.1.1", "debug": "^4.3.1", @@ -23232,6 +23215,7 @@ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -23247,6 +23231,7 @@ "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", "deprecated": "Glob versions prior to v9 are no longer supported", "license": "ISC", + "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -23266,13 +23251,15 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/typechain/node_modules/jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "license": "MIT", + "peer": true, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -23282,6 +23269,7 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "license": "MIT", + "peer": true, "bin": { "prettier": "bin-prettier.js" }, @@ -23297,6 +23285,7 @@ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "license": "MIT", + "peer": true, "engines": { "node": ">= 4.0.0" } @@ -23380,7 +23369,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -23647,7 +23635,6 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -24196,7 +24183,6 @@ "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "1.6.1", "@vitest/runner": "1.6.1", diff --git a/package.json b/package.json index a8c75ff..0c3e8ca 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,7 @@ "@tradetrust-tt/token-registry-v5": "npm:@tradetrust-tt/token-registry@^5.5.0", "@tradetrust-tt/tradetrust": "^6.10.2", "@tradetrust-tt/tt-verify": "^9.6.0", - "@trustvc/document-store": "^1.0.2", + "@trustvc/document-store": "^1.0.3", "@trustvc/w3c": "^2.0.0", "@trustvc/w3c-context": "^2.0.0", "@trustvc/w3c-credential-status": "^2.0.0", diff --git a/src/document-store/revoke.ts b/src/document-store/revoke.ts index 359f71d..bb0ab5b 100644 --- a/src/document-store/revoke.ts +++ b/src/document-store/revoke.ts @@ -114,9 +114,9 @@ const documentStoreRevoke = async ( const isV6 = isV6EthersProvider(signer.provider); if (isV6) { - await (documentStoreContract as ContractV6).revoke.staticCall(documentHash); + await (documentStoreContract as ContractV6)['revoke(bytes32)'].staticCall(documentHash); } else { - await (documentStoreContract as ContractV5).callStatic.revoke(documentHash); + await (documentStoreContract as ContractV5).callStatic['revoke(bytes32)'](documentHash); } } catch (e) { console.error('callStatic failed:', e); @@ -125,9 +125,8 @@ const documentStoreRevoke = async ( // Get transaction options (gas settings) const txOptions = await getTxOptions(signer, chainId, maxFeePerGas, maxPriorityFeePerGas); - // Send the actual transaction - return await documentStoreContract.revoke(documentHash, txOptions); + return await documentStoreContract['revoke(bytes32)'](documentHash, txOptions); }; export { documentStoreRevoke }; From 4bb6309fbcfbc5e8d7f87afb410f6084caf8abab Mon Sep 17 00:00:00 2001 From: Rishabh Singh Date: Thu, 12 Feb 2026 13:36:56 +0530 Subject: [PATCH 09/11] fix: use standard call static function --- src/document-store/revoke.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/document-store/revoke.ts b/src/document-store/revoke.ts index bb0ab5b..fa17f1e 100644 --- a/src/document-store/revoke.ts +++ b/src/document-store/revoke.ts @@ -114,9 +114,9 @@ const documentStoreRevoke = async ( const isV6 = isV6EthersProvider(signer.provider); if (isV6) { - await (documentStoreContract as ContractV6)['revoke(bytes32)'].staticCall(documentHash); + await (documentStoreContract as ContractV6).revoke.staticCall(documentHash); } else { - await (documentStoreContract as ContractV5).callStatic['revoke(bytes32)'](documentHash); + await (documentStoreContract as ContractV5).callStatic.revoke(documentHash); } } catch (e) { console.error('callStatic failed:', e); @@ -126,7 +126,7 @@ const documentStoreRevoke = async ( // Get transaction options (gas settings) const txOptions = await getTxOptions(signer, chainId, maxFeePerGas, maxPriorityFeePerGas); // Send the actual transaction - return await documentStoreContract['revoke(bytes32)'](documentHash, txOptions); + return await documentStoreContract.revoke(documentHash, txOptions); }; export { documentStoreRevoke }; From b1347c1a3aadad71ac09d63b85dff01c32028f60 Mon Sep 17 00:00:00 2001 From: Rishabh Singh Date: Thu, 12 Feb 2026 14:08:05 +0530 Subject: [PATCH 10/11] fix: tests --- src/__tests__/document-store/issue.test.ts | 6 +++++- src/__tests__/document-store/revoke.test.ts | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/__tests__/document-store/issue.test.ts b/src/__tests__/document-store/issue.test.ts index 2c57523..ca03917 100644 --- a/src/__tests__/document-store/issue.test.ts +++ b/src/__tests__/document-store/issue.test.ts @@ -208,7 +208,7 @@ describe('Issue Document', () => { // Mock the contract to use TT Document Store const mockContractConstructor = (mockContract: any) => vi.fn(() => mockContract); vi.mocked(getEthersContractFromProvider).mockReturnValue( - mockContractConstructor(mockContract), + mockContractConstructor(mockTTDocumentStoreContract), ); const result = await documentStoreIssue( @@ -227,6 +227,10 @@ describe('Issue Document', () => { }); it('should handle invalid document hash format gracefully', async () => { + const mockContractConstructor = (mockContract: any) => vi.fn(() => mockContract); + vi.mocked(getEthersContractFromProvider).mockReturnValue( + mockContractConstructor(mockContract), + ); const invalidHash = '0xinvalid'; mockContract.callStatic.issue.mockRejectedValue(new Error('Invalid hash format')); mockContract.issue.staticCall.mockRejectedValue(new Error('Invalid hash format')); diff --git a/src/__tests__/document-store/revoke.test.ts b/src/__tests__/document-store/revoke.test.ts index c351307..c276c22 100644 --- a/src/__tests__/document-store/revoke.test.ts +++ b/src/__tests__/document-store/revoke.test.ts @@ -197,7 +197,7 @@ describe('Revoke Document', () => { vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); const mockContractConstructor = (mockContract: any) => vi.fn(() => mockContract); vi.mocked(getEthersContractFromProvider).mockReturnValue( - mockContractConstructor(mockContract), + mockContractConstructor(mockTTDocumentStoreContract), ); const result = await documentStoreRevoke( mockDocumentStoreAddress, @@ -210,6 +210,9 @@ describe('Revoke Document', () => { ); expect(result).toBeDefined(); expect(coreModule.checkSupportsInterface).toHaveBeenCalledTimes(2); + vi.mocked(getEthersContractFromProvider).mockReturnValue( + mockContractConstructor(mockContract), + ); }); it('should handle invalid document hash format gracefully', async () => { From 21d69a0017d858a09141388d99602e305a8a4e5f Mon Sep 17 00:00:00 2001 From: Rishabh Singh Date: Thu, 12 Feb 2026 14:19:10 +0530 Subject: [PATCH 11/11] fix: test provider missing for both v5 and v6 --- src/__tests__/document-store/issue.test.ts | 4 +++- src/__tests__/document-store/revoke.test.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/__tests__/document-store/issue.test.ts b/src/__tests__/document-store/issue.test.ts index ca03917..1d4cdba 100644 --- a/src/__tests__/document-store/issue.test.ts +++ b/src/__tests__/document-store/issue.test.ts @@ -173,7 +173,9 @@ describe('Issue Document', () => { }); 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( documentStoreIssue(mockDocumentStoreAddress, mockDocumentHash, signerWithoutProvider, { diff --git a/src/__tests__/document-store/revoke.test.ts b/src/__tests__/document-store/revoke.test.ts index c276c22..bb7d718 100644 --- a/src/__tests__/document-store/revoke.test.ts +++ b/src/__tests__/document-store/revoke.test.ts @@ -167,7 +167,9 @@ describe('Revoke Document', () => { }); 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( documentStoreRevoke(mockDocumentStoreAddress, mockDocumentHash, signerWithoutProvider, { chainId: mockChainId,