From 0557d59d8568de6e0ac256a7153e07f4d0c6e9fd Mon Sep 17 00:00:00 2001 From: dan13ram Date: Mon, 30 Dec 2024 20:44:50 +0530 Subject: [PATCH 01/10] fixed copy in payments form --- packages/forms/src/PaymentsForm.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/forms/src/PaymentsForm.tsx b/packages/forms/src/PaymentsForm.tsx index 4b0e94ab..a2d50e26 100644 --- a/packages/forms/src/PaymentsForm.tsx +++ b/packages/forms/src/PaymentsForm.tsx @@ -133,12 +133,9 @@ export function PaymentsForm({ defaultValue={nativeWrappedToken.toLowerCase()} tooltip={ - {`This is the cryptocurrency you'll receive payment in. The - network your wallet is connected to determines which allTokens - display here.`} + {`This is the cryptocurrency you'll receive payment in. The network your wallet is connected to determines which tokens are displayed here.`}
- {`If you change your wallet network now, - you'll be forced to start the invoice over.`} + {`If you change your wallet network now, you'll be sent back to Step 1.`}
} localForm={localForm} @@ -154,7 +151,7 @@ export function PaymentsForm({ Milestones Date: Mon, 6 Jan 2025 17:07:47 +0530 Subject: [PATCH 02/10] deployed updateable for mainnet --- packages/contracts/deployments/mainnet.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/contracts/deployments/mainnet.json b/packages/contracts/deployments/mainnet.json index 256ffdde..62542363 100644 --- a/packages/contracts/deployments/mainnet.json +++ b/packages/contracts/deployments/mainnet.json @@ -5,7 +5,8 @@ "blockNumber": "16991083", "implementations": { "escrow": ["0xEfA83c32691e312d8c0c2973b05048fecCfab752"], - "instant": ["0xb54586d9032728b0d82F64845D3fC51577bB00cd"] + "instant": ["0xb54586d9032728b0d82F64845D3fC51577bB00cd"], + "updatable": ["0xd8e1f218021550fadda4b1e353578b80a1ce1a94"] }, "bundler": { "address": "0xb4cdef4aa610c046864467592fae456a58d3443a", From 5f71d42aa75ba84deeacbb2a0c191d96f88ac2b3 Mon Sep 17 00:00:00 2001 From: dan13ram Date: Mon, 6 Jan 2025 17:15:06 +0530 Subject: [PATCH 03/10] fixed links in invoice payment details --- packages/forms/src/InvoicePaymentDetails.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/forms/src/InvoicePaymentDetails.tsx b/packages/forms/src/InvoicePaymentDetails.tsx index 5a5977a8..31dba212 100644 --- a/packages/forms/src/InvoicePaymentDetails.tsx +++ b/packages/forms/src/InvoicePaymentDetails.tsx @@ -229,7 +229,7 @@ export function InvoicePaymentDetails({ isExternal color="grey" fontStyle="italic" - href={getTxLink(chainId, release.txHash)} + href={getTxLink(invoice?.chainId, release.txHash)} > Released{' '} {new Date( @@ -243,7 +243,7 @@ export function InvoicePaymentDetails({ isExternal color="grey" fontStyle="italic" - href={getTxLink(chainId, deposit?.txHash)} + href={getTxLink(invoice?.chainId, deposit?.txHash)} > {`${_.capitalize(depositedText)} `} {new Date( @@ -347,7 +347,7 @@ export function InvoicePaymentDetails({ {`A dispute is in progress with `}
{!isEmptyIpfsHash(dispute.ipfsHash) && ( @@ -363,7 +363,7 @@ export function InvoicePaymentDetails({ )} @@ -405,7 +405,7 @@ export function InvoicePaymentDetails({ { ' has resolved the dispute and dispersed remaining funds' @@ -436,7 +436,10 @@ export function InvoicePaymentDetails({ )} View transaction @@ -460,7 +463,7 @@ export function InvoicePaymentDetails({ )} ${tokenMetadata?.symbol} to `} ), From 0fbc43b2d5610f071eadd239c04547a38ed2334c Mon Sep 17 00:00:00 2001 From: dan13ram Date: Mon, 6 Jan 2025 17:20:24 +0530 Subject: [PATCH 04/10] fixed terms urls --- packages/constants/src/config.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/constants/src/config.ts b/packages/constants/src/config.ts index 010629ab..eff9be4e 100644 --- a/packages/constants/src/config.ts +++ b/packages/constants/src/config.ts @@ -84,8 +84,7 @@ const LEXDAO_DATA: ResolverWithoutAddress = { id: 'lexdao', name: 'LexDAO', logoUrl: '/assets/lex-dao.png', - termsUrl: - 'https://github.com/lexDAO/Arbitration/blob/master/rules/ToU.md#lexdao-resolver', + termsUrl: 'https://docs.smartinvoice.xyz/arbitration/lexdao-arbitration', }; const KLEROS_DATA: ResolverWithoutAddress = { @@ -94,8 +93,7 @@ const KLEROS_DATA: ResolverWithoutAddress = { disclaimer: 'Only choose Kleros if total invoice value is greater than 1000 USD', logoUrl: '/assets/kleros.svg', - termsUrl: - 'https://docs.google.com/document/d/1z_l2Wc8YHSspB0Lm5cmMDhu9h0W5G4thvDLqWRtuxbA/', + termsUrl: 'https://docs.smartinvoice.xyz/arbitration/kleros-arbitration', }; const SMART_INVOICE_ARBITRATION_DATA: ResolverWithoutAddress = { From 8555d1e7dafd05b6f5184f5d41e70a0c596facc4 Mon Sep 17 00:00:00 2001 From: dan13ram Date: Thu, 9 Jan 2025 19:04:41 +0530 Subject: [PATCH 05/10] fixed project details form --- packages/forms/src/ProjectDetailsForm.tsx | 18 +++++++++++++++++- packages/ui/src/forms/DatePicker.tsx | 2 +- packages/utils/src/resolvers.ts | 2 +- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/forms/src/ProjectDetailsForm.tsx b/packages/forms/src/ProjectDetailsForm.tsx index 2ac095b1..3357c909 100644 --- a/packages/forms/src/ProjectDetailsForm.tsx +++ b/packages/forms/src/ProjectDetailsForm.tsx @@ -12,6 +12,7 @@ import { import { oneMonthFromNow, projectDetailsSchema, + sevenDaysFromDate, sevenDaysFromNow, } from '@smartinvoicexyz/utils'; import _ from 'lodash'; @@ -64,7 +65,9 @@ export function ProjectDetailsForm({ const { handleSubmit, - formState: { isValid }, + setValue: setFormValue, + formState: { isValid, errors }, + trigger, } = localForm; const { primaryButtonSize } = useMediaStyles(); @@ -116,12 +119,21 @@ export function ProjectDetailsForm({ name="startDate" tooltip="The date the project is expected to start" localForm={localForm} + onChange={(v: Date) => { + setFormValue('startDate', v); + trigger(); + }} /> { + setFormValue('endDate', v); + setFormValue('deadline', sevenDaysFromDate(v)); + trigger(); + }} /> {type === INVOICE_TYPES.Instant ? ( { + setFormValue('safetyValveDate', v); + trigger(); + }} /> )} diff --git a/packages/ui/src/forms/DatePicker.tsx b/packages/ui/src/forms/DatePicker.tsx index 7d69b44c..455c0056 100644 --- a/packages/ui/src/forms/DatePicker.tsx +++ b/packages/ui/src/forms/DatePicker.tsx @@ -103,8 +103,8 @@ export function DatePicker({
diff --git a/packages/utils/src/resolvers.ts b/packages/utils/src/resolvers.ts index 5062a345..8fffab0c 100644 --- a/packages/utils/src/resolvers.ts +++ b/packages/utils/src/resolvers.ts @@ -190,7 +190,7 @@ export const projectDetailsSchema = Yup.object().shape({ Yup.ref('endDate'), 'Deadline must be after End Date', ), - safetyValveDate: Yup.date().when('endDate', (endDate, schema) => { + safetyValveDate: Yup.date().when(['endDate'], (endDate, schema) => { return schema.min( sevenDaysFromDate(endDate.toString()), 'Safety Valve Date must be at least 7 days after End Date', From 0d2c31f0c8257291a10516edf51161c67b5aedef Mon Sep 17 00:00:00 2001 From: sshmm Date: Sun, 2 Feb 2025 03:07:28 +0200 Subject: [PATCH 06/10] add custom hooks: useDebounce and usePollSubgraph; update useLock and useResolve to handle optional details --- packages/hooks/src/index.ts | 3 + packages/hooks/src/useDebounce.ts | 17 +++ packages/hooks/src/useEscrowZap.ts | 52 ++++---- packages/hooks/src/useLock.ts | 17 ++- packages/hooks/src/usePollSubgraph.ts | 45 +++++++ packages/hooks/src/useRegister.ts | 175 ++++++++++++++++++++++++++ packages/hooks/src/useResolve.ts | 25 ++-- 7 files changed, 301 insertions(+), 33 deletions(-) create mode 100644 packages/hooks/src/useDebounce.ts create mode 100644 packages/hooks/src/usePollSubgraph.ts create mode 100644 packages/hooks/src/useRegister.ts diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts index 2979d49b..7cfa8532 100644 --- a/packages/hooks/src/index.ts +++ b/packages/hooks/src/index.ts @@ -1,4 +1,5 @@ export * from './useAddMilestones'; +export * from './useDebounce'; export * from './useDeposit'; export * from './useEscrowZap'; export * from './useFetchTokens'; @@ -11,7 +12,9 @@ export * from './useInvoiceStatus'; export * from './useIpfsDetails'; export * from './useIsClient'; export * from './useLock'; +export * from './usePollSubgraph'; export * from './useRateForResolver'; +export * from './useRegister'; export * from './useRelease'; export * from './useResolve'; export * from './useTokenMetadata'; diff --git a/packages/hooks/src/useDebounce.ts b/packages/hooks/src/useDebounce.ts new file mode 100644 index 00000000..883b4154 --- /dev/null +++ b/packages/hooks/src/useDebounce.ts @@ -0,0 +1,17 @@ +import { useEffect, useState } from 'react'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const useDebounce = (value: any, delay: number) => { + const [debouncedValue, setDebouncedValue] = useState(value); + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + return debouncedValue; +}; + +export default useDebounce; diff --git a/packages/hooks/src/useEscrowZap.ts b/packages/hooks/src/useEscrowZap.ts index 22a4fa4e..63a84b13 100644 --- a/packages/hooks/src/useEscrowZap.ts +++ b/packages/hooks/src/useEscrowZap.ts @@ -1,12 +1,14 @@ -import { - ESCROW_ZAP_ABI, - NETWORK_CONFIG, - NetworkConfig, -} from '@smartinvoicexyz/constants'; +import { ESCROW_ZAP_ABI, NETWORK_CONFIG } from '@smartinvoicexyz/constants'; import { logDebug } from '@smartinvoicexyz/utils'; import _ from 'lodash'; import { useCallback, useMemo } from 'react'; -import { encodeAbiParameters, Hex, isAddress, parseUnits } from 'viem'; +import { + encodeAbiParameters, + Hex, + isAddress, + parseEther, + parseUnits, +} from 'viem'; import { useChainId, useSimulateContract, useWriteContract } from 'wagmi'; import { SimulateContractErrorType, WriteContractErrorType } from './types'; @@ -39,8 +41,7 @@ export const useEscrowZap = ({ safetyValveDate, details, enabled = true, - networkConfig = NETWORK_CONFIG, - token, + networkConfig, onSuccess, }: UseEscrowZapProps): { writeAsync: () => Promise; @@ -53,21 +54,24 @@ export const useEscrowZap = ({ const { owners, percentAllocations } = separateOwnersAndAllocations(ownersAndAllocations); const saltNonce = Math.floor(new Date().getTime() / 1000); + const milestoneAmounts = networkConfig?.tokenDecimals + ? _.map( + milestones, + (a: { value: string }) => + a.value && parseUnits(a.value, networkConfig.tokenDecimals), + ) + : _.map( + milestones, + (a: { value: string }) => a.value && parseEther(a.value), + ); - const tokenDecimals = - _.get(networkConfig[chainId], `TOKENS.${token}.decimals`) ?? 18; - - const milestoneAmounts = _.map( - NETWORK_CONFIG[chainId] ? milestones : [], - (a: { value: string }) => a.value && parseUnits(a.value, tokenDecimals), - ); - - const tokenAddress = - _.get(networkConfig[chainId], `TOKENS.${token}.address`) ?? '0x0'; + const tokenAddress = networkConfig?.tokenAddress + ? networkConfig?.tokenAddress + : '0x0'; const resolver = daoSplit - ? (_.first(_.keys(_.get(networkConfig[chainId], 'RESOLVERS'))) as Hex) - : (networkConfig[chainId].DAO_ADDRESS ?? ''); + ? (_.first(_.keys(_.get(NETWORK_CONFIG[chainId], 'RESOLVERS'))) as Hex) + : (NETWORK_CONFIG[chainId].DAO_ADDRESS ?? ''); const encodedSafeData = useMemo(() => { if (!threshold || !saltNonce) @@ -152,7 +156,7 @@ export const useEscrowZap = ({ status, } = useSimulateContract({ chainId, - address: networkConfig[chainId].ZAP_ADDRESS ?? '0x0', + address: networkConfig?.ZAP_ADDRESS ?? '0x0', abi: ESCROW_ZAP_ABI, functionName: 'createSafeSplitEscrow', args: [ @@ -218,6 +222,10 @@ interface UseEscrowZapProps { safetyValveDate: Date; details?: `0x${string}` | null; enabled?: boolean; - networkConfig?: { [key: number]: NetworkConfig }; // to override the default network config + networkConfig?: { + tokenAddress: Hex; + tokenDecimals: number; + ZAP_ADDRESS: Hex; + }; onSuccess?: (hash: Hex) => void; } diff --git a/packages/hooks/src/useLock.ts b/packages/hooks/src/useLock.ts index 5067abc3..ada12206 100644 --- a/packages/hooks/src/useLock.ts +++ b/packages/hooks/src/useLock.ts @@ -38,11 +38,13 @@ export const useLock = ({ localForm, onTxSuccess, toast, + details, }: { invoice: InvoiceDetails; localForm: UseFormReturn; onTxSuccess?: () => void; toast: UseToastReturn; + details?: Hex | null; }): { writeAsync: () => Promise; isLoading: boolean; @@ -60,6 +62,9 @@ export const useLock = ({ const publicClient = usePublicClient(); const detailsData = useMemo(() => { + if (details) { + return null; + } const now = Math.floor(new Date().getTime() / 1000); const title = `Dispute ${metadata?.title} at ${getDateString(now)}`; return { @@ -70,7 +75,7 @@ export const useLock = ({ documents: document ? [uriToDocument(document)] : [], createdAt: now, } as BasicMetadata; - }, [description, document, metadata]); + }, [description, document, metadata, details]); const { data: detailsHash, isLoading: detailsLoading } = useDetailsPin( detailsData, @@ -85,12 +90,12 @@ export const useLock = ({ address: invoice?.address as Hex, functionName: 'lock', abi: SMART_INVOICE_UPDATABLE_ABI, - args: [detailsHash as Hex], + args: [details ?? (detailsHash as Hex)], query: { enabled: !!invoice?.address && !!description && - !!detailsHash && + (!!details || !!detailsHash) && currentChainId === invoiceChainId, }, }); @@ -134,7 +139,11 @@ export const useLock = ({ return { writeAsync, - isLoading: prepareLoading || writeLoading || waitingForTx || detailsLoading, + isLoading: + prepareLoading || + writeLoading || + waitingForTx || + !(details || !detailsLoading), prepareError, writeError, }; diff --git a/packages/hooks/src/usePollSubgraph.ts b/packages/hooks/src/usePollSubgraph.ts new file mode 100644 index 00000000..851b9b7f --- /dev/null +++ b/packages/hooks/src/usePollSubgraph.ts @@ -0,0 +1,45 @@ +const usePollSubgraph = ({ + label, + fetchHelper, + checkResult, + interval = 1000, +}: { + label: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + fetchHelper: () => any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + checkResult: (value: any) => boolean; + interval?: number; +}) => { + const waitForResult = async () => + new Promise(resolve => { + const checkResultHandler = async () => { + try { + const result = await fetchHelper(); + + if (result && checkResult(result)) { + // eslint-disable-next-line no-use-before-define + clearInterval(intervalId); + resolve(result); + } + // eslint-disable-next-line no-console + console.log(label); + } catch (e) { + // eslint-disable-next-line no-console + console.log(e); + } + }; + + const intervalId = setInterval(checkResultHandler, interval); + checkResultHandler(); // Check immediately + + setTimeout(() => { + clearInterval(intervalId); + resolve(null); // Resolve with null or handle the timeout case + }, 20000); + }); + + return waitForResult; +}; + +export default usePollSubgraph; diff --git a/packages/hooks/src/useRegister.ts b/packages/hooks/src/useRegister.ts new file mode 100644 index 00000000..ce8db74b --- /dev/null +++ b/packages/hooks/src/useRegister.ts @@ -0,0 +1,175 @@ +import { SMART_INVOICE_FACTORY_ABI } from '@smartinvoicexyz/constants'; +import { logDebug, logError } from '@smartinvoicexyz/utils'; +import _ from 'lodash'; +import { useCallback, useMemo } from 'react'; +import { UseFormReturn } from 'react-hook-form'; +import { encodeAbiParameters, Hex, parseUnits, stringToHex } from 'viem'; +import { useSimulateContract, useWriteContract } from 'wagmi'; + +import { SimulateContractErrorType, WriteContractErrorType } from './types'; + +const REQUIRES_VERIFICATION = true; + +interface UseRegister { + escrowForm: UseFormReturn; + netwrokConfig: { + resolver: Hex; + tokenAddress: Hex; + wrappedNativeToken: Hex; + tokenDecimals: number; + factoryAddress: Hex; + }; + onSuccess: (invoiceId: string | undefined) => Promise; + enabled?: boolean; + details?: `0x${string}` | null; +} + +export const useRegister = ({ + escrowForm, + enabled = true, + details, + onSuccess, + netwrokConfig, +}: UseRegister): { + writeAsync: () => Promise; + isLoading: boolean; + prepareError: SimulateContractErrorType | null; + writeError: WriteContractErrorType | null; +} => { + const { watch } = escrowForm; + const { + milestones, + safetyValveDate, + provider, + client: clientAddress, + } = watch(); + + const providerReceiver: Hex = provider; + + const { resolver } = netwrokConfig; + const { tokenAddress } = netwrokConfig; + const { wrappedNativeToken } = netwrokConfig; + const { tokenDecimals } = netwrokConfig; + const { factoryAddress } = netwrokConfig; + const terminationTime = BigInt(Math.floor(safetyValveDate.getTime() / 1000)); + + // TODO handle token decimals + const paymentsInWei = _.map(milestones, ({ value }: { value: string }) => + parseUnits(value, tokenDecimals), + ); + + const resolverType = 0; // 0 for individual, 1 for erc-792 arbitrator + const type = stringToHex('updatable', { size: 32 }); + + const escrowData = useMemo(() => { + if ( + !clientAddress || + !(resolverType === 0 || resolverType === 1) || + !resolver || + !tokenAddress || + !terminationTime || + !wrappedNativeToken || + !details || + !factoryAddress || + !provider + ) { + return '0x'; + } + + return encodeAbiParameters( + [ + { type: 'address' }, // _client, + { type: 'uint8' }, // _resolverType, + { type: 'address' }, // _resolver, + { type: 'address' }, // _token, + { type: 'uint256' }, // _terminationTime, // exact termination date in seconds since epoch + { type: 'bytes32' }, // _details, + { type: 'address' }, // _wrappedNativeToken, + { type: 'bool' }, // _requireVerification, // warns the client not to deposit funds until verifying they can release or lock funds + { type: 'address' }, // _factory, + { type: 'address' }, // _providerReceiver, + ], + [ + clientAddress, + resolverType, + resolver, // address _resolver (LEX DAO resolver address) + tokenAddress, // address _token (payment token address) + terminationTime, // safety valve date + details, // bytes32 _details detailHash + wrappedNativeToken, + REQUIRES_VERIFICATION, + factoryAddress, + providerReceiver, + ], + ); + }, [ + clientAddress, + resolverType, + resolver, + tokenAddress, + terminationTime, + wrappedNativeToken, + factoryAddress, + providerReceiver, + details, + ]); + + const { + data, + isLoading: prepareLoading, + error: prepareError, + } = useSimulateContract({ + address: factoryAddress, + functionName: 'create', + abi: SMART_INVOICE_FACTORY_ABI, + args: [ + provider, // address recipient, + paymentsInWei, // uint256[] memory amounts, + escrowData, // bytes memory escrowData, + type, // bytes32 escrowType, + ], + query: { + enabled: + !!terminationTime && + !_.isEmpty(paymentsInWei) && + escrowData !== '0x' && + enabled, + }, + }); + + const { + writeContractAsync, + isPending: writeLoading, + error: writeError, + } = useWriteContract({ + mutation: { + onSuccess: async tx => { + logDebug('success', tx); + const smartInvoiceId = _.get(tx, 'events[0].args.invoice'); + await onSuccess(smartInvoiceId); + }, + onError: error => { + logError('error', error); + }, + }, + }); + + const writeAsync = useCallback(async (): Promise => { + try { + if (!data) { + throw new Error('simulation data is not available'); + } + return writeContractAsync(data.request); + } catch (error) { + logError('useRegister error', error); + return undefined; + } + }, [writeContractAsync, data]); + + return { + writeAsync, + isLoading: prepareLoading || writeLoading, + prepareError, + writeError, + }; +}; diff --git a/packages/hooks/src/useResolve.ts b/packages/hooks/src/useResolve.ts index abc77b14..8cf7a4fa 100644 --- a/packages/hooks/src/useResolve.ts +++ b/packages/hooks/src/useResolve.ts @@ -35,11 +35,13 @@ export const useResolve = ({ localForm, onTxSuccess, toast, + details, }: { invoice: Partial; localForm: UseFormReturn; onTxSuccess: () => void; - toast: UseToastReturn; + toast?: UseToastReturn; + details?: Hex | null; }): { writeAsync: () => Promise; isLoading: boolean; @@ -61,6 +63,9 @@ export const useResolve = ({ ); const detailsData = useMemo(() => { + if (details) { + return null; + } const now = Math.floor(new Date().getTime() / 1000); const title = `Resolve ${metadata?.title} at ${getDateString(now)}`; return { @@ -71,7 +76,7 @@ export const useResolve = ({ documents: document ? [uriToDocument(document)] : [], createdAt: now, } as BasicMetadata; - }, [description, document, metadata]); + }, [description, document, metadata, details]); const { data: detailsHash, isLoading: detailsLoading } = useDetailsPin( detailsData, @@ -102,14 +107,14 @@ export const useResolve = ({ address: address as Hex, functionName: 'resolve', abi: SMART_INVOICE_UPDATABLE_ABI, - args: [clientAward, providerAward, detailsHash as Hex], + args: [clientAward, providerAward, details ?? (detailsHash as Hex)], query: { enabled: !!address && fullBalance && isLocked && tokenBalance.value > BigInt(0) && - !!detailsHash && + (!!details || !!detailsHash) && !!description, }, }); @@ -135,7 +140,9 @@ export const useResolve = ({ onTxSuccess?.(); }, - onError: error => errorToastHandler('useResolve', error, toast), + onError: error => { + if (toast) errorToastHandler('useResolve', error, toast); + }, }, }); @@ -146,14 +153,18 @@ export const useResolve = ({ } return writeContractAsync(data.request); } catch (error) { - errorToastHandler('useResolve', error as Error, toast); + if (toast) errorToastHandler('useResolve', error as Error, toast); return undefined; } }, [writeContractAsync, data]); return { writeAsync, - isLoading: prepareLoading || writeLoading || waitingForTx || detailsLoading, + isLoading: + prepareLoading || + writeLoading || + waitingForTx || + !(details || !detailsLoading), prepareError, writeError, }; From b9252a03298a3afbd5baeaecb30921e52009dd81 Mon Sep 17 00:00:00 2001 From: sshmm Date: Mon, 10 Mar 2025 03:27:47 +0200 Subject: [PATCH 07/10] refactor hooks: enhance useDetailsPin and useFetchTokens for better null handling; update useInvoiceCreate to support optional details and network configuration --- packages/hooks/src/useDetailsPin.ts | 3 ++ packages/hooks/src/useFetchTokens.ts | 3 +- packages/hooks/src/useInvoiceCreate.ts | 44 ++++++++++++++++++++------ 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/packages/hooks/src/useDetailsPin.ts b/packages/hooks/src/useDetailsPin.ts index 13e0235e..86b2ed88 100644 --- a/packages/hooks/src/useDetailsPin.ts +++ b/packages/hooks/src/useDetailsPin.ts @@ -23,6 +23,9 @@ export const useDetailsPin = ( isBasic = false, ) => { const validatedDetails = useMemo((): InvoiceMetadata | null => { + if (details === null) { + return null; + } if (isBasic) { if (!validateBasicMetadata(details)) { logDebug('Invalid basic metadata: ', details); diff --git a/packages/hooks/src/useFetchTokens.ts b/packages/hooks/src/useFetchTokens.ts index 941224d9..75695024 100644 --- a/packages/hooks/src/useFetchTokens.ts +++ b/packages/hooks/src/useFetchTokens.ts @@ -52,12 +52,13 @@ const fetchTokens = async () => { return [] as IToken[]; }; -export const useFetchTokens = () => { +export const useFetchTokens = ({ enabled = true }: { enabled: boolean }) => { const { data, isLoading, error } = useQuery({ queryKey: ['tokens'], queryFn: fetchTokens, staleTime: Infinity, refetchInterval: false, + enabled, }); const allTokens = useMemo( diff --git a/packages/hooks/src/useInvoiceCreate.ts b/packages/hooks/src/useInvoiceCreate.ts index a4e15b11..a916f68b 100644 --- a/packages/hooks/src/useInvoiceCreate.ts +++ b/packages/hooks/src/useInvoiceCreate.ts @@ -41,7 +41,14 @@ const ESCROW_TYPE = toHex('updatable', { size: 32 }); interface UseInvoiceCreate { invoiceForm: UseFormReturn>; toast: UseToastReturn; + networkConfig?: { + resolver: Hex; + token: Hex; + tokenDecimals: number; + }; onTxSuccess?: (result: Hex) => void; + enabled?: boolean; + details?: `0x${string}` | null; } const REQUIRES_VERIFICATION = true; @@ -50,6 +57,9 @@ export const useInvoiceCreate = ({ invoiceForm, toast, onTxSuccess, + networkConfig, + details, + enabled = true, }: UseInvoiceCreate): { writeAsync: () => Promise; isLoading: boolean; @@ -94,7 +104,7 @@ export const useInvoiceCreate = ({ 'endDate', ]); - const { data: tokens } = useFetchTokens(); + const { data: tokens } = useFetchTokens({ enabled: !!networkConfig }); const invoiceToken = _.find( tokens, t => @@ -102,6 +112,9 @@ export const useInvoiceCreate = ({ ); const detailsData = useMemo(() => { + if (details) { + return null; + } const now = Math.floor(new Date().getTime() / 1000); const start = startDate ? Math.floor(new Date(startDate).getTime() / 1000) @@ -140,10 +153,13 @@ export const useInvoiceCreate = ({ JSON.stringify(milestones), ]); - const { data: details, isLoading: detailsLoading } = + const { data: detailsPin, isLoading: detailsLoading } = useDetailsPin(detailsData); const resolverAddress = useMemo(() => { + if (networkConfig?.resolver) { + return networkConfig.resolver; + } if (resolverType === 'custom') { return customResolverAddress; } @@ -154,6 +170,8 @@ export const useInvoiceCreate = ({ return resolverInfo?.address; }, [resolverType, customResolverAddress]); + const detailHash = details ?? detailsPin; + const escrowData = useMemo(() => { const wrappedNativeToken = getWrappedNativeToken(chainId); const invoiceFactory = getInvoiceFactoryAddress(chainId); @@ -163,7 +181,7 @@ export const useInvoiceCreate = ({ !token || !safetyValveDate || !wrappedNativeToken || - !details || + !detailHash || !invoiceFactory || !provider ) { @@ -187,19 +205,22 @@ export const useInvoiceCreate = ({ client as Address, 0, // all are individual resolvers resolverAddress as Address, - token as Address, // address _token (payment token address) + networkConfig?.token ?? (token as Address), // address _token (payment token address) BigInt(new Date(safetyValveDate.toString()).getTime() / 1000), // safety valve date - details, // bytes32 _details detailHash + detailHash ?? '0x', // bytes32 _details detailHash wrappedNativeToken, REQUIRES_VERIFICATION, invoiceFactory, provider as Address, // TODO: replace with providerReceiver ], ); - }, [client, resolverType, token, details, safetyValveDate, provider]); + }, [client, resolverType, token, detailHash, safetyValveDate, provider]); const amounts = _.map(milestones, m => - parseUnits(m.value, invoiceToken?.decimals ?? 18), + parseUnits( + m.value, + networkConfig?.tokenDecimals ?? invoiceToken?.decimals ?? 18, + ), ); const { @@ -212,7 +233,8 @@ export const useInvoiceCreate = ({ functionName: 'create', args: [provider as Address, amounts, escrowData, ESCROW_TYPE], query: { - enabled: escrowData !== '0x' && !!provider && !_.isEmpty(milestones), + enabled: + escrowData !== '0x' && !!provider && !_.isEmpty(milestones) && enabled, }, }); @@ -273,6 +295,10 @@ export const useInvoiceCreate = ({ writeAsync, prepareError, writeError, - isLoading: isLoading || waitingForTx || prepareLoading || detailsLoading, + isLoading: + isLoading || + waitingForTx || + prepareLoading || + !(details || !detailsLoading), }; }; From 262176a93c5ac11140e0fd8e453719f7356a6df7 Mon Sep 17 00:00:00 2001 From: sshmm Date: Mon, 10 Mar 2025 03:28:50 +0200 Subject: [PATCH 08/10] remove unused hook: delete useRegister for improved code maintainability --- packages/hooks/src/useRegister.ts | 175 ------------------------------ 1 file changed, 175 deletions(-) delete mode 100644 packages/hooks/src/useRegister.ts diff --git a/packages/hooks/src/useRegister.ts b/packages/hooks/src/useRegister.ts deleted file mode 100644 index ce8db74b..00000000 --- a/packages/hooks/src/useRegister.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { SMART_INVOICE_FACTORY_ABI } from '@smartinvoicexyz/constants'; -import { logDebug, logError } from '@smartinvoicexyz/utils'; -import _ from 'lodash'; -import { useCallback, useMemo } from 'react'; -import { UseFormReturn } from 'react-hook-form'; -import { encodeAbiParameters, Hex, parseUnits, stringToHex } from 'viem'; -import { useSimulateContract, useWriteContract } from 'wagmi'; - -import { SimulateContractErrorType, WriteContractErrorType } from './types'; - -const REQUIRES_VERIFICATION = true; - -interface UseRegister { - escrowForm: UseFormReturn; - netwrokConfig: { - resolver: Hex; - tokenAddress: Hex; - wrappedNativeToken: Hex; - tokenDecimals: number; - factoryAddress: Hex; - }; - onSuccess: (invoiceId: string | undefined) => Promise; - enabled?: boolean; - details?: `0x${string}` | null; -} - -export const useRegister = ({ - escrowForm, - enabled = true, - details, - onSuccess, - netwrokConfig, -}: UseRegister): { - writeAsync: () => Promise; - isLoading: boolean; - prepareError: SimulateContractErrorType | null; - writeError: WriteContractErrorType | null; -} => { - const { watch } = escrowForm; - const { - milestones, - safetyValveDate, - provider, - client: clientAddress, - } = watch(); - - const providerReceiver: Hex = provider; - - const { resolver } = netwrokConfig; - const { tokenAddress } = netwrokConfig; - const { wrappedNativeToken } = netwrokConfig; - const { tokenDecimals } = netwrokConfig; - const { factoryAddress } = netwrokConfig; - const terminationTime = BigInt(Math.floor(safetyValveDate.getTime() / 1000)); - - // TODO handle token decimals - const paymentsInWei = _.map(milestones, ({ value }: { value: string }) => - parseUnits(value, tokenDecimals), - ); - - const resolverType = 0; // 0 for individual, 1 for erc-792 arbitrator - const type = stringToHex('updatable', { size: 32 }); - - const escrowData = useMemo(() => { - if ( - !clientAddress || - !(resolverType === 0 || resolverType === 1) || - !resolver || - !tokenAddress || - !terminationTime || - !wrappedNativeToken || - !details || - !factoryAddress || - !provider - ) { - return '0x'; - } - - return encodeAbiParameters( - [ - { type: 'address' }, // _client, - { type: 'uint8' }, // _resolverType, - { type: 'address' }, // _resolver, - { type: 'address' }, // _token, - { type: 'uint256' }, // _terminationTime, // exact termination date in seconds since epoch - { type: 'bytes32' }, // _details, - { type: 'address' }, // _wrappedNativeToken, - { type: 'bool' }, // _requireVerification, // warns the client not to deposit funds until verifying they can release or lock funds - { type: 'address' }, // _factory, - { type: 'address' }, // _providerReceiver, - ], - [ - clientAddress, - resolverType, - resolver, // address _resolver (LEX DAO resolver address) - tokenAddress, // address _token (payment token address) - terminationTime, // safety valve date - details, // bytes32 _details detailHash - wrappedNativeToken, - REQUIRES_VERIFICATION, - factoryAddress, - providerReceiver, - ], - ); - }, [ - clientAddress, - resolverType, - resolver, - tokenAddress, - terminationTime, - wrappedNativeToken, - factoryAddress, - providerReceiver, - details, - ]); - - const { - data, - isLoading: prepareLoading, - error: prepareError, - } = useSimulateContract({ - address: factoryAddress, - functionName: 'create', - abi: SMART_INVOICE_FACTORY_ABI, - args: [ - provider, // address recipient, - paymentsInWei, // uint256[] memory amounts, - escrowData, // bytes memory escrowData, - type, // bytes32 escrowType, - ], - query: { - enabled: - !!terminationTime && - !_.isEmpty(paymentsInWei) && - escrowData !== '0x' && - enabled, - }, - }); - - const { - writeContractAsync, - isPending: writeLoading, - error: writeError, - } = useWriteContract({ - mutation: { - onSuccess: async tx => { - logDebug('success', tx); - const smartInvoiceId = _.get(tx, 'events[0].args.invoice'); - await onSuccess(smartInvoiceId); - }, - onError: error => { - logError('error', error); - }, - }, - }); - - const writeAsync = useCallback(async (): Promise => { - try { - if (!data) { - throw new Error('simulation data is not available'); - } - return writeContractAsync(data.request); - } catch (error) { - logError('useRegister error', error); - return undefined; - } - }, [writeContractAsync, data]); - - return { - writeAsync, - isLoading: prepareLoading || writeLoading, - prepareError, - writeError, - }; -}; From 0f90e3a18684dbd7706792521b154027b979c36e Mon Sep 17 00:00:00 2001 From: sshmm Date: Mon, 10 Mar 2025 16:42:33 +0200 Subject: [PATCH 09/10] remove unused hook: delete usePollSubgraph for improved code clarity; fix useFetchTokens to handle network configuration correctly --- packages/hooks/src/index.ts | 2 -- packages/hooks/src/useInvoiceCreate.ts | 2 +- packages/hooks/src/usePollSubgraph.ts | 45 -------------------------- 3 files changed, 1 insertion(+), 48 deletions(-) delete mode 100644 packages/hooks/src/usePollSubgraph.ts diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts index 7cfa8532..1b1a6aab 100644 --- a/packages/hooks/src/index.ts +++ b/packages/hooks/src/index.ts @@ -12,9 +12,7 @@ export * from './useInvoiceStatus'; export * from './useIpfsDetails'; export * from './useIsClient'; export * from './useLock'; -export * from './usePollSubgraph'; export * from './useRateForResolver'; -export * from './useRegister'; export * from './useRelease'; export * from './useResolve'; export * from './useTokenMetadata'; diff --git a/packages/hooks/src/useInvoiceCreate.ts b/packages/hooks/src/useInvoiceCreate.ts index a916f68b..f0e86743 100644 --- a/packages/hooks/src/useInvoiceCreate.ts +++ b/packages/hooks/src/useInvoiceCreate.ts @@ -104,7 +104,7 @@ export const useInvoiceCreate = ({ 'endDate', ]); - const { data: tokens } = useFetchTokens({ enabled: !!networkConfig }); + const { data: tokens } = useFetchTokens({ enabled: !networkConfig }); const invoiceToken = _.find( tokens, t => diff --git a/packages/hooks/src/usePollSubgraph.ts b/packages/hooks/src/usePollSubgraph.ts deleted file mode 100644 index 851b9b7f..00000000 --- a/packages/hooks/src/usePollSubgraph.ts +++ /dev/null @@ -1,45 +0,0 @@ -const usePollSubgraph = ({ - label, - fetchHelper, - checkResult, - interval = 1000, -}: { - label: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - fetchHelper: () => any; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - checkResult: (value: any) => boolean; - interval?: number; -}) => { - const waitForResult = async () => - new Promise(resolve => { - const checkResultHandler = async () => { - try { - const result = await fetchHelper(); - - if (result && checkResult(result)) { - // eslint-disable-next-line no-use-before-define - clearInterval(intervalId); - resolve(result); - } - // eslint-disable-next-line no-console - console.log(label); - } catch (e) { - // eslint-disable-next-line no-console - console.log(e); - } - }; - - const intervalId = setInterval(checkResultHandler, interval); - checkResultHandler(); // Check immediately - - setTimeout(() => { - clearInterval(intervalId); - resolve(null); // Resolve with null or handle the timeout case - }, 20000); - }); - - return waitForResult; -}; - -export default usePollSubgraph; From 7e049eea75353eadb8b6ef8e0b1aec209c42b137 Mon Sep 17 00:00:00 2001 From: sshmm Date: Mon, 10 Mar 2025 17:22:53 +0200 Subject: [PATCH 10/10] refactor useFetchTokens: set default value for enabled parameter to improve usability --- packages/hooks/src/useFetchTokens.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/hooks/src/useFetchTokens.ts b/packages/hooks/src/useFetchTokens.ts index 75695024..32aeaa0a 100644 --- a/packages/hooks/src/useFetchTokens.ts +++ b/packages/hooks/src/useFetchTokens.ts @@ -52,7 +52,9 @@ const fetchTokens = async () => { return [] as IToken[]; }; -export const useFetchTokens = ({ enabled = true }: { enabled: boolean }) => { +export const useFetchTokens = ( + { enabled }: { enabled: boolean } = { enabled: true }, +) => { const { data, isLoading, error } = useQuery({ queryKey: ['tokens'], queryFn: fetchTokens,