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',