From aaf44d791656d5016f67942de017cfd860d38f2c Mon Sep 17 00:00:00 2001 From: chaojun Date: Fri, 18 Oct 2024 18:18:30 +0800 Subject: [PATCH] Hide proxy and snapshot for permanence --- .../backend/src/scripts/init-spaces.js | 4 +- .../{permanencedao.js => permanence.js} | 8 +- .../createWhitelistProposal.js | 3 +- .../src/services/proposal.service/vote.js | 12 +- next/components/postCreate/information.js | 96 +++-- next/components/postCreate/more.js | 153 +++++--- next/components/postDetail/postInfo.js | 78 ++-- next/components/postDetail/postVote.js | 369 +++++++++++------- next/frontedUtils/isNeedProxy.js | 12 + next/frontedUtils/isNeedSnapshot.js | 20 + .../icons/projects/project-permanence.svg | 4 + .../icons/projects/project-permanencedao.svg | 5 - next/public/imgs/icons/space/permanence.svg | 4 + .../public/imgs/icons/space/permanencedao.svg | 5 - 14 files changed, 471 insertions(+), 302 deletions(-) rename backend/packages/backend/src/scripts/spaces/{permanencedao.js => permanence.js} (92%) create mode 100644 next/frontedUtils/isNeedProxy.js create mode 100644 next/frontedUtils/isNeedSnapshot.js create mode 100644 next/public/imgs/icons/projects/project-permanence.svg delete mode 100644 next/public/imgs/icons/projects/project-permanencedao.svg create mode 100644 next/public/imgs/icons/space/permanence.svg delete mode 100644 next/public/imgs/icons/space/permanencedao.svg diff --git a/backend/packages/backend/src/scripts/init-spaces.js b/backend/packages/backend/src/scripts/init-spaces.js index 46c24715..5d09054a 100644 --- a/backend/packages/backend/src/scripts/init-spaces.js +++ b/backend/packages/backend/src/scripts/init-spaces.js @@ -37,7 +37,7 @@ const { stafiConfig } = require("./spaces/stafi"); const { creditcoinConfig } = require("./spaces/creditcoin"); const { creditcoinEnterpriseConfig } = require("./spaces/creditcoinEnterprise"); const { dotaConfig } = require("./spaces/dota"); -const { permanenceDaoConfig } = require("./spaces/permanencedao"); +const { permanenceConfig } = require("./spaces/permanence"); const spaces = [ polkadotConfig, @@ -75,7 +75,7 @@ const spaces = [ creditcoinConfig, creditcoinEnterpriseConfig, dotaConfig, - permanenceDaoConfig, + permanenceConfig, ]; if (["1", "true", "TRUE"].includes(process.env.DEVELOPMENT)) { diff --git a/backend/packages/backend/src/scripts/spaces/permanencedao.js b/backend/packages/backend/src/scripts/spaces/permanence.js similarity index 92% rename from backend/packages/backend/src/scripts/spaces/permanencedao.js rename to backend/packages/backend/src/scripts/spaces/permanence.js index aca61eb4..f3bdbb8e 100644 --- a/backend/packages/backend/src/scripts/spaces/permanencedao.js +++ b/backend/packages/backend/src/scripts/spaces/permanence.js @@ -2,8 +2,8 @@ const { Accessibility } = require("../../consts/space"); const { networks, strategies } = require("./consts"); const config = { - id: "permanencedao", - name: "PermanenceDAO", + id: "permanence", + name: "Permanence", symbol: "DOT", decimals: 10, accessibility: Accessibility.WHITELIST, @@ -34,11 +34,11 @@ const config = { ], weightStrategy: [strategies.onePersonOneVote], version: "4", - spaceIcon: "permanencedao.svg", + spaceIcon: "permanence.svg", seoCoverFilename: "permanence_dao.jpg", admins: [], }; module.exports = { - permanenceDaoConfig: config, + permanenceConfig: config, }; diff --git a/backend/packages/backend/src/services/proposal.service/createWhitelistProposal.js b/backend/packages/backend/src/services/proposal.service/createWhitelistProposal.js index bf641ac7..c91258ba 100644 --- a/backend/packages/backend/src/services/proposal.service/createWhitelistProposal.js +++ b/backend/packages/backend/src/services/proposal.service/createWhitelistProposal.js @@ -9,7 +9,7 @@ async function checkWhitelistMember(networksConfig, address) { isSameAddress(item, address), ) === -1 ) { - throw new HttpError(400, "You are not the member"); + throw new HttpError(400, "Only members can create a proposal"); } } @@ -61,5 +61,4 @@ async function createWhitelistProposal({ module.exports = { createWhitelistProposal, - checkWhitelistMember, }; diff --git a/backend/packages/backend/src/services/proposal.service/vote.js b/backend/packages/backend/src/services/proposal.service/vote.js index 03b7083b..426ac726 100644 --- a/backend/packages/backend/src/services/proposal.service/vote.js +++ b/backend/packages/backend/src/services/proposal.service/vote.js @@ -22,7 +22,7 @@ const { hasSocietyStrategy, hasOnePersonOneVoteStrategy, } = require("../../utils/strategy"); -const { checkWhitelistMember } = require("./createWhitelistProposal"); +const { isSameAddress } = require("../../utils/address"); async function getDelegatorBalances({ proposal, voter, voterNetwork }) { const snapshotHeight = proposal.snapshotHeights?.[voterNetwork]; @@ -213,6 +213,16 @@ async function checkSocietyVote({ proposal, voterNetwork, voter }) { } } +async function checkWhitelistMember(networksConfig, address) { + if ( + (networksConfig.whitelist || []).findIndex((item) => + isSameAddress(item, address), + ) === -1 + ) { + throw new HttpError(400, "Only members can vote on this proposal"); + } +} + async function getSocietyVote({ voterNetwork, voter, snapshotHeight }) { const societyMember = await getSocietyMember( voterNetwork, diff --git a/next/components/postCreate/information.js b/next/components/postCreate/information.js index 7a76a143..1ea549e0 100644 --- a/next/components/postCreate/information.js +++ b/next/components/postCreate/information.js @@ -21,6 +21,7 @@ import { import BalanceRow from "@/components/postCreate/BalanceRow"; import { hasBalanceStrategy } from "frontedUtils/strategy"; import SocietyMemberHit from "./societyMemberHit"; +import { isSpaceNeedProxy } from "frontedUtils/isNeedProxy"; const Hint = styled.div` margin-top: 4px !important; @@ -35,62 +36,71 @@ const PostAddressWrapper = styled.div` margin-top: 4px !important; `; -export default function Information({ space }) { - const { - proposeThreshold: threshold, - decimals, - symbol, - weightStrategy, - } = space; - +function Proxy({ space }) { const dispatch = useDispatch(); - const balance = useSelector(targetBalanceSelector); + const useProxy = useSelector(useProxySelector); const canUseProxy = useSelector(canUseProxySelector); + if (!canUseProxy) { + return null; + } + + const needProxy = isSpaceNeedProxy(space); + if (!needProxy) { + return null; + } + + return ( + <> + + dispatch(setUseProxy(!useProxy))} + /> + } + /> + + {useProxy && ( + + + + )} + + ); +} + +function InfoHint({ space }) { + const { proposeThreshold: threshold, decimals, symbol } = space; + + const balance = useSelector(targetBalanceSelector); const loadBalanceError = useSelector(loadBalanceErrorSelector); - const useProxy = useSelector(useProxySelector); const belowThreshold = new BigNumber(balance).isLessThan(threshold); const loginAddress = useSelector(loginAddressSelector); - const proxyBalanceLoading = useSelector(proxyBalanceLoadingSelector); - const balanceLoading = useSelector(balanceLoadingSelector); - - let proxyElements = null; - if (canUseProxy) { - proxyElements = ( - <> - - dispatch(setUseProxy(!useProxy))} - /> - } - /> - - {useProxy && ( - - - - )} - - ); - } - - let hint = null; if (!loginAddress) { - hint = Link an address to create proposal.; + return Link an address to create proposal.; } else if (loadBalanceError) { - hint = {loadBalanceError}; + return {loadBalanceError}; } else if (belowThreshold) { - hint = ( + return ( You need to have a minimum of {toPrecision(threshold, decimals)}{" "} {symbol} in order to publish a proposal. ); } +} + +export default function Information({ space }) { + const { decimals, symbol, weightStrategy } = space; + + const balance = useSelector(targetBalanceSelector); + const useProxy = useSelector(useProxySelector); + + const proxyBalanceLoading = useSelector(proxyBalanceLoadingSelector); + const balanceLoading = useSelector(balanceLoadingSelector); return ( <> @@ -102,9 +112,9 @@ export default function Information({ space }) { symbol={symbol} /> )} - {hint} + {space.accessibility === "society" && } - {proxyElements} + ); } diff --git a/next/components/postCreate/more.js b/next/components/postCreate/more.js index d7dc831f..7e054966 100644 --- a/next/components/postCreate/more.js +++ b/next/components/postCreate/more.js @@ -22,6 +22,7 @@ import DropdownSelector from "@osn/common-ui/es/DropdownSelector"; import { hasSocietyVoteStrategyOnly } from "frontedUtils/strategy"; import dayjs from "dayjs"; import { getChainDisplayName } from "frontedUtils/chain"; +import { isSpaceNeedSnapshot } from "frontedUtils/isNeedSnapshot"; const Wrapper = styled.div` min-width: 302px; @@ -85,12 +86,9 @@ const ChoiceWrapper = styled.div` color: var(--textPrimary); `; -export default function More({ onPublish, space }) { +function SnapshotHeight({ space }) { const dispatch = useDispatch(); const snapshotHeights = useSelector(snapshotHeightsSelector); - const authoringStartDate = useSelector(authoringStartDateSelector); - const authoringEndDate = useSelector(authoringEndDateSelector); - const choiceTypeIndex = useSelector(choiceTypeIndexSelector); useEffect(() => { if (space?.networks) { @@ -105,6 +103,33 @@ export default function More({ onPublish, space }) { } }, [dispatch, space?.networks]); + return ( + + + + + {space.networks?.map((network) => ( + + {getChainDisplayName(network.network)} + {snapshotHeights.find( + (snapshotHeight) => snapshotHeight.network === network.network, + )?.height || -} + + ))} + + + ); +} + +function Period({ space }) { + const dispatch = useDispatch(); + const authoringStartDate = useSelector(authoringStartDateSelector); + const authoringEndDate = useSelector(authoringEndDateSelector); + function getMinStartDate() { return dayjs().startOf("day").toDate(); } @@ -116,14 +141,6 @@ export default function More({ onPublish, space }) { return authoringStartDate; } - const choiceTypes = ["Single choice voting", "Multiple choice voting"].map( - (item, i) => ({ - key: i, - value: i, - content: {item}, - }), - ); - const isSocietyOnly = hasSocietyVoteStrategyOnly(space.weightStrategy); useEffect(() => { if (isSocietyOnly && authoringStartDate) { @@ -133,60 +150,68 @@ export default function More({ onPublish, space }) { }, [dispatch, isSocietyOnly, authoringStartDate]); return ( - - - - dispatch(setChoiceTypeIndex(value))} + + + + { + if (value?.getTime) { + dispatch(setStartTimestamp(value.getTime())); + } + }} + placeholder="Start date" + defaultTime="now" /> - - - - - { - if (value?.getTime) { - dispatch(setStartTimestamp(value.getTime())); - } - }} - placeholder="Start date" - defaultTime="now" - /> - { - if (value?.getTime) { - dispatch(setEndTimestamp(value?.getTime())); - } - }} - placeholder="End date" - disabled={isSocietyOnly} - /> - - - - { + if (value?.getTime) { + dispatch(setEndTimestamp(value?.getTime())); + } + }} + placeholder="End date" + disabled={isSocietyOnly} /> - - - {space.networks?.map((network) => ( - - {getChainDisplayName(network.network)} - {snapshotHeights.find( - (snapshotHeight) => snapshotHeight.network === network.network, - )?.height || -} - - ))} - - + + + ); +} + +function ChoiceType() { + const dispatch = useDispatch(); + const choiceTypeIndex = useSelector(choiceTypeIndexSelector); + + const choiceTypes = ["Single choice voting", "Multiple choice voting"].map( + (item, i) => ({ + key: i, + value: i, + content: {item}, + }), + ); + + return ( + + + dispatch(setChoiceTypeIndex(value))} + /> + + ); +} + +export default function More({ onPublish, space }) { + const needSnapshot = isSpaceNeedSnapshot(space); + + return ( + + + + {needSnapshot && } diff --git a/next/components/postDetail/postInfo.js b/next/components/postDetail/postInfo.js index 6d89c746..ec4abc0d 100644 --- a/next/components/postDetail/postInfo.js +++ b/next/components/postDetail/postInfo.js @@ -11,6 +11,7 @@ import AssetList from "../assetList"; import { getSpaceAssets } from "frontedUtils/getSpaceAssets"; import { hasBalanceStrategy } from "frontedUtils/strategy"; import { getChainDisplayName } from "frontedUtils/chain"; +import { isProposalNeedSnapshot } from "frontedUtils/isNeedSnapshot"; const Wrapper = styled(Panel)` > :not(:first-child) { @@ -75,48 +76,59 @@ const SnapshotsWrapper = styled.div` } `; +function Snapshot({ snapshotHeights }) { + return ( + +
Snapshot
+ + {Object.keys(snapshotHeights).map((networkName) => { + const height = snapshotHeights[networkName]; + const explorer = getExplorer(networkName); + const link = `https://${networkName}.${explorer}.io/block/${height}`; + return ( + +
+ + + +
+
+ ); + })} +
+
+ ); +} + +function PinHash({ pinHash }) { + return ( + +
IPFS
+ {`#${pinHash?.slice(0, 7)}`} +
+ ); +} + // eslint-disable-next-line export default function PostInfo({ data, space }) { const assets = getSpaceAssets(data.networksConfig); + const needSnapshot = isProposalNeedSnapshot(data); return (
- -
Snapshot
- - {Object.keys(data.snapshotHeights).map((networkName) => { - const height = data.snapshotHeights[networkName]; - const explorer = getExplorer(networkName); - const link = `https://${networkName}.${explorer}.io/block/${height}`; - return ( - -
- - - -
-
- ); - })} -
-
- {data?.pinHash && ( - -
IPFS
- {`#${data?.pinHash?.slice(0, 7)}`} -
- )} + {needSnapshot && } + {data?.pinHash && }
{hasBalanceStrategy(space?.weightStrategy) && ( diff --git a/next/components/postDetail/postVote.js b/next/components/postDetail/postVote.js index 2380d84c..68dba634 100644 --- a/next/components/postDetail/postVote.js +++ b/next/components/postDetail/postVote.js @@ -43,6 +43,7 @@ import DelegationInfo from "./delegationInfo"; import { hasBalanceStrategy } from "frontedUtils/strategy"; import SocietyMemberHit from "../postCreate/societyMemberHit"; import SocietyMemberButton from "../societyMemberButton"; +import { isProposalNeedProxy } from "frontedUtils/isNeedProxy"; const Wrapper = styled.div` > :not(:first-child) { @@ -107,24 +108,113 @@ const RedText = styled.span` ${text_secondary_red_500}; `; -export default function PostVote({ proposal }) { - const router = useRouter(); +function ProxySwitch() { const dispatch = useDispatch(); - const [choiceIndexes, setChoiceIndexes] = useState([]); - const [remark, setRemark] = useState(""); - const [isLoading, setIsLoading] = useState(false); - const [balance, setBalance] = useState(); - const [balanceDetail, setBalanceDetail] = useState([]); - const [delegation, setDelegation] = useState(); - const viewfunc = useViewfunc(); const useProxy = useSelector(useProxySelector); - const proxyAddress = useSelector(proxySelector); + + return ( + +
Proxy vote
+ dispatch(setUseProxy(!useProxy))} /> +
+ ); +} + +function VoteBalance({ voteBalance, balanceDetail, proposal }) { + return ( +
+ + ) : null + } + > + {`Available ${toApproximatelyFixed( + bigNumber2Locale( + fromAssetUnit(voteBalance, proposal?.networksConfig?.decimals), + ), + )} ${proposal.networksConfig?.symbol}`} + +
+ ); +} + +function BalanceInfo({ proposal, balance, balanceDetail, delegation }) { + const useProxy = useSelector(useProxySelector); const proxyBalance = useSelector(proxyBalanceSelector); const proxyDelegation = useSelector(proxyDelegationSelector); const isSocietyProposal = proposal.networksConfig?.accessibility === "society"; - const VoteButton = isSocietyProposal ? SocietyMemberButton : Button; + const { network: loginNetwork } = useSelector(loginNetworkSelector) || {}; + + const voteBalance = useProxy ? proxyBalance : balance; + const voteDelegation = useProxy ? proxyDelegation : delegation; + + const belowThreshold = new BigNumber(voteBalance).eq(0); + + const supportProxy = useSelector(canUseProxySelector); + const needProxy = isProposalNeedProxy(proposal); + const snapshot = proposal.snapshotHeights[loginNetwork]; + + let balanceInfo = null; + + if (hasBalanceStrategy(proposal?.weightStrategy)) { + if (voteDelegation) { + balanceInfo = ( + + ); + } else { + balanceInfo = ( + <> + {!isNil(voteBalance) && ( + + )} + {belowThreshold && Insufficient} + + ); + } + } else if (isSocietyProposal) { + balanceInfo = ; + } + + return ( + <> + + {balanceInfo} + {supportProxy && needProxy && } + + {useProxy && ( + + )} + + ); +} + +function MyVoteButton({ + proposal, + balance, + balanceDetail, + delegation, + onVote, + isLoading, + choiceIndexes, +}) { + const useProxy = useSelector(useProxySelector); + const proxyBalance = useSelector(proxyBalanceSelector); + const proxyDelegation = useSelector(proxyDelegationSelector); const loginAddress = useSelector(loginAddressSelector); const { network: loginNetwork } = useSelector(loginNetworkSelector) || {}; @@ -145,7 +235,113 @@ export default function PostVote({ proposal }) { proposalStatus.active === proposal?.status && !voteDelegation; - const supportProxy = useSelector(canUseProxySelector); + const isSocietyProposal = + proposal.networksConfig?.accessibility === "society"; + + const VoteButton = isSocietyProposal ? SocietyMemberButton : Button; + + return ( + + + + + + {useProxy ? "Proxy Vote" : "Vote"} + + + {terminateButton} + + + ); +} + +function Remark({ remark, setRemark }) { + return ( + + Remark + setRemark(e.target.value)} + /> + + ); +} + +function Options({ proposal, choiceIndexes, setChoiceIndexes }) { + const onClickChoice = (index) => { + if (choiceIndexes.includes(index)) { + setChoiceIndexes( + choiceIndexes.filter((choiceIndex) => choiceIndex !== index), + ); + } else { + if (proposal.choiceType === "single") { + setChoiceIndexes([index]); + } else { + setChoiceIndexes([...choiceIndexes, index]); + } + } + }; + + const proposalClosed = isProposalClosed(proposal); + + return ( + + + {proposalClosed ? "Options" : "Cast your vote"} + <span className="type">{proposal.choiceType}</span> + + + {(proposal.choices || []).map((item, index) => ( + + ))} + + + ); +} + +function isProposalClosed(proposal) { + return [proposalStatus.closed, proposalStatus.terminated].includes( + proposal?.status, + ); +} + +export default function PostVote({ proposal }) { + const router = useRouter(); + const dispatch = useDispatch(); + const [choiceIndexes, setChoiceIndexes] = useState([]); + const [remark, setRemark] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [balance, setBalance] = useState(); + const [balanceDetail, setBalanceDetail] = useState([]); + const [delegation, setDelegation] = useState(); + const viewfunc = useViewfunc(); + const useProxy = useSelector(useProxySelector); + const proxyAddress = useSelector(proxySelector); + const loginAddress = useSelector(loginAddressSelector); + const { network: loginNetwork } = useSelector(loginNetworkSelector) || {}; + const snapshot = proposal.snapshotHeights[loginNetwork]; const reset = () => { @@ -153,10 +349,7 @@ export default function PostVote({ proposal }) { setRemark(""); }; - const proposalClosed = [ - proposalStatus.closed, - proposalStatus.terminated, - ].includes(proposal?.status); + const proposalClosed = isProposalClosed(proposal); useEffect(() => { if (proposal && loginAddress && loginNetwork) { @@ -246,137 +439,27 @@ export default function PostVote({ proposal }) { } }; - const onClickChoice = (index) => { - if (choiceIndexes.includes(index)) { - setChoiceIndexes( - choiceIndexes.filter((choiceIndex) => choiceIndex !== index), - ); - } else { - if (proposal.choiceType === "single") { - setChoiceIndexes([index]); - } else { - setChoiceIndexes([...choiceIndexes, index]); - } - } - }; - - let voteButton = null; - - if (!proposalClosed) { - let balanceInfo = null; - - if (hasBalanceStrategy(proposal?.weightStrategy)) { - if (voteDelegation) { - balanceInfo = ( - - ); - } else { - balanceInfo = ( - <> - {!isNil(voteBalance) && ( -
- - ) : null - } - > - {`Available ${toApproximatelyFixed( - bigNumber2Locale( - fromAssetUnit( - voteBalance, - proposal?.networksConfig?.decimals, - ), - ), - )} ${proposal.networksConfig?.symbol}`} - -
- )} - {belowThreshold && Insufficient} - - ); - } - } else if (isSocietyProposal) { - balanceInfo = ; - } - - voteButton = ( - - - {balanceInfo} - {supportProxy && ( - -
Proxy vote
- dispatch(setUseProxy(!useProxy))} - /> -
- )} -
- {useProxy && ( - - )} - - - - {useProxy ? "Proxy Vote" : "Vote"} - - - {terminateButton} - -
- ); - } - return ( - - - {proposalClosed ? "Options" : "Cast your vote"} - <span className="type">{proposal.choiceType}</span> - - - {(proposal.choices || []).map((item, index) => ( - - ))} - - + {choiceIndexes.length > 0 && ( - - Remark - setRemark(e.target.value)} - /> - + + )} + {!proposalClosed && ( + )} - {voteButton} ); } diff --git a/next/frontedUtils/isNeedProxy.js b/next/frontedUtils/isNeedProxy.js new file mode 100644 index 00000000..688cabfa --- /dev/null +++ b/next/frontedUtils/isNeedProxy.js @@ -0,0 +1,12 @@ +import { isNeedChainAccess } from "./isNeedSnapshot"; + +export function isSpaceNeedProxy(space) { + return isNeedChainAccess(space.accessibility, space.weightStrategy); +} + +export function isProposalNeedProxy(proposal) { + return isNeedChainAccess( + proposal.networksConfig.accessibility, + proposal.networksConfig.strategies, + ); +} diff --git a/next/frontedUtils/isNeedSnapshot.js b/next/frontedUtils/isNeedSnapshot.js new file mode 100644 index 00000000..bc351faf --- /dev/null +++ b/next/frontedUtils/isNeedSnapshot.js @@ -0,0 +1,20 @@ +import { hasOnePersonOneVoteStrategyOnly } from "./strategy"; + +export function isNeedChainAccess(accessibility, strategies) { + const isWhitelist = accessibility === "whitelist"; + const isOnePersonOneVote = hasOnePersonOneVoteStrategyOnly(strategies); + const noChainAccess = isWhitelist && isOnePersonOneVote; + + return !noChainAccess; +} + +export function isSpaceNeedSnapshot(space) { + return isNeedChainAccess(space.accessibility, space.weightStrategy); +} + +export function isProposalNeedSnapshot(proposal) { + return isNeedChainAccess( + proposal.networksConfig.accessibility, + proposal.networksConfig.strategies, + ); +} diff --git a/next/public/imgs/icons/projects/project-permanence.svg b/next/public/imgs/icons/projects/project-permanence.svg new file mode 100644 index 00000000..60929275 --- /dev/null +++ b/next/public/imgs/icons/projects/project-permanence.svg @@ -0,0 +1,4 @@ + + + + diff --git a/next/public/imgs/icons/projects/project-permanencedao.svg b/next/public/imgs/icons/projects/project-permanencedao.svg deleted file mode 100644 index 13decb63..00000000 --- a/next/public/imgs/icons/projects/project-permanencedao.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/next/public/imgs/icons/space/permanence.svg b/next/public/imgs/icons/space/permanence.svg new file mode 100644 index 00000000..60929275 --- /dev/null +++ b/next/public/imgs/icons/space/permanence.svg @@ -0,0 +1,4 @@ + + + + diff --git a/next/public/imgs/icons/space/permanencedao.svg b/next/public/imgs/icons/space/permanencedao.svg deleted file mode 100644 index 13decb63..00000000 --- a/next/public/imgs/icons/space/permanencedao.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - -