diff --git a/src/__tests__/document-store/deploy.test.ts b/src/__tests__/document-store/deploy.test.ts index 8b870b2..433b71f 100644 --- a/src/__tests__/document-store/deploy.test.ts +++ b/src/__tests__/document-store/deploy.test.ts @@ -2,7 +2,7 @@ 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 { deployDocumentStore } from '../../deploy/document-store'; import { PRIVATE_KEY, providerV5, providerV6 } from './fixtures'; import { CHAIN_ID } from '../../utils'; diff --git a/src/__tests__/token-registry-functions/deploy.test.ts b/src/__tests__/token-registry-functions/deploy.test.ts new file mode 100644 index 0000000..290ad3c --- /dev/null +++ b/src/__tests__/token-registry-functions/deploy.test.ts @@ -0,0 +1,457 @@ +import './fixtures'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { Wallet as WalletV5 } from 'ethers'; +import { deployTokenRegistry } from '../../deploy/token-registry'; +import { PRIVATE_KEY, providerV5 } from './fixtures'; +import { CHAIN_ID } from '../../utils'; +import { + isV6EthersProvider, + getEthersContractFromProvider, + getEthersContractFactoryFromProvider, +} from '../../utils/ethers'; +import { + getChainIdSafe, + getDefaultContractAddress, + isSupportedTitleEscrowFactory, + isValidAddress, +} from '../../token-registry-functions/utils.js'; + +describe('Deploy Token Registry', () => { + const mockRegistryName = 'Test Token Registry'; + const mockRegistrySymbol = 'TTR'; + const mockChainId = CHAIN_ID.sepolia; + const mockFactoryAddress = '0x1234567890123456789012345678901234567890'; + const mockDeployerAddress = '0xabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd'; + const mockImplAddress = '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef'; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('Ethers v6 deployment scenarios', () => { + it('should deploy in quick-start mode with ethers v6', async () => { + (isSupportedTitleEscrowFactory as any).mockResolvedValue(true); + + const mockContract = { + deploy: vi.fn().mockResolvedValue({ + address: '0xDeployedTokenRegistry', + transactionHash: 'quick_start_tx_hash', + }), + }; + + const providerV6Mock: any = { + getNetwork: vi.fn().mockResolvedValue({ chainId: mockChainId }), + provider: {}, + }; + + const wallet: any = { + provider: providerV6Mock, + getAddress: vi.fn().mockResolvedValue('0xdeployer'), + }; + + // Mock getEthersContractFromProvider to return our mock contract + (getEthersContractFromProvider as any).mockReturnValue( + vi.fn().mockReturnValue(mockContract) as any, + ); + + const result = await deployTokenRegistry(mockRegistryName, mockRegistrySymbol, wallet, { + chainId: mockChainId, + standalone: false, + deployerContractAddress: mockDeployerAddress, + tokenRegistryImplAddress: mockImplAddress, + }); + + expect(mockContract.deploy).toHaveBeenCalledWith( + mockImplAddress, + '0xencodedparams', + expect.any(Object), + ); + expect(result).toEqual({ + address: '0xDeployedTokenRegistry', + transactionHash: 'quick_start_tx_hash', + }); + }); + + it('should deploy in standalone mode with ethers v6', async () => { + (isSupportedTitleEscrowFactory as any).mockResolvedValue(true); + + const mockDeployedContract = { + deploymentTransaction: vi.fn().mockReturnValue({ + wait: vi.fn().mockResolvedValue({ + address: '0xStandaloneTokenRegistryV6', + transactionHash: 'standalone_v6_tx_hash', + }), + }), + }; + + const mockFactory = { + deploy: vi.fn().mockResolvedValue(mockDeployedContract), + }; + + const providerV6Mock: any = { + getNetwork: vi.fn().mockResolvedValue({ chainId: mockChainId }), + provider: {}, + }; + + const wallet: any = { + provider: providerV6Mock, + getAddress: vi.fn().mockResolvedValue('0xdeployer'), + }; + + // const { getEthersContractFactoryFromProvider, isV6EthersProvider } = await import( + // '../../utils/ethers/index.js' + // ); + (getEthersContractFactoryFromProvider as any).mockReturnValue( + vi.fn().mockReturnValue(mockFactory) as any, + ); + (isV6EthersProvider as any).mockReturnValue(true); + + const result = await deployTokenRegistry(mockRegistryName, mockRegistrySymbol, wallet, { + chainId: mockChainId, + standalone: true, + factoryAddress: mockFactoryAddress, + }); + + expect(mockFactory.deploy).toHaveBeenCalledWith( + mockRegistryName, + mockRegistrySymbol, + mockFactoryAddress, + expect.any(Object), + ); + expect(result).toEqual({ + address: '0xStandaloneTokenRegistryV6', + transactionHash: 'standalone_v6_tx_hash', + }); + }); + }); + + describe('Ethers v5 deployment scenarios', () => { + it('should deploy in quick-start mode with ethers v5', async () => { + const mockContract = { + deploy: vi.fn().mockResolvedValue({ + address: '0xDeployedTokenRegistry', + transactionHash: 'quick_start_v5_tx_hash', + }), + }; + + const wallet = new WalletV5(PRIVATE_KEY, providerV5); + vi.spyOn(wallet, 'getAddress').mockResolvedValue('0xdeployer'); + + (getEthersContractFromProvider as any).mockReturnValue( + vi.fn().mockReturnValue(mockContract) as any, + ); + (isV6EthersProvider as any).mockReturnValue(false); + + const result = await deployTokenRegistry(mockRegistryName, mockRegistrySymbol, wallet, { + chainId: mockChainId, + standalone: false, + deployerContractAddress: mockDeployerAddress, + tokenRegistryImplAddress: mockImplAddress, + }); + + expect(result).toEqual({ + address: '0xDeployedTokenRegistry', + transactionHash: 'quick_start_v5_tx_hash', + }); + }); + + it('should deploy in standalone mode with ethers v5', async () => { + const mockDeployedContract = { + deployTransaction: { + wait: vi.fn().mockResolvedValue({ + address: '0xStandaloneTokenRegistryV5', + transactionHash: 'standalone_v5_tx_hash', + }), + }, + }; + + const mockFactory = { + deploy: vi.fn().mockResolvedValue(mockDeployedContract), + }; + + const wallet = new WalletV5(PRIVATE_KEY, providerV5); + vi.spyOn(wallet, 'getAddress').mockResolvedValue('0xdeployer'); + + // const { getEthersContractFactoryFromProvider, isV6EthersProvider } = await import( + // '../../utils/ethers/index.js' + // ); + (getEthersContractFactoryFromProvider as any).mockReturnValue( + vi.fn().mockReturnValue(mockFactory) as any, + ); + (isV6EthersProvider as any).mockReturnValue(false); + + const result = await deployTokenRegistry(mockRegistryName, mockRegistrySymbol, wallet, { + chainId: mockChainId, + standalone: true, + factoryAddress: mockFactoryAddress, + }); + + expect(result).toEqual({ + address: '0xStandaloneTokenRegistryV5', + transactionHash: 'standalone_v5_tx_hash', + }); + }); + + it('should use default addresses when custom addresses not provided', async () => { + const mockContract = { + deploy: vi.fn().mockResolvedValue({ + address: '0xDeployedTokenRegistry', + transactionHash: 'default_addr_tx_hash', + }), + }; + + const wallet: any = { + provider: { provider: {}, getNetwork: vi.fn() }, + getAddress: vi.fn().mockResolvedValue('0xdeployer'), + }; + + (getEthersContractFromProvider as any).mockReturnValue( + vi.fn().mockReturnValue(mockContract) as any, + ); + + await deployTokenRegistry(mockRegistryName, mockRegistrySymbol, wallet, { + chainId: mockChainId, + standalone: false, + }); + + expect(mockContract.deploy).toHaveBeenCalled(); + }); + + it('should throw error when network not supported in quick-start mode', async () => { + (getDefaultContractAddress as any).mockReturnValue({ + TitleEscrowFactory: undefined, + TokenImplementation: undefined, + Deployer: undefined, + }); + + const wallet: any = { + provider: { provider: {}, getNetwork: vi.fn() }, + getAddress: vi.fn().mockResolvedValue('0xdeployer'), + }; + + await expect( + deployTokenRegistry(mockRegistryName, mockRegistrySymbol, wallet, { + chainId: 999 as unknown as CHAIN_ID, + standalone: false, + }), + ).rejects.toThrow('currently is not supported'); + }); + }); + + describe('Configuration and defaults', () => { + it('should use default factory address when not provided', async () => { + (isSupportedTitleEscrowFactory as any).mockResolvedValue(true); + (getDefaultContractAddress as any).mockReturnValue({ + TitleEscrowFactory: mockFactoryAddress, + TokenImplementation: mockImplAddress, + Deployer: mockDeployerAddress, + }); + + const mockDeployedContract = { + deployTransaction: { + wait: vi.fn().mockResolvedValue({ + address: '0xDefaultFactoryTokenRegistry', + transactionHash: 'default_factory_tx_hash', + }), + }, + }; + + const mockFactory = { + deploy: vi.fn().mockResolvedValue(mockDeployedContract), + }; + + const wallet: any = { + provider: { provider: {}, getNetwork: vi.fn() }, + getAddress: vi.fn().mockResolvedValue('0xdeployer'), + }; + + (getEthersContractFactoryFromProvider as any).mockReturnValue( + vi.fn().mockReturnValue(mockFactory) as any, + ); + + await deployTokenRegistry(mockRegistryName, mockRegistrySymbol, wallet, { + chainId: mockChainId, + standalone: true, + }); + + expect(mockFactory.deploy).toHaveBeenCalledWith( + mockRegistryName, + mockRegistrySymbol, + mockFactoryAddress, + expect.any(Object), + ); + }); + + it('should throw error when factory address not supported', async () => { + (isSupportedTitleEscrowFactory as any).mockResolvedValue(false); + + const wallet: any = { + provider: { provider: {}, getNetwork: vi.fn() }, + getAddress: vi.fn().mockResolvedValue('0xdeployer'), + }; + + await expect( + deployTokenRegistry(mockRegistryName, mockRegistrySymbol, wallet, { + chainId: mockChainId, + standalone: true, + factoryAddress: '0x1234567890123456789012345678901234567890', + }), + ).rejects.toThrow('is not supported'); + }); + + it('should throw error when network not supported and no factory provided', async () => { + (getDefaultContractAddress as any).mockReturnValue({ + TitleEscrowFactory: undefined, + TokenImplementation: undefined, + Deployer: undefined, + }); + + const wallet: any = { + provider: { provider: {}, getNetwork: vi.fn() }, + getAddress: vi.fn().mockResolvedValue('0xdeployer'), + }; + + await expect( + deployTokenRegistry(mockRegistryName, mockRegistrySymbol, wallet, { + chainId: 999 as unknown as CHAIN_ID, + standalone: true, + }), + ).rejects.toThrow('currently is not supported'); + }); + }); + + describe('Auto mode selection', () => { + it('should auto-switch to standalone when quick-start contracts unavailable', async () => { + // Mock to return true for standalone factory check + (isSupportedTitleEscrowFactory as any).mockResolvedValue(true); + (getDefaultContractAddress as any).mockReturnValue({ + TitleEscrowFactory: '0x1234567890123456789012345678901234567890', + TokenImplementation: undefined, // Missing implementation + Deployer: undefined, // Missing deployer + }); + + const mockDeployedContract = { + deployTransaction: { + wait: vi.fn().mockResolvedValue({ + address: '0xAutoStandaloneRegistry', + transactionHash: 'auto_standalone_tx_hash', + }), + }, + }; + + const mockFactory = { + deploy: vi.fn().mockResolvedValue(mockDeployedContract), + }; + + const wallet: any = { + provider: { provider: {}, getNetwork: vi.fn() }, + getAddress: vi.fn().mockResolvedValue('0xdeployer'), + }; + + (getEthersContractFactoryFromProvider as any).mockReturnValue( + vi.fn().mockReturnValue(mockFactory) as any, + ); + + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + await deployTokenRegistry(mockRegistryName, mockRegistrySymbol, wallet, { + chainId: mockChainId, + // standalone not explicitly set, should auto-detect + }); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringContaining('Defaulting to standalone mode'), + ); + expect(mockFactory.deploy).toHaveBeenCalled(); + + consoleErrorSpy.mockRestore(); + }); + }); + + describe('Chain ID handling', () => { + it('should auto-detect chain ID when not provided', async () => { + // Reset isSupportedTitleEscrowFactory to return true (default behavior) + (isSupportedTitleEscrowFactory as any).mockResolvedValue(true); + + const mockDeployTransaction = { + wait: vi.fn().mockResolvedValue({ + address: '0xAutoChainIdRegistry', + transactionHash: 'auto_chain_tx_hash', + }), + }; + + const mockContract = { + deploy: vi.fn().mockResolvedValue({ + address: '0xAutoChainIdRegistry', + transactionHash: 'auto_chain_tx_hash', + }), + deployTransaction: mockDeployTransaction, + }; + + const wallet: any = { + provider: { + provider: {}, + getNetwork: vi.fn().mockResolvedValue({ chainId: mockChainId }), + }, + getAddress: vi.fn().mockResolvedValue('0xdeployer'), + }; + + (getEthersContractFromProvider as any).mockReturnValue( + vi.fn().mockReturnValue(mockContract) as any, + ); + + (getChainIdSafe as any).mockResolvedValue(CHAIN_ID.sepolia as unknown as number); + + await deployTokenRegistry(mockRegistryName, mockRegistrySymbol, wallet, { + chainId: mockChainId, + }); + }); + }); + + describe('Address validation', () => { + it('should validate custom factory address format', async () => { + // Use ethers v5 for this test + (isV6EthersProvider as any).mockReturnValue(false); + + (isValidAddress as any).mockImplementation( + (addr: any) => addr && addr.startsWith('0x') && addr.length === 42, + ); + + const wallet: any = { + provider: { provider: {}, getNetwork: vi.fn() }, + getAddress: vi.fn().mockResolvedValue('0xdeployer'), + }; + + const mockDeployedContract = { + deployTransaction: { + wait: vi.fn().mockResolvedValue({ + address: '0xStandaloneTokenRegistry', + transactionHash: 'invalid_addr_tx_hash', + }), + }, + }; + + const mockFactory = { + deploy: vi.fn().mockResolvedValue(mockDeployedContract), + }; + + (getEthersContractFactoryFromProvider as any).mockReturnValue( + vi.fn().mockReturnValue(mockFactory) as any, + ); + + // Should use default address when custom address is invalid + await deployTokenRegistry(mockRegistryName, mockRegistrySymbol, wallet, { + chainId: mockChainId, + standalone: true, + factoryAddress: '0xinvalidaddress', + }); + + expect(mockFactory.deploy).toHaveBeenCalledWith( + mockRegistryName, + mockRegistrySymbol, + '0x1234567890123456789012345678901234567890', + expect.any(Object), + ); + }); + }); +}); diff --git a/src/__tests__/token-registry-functions/fixtures.ts b/src/__tests__/token-registry-functions/fixtures.ts index 27c217f..9ab7c5d 100644 --- a/src/__tests__/token-registry-functions/fixtures.ts +++ b/src/__tests__/token-registry-functions/fixtures.ts @@ -2,17 +2,53 @@ import { vi } from 'vitest'; import { ethers as ethersV5 } from 'ethers'; import { JsonRpcProvider as JsonRpcProviderV6 } from 'ethersV6'; import * as originalModule from '../../utils/ethers'; +import * as tokenRegistryFunctions from '../../token-registry-functions/utils'; +import * as tokenRegistryV5 from '../../token-registry-v5/utils'; export const MOCK_V5_ADDRESS = '0xV5TokenRegistryContract'; export const MOCK_V4_ADDRESS = '0xV4TokenRegistryContract'; export const MOCK_OWNER_ADDRESS = '0xowner'; +vi.mock('../../token-registry-v5/utils', async (importOriginal) => { + const original = (await importOriginal()) as typeof tokenRegistryV5; + return { + ...original, + encodeInitParams: vi.fn().mockReturnValue('0xencodedparams'), + }; +}); + +// Mock the utility functions - don't use importOriginal to avoid loading the actual module +vi.mock('../../token-registry-functions/utils', async (importOriginal) => { + const original = (await importOriginal()) as typeof tokenRegistryFunctions; + + return { + ...original, + getChainIdSafe: vi.fn().mockImplementation(original.getChainIdSafe), + getDefaultContractAddress: vi.fn().mockReturnValue({ + TitleEscrowFactory: '0x1234567890123456789012345678901234567890', + TokenImplementation: '0x2234567890123456789012345678901234567890', + Deployer: '0x3234567890123456789012345678901234567890', + }), + isSupportedTitleEscrowFactory: vi.fn().mockResolvedValue(true), + isValidAddress: vi.fn((addr: string) => addr && addr.startsWith('0x') && addr.length === 42), + }; +}); + vi.mock('../../utils/ethers', async (importOriginal) => { const original = (await importOriginal()) as typeof originalModule; + // Mock contract constructor that returns contract with ownerOf method + const MockContractConstructor = vi.fn((address: string, abi: string) => { + // Determine which mock to return based on the address or ABI + const isV5 = address === MOCK_V5_ADDRESS || abi === 'TradeTrustToken'; + return isV5 ? mockV5TradeTrustTokenContract : mockV4TradeTrustTokenContract; + }); + return { ...original, // Keep all original exports - getEthersContractFromProvider: vi.fn(() => vi.fn()), // Only mock this function + getEthersContractFromProvider: vi.fn(() => MockContractConstructor), + getEthersContractFactoryFromProvider: vi.fn(() => vi.fn()), + isV6EthersProvider: vi.fn(() => true), }; }); @@ -27,8 +63,38 @@ vi.mock('../../core', () => ({ }, })); +vi.mock('@tradetrust-tt/token-registry-v5', () => ({ + constants: { + contractInterfaceId: {}, + contractAddress: { + TitleEscrowFactory: {}, + TokenImplementation: {}, + Deployer: {}, + }, + }, + utils: {}, +})); + +vi.mock('@tradetrust-tt/token-registry/contracts', () => ({ + TDocDeployer__factory: { + abi: ['function deploy(address, bytes)'], + }, + TradeTrustToken__factory: { + abi: ['constructor(string, string, address)'], + bytecode: '0x60806040', + }, +})); + vi.mock('../../token-registry-v5', () => { return { + constants: { + contractInterfaceId: {}, + contractAddress: { + TitleEscrowFactory: {}, + TokenImplementation: {}, + Deployer: {}, + }, + }, v5Contracts: { TitleEscrow__factory: { connect: vi.fn(() => mockV5TitleEscrowContract), @@ -300,4 +366,10 @@ export const mockV4TradeTrustTokenContract = { export const PRIVATE_KEY = '0x59c6995e998f97a5a004497e5f1ebce0c16828d44b3f8d0bfa3a89d271d5b6b9'; export const providerV5 = new ethersV5.providers.JsonRpcProvider(); +// Mock the getNetwork method for v5 provider +vi.spyOn(providerV5, 'getNetwork').mockResolvedValue({ + name: 'mainnet', + chainId: 1, +}); + export const providerV6 = new JsonRpcProviderV6(); diff --git a/src/__tests__/token-registry-functions/ownerOf.test.ts b/src/__tests__/token-registry-functions/ownerOf.test.ts index 05fb72a..527f902 100644 --- a/src/__tests__/token-registry-functions/ownerOf.test.ts +++ b/src/__tests__/token-registry-functions/ownerOf.test.ts @@ -5,8 +5,6 @@ import { Wallet as WalletV6, Network, ethers as ethersV6 } from 'ethersV6'; import * as coreModule from '../../core'; import { CHAIN_ID } from '../../utils'; import { ownerOf } from '../../token-registry-functions'; -import { v5Contracts } from '../../token-registry-v5'; -import { v4Contracts } from '../../token-registry-v4'; import { MOCK_OWNER_ADDRESS, MOCK_V4_ADDRESS, @@ -86,9 +84,7 @@ describe.each(providers)( ); expect(result).toBe(MOCK_OWNER_ADDRESS); - expect( - (isV5TT ? v5Contracts : v4Contracts).TradeTrustToken__factory.connect, - ).toHaveBeenCalled(); + expect(coreModule.checkSupportsInterface).toHaveBeenCalled(); }); it('should return owner for V5/v4 contract (explicit version)', async () => { diff --git a/src/__tests__/token-registry-functions/transfers.test.ts b/src/__tests__/token-registry-functions/transfers.test.ts index 6602553..f048598 100644 --- a/src/__tests__/token-registry-functions/transfers.test.ts +++ b/src/__tests__/token-registry-functions/transfers.test.ts @@ -430,8 +430,8 @@ describe.each(providers)('Transfers', async ({ Provider, ethersVersion, titleEsc if (isV5TT) expect(encrypt).toHaveBeenCalledWith('0xencrypted_remarks', 'doc-id'); const resultOptions = isV5TT - ? ['0xbeneficiary', '0xencrypted_remarks', {}] - : ['0xbeneficiary', {}]; + ? ['0xbeneficiary', '0xencrypted_remarks', { maxFeePerGas: 100, maxPriorityFeePerGas: 50 }] + : ['0xbeneficiary', { maxFeePerGas: 100, maxPriorityFeePerGas: 50 }]; expect(mockTitleEscrowContract.transferBeneficiary).toHaveBeenCalledWith(...resultOptions); expect(tx).toBe(txHash); @@ -471,7 +471,7 @@ describe.each(providers)('Transfers', async ({ Provider, ethersVersion, titleEsc vi.spyOn(coreModule, 'getTitleEscrowAddress').mockImplementation(() => Promise.resolve(titleEscrowAddress), ); - mockTitleEscrowContract.callStatic.transferBeneficiary.mockResolvedValue(true); + mockTitleEscrowContract.callStatic.transferOwners.mockResolvedValue(true); const tx = await transferOwners( { @@ -491,30 +491,6 @@ describe.each(providers)('Transfers', async ({ Provider, ethersVersion, titleEsc expect(tx).toBe(txHash); }); - it(`detects version automatically via supportsInterface for ${titleEscrowVersion}`, async () => { - vi.spyOn(coreModule, 'isTitleEscrowVersion').mockImplementation( - async ({ versionInterface }) => - versionInterface === (isV5TT ? '0xTitleEscrowIdV5' : '0xTitleEscrowIdV4'), - ); - mockTitleEscrowContract.callStatic.transferOwners.mockResolvedValue(true); - - const tx = await transferOwners( - { - titleEscrowAddress: '0xauto', - }, - wallet, - params, - {}, // no isV5TT provided - ); - - expect(coreModule.isTitleEscrowVersion).toHaveBeenCalledWith({ - provider: wallet.provider, - titleEscrowAddress: '0xauto', - versionInterface: isV5TT ? '0xTitleEscrowIdV5' : '0xTitleEscrowIdV4', - }); - expect(tx).toBe(txHash); - }); - it('calls gas station when gas options are missing', async () => { const gasStationMock = vi.fn().mockResolvedValue({ maxFeePerGas: 100, @@ -610,8 +586,13 @@ describe.each(providers)('Transfers', async ({ Provider, ethersVersion, titleEsc if (isV5TT) expect(encrypt).toHaveBeenCalledWith('0xencrypted_remarks', 'doc-id'); const resultOptions = isV5TT - ? ['0xbeneficiary', '0xholder', '0xencrypted_remarks', {}] - : ['0xbeneficiary', '0xholder', {}]; + ? [ + '0xbeneficiary', + '0xholder', + '0xencrypted_remarks', + { maxFeePerGas: 100, maxPriorityFeePerGas: 50 }, + ] + : ['0xbeneficiary', '0xholder', { maxFeePerGas: 100, maxPriorityFeePerGas: 50 }]; expect(mockTitleEscrowContract.transferOwners).toHaveBeenCalledWith(...resultOptions); expect(tx).toBe(txHash); @@ -778,8 +759,8 @@ describe.each(providers)('Transfers', async ({ Provider, ethersVersion, titleEsc if (isV5TT) expect(encrypt).toHaveBeenCalledWith('0xencrypted_remarks', 'doc-id'); const resultOptions = isV5TT - ? ['0xbeneficiary', '0xencrypted_remarks', {}] - : ['0xbeneficiary', {}]; + ? ['0xbeneficiary', '0xencrypted_remarks', { maxFeePerGas: 100, maxPriorityFeePerGas: 50 }] + : ['0xbeneficiary', { maxFeePerGas: 100, maxPriorityFeePerGas: 50 }]; expect(mockTitleEscrowContract.nominate).toHaveBeenCalledWith(...resultOptions); expect(tx).toBe(txHash); diff --git a/src/deploy/document-store.ts b/src/deploy/document-store.ts new file mode 100644 index 0000000..43fa682 --- /dev/null +++ b/src/deploy/document-store.ts @@ -0,0 +1,156 @@ +/** + * Document Store Deployment Module + * + * This module provides functionality to deploy TrustVC Document Store contracts + * with support for both ethers v5 and v6 signers. It supports two types of stores: + * + * 1. **Standard Document Store**: For issuing and revoking verifiable documents + * - Immutable ownership (documents cannot be transferred) + * - Suitable for most credential use cases + * + * 2. **Transferable Document Store**: For documents that can change ownership + * - Supports ownership transfers between addresses + * - Useful for transferable credentials or certificates + */ + +import { + DocumentStore__factory, + TransferableDocumentStore__factory, +} from '@trustvc/document-store'; +import { + Signer as SignerV6, + ContractTransactionReceipt as ContractReceiptV6, + ContractFactory as ContractFactoryV6, +} from 'ethersV6'; +import { + Signer as SignerV5, + ContractReceipt as ContractReceiptV5, + ContractFactory as ContractFactoryV5, +} from 'ethers'; +import { getEthersContractFactoryFromProvider, isV6EthersProvider } from '../utils/ethers'; +import { CHAIN_ID } from '../utils'; +import { GasValue } from '../token-registry-functions/types'; +import { getTxOptions } from '../token-registry-functions/utils'; + +/** + * Configuration options for Document Store deployment + */ +export interface DeployOptions { + // Chain ID for deployment (auto-detected if not provided) + chainId?: CHAIN_ID; + // Maximum fee per gas unit (EIP-1559) + maxFeePerGas?: GasValue; + // Maximum priority fee per gas (EIP-1559) + maxPriorityFeePerGas?: GasValue; + // If true, deploys TransferableDocumentStore; if false, deploys standard DocumentStore + isTransferable?: boolean; +} + +/** + * Union type for transaction receipts from both ethers v5 and v6 + */ +export type TransactionReceipt = ContractReceiptV5 | ContractReceiptV6; + +/** + * Deploys a new Document Store contract with automatic type selection. + * **Store Types:** + * - **Standard** (default): Documents are immutable and cannot be transferred + * - **Transferable**: Documents can be transferred to different owners + * **Ethers Compatibility:** + * - Automatically detects and handles both ethers v5 and v6 signers + * - Returns appropriate receipt type based on signer version + * @param {string} storeName - The name of the document store (e.g., "My University Credentials") + * @param {string} owner - The owner address that will control the document store + * @param {SignerV5 | SignerV6} signer - Signer instance that authorizes the deployment + * @param {DeployOptions} options - Configuration options for deployment + * @returns {Promise} Transaction receipt with deployed contract address + * @throws {Error} If store name is not provided + * @throws {Error} If owner address is not provided + * @throws {Error} If signer provider is not available + * @throws {Error} If deployment transaction fails + * @example + * ```typescript + * Deploy standard document store + * const receipt = await deployDocumentStore( + * "My Document Store", + * "0x1234...", + * signer, + * { chainId: CHAIN_ID.SEPOLIA } + * ); + * + * Deploy transferable document store + * const receipt = await deployDocumentStore( + * "My Transferable Store", + * "0x1234...", + * signer, + * { + * isTransferable: true, + * maxFeePerGas: 50000000000n + * } + * ); + * ``` + */ +const deployDocumentStore = async ( + storeName: string, + owner: string, + signer: SignerV5 | SignerV6, + options: DeployOptions = {}, +): Promise => { + // Validate required parameters + 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'); + + // Extract deployment options + const { chainId, maxFeePerGas, maxPriorityFeePerGas } = options; + + // Get transaction options (gas settings) + const txOptions = await getTxOptions(signer, chainId, maxFeePerGas, maxPriorityFeePerGas); + + // Detect signer version for proper deployment handling + const isV6 = isV6EthersProvider(signer.provider); + + // Select appropriate factory based on transferability requirement + const DocumentStoreFactory = options.isTransferable + ? TransferableDocumentStore__factory + : DocumentStore__factory; + + // Get appropriate ContractFactory class for signer version + const ContractFactory = getEthersContractFactoryFromProvider(signer.provider); + + // Create contract factory with Document Store bytecode + const contractFactory = new ContractFactory( + DocumentStoreFactory.abi, + DocumentStoreFactory.bytecode, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, // Type assertion needed for v5/v6 compatibility + ); + try { + // Deploy contract with version-specific handling + if (isV6) { + // Ethers v6: Use deploymentTransaction() method + const contract = await (contractFactory as ContractFactoryV6).deploy( + storeName, + owner, + txOptions, + ); + return await contract.deploymentTransaction().wait(); + } else { + // Ethers v5: Use deployTransaction property + const contract = await (contractFactory as ContractFactoryV5).deploy( + storeName, + owner, + txOptions, + ); + return await contract.deployTransaction.wait(); + } + } catch (e) { + // Provide detailed error message on deployment failure + 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/deploy/token-registry.ts b/src/deploy/token-registry.ts new file mode 100644 index 0000000..f4cda14 --- /dev/null +++ b/src/deploy/token-registry.ts @@ -0,0 +1,240 @@ +/** + * Token Registry Deployment Module + * + * This module provides functionality to deploy TradeTrust Token Registry contracts + * with support for both ethers v5 and v6 signers. It handles two deployment modes: + * + * 1. **Quick-start mode** (default): Uses a pre-deployed deployer contract and implementation + * - Faster deployment with lower gas costs + * - Requires network to have deployer and implementation contracts + * + * 2. **Standalone mode**: Deploys a fresh Token Registry contract from scratch + * - Works on any network with a supported Title Escrow Factory + * - Higher gas costs but more flexible + */ + +import { + TDocDeployer__factory, + TradeTrustToken__factory, +} from '@tradetrust-tt/token-registry/contracts'; +import { GasValue } from '../token-registry-functions/types'; +import { + getChainIdSafe, + getDefaultContractAddress, + getTxOptions, + isSupportedTitleEscrowFactory, + isValidAddress, +} from '../token-registry-functions/utils'; +import { encodeInitParams } from '../token-registry-v5/utils'; +import { CHAIN_ID } from '../utils'; +import { + getEthersContractFactoryFromProvider, + getEthersContractFromProvider, + isV6EthersProvider, +} from '../utils/ethers'; +import { + Signer as SignerV6, + ContractTransactionReceipt as ContractReceiptV6, + ContractFactory as ContractFactoryV6, + ContractTransactionResponse as ContractTransactionV6, +} from 'ethersV6'; +import { + Signer as SignerV5, + ContractReceipt as ContractReceiptV5, + ContractFactory as ContractFactoryV5, + ContractTransaction as ContractTransactionV5, +} from 'ethers'; + +/** + * Union type for transaction receipts from both ethers v5 and v6 + */ +type ContractTransaction = ContractTransactionV5 | ContractTransactionV6; +export type TransactionReceipt = ContractReceiptV5 | ContractReceiptV6 | ContractTransaction; + +/** + * Configuration options for Token Registry deployment + */ +export interface DeployOptions { + // Chain ID for deployment (auto-detected if not provided) + chainId?: CHAIN_ID; + // Maximum fee per gas unit (EIP-1559) + maxFeePerGas?: GasValue; + // Maximum priority fee per gas (EIP-1559) + maxPriorityFeePerGas?: GasValue; + // If true, deploys standalone contract; if false, uses deployer contract + standalone?: boolean; + // Custom Title Escrow Factory address (for standalone mode) + factoryAddress?: string; + // Custom Token Registry implementation address (for quick-start mode) + tokenRegistryImplAddress?: string; + // Custom deployer contract address (for quick-start mode) + deployerContractAddress?: string; +} + +/** + * Deploys a new Token Registry contract with automatic mode selection. + * + * **Deployment Modes:** + * - **Quick-start** (default): Uses pre-deployed contracts for faster, cheaper deployment + * - **Standalone**: Deploys fresh contract when quick-start is unavailable + * + * **Ethers Compatibility:** + * - Automatically detects and handles both ethers v5 and v6 signers + * - Returns appropriate receipt type based on signer version + * @param {string} registryName - The name of the token registry (e.g., "My Token Registry") + * @param {string} registrySymbol - The symbol of the token registry (e.g., "MTR") + * @param {SignerV5 | SignerV6} signer - Signer instance that authorizes the deployment + * @param {DeployOptions} options - Configuration options for deployment + * @returns {Promise} Transaction receipt with deployed contract address + * @throws {Error} If network is not supported and no custom addresses provided + * @throws {Error} If Title Escrow Factory is not supported (standalone mode) + * @throws {Error} If deployment transaction fails + * @example + * ```typescript + * // Quick-start deployment + * const receipt = await deployTokenRegistry( + * "My Registry", + * "MTR", + * signer, + * { chainId: CHAIN_ID.SEPOLIA } + * ); + * + * // Standalone deployment with custom factory + * const receipt = await deployTokenRegistry( + * "My Registry", + * "MYR", + * signer, + * { + * standalone: true, + * factoryAddress: "0x..." + * } + * ); + * ``` + */ + +export const deployTokenRegistry = async ( + registryName: string, + registrySymbol: string, + signer: SignerV5 | SignerV6, + options: DeployOptions = {}, +): Promise => { + // Extract gas options + const { maxFeePerGas, maxPriorityFeePerGas } = options; + let { chainId, standalone, factoryAddress, tokenRegistryImplAddress, deployerContractAddress } = + options; + + // Get deployer's address for initialization + const deployerAddress = await signer.getAddress(); + + // Auto-detect chain ID if not provided + if (!chainId) { + chainId = (await getChainIdSafe(signer)) as unknown as CHAIN_ID; + } + // Get default contract addresses for this chain + const { + TitleEscrowFactory: defaultTitleEscrowFactoryAddress, + TokenImplementation: defaultTokenImplementationContractAddress, + Deployer: defaultDeployerContractAddress, + } = getDefaultContractAddress(chainId); + + // Use default addresses if custom ones not provided or invalid + if (!isValidAddress(deployerContractAddress)) { + deployerContractAddress = defaultDeployerContractAddress; + } + if (!isValidAddress(tokenRegistryImplAddress)) { + tokenRegistryImplAddress = defaultTokenImplementationContractAddress; + } + + // Auto-switch to standalone mode if quick-start contracts unavailable + if (standalone !== false && (!deployerContractAddress || !tokenRegistryImplAddress)) { + console.error( + `Network ${chainId} does not support "quick-start" mode. Defaulting to standalone mode.`, + ); + standalone = true; + } + + // === QUICK-START MODE: Use pre-deployed contracts === + if (!standalone) { + // Validate required contracts are available + if (!deployerContractAddress || !tokenRegistryImplAddress) { + throw new Error(`Network ${chainId} currently is not supported. Use --standalone instead.`); + } + + // Get appropriate Contract class for signer version (v5 or v6) + const Contract = getEthersContractFromProvider(signer.provider); + + // Connect to the deployer contract + const deployerContract = new Contract( + deployerContractAddress, + TDocDeployer__factory.abi, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, // Type assertion needed for v5/v6 compatibility + ); + + // Encode initialization parameters for the Token Registry + const initParam = encodeInitParams({ + name: registryName, + symbol: registrySymbol, + deployer: deployerAddress, + }); + + // Get transaction options (gas settings) + const txOptions = await getTxOptions(signer, chainId, maxFeePerGas, maxPriorityFeePerGas); + + // Deploy using the deployer contract (creates a minimal proxy) + return await deployerContract.deploy(tokenRegistryImplAddress, initParam, txOptions); + } else { + // === STANDALONE MODE: Deploy fresh contract === + + // Validate or use default Title Escrow Factory address + if (!factoryAddress || !isValidAddress(factoryAddress)) { + factoryAddress = defaultTitleEscrowFactoryAddress; + if (!factoryAddress) { + throw new Error(`Network ${chainId} currently is not supported. Supply a factory address.`); + } + } + + // Verify the Title Escrow Factory supports required interface + const supportedTitleEscrowFactory = await isSupportedTitleEscrowFactory( + factoryAddress, + signer.provider, + ); + if (!supportedTitleEscrowFactory) { + throw new Error(`Title Escrow Factory ${factoryAddress} is not supported.`); + } + + // Get appropriate ContractFactory class for signer version + const Contract = getEthersContractFactoryFromProvider(signer.provider); + // Create contract factory with Token Registry bytecode + const tokenFactory = new Contract( + TradeTrustToken__factory.abi, + TradeTrustToken__factory.bytecode, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, // Type assertion needed for v5/v6 compatibility + ); + + // Detect signer version for proper deployment handling + const isV6 = isV6EthersProvider(signer.provider); + const txOptions = await getTxOptions(signer, chainId, maxFeePerGas, maxPriorityFeePerGas); + // Deploy contract with version-specific handling + if (isV6) { + // Ethers v6: Use deploymentTransaction() method + const contract = await (tokenFactory as ContractFactoryV6).deploy( + registryName, + registrySymbol, + factoryAddress, + txOptions, + ); + return await contract.deploymentTransaction().wait(); + } else { + // Ethers v5: Use deployTransaction property + const contract = await (tokenFactory as ContractFactoryV5).deploy( + registryName, + registrySymbol, + factoryAddress, + txOptions, + ); + return await contract.deployTransaction.wait(); + } + } +}; diff --git a/src/document-store/deploy.ts b/src/document-store/deploy.ts deleted file mode 100644 index 4294d52..0000000 --- a/src/document-store/deploy.ts +++ /dev/null @@ -1,96 +0,0 @@ -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); - 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, - ); - const contract = await ContractFactory.deploy(storeName, owner, txOptions); - 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/grant-role.ts b/src/document-store/grant-role.ts index d09a823..960a2cc 100644 --- a/src/document-store/grant-role.ts +++ b/src/document-store/grant-role.ts @@ -1,7 +1,7 @@ import { Signer as SignerV6, Contract as ContractV6, - ContractTransaction as ContractTransactionV6, + ContractTransactionResponse as ContractTransactionV6, } from 'ethersV6'; import { Contract as ContractV5, diff --git a/src/document-store/index.ts b/src/document-store/index.ts index c8bd451..a84743f 100644 --- a/src/document-store/index.ts +++ b/src/document-store/index.ts @@ -3,7 +3,7 @@ export { documentStoreRevoke } from './revoke'; export { documentStoreRevokeRole } from './revoke-role'; export { documentStoreGrantRole } from './grant-role'; export { documentStoreTransferOwnership } from './transferOwnership'; -export { deployDocumentStore } from './deploy'; +export { deployDocumentStore } from '../deploy/document-store'; export { supportInterfaceIds } from './supportInterfaceIds'; export { DocumentStore__factory, diff --git a/src/document-store/transferOwnership.ts b/src/document-store/transferOwnership.ts index e8464fd..c06e114 100644 --- a/src/document-store/transferOwnership.ts +++ b/src/document-store/transferOwnership.ts @@ -1,4 +1,4 @@ -import { Signer as SignerV6, ContractTransaction as ContractTransactionV6 } from 'ethersV6'; +import { Signer as SignerV6, ContractTransactionResponse as ContractTransactionV6 } from 'ethersV6'; import { ContractTransaction as ContractTransactionV5, Signer as SignerV5 } from 'ethers'; import { documentStoreRevokeRole } from './revoke-role'; diff --git a/src/token-registry-functions/index.ts b/src/token-registry-functions/index.ts index 607e167..0e37d05 100644 --- a/src/token-registry-functions/index.ts +++ b/src/token-registry-functions/index.ts @@ -3,3 +3,4 @@ export * from './rejectTransfers'; export * from './returnToken'; export * from './mint'; export * from './ownerOf'; +export * from '../deploy/token-registry'; diff --git a/src/token-registry-functions/ownerOf.ts b/src/token-registry-functions/ownerOf.ts index 9cd2c32..be5a0e1 100644 --- a/src/token-registry-functions/ownerOf.ts +++ b/src/token-registry-functions/ownerOf.ts @@ -1,15 +1,16 @@ import { checkSupportsInterface } from '../core'; import { v5Contracts, v5SupportInterfaceIds } from '../token-registry-v5'; import { v4Contracts, v4SupportInterfaceIds } from '../token-registry-v4'; -import { Signer as SignerV6 } from 'ethersV6'; -import { Signer } from 'ethers'; +import { Signer as SignerV6, Contract as ContractV6 } from 'ethersV6'; +import { Signer as SignerV5, Contract as ContractV5 } from 'ethers'; import { OwnerOfTokenOptions, OwnerOfTokenParams, TransactionOptions } from './types'; +import { getEthersContractFromProvider } from '../utils/ethers'; /** * Retrieves the owner of a given token from the TradeTrustToken contract. * Supports both Token Registry V4 and V5 implementations. * @param {OwnerOfTokenOptions} contractOptions - Options containing the token registry address. - * @param {Signer | SignerV6} signer - Signer instance (v5 or v6) used to query the blockchain. + * @param {SignerV5 | SignerV6} signer - Signer instance (v5 or v6) used to query the blockchain. * @param {OwnerOfTokenParams} params - Contains the `tokenId` of the token to query ownership for. * @param {TransactionOptions} options - Includes the `titleEscrowVersion` and other optional metadata for interface detection. * @returns {Promise} A promise that resolves to the owner address of the specified token. @@ -18,7 +19,7 @@ import { OwnerOfTokenOptions, OwnerOfTokenParams, TransactionOptions } from './t */ const ownerOf = async ( contractOptions: OwnerOfTokenOptions, - signer: Signer | SignerV6, + signer: SignerV5 | SignerV6, params: OwnerOfTokenParams, options: TransactionOptions, ): Promise => { @@ -43,26 +44,27 @@ const ownerOf = async ( if (!isV4TT && !isV5TT) { throw new Error('Only Token Registry V4/V5 is supported'); } + const Contract = getEthersContractFromProvider(signer.provider); // Connect V5 contract by default - let tradeTrustTokenContract: v5Contracts.TradeTrustToken | v4Contracts.TradeTrustToken; + let tradeTrustTokenContract: ContractV5 | ContractV6; if (isV5TT) { - tradeTrustTokenContract = v5Contracts.TradeTrustToken__factory.connect( + tradeTrustTokenContract = new Contract( tokenRegistryAddress, - signer, + v5Contracts.TradeTrustToken__factory.abi, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, ); } else if (isV4TT) { - tradeTrustTokenContract = v4Contracts.TradeTrustToken__factory.connect( + tradeTrustTokenContract = new Contract( tokenRegistryAddress, - signer as Signer, + v4Contracts.TradeTrustToken__factory.abi, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, ); } // Send the actual transaction - if (isV5TT) { - return await (tradeTrustTokenContract as v5Contracts.TradeTrustToken).ownerOf(tokenId); - } else if (isV4TT) { - return await (tradeTrustTokenContract as v4Contracts.TradeTrustToken).ownerOf(tokenId); - } + return await tradeTrustTokenContract.ownerOf(tokenId); }; export { ownerOf }; diff --git a/src/token-registry-functions/types.ts b/src/token-registry-functions/types.ts index c47e7e4..da89382 100644 --- a/src/token-registry-functions/types.ts +++ b/src/token-registry-functions/types.ts @@ -2,6 +2,13 @@ import { CHAIN_ID } from '../utils'; import { BigNumber, providers as providersV5 } from 'ethers'; import { BigNumberish, Provider as ProviderV6 } from 'ethersV6'; +export interface GasPriceScale { + maxPriorityFeePerGasScale: number; +} +export interface GasOption extends GasPriceScale { + dryRun: boolean; +} + export type GasValue = BigNumber | BigNumberish | string | number; export interface RejectTransferParams { @@ -87,3 +94,30 @@ export interface ProviderInfo { ethersVersion: 'v5' | 'v6'; titleEscrowVersion: 'v4' | 'v5'; } + +export interface NetworkOption { + network: string; +} + +export type WalletOption = { + encryptedWalletPath: string; +}; + +export type PrivateKeyOption = + | { + key?: string; + keyFile?: never; + } + | { + key?: never; + keyFile?: string; + }; + +export type NetworkAndWalletSignerOption = NetworkOption & + (Partial | Partial); + +export interface DeployContractAddress { + TitleEscrowFactory?: string; + TokenImplementation?: string; + Deployer?: string; +} diff --git a/src/token-registry-functions/utils.ts b/src/token-registry-functions/utils.ts index 6bfe568..061b33a 100644 --- a/src/token-registry-functions/utils.ts +++ b/src/token-registry-functions/utils.ts @@ -1,8 +1,9 @@ -import { isV6EthersProvider } from '../utils/ethers'; -import { GasValue } from './types'; +import { getEthersContractFromProvider, isV6EthersProvider } from '../utils/ethers'; +import { DeployContractAddress, GasValue } from './types'; import { CHAIN_ID, SUPPORTED_CHAINS } from '../utils'; -import { Signer } from 'ethers'; -import { Signer as SignerV6 } from 'ethersV6'; +import { Signer, providers } from 'ethers'; +import { isAddress, Signer as SignerV6, Provider as ProviderV6 } from 'ethersV6'; +import { constants, v5Contracts } from '../token-registry-v5'; const getTxOptions = async ( signer: SignerV6 | Signer, @@ -41,4 +42,52 @@ const getSignerAddressSafe = async (signer: SignerV6 | Signer): Promise return await (signer as unknown as Signer).getAddress(); }; -export { getChainIdSafe, getTxOptions, getSignerAddressSafe }; +const { contractInterfaceId: CONTRACT_INTERFACE_ID, contractAddress: CONTRACT_ADDRESS } = constants; + +const getDefaultContractAddress = (chainId: CHAIN_ID): DeployContractAddress => { + const { TitleEscrowFactory, TokenImplementation, Deployer } = CONTRACT_ADDRESS; + const chainTitleEscrowFactory = TitleEscrowFactory[chainId]; + const chainTokenImplementation = TokenImplementation[chainId]; + const chainDeployer = Deployer[chainId]; + return { + TitleEscrowFactory: chainTitleEscrowFactory, + TokenImplementation: chainTokenImplementation, + Deployer: chainDeployer, + }; +}; + +const isValidAddress = (address?: string): boolean => { + if (!address) return false; + return isAddress(address); +}; + +export const isSupportedTitleEscrowFactory = async ( + factoryAddress: string, + provider: providers.Provider | ProviderV6, +): Promise => { + const Contract = getEthersContractFromProvider(provider); + const titleEscrowFactoryContract = new Contract( + factoryAddress, + ['function implementation() view returns (address)'], + // eslint-disable-next-line @typescript-eslint/no-explicit-any + provider as any, + ) as unknown as v5Contracts.TitleEscrowFactory; + const implAddr = await titleEscrowFactoryContract.implementation(); + + const implContract = new Contract( + implAddr, + ['function supportsInterface(bytes4 interfaceId) view returns (bool)'], + // eslint-disable-next-line @typescript-eslint/no-explicit-any + provider as any, + ); + const { TitleEscrow: titleEscrowInterfaceId } = CONTRACT_INTERFACE_ID; + return implContract.supportsInterface(titleEscrowInterfaceId); +}; + +export { + getChainIdSafe, + getTxOptions, + getSignerAddressSafe, + getDefaultContractAddress, + isValidAddress, +}; diff --git a/src/token-registry-v5/index.ts b/src/token-registry-v5/index.ts index 269bdcf..78fe8ec 100644 --- a/src/token-registry-v5/index.ts +++ b/src/token-registry-v5/index.ts @@ -3,9 +3,9 @@ import { roleHash } from './roleHash'; import { supportInterfaceIds } from './supportInterfaceIds'; import * as v5Contracts from './contracts'; import { encodeInitParams, getEventFromReceipt, computeInterfaceId } from './utils'; -import { utils } from '@tradetrust-tt/token-registry-v4'; +import { utils } from '@tradetrust-tt/token-registry-v5'; -export { constants } from '@tradetrust-tt/token-registry-v4'; +export { constants } from '@tradetrust-tt/token-registry-v5'; export type { TypedContractMethod } from './typedContractMethod'; export { contractAddress as v5ContractAddress, diff --git a/src/utils/ethers/index.ts b/src/utils/ethers/index.ts index 294b6e6..2149e79 100644 --- a/src/utils/ethers/index.ts +++ b/src/utils/ethers/index.ts @@ -1,6 +1,10 @@ -import { ethers } from 'ethers'; -import { ethers as ethersV6 } from 'ethersV6'; -import { Provider } from '@ethersproject/abstract-provider'; +import { + Provider as ProviderV6, + Contract as ContractV6, + ContractFactory as ContractFactoryV6, +} from 'ethersV6'; +import { providers, Contract as ContractV5, ContractFactory as ContractFactoryV5 } from 'ethers'; +type ProviderV5 = providers.Provider; // eslint-disable-next-line @typescript-eslint/no-explicit-any export const isV6EthersProvider = (provider: any): boolean => { @@ -22,7 +26,13 @@ export const isV6EthersProvider = (provider: any): boolean => { }; export const getEthersContractFromProvider = ( - provider: Provider | ethersV6.Provider, -): typeof ethers.Contract | typeof ethersV6.Contract => { - return isV6EthersProvider(provider) ? ethersV6.Contract : ethers.Contract; + provider: ProviderV5 | ProviderV6, +): typeof ContractV5 | typeof ContractV6 => { + return isV6EthersProvider(provider) ? ContractV6 : ContractV5; +}; + +export const getEthersContractFactoryFromProvider = ( + provider: ProviderV5 | ProviderV6, +): typeof ContractFactoryV5 | typeof ContractFactoryV6 => { + return isV6EthersProvider(provider) ? ContractFactoryV6 : ContractFactoryV5; };