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 = { 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", 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 `} ), 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 { + setFormValue('startDate', v); + trigger(); + }} /> { + setFormValue('endDate', v); + setFormValue('deadline', sevenDaysFromDate(v)); + trigger(); + }} /> {type === INVOICE_TYPES.Instant ? ( { + setFormValue('safetyValveDate', v); + trigger(); + }} /> )} diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts index 2979d49b..1b1a6aab 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'; 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/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/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/useFetchTokens.ts b/packages/hooks/src/useFetchTokens.ts index 941224d9..32aeaa0a 100644 --- a/packages/hooks/src/useFetchTokens.ts +++ b/packages/hooks/src/useFetchTokens.ts @@ -52,12 +52,15 @@ const fetchTokens = async () => { return [] as IToken[]; }; -export const useFetchTokens = () => { +export const useFetchTokens = ( + { enabled }: { enabled: boolean } = { enabled: true }, +) => { 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..f0e86743 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), }; }; 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/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, }; 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',