From 95655a1e88fd83d0469edf0b9a2d382164b635e0 Mon Sep 17 00:00:00 2001 From: veeso Date: Thu, 5 Dec 2024 12:54:10 +0100 Subject: [PATCH] feat: Token page with interaction with deferred and marketplace --- .../App/pages/Marketplace/pages/Contract.tsx | 4 +- .../pages/Contract/BuyTokenForm.tsx | 211 +++++++++++++++++ .../pages/Contract/RealEstateCard.tsx | 7 +- .../Contract/RealEstateCard/Progress.tsx | 68 ++++++ .../Marketplace/pages/Contract/TokensList.tsx | 31 --- .../pages/Contract/TokensList/TokenItem.tsx | 33 --- src/js/components/MetamaskConnect.tsx | 11 +- src/js/components/Status/AppError.tsx | 29 +-- src/js/components/Status/AppSuccess.tsx | 27 +-- src/js/components/Task/Entry.tsx | 36 +++ src/js/components/Task/TaskList.tsx | 132 +++++++++++ src/js/components/reusable/ProgressBar.tsx | 11 +- .../components/reusable/WaitForMetamask.tsx | 19 ++ src/js/utils/format.ts | 26 +++ src/js/web3/DeferredClient.ts | 17 ++ src/js/web3/MarketplaceClient.ts | 16 +- src/js/web3/UsdtClient.ts | 2 +- src/js/web3/contracts/Deferred.ts | 213 +++++++++++++++++- src/js/web3/contracts/Marketplace.ts | 61 ++++- 19 files changed, 823 insertions(+), 131 deletions(-) create mode 100644 src/js/components/App/pages/Marketplace/pages/Contract/BuyTokenForm.tsx create mode 100644 src/js/components/App/pages/Marketplace/pages/Contract/RealEstateCard/Progress.tsx delete mode 100644 src/js/components/App/pages/Marketplace/pages/Contract/TokensList.tsx delete mode 100644 src/js/components/App/pages/Marketplace/pages/Contract/TokensList/TokenItem.tsx create mode 100644 src/js/components/Task/Entry.tsx create mode 100644 src/js/components/Task/TaskList.tsx create mode 100644 src/js/components/reusable/WaitForMetamask.tsx create mode 100644 src/js/utils/format.ts diff --git a/src/js/components/App/pages/Marketplace/pages/Contract.tsx b/src/js/components/App/pages/Marketplace/pages/Contract.tsx index 3526613..0582dfd 100644 --- a/src/js/components/App/pages/Marketplace/pages/Contract.tsx +++ b/src/js/components/App/pages/Marketplace/pages/Contract.tsx @@ -4,7 +4,7 @@ import { useParams } from 'react-router-dom'; import { Contract } from '../../../../../data/contract'; import Container from '../../../../reusable/Container'; import RealEstateCard from './Contract/RealEstateCard'; -import TokensList from './Contract/TokensList'; +import BuyTokenForm from './Contract/BuyTokenForm'; import getContractById from '../../../../../api/getContractById'; import { useAppContext } from '../../../AppContext'; import { Helmet } from 'react-helmet'; @@ -44,7 +44,7 @@ const ContractPage = () => { - + diff --git a/src/js/components/App/pages/Marketplace/pages/Contract/BuyTokenForm.tsx b/src/js/components/App/pages/Marketplace/pages/Contract/BuyTokenForm.tsx new file mode 100644 index 0000000..0f85f27 --- /dev/null +++ b/src/js/components/App/pages/Marketplace/pages/Contract/BuyTokenForm.tsx @@ -0,0 +1,211 @@ +import * as React from 'react'; + +import { Contract } from '../../../../../../data/contract'; +import Container from '../../../../../reusable/Container'; +import Heading from '../../../../../reusable/Heading'; +import WaitForMetamask from '../../../../../reusable/WaitForMetamask'; +import MetamaskConnect, { ChainId } from '../../../../../MetamaskConnect'; +import Paragraph from '../../../../../reusable/Paragraph'; +import { useConnectedMetaMask } from 'metamask-react'; +import DeferredClient from '../../../../../../web3/DeferredClient'; +import MarketplaceClient from '../../../../../../web3/MarketplaceClient'; +import { + convertToHumanReadable, + USDT_DECIMALS, +} from '../../../../../../utils/format'; +import Button from '../../../../../reusable/Button'; +import TaskList, { Result } from '../../../../../Task/TaskList'; +import { useAppContext } from '../../../../AppContext'; +import UsdtClient from '../../../../../../web3/UsdtClient'; +import Skeleton from 'react-loading-skeleton'; + +interface Props { + contract: Contract; +} + +const BuyTokenForm = (props: Props) => ( + + Buy contract tokens + }> + + + +); + +const LogWithMetamask = () => ( + + + Please connect to MetaMask to buy tokens. + + + +); + +const BuyTokenFormInner = ({ contract }: Props) => { + const { setAppError, setAppSuccess } = useAppContext(); + const { account, ethereum, chainId } = useConnectedMetaMask(); + + const [tokenPrice, setTokenPrice] = React.useState(); + const [tokenId, setTokenId] = React.useState(null); + const [fetchedData, setFetchedData] = React.useState(false); + + const [pendingTx, setPendingTx] = React.useState(false); + + const onBuyToken = () => { + setPendingTx(true); + }; + + const approveUsdt = async (): Promise => { + if (tokenPrice === undefined) { + return { error: 'Token price is not available' }; + } + // get current approval for marketplace + const marketplaceClient = new MarketplaceClient( + account, + ethereum, + chainId as ChainId, + ); + const marketplaceAddress = marketplaceClient.marketplaceAddress(); + + const usdtClient = new UsdtClient(account, ethereum, chainId as ChainId); + // get allowance + const allowance = await usdtClient.allowance(account, marketplaceAddress); + // check if allowance is enough + if (allowance >= tokenPrice) { + return true; + } + + // approve + try { + await usdtClient.approve(marketplaceAddress, tokenPrice); + } catch (e) { + console.error(`Failed to approve marketplace: ${e}`); + return { + error: `Failed to approve marketplace to spend ${convertToHumanReadable(tokenPrice, USDT_DECIMALS)} USDT`, + }; + } + + return true; + }; + + const buyToken = async (): Promise => { + const marketplaceClient = new MarketplaceClient( + account, + ethereum, + chainId as ChainId, + ); + + try { + await marketplaceClient.buyNextToken(contract.id); + } catch (e) { + console.error(`Failed to buy token: ${e}`); + return { error: 'Failed to buy token' }; + } + + return true; + }; + + const onTokenBought = (result: Result) => { + setPendingTx(false); + + if (result === true) { + setAppSuccess(`Token #${tokenId} bought successfully`); + // reload token + loadToken(); + } else { + setAppError(`Failed to buy token: ${result.error}`); + } + }; + + const loadToken = () => { + const deferredClient = new DeferredClient( + account, + ethereum, + chainId as ChainId, + ); + const marketplaceClient = new MarketplaceClient( + account, + ethereum, + chainId as ChainId, + ); + + deferredClient + .nextTokenIdToBuy(contract.id) + .then((id) => { + setTokenId(id); + setFetchedData(true); + }) + .catch((e) => { + console.error(`Failed to load token data: ${e.message}`); + setFetchedData(true); + }); + marketplaceClient + .tokenPriceForCaller(contract.id) + .then(setTokenPrice) + .catch((e) => { + console.error(`Failed to load token data: ${e.message}`); + }); + }; + + React.useEffect(() => { + loadToken(); + }, [contract]); + + if (!fetchedData) { + return ( + + + + ); + } + + if (tokenId === null || tokenPrice === undefined) { + return ( + + + All the tokens for this contract have already been sold. + + + ); + } + + const tokenPriceUsd = Number( + convertToHumanReadable(tokenPrice, USDT_DECIMALS, true), + ); + const tokenPriceUsdString = tokenPriceUsd.toLocaleString('en-US', { + style: 'currency', + currency: contract.currency, + minimumFractionDigits: 2, + }); + + return ( + + + Buy token #{tokenId.toString()} + + + Token Price: {tokenPriceUsdString} + + + Buy token for {tokenPriceUsdString} + + + + ); +}; + +export default BuyTokenForm; diff --git a/src/js/components/App/pages/Marketplace/pages/Contract/RealEstateCard.tsx b/src/js/components/App/pages/Marketplace/pages/Contract/RealEstateCard.tsx index efe0b00..2411ddb 100644 --- a/src/js/components/App/pages/Marketplace/pages/Contract/RealEstateCard.tsx +++ b/src/js/components/App/pages/Marketplace/pages/Contract/RealEstateCard.tsx @@ -5,7 +5,7 @@ import { Contract } from '../../../../../../data/contract'; import Container from '../../../../../reusable/Container'; import Heading from '../../../../../reusable/Heading'; import Paragraph from '../../../../../reusable/Paragraph'; -import ProgressBar from '../../../../../reusable/ProgressBar'; +import Progress from './RealEstateCard/Progress'; interface Props { contract: Contract; @@ -48,10 +48,7 @@ const RealEstateCard = ({ contract }: Props) => ( - - Mortgage payment progress - - + {contract.realEstate.description} diff --git a/src/js/components/App/pages/Marketplace/pages/Contract/RealEstateCard/Progress.tsx b/src/js/components/App/pages/Marketplace/pages/Contract/RealEstateCard/Progress.tsx new file mode 100644 index 0000000..23f2694 --- /dev/null +++ b/src/js/components/App/pages/Marketplace/pages/Contract/RealEstateCard/Progress.tsx @@ -0,0 +1,68 @@ +import { useConnectedMetaMask } from 'metamask-react'; +import * as React from 'react'; +import WaitForMetamask from '../../../../../../reusable/WaitForMetamask'; +import DeferredClient from '../../../../../../../web3/DeferredClient'; +import { ChainId } from '../../../../../../MetamaskConnect'; +import Container from '../../../../../../reusable/Container'; +import ProgressBar from '../../../../../../reusable/ProgressBar'; + +interface Props { + contractId: bigint; + installments: number; +} + +const Progress = (props: Props) => ( + + + +); + +const InnerProgress = ({ contractId, installments }: Props) => { + const { account, ethereum, chainId } = useConnectedMetaMask(); + + const [progress, setProgress] = React.useState(0); + const [completed, setCompleted] = React.useState(false); + const [fetchedData, setFetchedData] = React.useState(false); + + React.useEffect(() => { + const deferredClient = new DeferredClient( + account, + ethereum, + chainId as ChainId, + ); + + deferredClient.contractProgress(contractId).then((progressBig) => { + const progress = Number(progressBig); + setProgress(progress); + setFetchedData(true); + }); + deferredClient.contractCompleted(contractId).then(setCompleted); + }, [contractId]); + + if (!fetchedData) { + return null; + } + + if (completed) { + return ( + + Mortgage payment progress + + + ); + } + + return ( + + Mortgage payment progress + + + ); +}; + +export default Progress; diff --git a/src/js/components/App/pages/Marketplace/pages/Contract/TokensList.tsx b/src/js/components/App/pages/Marketplace/pages/Contract/TokensList.tsx deleted file mode 100644 index dcb081a..0000000 --- a/src/js/components/App/pages/Marketplace/pages/Contract/TokensList.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import * as React from 'react'; - -import { Contract } from '../../../../../../data/contract'; -import Container from '../../../../../reusable/Container'; -import Heading from '../../../../../reusable/Heading'; -import TokenItem from './TokensList/TokenItem'; - -interface Props { - contract: Contract; -} - -const TokensList = ({ contract }: Props) => { - const tokenValue = contract.price / contract.installments; - const tokens = Array.from({ length: 10 }, (_, i) => ({ - id: i, - value: tokenValue, - })); - - return ( - - Tokens for sale - - {tokens.map((token) => ( - - ))} - - - ); -}; - -export default TokensList; diff --git a/src/js/components/App/pages/Marketplace/pages/Contract/TokensList/TokenItem.tsx b/src/js/components/App/pages/Marketplace/pages/Contract/TokensList/TokenItem.tsx deleted file mode 100644 index a65adfe..0000000 --- a/src/js/components/App/pages/Marketplace/pages/Contract/TokensList/TokenItem.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import * as React from 'react'; -import * as Icon from 'react-feather'; - -import Container from '../../../../../../reusable/Container'; -import Heading from '../../../../../../reusable/Heading'; -import Button from '../../../../../../reusable/Button'; - -interface Props { - id: number; - value: number; -} - -const TokenItem = ({ id, value }: Props) => ( - - - - Token #{id} - - - {value.toLocaleString('en-US', { - style: 'currency', - currency: 'USD', - })} - - - - Buy Token - - - -); - -export default TokenItem; diff --git a/src/js/components/MetamaskConnect.tsx b/src/js/components/MetamaskConnect.tsx index 8d99903..7fb72f7 100644 --- a/src/js/components/MetamaskConnect.tsx +++ b/src/js/components/MetamaskConnect.tsx @@ -10,6 +10,9 @@ export enum ChainId { Sepolia = '0xaa36a7', } +const DEFAULT_CHAIN_ID = + process.env.NODE_ENV === 'development' ? ChainId.Sepolia : ChainId.Mainnet; + const MetamaskConnect = () => { const { status, @@ -28,8 +31,8 @@ const MetamaskConnect = () => { const onClick = () => { if (status === 'notConnected') { - if (ChainId.Mainnet !== currentChainId) { - switchChain(ChainId.Mainnet); + if (DEFAULT_CHAIN_ID !== currentChainId) { + switchChain(DEFAULT_CHAIN_ID); } return connect(); } @@ -52,8 +55,8 @@ const MetamaskConnect = () => { }; React.useEffect(() => { - if (currentChainId && currentChainId !== ChainId.Mainnet) { - switchChain(ChainId.Mainnet); + if (currentChainId && currentChainId !== DEFAULT_CHAIN_ID) { + switchChain(DEFAULT_CHAIN_ID); } }, [switchChain]); diff --git a/src/js/components/Status/AppError.tsx b/src/js/components/Status/AppError.tsx index 02a59ea..998d3cf 100644 --- a/src/js/components/Status/AppError.tsx +++ b/src/js/components/Status/AppError.tsx @@ -3,13 +3,10 @@ import * as Icon from 'react-feather'; import Container from '../reusable/Container'; import { useAppContext } from '../App/AppContext'; -import ProgressBar from '../reusable/ProgressBar'; const AppError = () => { const { appError, setAppError } = useAppContext(); - const [dismissInterval, setDismissInterval] = - React.useState(); - const [progress, setProgress] = React.useState(0); + const [dismissTimeout, setDismissTimeout] = React.useState(); const onDismiss = () => { setAppError(undefined); @@ -17,29 +14,28 @@ const AppError = () => { React.useEffect(() => { return () => { - if (dismissInterval) { - clearInterval(dismissInterval); + if (dismissTimeout) { + clearInterval(dismissTimeout); } }; }, []); React.useEffect(() => { - setDismissInterval( - setInterval(() => { - if (progress < 5000) { - setProgress(progress + 10); - } else { - onDismiss(); - clearInterval(dismissInterval); - } - }, 10), + if (appError === undefined) { + return; + } + setDismissTimeout( + setTimeout(() => { + onDismiss(); + clearTimeout(dismissTimeout); + }, 5_000), ); }, [appError]); if (appError) { return ( - + {appError} @@ -48,7 +44,6 @@ const AppError = () => { - ); } else { diff --git a/src/js/components/Status/AppSuccess.tsx b/src/js/components/Status/AppSuccess.tsx index fa73294..1d4b1d5 100644 --- a/src/js/components/Status/AppSuccess.tsx +++ b/src/js/components/Status/AppSuccess.tsx @@ -3,13 +3,10 @@ import * as Icon from 'react-feather'; import Container from '../reusable/Container'; import { useAppContext } from '../App/AppContext'; -import ProgressBar from '../reusable/ProgressBar'; const AppSuccess = () => { const { appSuccess, setAppSuccess } = useAppContext(); - const [dismissInterval, setDismissInterval] = - React.useState(); - const [progress, setProgress] = React.useState(0); + const [dismissTimeout, setDismissTimeout] = React.useState(); const onDismiss = () => { setAppSuccess(undefined); @@ -17,22 +14,21 @@ const AppSuccess = () => { React.useEffect(() => { return () => { - if (dismissInterval) { - clearInterval(dismissInterval); + if (dismissTimeout) { + clearInterval(dismissTimeout); } }; }, []); React.useEffect(() => { - setDismissInterval( - setInterval(() => { - if (progress < 5000) { - setProgress(progress + 10); - } else { - onDismiss(); - clearInterval(dismissInterval); - } - }, 10), + if (appSuccess === undefined) { + return; + } + setDismissTimeout( + setTimeout(() => { + onDismiss(); + clearTimeout(dismissTimeout); + }, 5_000), ); }, [appSuccess]); @@ -48,7 +44,6 @@ const AppSuccess = () => { - ); } else { diff --git a/src/js/components/Task/Entry.tsx b/src/js/components/Task/Entry.tsx new file mode 100644 index 0000000..0ce1a55 --- /dev/null +++ b/src/js/components/Task/Entry.tsx @@ -0,0 +1,36 @@ +import * as React from 'react'; +import * as Icon from 'react-feather'; +import { Task, TaskStatus } from './TaskList'; +import Container from '../reusable/Container'; + +interface Props { + task: Task; + state: TaskStatus; +} + +const TaskEntry = ({ task, state }: Props) => ( + + + {task.label} + + + {state === TaskStatus.Pending && ( + + )} + {state === TaskStatus.Running && ( + + )} + {state === TaskStatus.Success && ( + + )} + {state === TaskStatus.Error && ( + + )} + {state === TaskStatus.Aborted && ( + + )} + + +); + +export default TaskEntry; diff --git a/src/js/components/Task/TaskList.tsx b/src/js/components/Task/TaskList.tsx new file mode 100644 index 0000000..a356cc5 --- /dev/null +++ b/src/js/components/Task/TaskList.tsx @@ -0,0 +1,132 @@ +import * as React from 'react'; +import Container from '../reusable/Container'; +import Button from '../reusable/Button'; +import TaskEntry from './Entry'; + +export type Result = true | { error: string }; + +export enum TaskStatus { + Pending, + Running, + Success, + Error, + Aborted, +} + +export interface Task { + label: string; + action: () => Promise; +} + +interface TaskListProps { + onDone: (result: Result) => void; + run: boolean; + tasks: Task[]; + title: string; +} + +const TaskList = ({ onDone, run, tasks, title }: TaskListProps) => { + const [tasksStatus, setTasksStatus] = React.useState( + tasks.map(() => TaskStatus.Pending), + ); + const [error, setError] = React.useState(null); + + React.useEffect(() => { + if (!run) { + return; + } + // check if a task is running + const running = tasksStatus.some((status) => status === TaskStatus.Running); + if (running) { + return; + } + // get the first pending task + const index = tasksStatus.indexOf(TaskStatus.Pending); + if (index === -1) { + return; + } + + const task = tasks[index]; + // set the task status to running + setTasksStatus((prev) => { + const next = [...prev]; + next[index] = TaskStatus.Running; + return next; + }); + + // run the task + task + .action() + .then((result) => { + setTasksStatus((prev) => { + const newStates = [...prev]; + newStates[index] = + result === true ? TaskStatus.Success : TaskStatus.Error; + if (result !== true) { + setError(result.error); + // abort all the tasks + for (let i = index + 1; i < tasks.length; i++) { + newStates[i] = TaskStatus.Aborted; + } + } + + return newStates; + }); + }) + .catch(() => { + setTasksStatus((prev) => { + const newStates = [...prev]; + newStates[index] = TaskStatus.Error; + // abort all the tasks + for (let i = index + 1; i < tasks.length; i++) { + newStates[i] = TaskStatus.Aborted; + } + return newStates; + }); + }); + }, [tasksStatus, run]); + + React.useEffect(() => { + setTasksStatus(tasks.map(() => TaskStatus.Pending)); + }, [tasks]); + + const allTasksCompleted = tasksStatus.every( + (status) => status !== TaskStatus.Pending && status !== TaskStatus.Running, + ); + + if (!run) { + return null; + } + + return ( + + + + + {title} + {tasks.map((task, index) => ( + + ))} + + {allTasksCompleted && ( + + {error && {error}} + { + if (error === null) { + onDone(true); + } else { + onDone({ error }); + } + }} + > + {error ? 'Close' : 'Done'} + + + )} + + + ); +}; + +export default TaskList; diff --git a/src/js/components/reusable/ProgressBar.tsx b/src/js/components/reusable/ProgressBar.tsx index 7720556..f56d994 100644 --- a/src/js/components/reusable/ProgressBar.tsx +++ b/src/js/components/reusable/ProgressBar.tsx @@ -4,6 +4,8 @@ interface Props { progress: number; max: number; percentage?: boolean; + bgColor?: string; + textColor?: string; } const ProgressBar = (props: Props) => { @@ -13,16 +15,19 @@ const ProgressBar = (props: Props) => { label = `${percentage.toString()}%`; } + const bgColor = props.bgColor || 'bg-brand'; + const textColor = props.textColor || 'text-gray-300'; + const className = `${ - props.progress > 0 ? 'bg-brand' : '' - } text-lg font-medium text-blue-100 text-center p-0.5 leading-none rounded-full`; + props.progress > 0 ? bgColor : '' + } text-lg font-medium ${textColor} text-center p-0.5 leading-none rounded-full`; const fillerStyles = { width: `${percentage}%`, }; return ( -
+
{label}
diff --git a/src/js/components/reusable/WaitForMetamask.tsx b/src/js/components/reusable/WaitForMetamask.tsx new file mode 100644 index 0000000..37c6af7 --- /dev/null +++ b/src/js/components/reusable/WaitForMetamask.tsx @@ -0,0 +1,19 @@ +import { useMetaMask } from 'metamask-react'; +import * as React from 'react'; + +interface Props { + children: React.ReactNode | React.ReactNode[]; + otherwise?: React.ReactNode; +} + +const WaitForMetamask = ({ children, otherwise }: Props) => { + const { status } = useMetaMask(); + + if (status === 'connected') { + return <>{children}; + } + + return otherwise ? otherwise : null; +}; + +export default WaitForMetamask; diff --git a/src/js/utils/format.ts b/src/js/utils/format.ts new file mode 100644 index 0000000..88a718a --- /dev/null +++ b/src/js/utils/format.ts @@ -0,0 +1,26 @@ +export const EKOKE_DECIMALS = 8; +export const USDT_DECIMALS = 6; + +export const convertToHumanReadable = ( + value: bigint, + decimals: number, + hideDecimals: boolean = false, +): string => { + if (value === BigInt(0)) { + return '0'; + } + + const divisor = BigInt(10 ** decimals); + + const wholePart = value / divisor; + + if (hideDecimals) { + return wholePart.toString(); + } + + const fractionalPart = value % divisor; + + const fractionalString = fractionalPart.toString().padStart(decimals, '0'); + + return `${wholePart.toString()}.${fractionalString}`; +}; diff --git a/src/js/web3/DeferredClient.ts b/src/js/web3/DeferredClient.ts index bfd4ce7..4084b90 100644 --- a/src/js/web3/DeferredClient.ts +++ b/src/js/web3/DeferredClient.ts @@ -14,6 +14,23 @@ export default class DeferredClient { this.chainId = chainId; } + async nextTokenIdToBuy(contractId: bigint): Promise { + const contract = this.getContract(); + return contract.methods.nextTokenIdToBuy(contractId).call({ + from: this.address, + }); + } + + async contractProgress(contractId: bigint): Promise { + const contract = this.getContract(); + return contract.methods.contractProgress(contractId).call(); + } + + async contractCompleted(contractId: bigint): Promise { + const contract = this.getContract(); + return contract.methods.contractCompleted(contractId).call(); + } + async tokenUri(tokenId: bigint): Promise { const contract = this.getContract(); return contract.methods.tokenUri(tokenId).call(); diff --git a/src/js/web3/MarketplaceClient.ts b/src/js/web3/MarketplaceClient.ts index 0b8c24c..4e5513c 100644 --- a/src/js/web3/MarketplaceClient.ts +++ b/src/js/web3/MarketplaceClient.ts @@ -14,14 +14,18 @@ export default class MarketplaceClient { this.chainId = chainId; } - async buyToken(tokenId: bigint) { + async buyNextToken(contractId: bigint) { const contract = this.getContract(); - return contract.methods.buyToken(tokenId).send({ from: this.address }); + return contract.methods + .buyNextToken(contractId) + .send({ from: this.address }); } - async tokenPriceForCaller(): Promise { + async tokenPriceForCaller(contractId: bigint): Promise { const contract = this.getContract(); - return contract.methods.tokenPriceForCaller().call(); + return contract.methods.tokenPriceForCaller(contractId).call({ + from: this.address, + }); } async interestRate(): Promise { @@ -34,6 +38,10 @@ export default class MarketplaceClient { return contract.methods.usdErc20().call(); } + marketplaceAddress(): string { + return CONTRACT_ADDRESS[this.chainId]; + } + private getContract() { return new this.web3.eth.Contract(ABI, CONTRACT_ADDRESS[this.chainId]); } diff --git a/src/js/web3/UsdtClient.ts b/src/js/web3/UsdtClient.ts index 3d613ba..00e941e 100644 --- a/src/js/web3/UsdtClient.ts +++ b/src/js/web3/UsdtClient.ts @@ -19,7 +19,7 @@ export default class UsdtClient { return contract.methods.allowance(owner, spender).call(); } - async approve(spender: string, amount: number) { + async approve(spender: string, amount: bigint) { const contract = this.getContract(); return contract.methods .approve(spender, amount) diff --git a/src/js/web3/contracts/Deferred.ts b/src/js/web3/contracts/Deferred.ts index fa6f9a6..2b9f09d 100644 --- a/src/js/web3/contracts/Deferred.ts +++ b/src/js/web3/contracts/Deferred.ts @@ -365,6 +365,44 @@ export const ABI = [ stateMutability: 'nonpayable', type: 'function', }, + { + inputs: [ + { + internalType: 'uint256', + name: '_contractId', + type: 'uint256', + }, + ], + name: 'contractCompleted', + outputs: [ + { + internalType: 'bool', + name: 'completed', + type: 'bool', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint256', + name: '_contractId', + type: 'uint256', + }, + ], + name: 'contractProgress', + outputs: [ + { + internalType: 'uint256', + name: '_progress', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, { inputs: [ { @@ -459,6 +497,89 @@ export const ABI = [ stateMutability: 'view', type: 'function', }, + { + inputs: [ + { + internalType: 'uint256', + name: '_contractId', + type: 'uint256', + }, + ], + name: 'getContract', + outputs: [ + { + components: [ + { + internalType: 'string', + name: 'metadataUri', + type: 'string', + }, + { + components: [ + { + internalType: 'address', + name: 'seller', + type: 'address', + }, + { + internalType: 'uint256', + name: 'tokenFromId', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'tokenToId', + type: 'uint256', + }, + ], + internalType: 'struct Deferred.Seller[]', + name: 'sellers', + type: 'tuple[]', + }, + { + internalType: 'address[]', + name: 'buyers', + type: 'address[]', + }, + { + internalType: 'uint256', + name: 'ekokeReward', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'tokenPriceUsd', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'tokenFromId', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'tokenToId', + type: 'uint256', + }, + { + internalType: 'bool', + name: 'closed', + type: 'bool', + }, + { + internalType: 'bool', + name: 'created', + type: 'bool', + }, + ], + internalType: 'struct Deferred.SellContract', + name: '_sellContract', + type: 'tuple', + }, + ], + stateMutability: 'view', + type: 'function', + }, { inputs: [ { @@ -509,6 +630,49 @@ export const ABI = [ stateMutability: 'view', type: 'function', }, + { + inputs: [ + { + internalType: 'uint256', + name: '_contractId', + type: 'uint256', + }, + ], + name: 'nextTokenIdToBuy', + outputs: [ + { + internalType: 'uint256', + name: '_nextTokenId', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint256', + name: '_contractId', + type: 'uint256', + }, + { + internalType: 'address', + name: '_caller', + type: 'address', + }, + ], + name: 'nextTokenIdToBuyFor', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, { inputs: [], name: 'owner', @@ -588,28 +752,28 @@ export const ABI = [ inputs: [ { internalType: 'address', - name: 'from', + name: '', type: 'address', }, { internalType: 'address', - name: 'to', + name: '', type: 'address', }, { internalType: 'uint256', - name: 'tokenId', + name: '', type: 'uint256', }, { internalType: 'bytes', - name: 'data', + name: '', type: 'bytes', }, ], name: 'safeTransferFrom', outputs: [], - stateMutability: 'nonpayable', + stateMutability: 'pure', type: 'function', }, { @@ -787,23 +951,23 @@ export const ABI = [ inputs: [ { internalType: 'address', - name: 'from', + name: '', type: 'address', }, { internalType: 'address', - name: 'to', + name: '', type: 'address', }, { internalType: 'uint256', - name: 'tokenId', + name: '', type: 'uint256', }, ], name: 'transferFrom', outputs: [], - stateMutability: 'nonpayable', + stateMutability: 'pure', type: 'function', }, { @@ -819,6 +983,35 @@ export const ABI = [ stateMutability: 'nonpayable', type: 'function', }, + { + inputs: [ + { + internalType: 'uint256', + name: '_contractId', + type: 'uint256', + }, + { + internalType: 'address', + name: 'from', + type: 'address', + }, + { + internalType: 'address', + name: 'to', + type: 'address', + }, + ], + name: 'transferToken', + outputs: [ + { + internalType: 'uint256', + name: '_tokenId', + type: 'uint256', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, ]; interface ContractAddress { @@ -828,5 +1021,5 @@ interface ContractAddress { export const CONTRACT_ADDRESS: ContractAddress = { [ChainId.Mainnet]: '', - [ChainId.Sepolia]: '', + [ChainId.Sepolia]: '0xa5fF566D68E3521929F47447b975C4683618C35f', }; diff --git a/src/js/web3/contracts/Marketplace.ts b/src/js/web3/contracts/Marketplace.ts index 71cbaa7..4038926 100644 --- a/src/js/web3/contracts/Marketplace.ts +++ b/src/js/web3/contracts/Marketplace.ts @@ -83,6 +83,12 @@ export const ABI = [ name: 'seller', type: 'address', }, + { + indexed: false, + internalType: 'uint256', + name: 'contractId', + type: 'uint256', + }, { indexed: false, internalType: 'uint256', @@ -135,15 +141,47 @@ export const ABI = [ inputs: [ { internalType: 'uint256', - name: '_tokenId', + name: '_contractId', + type: 'uint256', + }, + ], + name: 'buyNextToken', + outputs: [ + { + internalType: 'uint256', + name: 'tokenId', type: 'uint256', }, ], - name: 'buyToken', - outputs: [], stateMutability: 'nonpayable', type: 'function', }, + { + inputs: [], + name: 'deferred', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'ekoke', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, { inputs: [], name: 'interestRate', @@ -177,11 +215,24 @@ export const ABI = [ stateMutability: 'nonpayable', type: 'function', }, + { + inputs: [], + name: 'rewardPool', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, { inputs: [ { internalType: 'uint256', - name: '_tokenId', + name: '_contractId', type: 'uint256', }, ], @@ -231,5 +282,5 @@ interface ContractAddress { export const CONTRACT_ADDRESS: ContractAddress = { [ChainId.Mainnet]: '', - [ChainId.Sepolia]: '', + [ChainId.Sepolia]: '0x9BDF7DdB6b24e554e2b58D6f32241b9b1C000674', };