Skip to content

Commit

Permalink
added withdraw feature. Reorg the code for setting up algod, indexer …
Browse files Browse the repository at this point in the history
…client
  • Loading branch information
iskysun96 committed Nov 30, 2023
1 parent 4f06151 commit c12a627
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 43 deletions.
10 changes: 9 additions & 1 deletion frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ export default function App() {
setSubmissions((prevSubmissions: any) => [...prevSubmissions, newSubmission])
}

const handleRemoveFundraiser = (submission: FormData) => {
const newSubmissions = submissions.filter((s: FormData) => s !== submission)
setSubmissions(newSubmissions)
}

const algodConfig = getAlgodConfigFromViteEnvironment()

const walletProviders = useInitializeProviders({
Expand All @@ -55,7 +60,10 @@ export default function App() {
<Navbar />
<Routes>
<Route path="/" element={<Home submissions={submissions} />} />
<Route path="/create" element={<Create onFormSubmit={handleFormSubmit} />} />
<Route
path="/create"
element={<Create onFormSubmit={handleFormSubmit} handleRemoveFundraiser={handleRemoveFundraiser} submissions={submissions} />}
/>
</Routes>
</BrowserRouter>
</WalletProvider>
Expand Down
9 changes: 2 additions & 7 deletions frontend/src/components/DonationOptinPopup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import algosdk from 'algosdk'
import { useSnackbar } from 'notistack'
import { useState } from 'react'
import { FormData } from '../interfaces/formData'
import { getAlgodConfigFromViteEnvironment } from '../utils/network/getAlgoClientConfigs'
import { getAlgodClient } from '../utils/setupClients'

interface DonationPopupProps {
openModal: boolean
Expand All @@ -19,12 +19,7 @@ export function DonationOptinPopup({ openModal, closeModal, submission }: Donati
const { enqueueSnackbar } = useSnackbar()
const { signer, activeAddress } = useWallet()

const algodConfig = getAlgodConfigFromViteEnvironment()
const algodClient = algokit.getAlgoClient({
server: algodConfig.server,
port: algodConfig.port,
token: algodConfig.token,
})
const algodClient = getAlgodClient()

const handleYesClick = async () => {
setLoading(true)
Expand Down
11 changes: 3 additions & 8 deletions frontend/src/components/FundraiseItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { useSnackbar } from 'notistack'
import { useEffect, useRef, useState } from 'react'
import { CharityCrowdfundingAppClient } from '../contracts/charityCrowdfundingApp'
import { FormData } from '../interfaces/formData'
import { getAlgodConfigFromViteEnvironment } from '../utils/network/getAlgoClientConfigs'
import { getAlgodClient } from '../utils/setupClients'

import { DonationOptinPopup } from './DonationOptinPopup'

interface FundraiseItemProps {
Expand All @@ -24,12 +25,7 @@ export function FundraiseItem({ submission }: FundraiseItemProps) {
const { enqueueSnackbar } = useSnackbar()
const { signer, activeAddress } = useWallet()

const algodConfig = getAlgodConfigFromViteEnvironment()
const algodClient = algokit.getAlgoClient({
server: algodConfig.server,
port: algodConfig.port,
token: algodConfig.token,
})
const algodClient = getAlgodClient()

const handleDonateClick = async () => {
setLoading(true)
Expand Down Expand Up @@ -100,7 +96,6 @@ export function FundraiseItem({ submission }: FundraiseItemProps) {
{
sendParams: { fee: algokit.transactionFees(2), suppressLog: true },
assets: [submission.nftID],
boxes: [{ appId: appID, name: signingAccount }],
},
)
.catch((e: Error) => {
Expand Down
186 changes: 161 additions & 25 deletions frontend/src/components/StartCreate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,32 @@ import { useEffect, useRef, useState } from 'react'
import { Web3Storage } from 'web3.storage'
import { CharityCrowdfundingAppClient } from '../contracts/charityCrowdfundingApp'
import { FormData } from '../interfaces/formData'
import { getAlgodConfigFromViteEnvironment, getIndexerConfigFromViteEnvironment } from '../utils/network/getAlgoClientConfigs'
import { getAlgodClient, getIndexerClient } from '../utils/setupClients'

/**
* Interface
*
* onFormSubmit - function to submit the form data and add to the submissions array in App.tsx
* handleRemoveFundraiser - function to remove the fundraiser from the submissions array in App.tsx
* submissions - array of submissions in App.tsx
*/

interface StartCreateComponentProps {
onFormSubmit: (formData: FormData) => void
handleRemoveFundraiser: (submission: FormData) => void
submissions: FormData[]
}

export function StartCreate({ onFormSubmit }: StartCreateComponentProps) {
/**
* StartCreate Component Explained
*
* This component is used to create a new fundraiser. It is rendered in the Create page.
* Also contains the Withdraw Funds feature. If the connected wallet created a fundraiser, the Withdraw Funds button will appear.
*/

export function StartCreate({ onFormSubmit, handleRemoveFundraiser, submissions }: StartCreateComponentProps) {
const [loading, setLoading] = useState<boolean>(false)
const [currentFundraiserBalance, setCurrentFundraiserBalance] = useState<number>(0)
const [w3s, setW3s] = useState<Web3Storage>()
const [formData, setFormData] = useState<FormData>({
title: '',
Expand All @@ -38,6 +56,8 @@ export function StartCreate({ onFormSubmit }: StartCreateComponentProps) {
const { signer, activeAddress } = useWallet()

const isFirstRender = useRef(true)

// Initialize Web3 Storage. Used to store the NFT image and metadata on IPFS
useEffect(() => {
const W3S_TOKEN = import.meta.env.VITE_WEB3STORAGE_TOKEN
if (W3S_TOKEN === undefined) {
Expand All @@ -48,20 +68,11 @@ export function StartCreate({ onFormSubmit }: StartCreateComponentProps) {
setW3s(w3s)
}, [])

const algodConfig = getAlgodConfigFromViteEnvironment()
const algodClient = algokit.getAlgoClient({
server: algodConfig.server,
port: algodConfig.port,
token: algodConfig.token,
})

const indexerConfig = getIndexerConfigFromViteEnvironment()
const indexer = algokit.getAlgoIndexerClient({
server: indexerConfig.server,
port: indexerConfig.port,
token: indexerConfig.token,
})
// Set up algod, Indexer
const algodClient = getAlgodClient()
const indexer = getIndexerClient()

// Function to convert the NFT image to Arc3 format (https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0003.md)
async function imageToArc3(file: any) {
if (!w3s) {
enqueueSnackbar('Web3 Storage not initialized', { variant: 'warning' })
Expand All @@ -86,11 +97,13 @@ export function StartCreate({ onFormSubmit }: StartCreateComponentProps) {
return metadataRoot
}

// store user form input to formData
const handleInputChange = (e: { target: { id: any; value: any } }) => {
const { id, value } = e.target
setFormData((prevFormData) => ({ ...prevFormData, [id]: value }))
}

// store user charity image file upload to formData
const handleCharityFileChange = (e: { target: { files: FileList | null } }) => {
const file = e.target.files?.[0]
if (file) {
Expand All @@ -103,6 +116,7 @@ export function StartCreate({ onFormSubmit }: StartCreateComponentProps) {
}
}

// store user nft image file upload to formData
const handleNftFileChange = (e: { target: { files: FileList | null } }) => {
const file = e.target.files?.[0]
if (file) {
Expand All @@ -115,17 +129,35 @@ export function StartCreate({ onFormSubmit }: StartCreateComponentProps) {
}
}

/**
* handleSubmit
*
* This function does multiple things
* 1. Deploy the CharityCrowdfundingApp contract
* 2. Bootstrap the contract with the following:
* - App Details
* * Title
* * Detail
* * Goal
* * Minimum Donation
* * Asset Name
* * Unit Name
* * NFTAmount
* * Asset URL
* - Send 0.2 ALGO to the contract to cover the MBR
* - Create the Reward NFT
*/
const handleSubmit = async () => {
setLoading(true)
if (!signer || !activeAddress) {
enqueueSnackbar('Please connect wallet first', { variant: 'warning' })
setLoading(false)
return
}
console.log('W3S Token in handleSubmit', w3s)

const signingAccount = { signer, addr: activeAddress } as TransactionSignerAccount

// Initialize the CharityCrowdfundingAppClient
const appDetails = {
resolveBy: 'creatorAndName',
sender: signingAccount,
Expand All @@ -135,23 +167,20 @@ export function StartCreate({ onFormSubmit }: StartCreateComponentProps) {

const appClient = new CharityCrowdfundingAppClient(appDetails, algodClient)

//Use the appClient to deploy the contract
const app = await appClient.appClient.create().catch((e: Error) => {
enqueueSnackbar(`Error deploying the contract: ${e.message}`, { variant: 'error' })
setLoading(false)
return
})

if (!app) {
enqueueSnackbar('App is Not Created!', { variant: 'warning' })
return
}

const sp = await algodClient.getTransactionParams().do()

// Create a payment transaction to send 0.2 ALGO to the contract to cover the MBR
const payMbrTxn = algosdk.makePaymentTxnWithSuggestedParamsFromObject({
from: activeAddress,
to: app?.appAddress as string,
amount: 200_000, // 0.1 ALGO to cover Asset MBR
amount: 200_000, // 0.2 ALGO to cover Asset MBR and contract MBR
suggestedParams: sp,
})

Expand All @@ -167,6 +196,7 @@ export function StartCreate({ onFormSubmit }: StartCreateComponentProps) {
return
})

// Use the appClient to call the bootstrap contract method
const bootstrapOutput = await appClient
.bootstrap(
{
Expand All @@ -190,6 +220,7 @@ export function StartCreate({ onFormSubmit }: StartCreateComponentProps) {

const rewardNftID = Number(bootstrapOutput?.return?.valueOf())
console.log('The created Reward NFT ID is: ', rewardNftID)

if (!app || !rewardNftID) {
enqueueSnackbar('App and reward NFT is Not Created!', { variant: 'warning' })
return
Expand All @@ -198,6 +229,7 @@ export function StartCreate({ onFormSubmit }: StartCreateComponentProps) {
setFormData((prevFormData) => ({ ...prevFormData, appID: Number(app.appId), nftID: rewardNftID, organizer_address: activeAddress }))
}

// Using useEffect to wait for formData to be updated and then add the formData to the submissions array in App.tsx
useEffect(() => {
if (isFirstRender.current || formData.appID === 0 || formData.nftID === 0) {
isFirstRender.current = false
Expand All @@ -208,6 +240,7 @@ export function StartCreate({ onFormSubmit }: StartCreateComponentProps) {
onFormSubmit(formData)
enqueueSnackbar(`Charity Successfully Created!`, { variant: 'success' })

// Reset the formData
setFormData({
title: '',
detail: '',
Expand All @@ -227,10 +260,113 @@ export function StartCreate({ onFormSubmit }: StartCreateComponentProps) {
setLoading(false)
}, [formData.appID, formData.nftID])

//################## Withdraw Funds Feature ##################

// Check if the current wallet has created a fundraiser
function checkCurrentFundraiser() {
for (let i = 0; i < submissions.length; i++) {
if (activeAddress === submissions[i].organizer_address) {
return submissions[i]
}
}
return null
}

const currentFundraiser = checkCurrentFundraiser()

// Get the current fundraiser balance
async function getCurrentFundraiserBalance() {
setLoading(true)

if (!currentFundraiser) {
enqueueSnackbar('This wallet did not create a fundraiser', { variant: 'warning' })
setLoading(false)
return
}

const appAddr = await algosdk.getApplicationAddress(currentFundraiser.appID)

const fundraiserBalance = await algodClient
.accountInformation(appAddr)
.do()
.catch((e: Error) => {
enqueueSnackbar(`Error getting the contract balance: ${e.message}`, { variant: 'error' })
setLoading(false)
return
})

setCurrentFundraiserBalance(fundraiserBalance?.amount / 1_000_000)
setLoading(false)
}

// Using useEffect to wait for currentFundraiser to be updated and then get the current fundraiser balance
useEffect(() => {
if (isFirstRender.current || !currentFundraiser) {
isFirstRender.current = false
return
}
getCurrentFundraiserBalance()
}, [currentFundraiser])

// Withdraw the funds from the fundraiser
async function handleWithdraw() {
setLoading(true)
if (!signer || !activeAddress) {
enqueueSnackbar('Please connect wallet first', { variant: 'warning' })
setLoading(false)
return
}

if (!currentFundraiser) {
enqueueSnackbar('This wallet did not create a fundraiser', { variant: 'warning' })
setLoading(false)
return
}

const signingAccount = { signer, addr: activeAddress } as TransactionSignerAccount

// Initialize the CharityCrowdfundingAppClient with the current fundraiser appID
const appID = currentFundraiser?.appID
const appClient = new CharityCrowdfundingAppClient(
{
resolveBy: 'id',
id: appID,
sender: signingAccount,
},
algodClient,
)

// Use the appClient to call the claimFund contract method
const claimedFunds = await appClient.claimFund({}, { sendParams: { fee: algokit.transactionFees(2) } }).catch((e: Error) => {
enqueueSnackbar(`Error withdrawing the funds: ${e.message}`, { variant: 'error' })
setLoading(false)
return
})

enqueueSnackbar(`Successfully withdrew ${Number(claimedFunds?.return) / 1_000_000} ALGOs`, { variant: 'success' })

handleRemoveFundraiser(currentFundraiser)
setLoading(false)
}

return (
<div className="flex justify-center items-center h-screen w-screen">
<div className="form-control mx-auto py-8 text-center max-w-lg">
<h1 className="text-2xl font-bold mb-4">Charity Details</h1>
<div className="flex justify-center items-center h-screen w-screen mt-10">
<div className="form-control mx-auto py-8 text-center max-w-lg mt-10">
{activeAddress === currentFundraiser?.organizer_address && (
<div className="form-control max-w-lg">
<h1 className="text-2xl font-bold mb-4 mt-10">Withdraw Funds</h1>
<h2 className="mb-4 max-w-lg">Total Raised Funds: {currentFundraiserBalance - 0.2} ALGOs</h2>
<button
className="btn join-item rounded-r bg-green-500 border-none hover:bg-green-600 shadow-md transition-colors duration-300"
onClick={handleWithdraw}
disabled={loading}
>
{loading ? <span className="loading loading-spinner" /> : <p className="text-white">Withdraw funds</p>}
</button>
</div>
)}

<h1 className="text-2xl font-bold mb-4 mt-4">Charity Details</h1>
<div className="flex flex-wrap -mx-2 mb-4">
<div className=" form-control w-full md:w-1/ px-2 mb-4">
<label className="label">
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/pages/Create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { FormData } from '../interfaces/formData'

interface CreateComponentProps {
onFormSubmit: (formData: FormData) => void
handleRemoveFundraiser: (submission: FormData) => void
submissions: FormData[]
}

export function Create({ onFormSubmit }: CreateComponentProps) {
export function Create({ onFormSubmit, handleRemoveFundraiser, submissions }: CreateComponentProps) {
return (
<div className="container d-flex align-items-center justify-content-center" style={{ height: '80vh', maxWidth: '400px' }}>
<StartCreate onFormSubmit={onFormSubmit} />
<StartCreate onFormSubmit={onFormSubmit} handleRemoveFundraiser={handleRemoveFundraiser} submissions={submissions} />
</div>
)
}
Loading

0 comments on commit c12a627

Please sign in to comment.