diff --git a/src/components/Connection.tsx b/src/components/Connection.tsx index ebb252e..2f477d9 100644 --- a/src/components/Connection.tsx +++ b/src/components/Connection.tsx @@ -94,7 +94,7 @@ export const ConnectedWallet : React.FC = ({ runtimeURL,selecte     - {(adas).toString()}. + {(adas).toLocaleString()}. {decimalADAs + ' '} {isMainnet ? ' ₳' : ' t₳' } diff --git a/src/components/modals/NewVesting.tsx b/src/components/modals/NewVesting.tsx index 8ad85bb..e47ad46 100644 --- a/src/components/modals/NewVesting.tsx +++ b/src/components/modals/NewVesting.tsx @@ -25,6 +25,8 @@ type FormData = { claimerAddress: string; }; + + const initialFormData : () => FormData = () => ({ firstName: '', lastName: '', @@ -36,7 +38,7 @@ const initialFormData : () => FormData = () => ({ claimerAddress: '', }) -const formErrorsInitialState = { +const formErrorsInitialState : FormDataError = { firstName: null, lastName: null, title: null, @@ -45,6 +47,15 @@ const formErrorsInitialState = { claimerAddress: null, } +type FormDataError = { + firstName: string | null; + lastName : string | null; + title : string | null; + initialDepositAmount: string | null; + startDate: string | null; + claimerAddress: string| null; +}; + const NewVestingScheduleModal: React.FC = ({ showModal, closeModal, handleCreateVestingContract, changeAddress}) => { @@ -52,7 +63,7 @@ const NewVestingScheduleModal: React.FC = ({ showM const [formData, setFormData] = useState(initialFormData()); - const [formErrors, setFormErrors] = useState(formErrorsInitialState); + const [formErrors, setFormErrors] = useState(formErrorsInitialState); const handleInputChange = (e: React.ChangeEvent) => { const { id, value } = e.target; @@ -79,6 +90,15 @@ const NewVestingScheduleModal: React.FC = ({ showM errors = { ...errors, [key]: 'This field is required' }; } } + if (lengthInUtf8Bytes(formData.firstName) >= 64){ + errors = { ...errors, "firstName": 'This field is too long to be stored on chain( 64 bytes maximum)' }; + } + if (lengthInUtf8Bytes(formData.lastName) >= 64){ + errors = { ...errors, "lastName": 'This field is too long to be stored on chain( 64 bytes maximum)' }; + } + if (lengthInUtf8Bytes(formData.title) >= 64){ + errors = { ...errors, "title": 'This field is too long to be stored on chain( 64 bytes maximum)' }; + } setFormErrors(errors); @@ -111,6 +131,11 @@ const NewVestingScheduleModal: React.FC = ({ showM closeModal(); } + function lengthInUtf8Bytes(str : string) { + // Matches only the 10.. bytes that are non-initial characters in a multi-byte sequence. + var m = encodeURIComponent(str).match(/%[89ABab]/g); + return str.length + (m ? m.length : 0); + } return ( <> @@ -153,7 +178,7 @@ const NewVestingScheduleModal: React.FC = ({ showM
= ({runtimeURL,marloweScanUR const fetchData = async () => { if(isFetching) return; try { - const runtimeLifecycleParameters : BrowserRuntimeLifecycleOptions = { runtimeURL:runtimeURL, walletName:selectedAWalletExtension as SupportedWallet} + const runtimeLifecycleParameters : BrowserRuntimeLifecycleOptions = { runtimeURL:runtimeURL, walletName:selectedAWalletExtension as SupportedWalletName} const runtimeLifecycle = await mkRuntimeLifecycle(runtimeLifecycleParameters).then((a) => {setRuntimeLifecycle(a);return a}) const restClient = mkRestClient(runtimeURL); const changeAddress = await runtimeLifecycle.wallet.getChangeAddress() .then((changeAddress : AddressBech32) => {setChangeAddress(unAddressBech32(changeAddress));return changeAddress;}) - const contractIdsAndTags : [ContractId,Tags][] = (await restClient.getContracts({ partyAddresses:[changeAddress],tags: [dAppId] })).headers.map((header) => [header.contractId,header.tags]); + const contractsClosedIds = contractsClosed.map(c => unContractId(c.contractId)) + const contractIdsAndTags : [ContractId,Tags][] = + (await restClient.getContracts({ partyAddresses:[changeAddress],tags: [dAppId] })) + .headers + .filter((header) => !contractsClosedIds.includes(unContractId(header.contractId))) + .filter(header => header.tags[dAppId].claimerId === (unAddressBech32(changeAddress).slice(0,18))) + .map((header) => [header.contractId,header.tags]); + const contractIdsAndDetails : [ContractId,Tags,ContractDetails] []= await Promise.all( contractIdsAndTags.map(([contractId,tags]) => restClient @@ -97,7 +104,8 @@ const YourTokenPlans: React.FC = ({runtimeURL,marloweScanUR isSelfAttributed : tags[dAppId].isSelfAttributed === 1, providerId : tags[dAppId].providerId, claimer : {firstName : tags[dAppId].firstName, lastName:tags[dAppId].lastName, id: tags[dAppId].claimerId }, - state : state}))))).filter(contract => contract.claimer.id === (unAddressBech32(changeAddress).slice(0,18))) + state : state}))))) + setContractsWithinVestingPeriod (allContracts @@ -107,10 +115,15 @@ const YourTokenPlans: React.FC = ({runtimeURL,marloweScanUR (allContracts .filter(c => c.state.name === "VestingEnded") .map (c => c as Contract)) - setContractsClosed - (allContracts - .filter(c => c.state.name === "Closed") - .map (c => c as Contract)) + + const newContractsClosed = allContracts + .filter(c => c.state.name === "Closed") + .map (c => c as Contract) + + if(newContractsClosed.length > 0 ) { + setContractsClosed(contractsClosed.concat(newContractsClosed)) + } + setIsFetchingFirstTime(false) setIsFetching(false) } catch (err : any) { @@ -125,7 +138,7 @@ const YourTokenPlans: React.FC = ({runtimeURL,marloweScanUR // Clear the interval when the component is unmounted return () => clearInterval(intervalId); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedAWalletExtension, navigate]); + }, [selectedAWalletExtension, contractsClosed,navigate]); @@ -212,7 +225,7 @@ const YourTokenPlans: React.FC = ({runtimeURL,marloweScanUR Plan Ended {contract.state.scheme.frequency} {contract.state.scheme.numberOfPeriods.toString()} - {formatADAs(contract.state.quantities.total)} + {formatADAs(contract.state.quantities.total)} {((contract.state.quantities.withdrawable* 100n) / contract.state.quantities.total) + '%'} {contract.state.withdrawInput? @@ -248,7 +261,7 @@ const YourTokenPlans: React.FC = ({runtimeURL,marloweScanUR } {contract.state.scheme.frequency} {contract.state.scheme.numberOfPeriods.toString()} - {formatADAs(contract.state.quantities.total)} + {formatADAs(contract.state.quantities.total)} {(((contract.state.quantities.vested - contract.state.quantities.claimed) * 100n) / contract.state.quantities.total) + '%'} {contract.state.withdrawInput? @@ -278,7 +291,7 @@ const YourTokenPlans: React.FC = ({runtimeURL,marloweScanUR Closed
({displayCloseCondition(contract.state.closeCondition)}) {contract.state.scheme.frequency} {contract.state.scheme.numberOfPeriods.toString()} - {formatADAs(contract.state.scheme.expectedInitialDeposit.amount)} + {formatADAs(contract.state.scheme.expectedInitialDeposit.amount)} Closed @@ -293,27 +306,4 @@ const YourTokenPlans: React.FC = ({runtimeURL,marloweScanUR ); }; - - -export type CurrencyF = String -export type WholeNumberF = string -export type DecimalF = string -const formatADAs = (lovelaces: bigint, isMainnet: Boolean = false, currencyName: string = "₳"): string=> { - const adas = (Math.trunc(Number(lovelaces).valueOf() / 1_000_000)) - const decimalADAs = (lovelaces % 1_000_000n) - const currency = isMainnet ? currencyName : "t" + currencyName - if (decimalADAs === 0n) - return adas.toString() + ' ' + currency; - else - return adas.toString() + ' ' + decimalADAs.toString().padStart(6, '0') + ' ' + currency; -} - -const cssOverrideSpinnerCentered - = ({display: "block", - marginLeft: "auto", - marginRight:"auto", - height: "auto", - witdth : "20px", - paddingTop: "10px"}) - export default YourTokenPlans; \ No newline at end of file diff --git a/src/components/vesting/Provider.tsx b/src/components/vesting/Provider.tsx index 5bd98e9..699df59 100644 --- a/src/components/vesting/Provider.tsx +++ b/src/components/vesting/Provider.tsx @@ -15,9 +15,10 @@ import { ContractDetails } from '@marlowe.io/runtime-rest-client/contract/detail import HashLoader from 'react-spinners/HashLoader'; import { Address, Input } from '@marlowe.io/language-core-v1'; import { Contract } from './Models'; -import { contractIdLink, displayCloseCondition } from './Utils'; +import { contractIdLink, cssOverrideSpinnerCentered, displayCloseCondition, formatADAs } from './Utils'; import { ConnectionWallet } from '../Connection'; import { Footer } from '../Footer'; +import { SupportedWalletName } from '@marlowe.io/wallet/browser'; type CreatePlansProps = { runtimeURL : string, @@ -53,13 +54,22 @@ const CreatePlans: React.FC = ({runtimeURL,marloweScanURL,dApp if(isFetching) return; try { setIsFetching(true) - const runtimeLifecycleParameters : BrowserRuntimeLifecycleOptions = { runtimeURL:runtimeURL, walletName:selectedAWalletExtension as SupportedWallet} + const runtimeLifecycleParameters : BrowserRuntimeLifecycleOptions = { runtimeURL:runtimeURL, walletName:selectedAWalletExtension as SupportedWalletName} const runtimeLifecycle = await mkRuntimeLifecycle(runtimeLifecycleParameters).then((a) => {setRuntimeLifecycle(a);return a}) const restClient = mkRestClient(runtimeURL); const changeAddress = await runtimeLifecycle.wallet.getChangeAddress() .then((changeAddress : AddressBech32) => {setChangeAddress(unAddressBech32(changeAddress));return unAddressBech32(changeAddress)}) - const contractIdsAndTags : [ContractId,Tags][] = (await restClient.getContracts({ partyAddresses:[addressBech32(changeAddress)],tags: [dAppId] })).headers.map((header) => [header.contractId,header.tags]); + const contractsClosedIds = contractsClosed.map(c => unContractId(c.contractId)) + + const contractIdsAndTags : [ContractId,Tags][] = + (await restClient.getContracts({ partyAddresses:[addressBech32(changeAddress)],tags: [dAppId] })) + .headers + .filter((header) => !contractsClosedIds.includes(unContractId(header.contractId))) + .filter(header => header.tags[dAppId].providerId === (changeAddress.slice(0,18))) + .map((header) => [header.contractId,header.tags]) + + const contractIdsAndDetails : [ContractId,Tags,ContractDetails] []= await Promise.all( contractIdsAndTags.map(([contractId,tags]) => restClient @@ -104,7 +114,7 @@ const CreatePlans: React.FC = ({runtimeURL,marloweScanURL,dApp providerId : tags[dAppId].providerId, claimer : {firstName : tags[dAppId].firstName, lastName:tags[dAppId].lastName, id: tags[dAppId].claimerId }, state : state} as Contract ))))) - .filter(contract => contract.providerId === (changeAddress.slice(0,18))) + setContractsWaitingForDeposit (allContracts @@ -122,10 +132,13 @@ const CreatePlans: React.FC = ({runtimeURL,marloweScanURL,dApp (allContracts .filter(c => c.state.name === "VestingEnded") .map (c => c as Contract)) - setContractsClosed - (allContracts - .filter(c => c.state.name === "Closed") - .map (c => c as Contract)) + const newContractsClosed = allContracts + .filter(c => c.state.name === "Closed") + .map (c => c as Contract) + if(newContractsClosed.length > 0 ) { + setContractsClosed(contractsClosed.concat(newContractsClosed)) + } + setIsFetchingFirstTime(false) setIsFetching(false) @@ -137,11 +150,11 @@ const CreatePlans: React.FC = ({runtimeURL,marloweScanURL,dApp } fetchData() - const intervalId = setInterval(() => {fetchData()}, 5_000); + const intervalId = setInterval(() => {fetchData()}, 10_000); // Clear the interval when the component is unmounted return () => clearInterval(intervalId); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedAWalletExtension, navigate]); + }, [selectedAWalletExtension,contractsClosed]); @@ -303,7 +316,9 @@ const CreatePlans: React.FC = ({runtimeURL,marloweScanURL,dApp Awaiting Deposit {contract.state.scheme.frequency} {contract.state.scheme.numberOfPeriods.toString()} - {formatADAs(contract.state.scheme.expectedInitialDeposit.amount)} + + {formatADAs(contract.state.scheme.expectedInitialDeposit.amount)} + 0% {formatDate(contract.state.initialDepositDeadline)} @@ -341,7 +356,9 @@ const CreatePlans: React.FC = ({runtimeURL,marloweScanURL,dApp } {contract.state.scheme.frequency} {contract.state.scheme.numberOfPeriods.toString()} - {formatADAs(contract.state.quantities.total)} + + {formatADAs(contract.state.quantities.total)} + {((contract.state.quantities.withdrawable* 100n) / contract.state.quantities.total) + '%'} {formatDate(contract.state.periodInterval[1])} @@ -373,7 +390,9 @@ const CreatePlans: React.FC = ({runtimeURL,marloweScanURL,dApp Plan Ended {contract.state.scheme.frequency} {contract.state.scheme.numberOfPeriods.toString()} - {formatADAs(contract.state.quantities.total)} + + {formatADAs(contract.state.quantities.total)} + {((contract.state.quantities.withdrawable* 100n) / contract.state.quantities.total) + '%'} Vested Tokens aren't yet fully claimed @@ -387,7 +406,10 @@ const CreatePlans: React.FC = ({runtimeURL,marloweScanURL,dApp Deposit Deadline Passed {contract.state.scheme.frequency} {contract.state.scheme.numberOfPeriods.toString()} - {formatADAs(contract.state.scheme.expectedInitialDeposit.amount)} + + + {formatADAs(contract.state.scheme.expectedInitialDeposit.amount)} + 0% @@ -418,7 +440,10 @@ const CreatePlans: React.FC = ({runtimeURL,marloweScanURL,dApp Closed
({displayCloseCondition(contract.state.closeCondition)}) {contract.state.scheme.frequency} {contract.state.scheme.numberOfPeriods.toString()} - {formatADAs(contract.state.scheme.expectedInitialDeposit.amount)} + + + {formatADAs(contract.state.scheme.expectedInitialDeposit.amount)} + 0% N/A @@ -440,28 +465,4 @@ const CreatePlans: React.FC = ({runtimeURL,marloweScanURL,dApp ); }; - - - -export type CurrencyF = String -export type WholeNumberF = string -export type DecimalF = string -const formatADAs = (lovelaces: bigint, isMainnet: Boolean = false, currencyName: string = "₳"): string=> { - const adas = (Math.trunc(Number(lovelaces).valueOf() / 1_000_000)) - const decimalADAs = (lovelaces % 1_000_000n) - const currency = isMainnet ? currencyName : "t" + currencyName - if (decimalADAs === 0n) - return adas.toString() + ' ' + currency; - else - return adas.toString() + ' ' + decimalADAs.toString().padStart(6, '0') + ' ' + currency; -} - -const cssOverrideSpinnerCentered - = ({display: "block", - marginLeft: "auto", - marginRight:"auto", - height: "auto", - witdth : "20px", - paddingTop: "10px"}) - export default CreatePlans; \ No newline at end of file diff --git a/src/components/vesting/Utils.tsx b/src/components/vesting/Utils.tsx index c668ed0..c392146 100644 --- a/src/components/vesting/Utils.tsx +++ b/src/components/vesting/Utils.tsx @@ -21,4 +21,25 @@ export function displayCloseCondition(closeCondition : Vesting.CloseCondition ) case "FullyClaimedCloseCondition" : return "Fully Claimed Plan" ; case "UnknownCloseCondition" : return "Unknown Close Condition" ; } -} \ No newline at end of file +} + +export type CurrencyF = String +export type WholeNumberF = string +export type DecimalF = string +export const formatADAs = (lovelaces: bigint, isMainnet: Boolean = false, currencyName: string = "₳"): string=> { + const adas = (Math.trunc(Number(lovelaces).valueOf() / 1_000_000)) + const decimalADAs = (lovelaces % 1_000_000n) + const currency = isMainnet ? currencyName : "t" + currencyName + if (decimalADAs === 0n) + return adas.toLocaleString() + ' ' + currency; + else + return adas.toLocaleString() + ' ' + decimalADAs.toString().padStart(6, '0') + ' ' + currency; +} + +export const cssOverrideSpinnerCentered + = ({display: "block", + marginLeft: "10px", + marginRight:"auto", + height: "auto", + witdth : "20px", + paddingTop: "10px"})