import { BaseContract, ContractReceipt, ContractTransaction } from 'ethers' import { useCallback, useState } from 'react' import { rejectTransferHolder, rejectTransferBeneficiary, transferBeneficiary, transferHolder, transferOwners, nominate, returnToIssuer, rejectReturned, acceptReturned, rejectTransferOwners, acceptObligationRegistry, rejectObligationRegistry, dischargeObligationRegistry, transferHolderObligationRegistry, transferBeneficiaryObligationRegistry, transferOwnersObligationRegistry, nominateObligationRegistry, returnToIssuerObligationRegistry, rejectTransferHolderObligationRegistry, rejectTransferBeneficiaryObligationRegistry, rejectTransferOwnersObligationRegistry, acceptReturnedObligationRegistry, rejectReturnedObligationRegistry, } from '@trustvc/trustvc' import { TitleEscrow, TradeTrustToken } from '../types' import { TypedContractMethod } from '@trustvc/trustvc' import { useDocumentContext } from '../components/common/contexts/DocumentContext' const METAMASK_NUMERIC_CODES: Record = { 4001: 'User Rejected Transaction', 4100: 'Unauthorized: Account or method not authorized', 4200: 'Unsupported Method', 4900: 'Wallet Disconnected', 4901: 'Chain Disconnected', [-32700]: 'Parse Error', [-32600]: 'Invalid Request', [-32601]: 'Method Not Found', [-32602]: 'Invalid Parameters', [-32000]: 'Invalid Input', [-32001]: 'Resource Not Found', [-32002]: 'Request Already Pending', [-32003]: 'Transaction Rejected', [-32004]: 'Method Not Supported', [-32005]: 'Request Limit Exceeded', } const ETHERS_STRING_CODES: Record = { ACTION_REJECTED: 'User Rejected Transaction', INSUFFICIENT_FUNDS: 'Insufficient Funds', UNPREDICTABLE_GAS_LIMIT: 'Unable to Estimate Gas', NETWORK_ERROR: 'Network Error', SERVER_ERROR: 'Server Error', TIMEOUT: 'Request Timed Out', CALL_EXCEPTION: 'Contract Call Failed', TRANSACTION_REPLACED: 'Transaction Replaced', NONCE_EXPIRED: 'Nonce Already Used', REPLACEMENT_UNDERPRICED: 'Replacement Transaction Underpriced', } export const getMetaMaskErrorMessage = (e: unknown): string => { const code = (e as any)?.code if (typeof code === 'number' && code in METAMASK_NUMERIC_CODES) { return METAMASK_NUMERIC_CODES[code] } if (typeof code === 'string' && code in ETHERS_STRING_CODES) { return ETHERS_STRING_CODES[code] } return '' } // Classic ETR title-escrow / token-registry methods const trustvcFunctions: Record any> = { transferHolder, transferBeneficiary, transferOwners, rejectTransferHolder, rejectTransferBeneficiary, rejectTransferOwners, nominate, returnToIssuer, rejectReturned, acceptReturned, } // BoE obligation-registry / obligation-escrow methods (same UI method names) const obligationFunctions: Record any> = { transferHolder: transferHolderObligationRegistry, transferBeneficiary: transferBeneficiaryObligationRegistry, transferOwners: transferOwnersObligationRegistry, rejectTransferHolder: rejectTransferHolderObligationRegistry, rejectTransferBeneficiary: rejectTransferBeneficiaryObligationRegistry, rejectTransferOwners: rejectTransferOwnersObligationRegistry, nominate: nominateObligationRegistry, returnToIssuer: returnToIssuerObligationRegistry, rejectReturned: rejectReturnedObligationRegistry, acceptReturned: acceptReturnedObligationRegistry, accept: acceptObligationRegistry, reject: rejectObligationRegistry, discharge: dischargeObligationRegistry, } /** Map classic { tokenRegistryAddress, titleEscrowAddress, tokenId } → obligation options. */ const toObligationContractOptions = (opts: any) => { if (!opts) return opts if (opts.obligationRegistryAddress || opts.obligationEscrowAddress) { return opts } return { obligationRegistryAddress: opts.tokenRegistryAddress, obligationEscrowAddress: opts.titleEscrowAddress, tokenId: opts.tokenId, } } export type ContractFunctionState = | 'UNINITIALIZED' | 'INITIALIZED' | 'PENDING_CONFIRMATION' | 'CONFIRMED' | 'ERROR' type UnwrapPromise = T extends Promise ? U : T // Todo // Deploy // Deploy & Initialize // interface useContractFunctionHookReturn { // state: ContractFunctionState; // receipt?: ContractReceipt; // transaction?: ContractTransaction; // error?: Error; // value: // } export function useContractFunctionHook< T extends BaseContract | TitleEscrow | TradeTrustToken, S extends keyof T, R = void, >( contract?: T, method?: S, contractOptions?: any, providerOrSigner?: any, /** Optional synchronous guard run before dispatch; throw to reject with a normal error message. */ preflightCheck?: () => void ): { call: TypedContractMethod< any[], ReturnType any ? T[S] : never>, 'nonpayable' > send: TypedContractMethod reset: () => void state: ContractFunctionState receipt?: ContractReceipt transaction?: ContractTransaction errorMessage?: string value?: UnwrapPromise< ReturnType any ? T[S] : never> > events?: ContractReceipt['events'] transactionHash?: string } { const [state, setState] = useState('UNINITIALIZED') const [receipt, setReceipt] = useState() const [transaction, setTransaction] = useState() const [errorMessage, setErrorMessage] = useState() const [value, setValue] = useState< UnwrapPromise< ReturnType any ? T[S] : never> > >() // Reset state is added to allow the same hook to be used for multiple transactions although // it is highly unrecommended. const resetState = (): void => { setState('UNINITIALIZED') setReceipt(undefined) setTransaction(undefined) setErrorMessage(undefined) setValue(undefined) } const { keyId, isObligation } = useDocumentContext() const sendFn = (async (params: any) => { if (!contract || !method) { setState('ERROR') setErrorMessage('Contract or method is not specified') return } resetState() try { preflightCheck?.() const methodName = method as string const methodMap = isObligation ? obligationFunctions : trustvcFunctions const trustvcContractMethod = methodMap[methodName] if (!trustvcContractMethod) { throw new Error( `Unsupported method '${methodName}' for ${ isObligation ? 'obligation' : 'title escrow' } trustvcFunctions mapping` ) } const options = { id: keyId ?? '' } const resolvedOptions = isObligation ? toObligationContractOptions(contractOptions) : contractOptions const deferredTx = trustvcContractMethod( resolvedOptions, providerOrSigner, params, options ) setState('INITIALIZED') const _transaction: ContractTransaction = await deferredTx setState('PENDING_CONFIRMATION') setTransaction(_transaction) const _receipt = await _transaction.wait() setState('CONFIRMED') setReceipt(_receipt) } catch (e) { setErrorMessage( getMetaMaskErrorMessage(e) || (e instanceof Error ? e.message : 'Transaction failed') ) setState('ERROR') } }) as TypedContractMethod< any[], ReturnType any ? T[S] : never>, 'nonpayable' > const callFn = (async (...params: any[]) => { if (!contract || !method) { setState('ERROR') return } resetState() // @ts-ignore: check for v4 contracts support const contractMethod = contract?.functions?.[method as string] ?? contract[method] if (!contractMethod) return const deferredTx = contractMethod(...params) setState('INITIALIZED') try { const response = await deferredTx setState('CONFIRMED') setValue(response) } catch (e) { setErrorMessage(getMetaMaskErrorMessage(e)) setState('ERROR') } }) as TypedContractMethod< any[], ReturnType any ? T[S] : never>, 'nonpayable' > const transactionHash = transaction?.hash const events = receipt?.events // eslint-disable-next-line react-hooks/exhaustive-deps const send = useCallback(sendFn, [ contract, method, keyId, isObligation, contractOptions, providerOrSigner, preflightCheck, ]) // eslint-disable-next-line react-hooks/exhaustive-deps const call = useCallback(callFn, [contract, method]) // eslint-disable-next-line react-hooks/exhaustive-deps const reset = useCallback(resetState, [contract, method]) return { state, call, events, send, receipt, transaction, transactionHash, errorMessage, value, reset, } }