Skip to content
This repository was archived by the owner on Jul 1, 2025. It is now read-only.

Commit 6f144ab

Browse files
committed
add support for escrow proposal type
1 parent f82bcce commit 6f144ab

File tree

17 files changed

+1037
-30
lines changed

17 files changed

+1037
-30
lines changed

apps/web/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"@fontsource/londrina-solid": "^4.5.9",
2121
"@rainbow-me/rainbowkit": "^1.3.1",
2222
"@sentry/nextjs": "^7.105.0",
23+
"@smartinvoicexyz/constants": "^0.1.17",
2324
"@types/lodash": "^4.14.186",
2425
"@types/react-portal": "^4.0.4",
2526
"@types/tinycolor2": "^1.4.3",
@@ -32,6 +33,7 @@
3233
"axios": "^0.27.2",
3334
"bignumber.js": "^9.1.0",
3435
"blocklist": "workspace:*",
36+
"bs58": "^5.0.0",
3537
"dayjs": "^1.11.3",
3638
"flatpickr": "^4.6.13",
3739
"formik": "^2.2.9",

apps/web/src/components/Fields/Date.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import React, { ReactElement } from 'react'
55

66
import {
77
defaultFieldsetStyle,
8+
defaultHelperTextStyle,
89
defaultInputErrorMessageStyle,
910
defaultInputErrorStyle,
1011
defaultInputLabelStyle,
@@ -18,6 +19,7 @@ interface DateProps {
1819
formik: FormikProps<any>
1920
id: string
2021
errorMessage: string | FormikErrors<any> | string[] | undefined | FormikErrors<any>[]
22+
helperText?: string
2123
value: any
2224
altFormat?: string
2325
dateFormat?: string
@@ -34,6 +36,7 @@ const Date: React.FC<DateProps> = (
3436
formik,
3537
id,
3638
errorMessage,
39+
helperText,
3740
autoSubmit,
3841
value,
3942
placeholder,
@@ -70,10 +73,10 @@ const Date: React.FC<DateProps> = (
7073
{inputLabel && <label className={defaultInputLabelStyle}>{inputLabel}</label>}
7174
{errorMessage && (
7275
<Box
73-
position={'absolute'}
7476
right={'x2'}
75-
top={'x8'}
76-
fontSize={12}
77+
top={'x15'}
78+
pt={'x4'}
79+
fontSize={14}
7780
className={defaultInputErrorMessageStyle}
7881
>
7982
{errorMessage as string}
@@ -89,6 +92,17 @@ const Date: React.FC<DateProps> = (
8992
readOnly={true}
9093
disabled={disabled}
9194
/>
95+
{helperText && (
96+
<Box
97+
right={'x2'}
98+
top={'x15'}
99+
pt={'x4'}
100+
fontSize={14}
101+
className={defaultHelperTextStyle}
102+
>
103+
{helperText}
104+
</Box>
105+
)}
92106
</Box>
93107
)
94108
}

apps/web/src/components/Home/accordian/AccordionItem.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ import { Icon } from 'src/components/Icon'
66

77
import { accordionItem, accordionName } from '../../../styles/home.css'
88

9-
const AccordionItem: React.FC<{ title: string; description: ReactElement }> = (
10-
{ title, description }
11-
) => {
9+
const AccordionItem: React.FC<{
10+
title: string | ReactElement
11+
description: ReactElement
12+
}> = ({ title, description }) => {
1213
const [isOpen, setIsOpen] = React.useState<boolean>(false)
1314
const variants = {
1415
initial: {

apps/web/src/components/Home/accordian/index.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import React, { ReactElement } from 'react'
33

44
import AccordionItem from './AccordionItem'
55

6-
const Accordion: React.FC<{ items: { title: string; description: ReactElement }[] }> = ({
7-
items,
8-
}) => {
6+
const Accordion: React.FC<{
7+
items: { title: string | ReactElement; description: ReactElement }[]
8+
}> = ({ items }) => {
99
return (
1010
<Stack>
1111
{items?.map((item, key) => (
Lines changed: 3 additions & 0 deletions
Loading

apps/web/src/components/Icon/icons.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import Dash from './assets/dash.svg'
1919
import Discord from './assets/discord.svg'
2020
import Dots from './assets/dots.svg'
2121
import Download from './assets/download.svg'
22+
import Escrow from './assets/escrow.svg'
2223
import Eth from './assets/eth.svg'
2324
import External from './assets/external-16.svg'
2425
import Github from './assets/github.svg'
@@ -84,6 +85,7 @@ export const icons = {
8485
share: Share,
8586
warning: Warning,
8687
'warning-16': Warning16,
88+
escrow: Escrow,
8789
}
8890

8991
export type IconType = keyof typeof icons

apps/web/src/constants/swrKeys.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const SWR_KEYS = {
2626
ENCODED_DAO_METADATA: 'encoded-dao-metadata',
2727
DAO_MIGRATED: 'dao-migrated',
2828
DAO_NEXT_AND_PREVIOUS_TOKENS: 'dao-next-and-previous-tokens',
29+
IPFS: 'ipfs',
2930
DYNAMIC: {
3031
MY_DAOS(str: string) {
3132
return `my-daos-${str}`

apps/web/src/modules/create-proposal/components/ReviewProposalForm/ReviewProposalForm.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { AddressType, CHAIN_ID } from 'src/typings'
2323

2424
import { BuilderTransaction, useProposalStore } from '../../stores'
2525
import { prepareProposalTransactions } from '../../utils/prepareTransactions'
26+
import { useEscrowFormStore } from '../TransactionForm/Escrow/EscrowUtils'
2627
import { Transactions } from './Transactions'
2728
import { ERROR_CODE, FormValues, validationSchema } from './fields'
2829

@@ -69,6 +70,7 @@ export const ReviewProposalForm = ({
6970
const [simulating, setSimulating] = useState<boolean>(false)
7071
const [simulations, setSimulations] = useState<Array<Simulation>>([])
7172
const [proposing, setProposing] = useState<boolean>(false)
73+
const { clear: clearEscrowForm } = useEscrowFormStore()
7274

7375
const { data: votes, isLoading } = useContractRead({
7476
address: addresses?.token as AddressType,
@@ -178,6 +180,7 @@ export const ReviewProposalForm = ({
178180
.then(() => {
179181
setProposing(false)
180182
clearProposal()
183+
clearEscrowForm()
181184
})
182185
} catch (err: any) {
183186
setProposing(false)
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import { Stack } from '@zoralabs/zord'
2+
import { useCallback } from 'hono/jsx'
3+
import { uploadFile } from 'ipfs-service'
4+
import { useRouter } from 'next/router'
5+
import { useState } from 'react'
6+
import useSWR from 'swr'
7+
import { encodeFunctionData, formatEther } from 'viem'
8+
9+
import SWR_KEYS from 'src/constants/swrKeys'
10+
import { ProposalsResponse } from 'src/data/subgraph/requests/proposalsQuery'
11+
import { getProposals } from 'src/data/subgraph/requests/proposalsQuery'
12+
import { TransactionType } from 'src/modules/create-proposal/constants'
13+
import { useProposalStore } from 'src/modules/create-proposal/stores'
14+
import { getChainFromLocalStorage } from 'src/utils/getChainFromLocalStorage'
15+
16+
import EscrowForm from './EscrowForm'
17+
import { EscrowFormValues } from './EscrowForm.schema'
18+
import { useEscrowFormStore } from './EscrowUtils'
19+
import {
20+
KLEROS_ARBITRATION_PROVIDER,
21+
createEscrowData,
22+
deployEscrowAbi,
23+
getEscrowBundler,
24+
} from './EscrowUtils'
25+
26+
export const Escrow: React.FC = () => {
27+
const [isSubmitting, setIsSubmitting] = useState(false)
28+
const [ipfsUploadError, setIpfsUploadError] = useState<Error | null>(null)
29+
30+
const { query, isReady } = useRouter()
31+
32+
const { id: chainId } = getChainFromLocalStorage()
33+
34+
const addTransaction = useProposalStore((state) => state.addTransaction)
35+
36+
const { data } = useSWR<ProposalsResponse>(
37+
isReady ? [SWR_KEYS.PROPOSALS, chainId, query.token, '0'] : null,
38+
(_, chainId, token, page) => getProposals(chainId, token, 1, Number(0))
39+
)
40+
41+
const lastProposalId = data?.proposals?.[0]?.proposalNumber ?? 0
42+
43+
const { formValues } = useEscrowFormStore()
44+
45+
const handleEscrowTransaction = useCallback(
46+
async (values: EscrowFormValues) => {
47+
const ipfsDataToUpload = {
48+
title: 'Proposal #' + (lastProposalId + 1),
49+
description: window?.location.href.replace(
50+
'/proposal/create',
51+
'/vote/' + lastProposalId + 1
52+
),
53+
endDate: new Date(
54+
values.milestones[values.milestones.length - 1].endDate
55+
).getTime(),
56+
milestones: values.milestones.map((x, index) => ({
57+
id: 'milestone-00' + index,
58+
title: x.title,
59+
description: x.description,
60+
endDate: new Date(x.endDate).getTime() / 1000, // in seconds
61+
createdAt: Date.now() / 1000, // in seconds
62+
startDate: Date.now() + 7 * 86400, // set start date 7 days from submission in seconds
63+
resolverType: 'kleros',
64+
totalAmount: values.milestones.reduce((acc, x) => acc + x.amount, 0),
65+
klerosCourt: 1,
66+
arbitrationProvider: KLEROS_ARBITRATION_PROVIDER,
67+
...(x.mediaType && x.mediaUrl
68+
? {
69+
documents: [
70+
{
71+
id: 'doc-001',
72+
type: 'ipfs',
73+
src: x.mediaUrl,
74+
mimeType: x.mediaType,
75+
createdAt: new Date().getTime() / 1000,
76+
},
77+
],
78+
}
79+
: {}),
80+
})),
81+
}
82+
83+
const jsonDataToUpload = JSON.stringify(ipfsDataToUpload, null, 2)
84+
const fileToUpload = new File([jsonDataToUpload], 'escrow-data.json', {
85+
type: 'application/json',
86+
})
87+
88+
let cid: string, uri: string;
89+
90+
try {
91+
console.log('Uploading to IPFS...')
92+
setIsSubmitting(true)
93+
const response = await uploadFile(fileToUpload, {
94+
cache: true,
95+
onProgress: (progress) => {
96+
console.log(`Upload progress: ${progress}%`)
97+
},
98+
})
99+
cid = response.cid
100+
uri = response.uri
101+
setIsSubmitting(false)
102+
setIpfsUploadError(null)
103+
console.log('IPFS upload successful. CID:', cid, 'URI:', uri)
104+
} catch (err: any) {
105+
console.log('IPFS upload error:', err)
106+
setIsSubmitting(false)
107+
setIpfsUploadError(
108+
new Error(
109+
`Sorry, there was an error with our file uploading service. ${err?.message}`
110+
)
111+
)
112+
return
113+
}
114+
115+
// create bundler transaction data
116+
const escrowData = createEscrowData(values, cid, chainId)
117+
const milestoneAmounts = values.milestones.map((x) => x.amount * 10 ** 18)
118+
const fundAmount = milestoneAmounts.reduce((acc, x) => acc + x, 0)
119+
120+
console.log(milestoneAmounts, fundAmount)
121+
122+
const escrow = {
123+
target: getEscrowBundler(chainId),
124+
functionSignature: 'deployEscrow()',
125+
calldata: encodeFunctionData({
126+
abi: deployEscrowAbi,
127+
functionName: 'deployEscrow',
128+
args: [milestoneAmounts, escrowData, fundAmount],
129+
}),
130+
value: formatEther(BigInt(fundAmount)),
131+
}
132+
133+
try {
134+
// add 2.5s delay here before adding to queue
135+
setTimeout(
136+
() =>
137+
addTransaction({
138+
type: TransactionType.ESCROW,
139+
summary: `Create and fund new Escrow with ${formatEther(
140+
BigInt(fundAmount)
141+
)} ETH`,
142+
transactions: [escrow],
143+
}),
144+
2500
145+
)
146+
} catch (err) {
147+
console.log('Error', err)
148+
}
149+
setIsSubmitting(false)
150+
},
151+
[formValues]
152+
)
153+
154+
return (
155+
<Stack>
156+
<EscrowForm
157+
onSubmit={handleEscrowTransaction}
158+
isSubmitting={isSubmitting}
159+
/>
160+
{ipfsUploadError?.message && <div>Error: {ipfsUploadError.message}</div>}
161+
</Stack>
162+
)
163+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { Box, Stack, Text } from '@zoralabs/zord'
2+
import { useFormikContext } from 'formik'
3+
import { useBalance } from 'wagmi'
4+
5+
import { useDaoStore } from 'src/modules/dao'
6+
import { useLayoutStore } from 'src/stores'
7+
import { useChainStore } from 'src/stores/useChainStore'
8+
9+
import { EscrowFormValues } from './EscrowForm.schema'
10+
11+
export default function EscrowDetailsDisplay() {
12+
const { values } = useFormikContext<EscrowFormValues>()
13+
const isMobile = useLayoutStore((x) => x.isMobile)
14+
15+
const { treasury } = useDaoStore((state) => state.addresses)
16+
const chain = useChainStore((x) => x.chain)
17+
const { data: treasuryBalance } = useBalance({
18+
address: treasury,
19+
chainId: chain.id,
20+
})
21+
22+
const totalEscrowAmount = values?.milestones
23+
.map((x) => x.amount)
24+
.reduce((acc, x) => acc + x, 0)
25+
return (
26+
<Box
27+
position={isMobile ? 'relative' : 'absolute'}
28+
style={{
29+
height: '100%',
30+
maxWidth: isMobile ? '100%' : '50%',
31+
}}
32+
top={'x0'}
33+
right={'x0'}
34+
>
35+
<Stack position={'sticky'} top={'x20'} right={'x0'} gap={'x5'} align="flex-end">
36+
{Number(totalEscrowAmount) > Number(treasuryBalance?.formatted) && (
37+
<Text variant="paragraph-sm" color="negative">
38+
Escrow amount exceeding treasury balance
39+
</Text>
40+
)}
41+
<Box>
42+
<Text fontSize={12} color="text4" style={{ fontWeight: 'bold' }}>
43+
Total Escrow Amount
44+
</Text>
45+
<Text variant="heading-sm" style={{ fontWeight: 'bold' }}>
46+
{totalEscrowAmount ?? '0.00'} ETH
47+
</Text>
48+
</Box>
49+
<Box style={{ textAlign: 'right' }}>
50+
<Text fontSize={12} color="text4" style={{ fontWeight: 'bold' }}>
51+
Escrow Service by
52+
</Text>
53+
<Text variant="heading-sm" style={{ fontWeight: 'bold' }}>
54+
SmartInvoice
55+
</Text>
56+
</Box>
57+
</Stack>
58+
</Box>
59+
)
60+
}

0 commit comments

Comments
 (0)