diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy-production.yaml similarity index 97% rename from .github/workflows/deploy.yaml rename to .github/workflows/deploy-production.yaml index d567c4f3..e1dead85 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy-production.yaml @@ -1,4 +1,4 @@ -name: Deploy +name: Deploy Production on: push: branches: diff --git a/.github/workflows/deploy-staging.yaml b/.github/workflows/deploy-staging.yaml new file mode 100644 index 00000000..06066ff2 --- /dev/null +++ b/.github/workflows/deploy-staging.yaml @@ -0,0 +1,35 @@ +name: Deploy Staging +on: + push: + branches: + - staging + +jobs: + push_to_registry: + name: Push Docker image to Docker Hub + runs-on: ubuntu-latest + steps: + - name: Check out the repo + uses: actions/checkout@v2 + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Update env variables + run: | + sed -i 's/MAILGUN_USER=secret/MAILGUN_USER=${{ secrets.MAILGUN_USER }}/g' .env + sed -i 's/MAILGUN_KEY=secret/MAILGUN_KEY=${{ secrets.MAILGUN_KEY }}/g' .env + - name: Build and push + id: docker_build + uses: docker/build-push-action@v2 + with: + context: . + push: true + tags: forbole/forbole-x-staging:latest + - name: Image digest + run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/Dockerfile b/Dockerfile index b6dac44e..c01672be 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,6 +10,7 @@ COPY patches /app/patches RUN yarn install COPY . /app +COPY /misc/cryptocurrencies.prod.ts /app/misc/cryptocurrencies.ts RUN yarn build CMD [ "yarn", "start" ] \ No newline at end of file diff --git a/assets/images/icons/icon_camera.svg b/assets/images/icons/icon_camera.svg new file mode 100644 index 00000000..190e9755 --- /dev/null +++ b/assets/images/icons/icon_camera.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/assets/images/icons/icon_profile_tx.svg b/assets/images/icons/icon_profile_tx.svg new file mode 100644 index 00000000..23560aef --- /dev/null +++ b/assets/images/icons/icon_profile_tx.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/components/AccountDetailCard/index.tsx b/components/AccountDetailCard/index.tsx index c7295a18..8ce8e01c 100644 --- a/components/AccountDetailCard/index.tsx +++ b/components/AccountDetailCard/index.tsx @@ -27,6 +27,8 @@ import useIsMobile from '../../misc/useIsMobile' import EditAccountDialog from '../EditAccountDialog' interface AccountDetailCardProps { + profileExist: boolean + onCreateProfile(): void wallet: Wallet account: Account validators: Validator[] @@ -35,6 +37,8 @@ interface AccountDetailCardProps { } const AccountDetailCard: React.FC = ({ + profileExist, + onCreateProfile, wallet, account, accountBalance, @@ -95,6 +99,15 @@ const AccountDetailCard: React.FC = ({ > + {/* !profileExist ? ( + + ) : null */} + + + {accounts.map((account) => { const crypto = cryptocurrencies[account.crypto] return account.fav ? ( diff --git a/components/Layout/styles.ts b/components/Layout/styles.ts index f5d1722c..dae9778a 100644 --- a/components/Layout/styles.ts +++ b/components/Layout/styles.ts @@ -56,7 +56,8 @@ const useStyles = makeStyles( starButton: { marginTop: theme.spacing(3), marginLeft: theme.spacing(2), - borderRadius: theme.spacing(1), + paddingLeft: theme.spacing(3), + paddingRight: theme.spacing(3), width: 'fit-content', }, favMenuItem: { diff --git a/components/ProfileCard/index.tsx b/components/ProfileCard/index.tsx new file mode 100644 index 00000000..4ea84e63 --- /dev/null +++ b/components/ProfileCard/index.tsx @@ -0,0 +1,45 @@ +import { Card, CardMedia, CardContent, Typography, Avatar, Box, Button } from '@material-ui/core' +import useTranslation from 'next-translate/useTranslation' +import React from 'react' +import useStyles from './styles' +import { useGeneralContext } from '../../contexts/GeneralContext' + +interface ProfileCardProps { + profile: Profile + onEditProfile(): void +} + +const ProfileCard: React.FC = ({ profile, onEditProfile }) => { + const classes = useStyles() + const { t } = useTranslation('common') + const { theme } = useGeneralContext() + + return ( + + + + + + + {profile.nickname} + @{profile.dtag} + + {/* */} + + {profile.bio} + + + ) +} + +export default ProfileCard diff --git a/components/ProfileCard/styles.ts b/components/ProfileCard/styles.ts new file mode 100644 index 00000000..e550813f --- /dev/null +++ b/components/ProfileCard/styles.ts @@ -0,0 +1,31 @@ +import { makeStyles } from '@material-ui/core/styles' +import { CustomTheme } from '../../misc/theme' + +const useStyles = makeStyles( + (theme: CustomTheme) => ({ + container: { + marginBottom: theme.spacing(2), + }, + content: { + padding: theme.spacing(3), + marginTop: theme.spacing(-10.5), + }, + coverImage: { + height: theme.spacing(27.5), + }, + avatar: { + width: theme.spacing(15), + height: theme.spacing(15), + marginBottom: theme.spacing(2), + }, + button: { + borderColor: theme.palette.iconBorder, + }, + }), + { + name: 'HookGlobalStyles', + index: 2, + } +) + +export default useStyles diff --git a/components/ProfileDialog/index.tsx b/components/ProfileDialog/index.tsx new file mode 100644 index 00000000..abf2dda7 --- /dev/null +++ b/components/ProfileDialog/index.tsx @@ -0,0 +1,178 @@ +import { + Avatar, + Dialog, + DialogTitle, + DialogContent, + Box, + Button, + IconButton, + TextField, + Typography, + CircularProgress, + useTheme, + ButtonBase, +} from '@material-ui/core' +import useTranslation from 'next-translate/useTranslation' +import React from 'react' +import invoke from 'lodash/invoke' +import CloseIcon from '../../assets/images/icons/icon_cross.svg' +import useStyles from './styles' +import useIconProps from '../../misc/useIconProps' +import CameraIcon from '../../assets/images/icons/icon_camera.svg' +import { useGeneralContext } from '../../contexts/GeneralContext' +import { useWalletsContext } from '../../contexts/WalletsContext' + +interface ProfileDialogProps { + open: boolean + onClose(): void + profile: Profile + account: Account +} + +const ProfileDialog: React.FC = ({ + profile: defaultProfile, + open, + onClose, + account, +}) => { + const { t } = useTranslation('common') + const classes = useStyles() + const iconProps = useIconProps() + const themeStyle = useTheme() + const { theme } = useGeneralContext() + const { password } = useWalletsContext() + + const [loading, setLoading] = React.useState(false) + const [profile, setProfile] = React.useState(defaultProfile) + + React.useEffect(() => { + if (open) { + setLoading(false) + setProfile(defaultProfile) + } + }, [open]) + + const onSubmit = React.useCallback(async () => { + try { + setLoading(true) + const msgs = [ + { + typeUrl: '/desmos.profiles.v1beta1.MsgSaveProfile', + value: { + dtag: profile.dtag, + nickname: profile.nickname, + bio: profile.bio, + profilePicture: profile.profilePic, + coverPicture: profile.coverPic, + creator: account.address, + }, + }, + ] + await invoke(window, 'forboleX.sendTransaction', password, account.address, { + msgs, + memo: '', + }) + setLoading(false) + onClose() + } catch (err) { + setLoading(false) + } + }, [password, account, profile]) + + return ( + + + + + {t(profile.dtag ? 'edit profile' : 'create profile')} +
{ + e.preventDefault() + onSubmit() + }} + > + + + + cover + + + + + + + + + + + + + + {t('nickname')} + setProfile((p) => ({ ...p, nickname: e.target.value }))} + /> + + + {t('dtag')} + setProfile((p) => ({ ...p, dtag: e.target.value }))} + /> + + + {t('bio')} + setProfile((p) => ({ ...p, bio: e.target.value }))} + /> + + + + + + +
+
+ ) +} + +export default ProfileDialog diff --git a/components/ProfileDialog/styles.ts b/components/ProfileDialog/styles.ts new file mode 100644 index 00000000..749c57da --- /dev/null +++ b/components/ProfileDialog/styles.ts @@ -0,0 +1,64 @@ +import { makeStyles } from '@material-ui/core/styles' +import { CustomTheme } from '../../misc/theme' + +const useStyles = makeStyles( + (theme: CustomTheme) => ({ + title: { + fontSize: theme.spacing(3), + marginTop: theme.spacing(2), + }, + closeButton: { + position: 'absolute', + top: theme.spacing(2), + right: theme.spacing(2), + }, + dialogContent: { + overflowY: 'auto', + }, + fullWidthButton: { + flex: 1, + margin: theme.spacing(2, 1), + }, + coverImg: { + width: '100%', + height: theme.spacing(12), + objectFit: 'cover', + }, + avatar: { + width: theme.spacing(9), + height: theme.spacing(9), + borderWidth: theme.spacing(0.25), + borderColor: theme.palette.background.default, + borderStyle: 'solid', + }, + imgOverlay: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + background: theme.palette.translucent, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + }, + avatarOverlay: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + background: theme.palette.translucent, + borderRadius: '50%', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + }, + }), + { + name: 'HookGlobalStyles', + index: 2, + } +) + +export default useStyles diff --git a/components/ProposalDetail/VoteResult.tsx b/components/ProposalDetail/VoteResult.tsx index 2f28620b..eb498412 100644 --- a/components/ProposalDetail/VoteResult.tsx +++ b/components/ProposalDetail/VoteResult.tsx @@ -5,7 +5,7 @@ import { PieChart, Pie, Cell as RawCell } from 'recharts' import { useGetStyles } from './styles' import Diagram from './Diagram' import { VoteSummary } from './index' -import { formatCrypto } from '../../misc/utils' +import { formatCrypto, formatPercentage } from '../../misc/utils' // HACK: bypass incorrect prop types const Cell = RawCell as any @@ -13,9 +13,10 @@ const Cell = RawCell as any interface VoteResultProps { voteSummary: VoteSummary crypto: Cryptocurrency + proposal: Proposal } -const VoteResult: React.FC = ({ voteSummary, crypto }) => { +const VoteResult: React.FC = ({ voteSummary, crypto, proposal }) => { const { classes } = useGetStyles() const { t, lang } = useTranslation('common') const theme = useTheme() @@ -70,13 +71,19 @@ const VoteResult: React.FC = ({ voteSummary, crypto }) => { - {t('voted')} + {t('voted and quorum percentage', { + voted: formatPercentage(voteSummary.amount / proposal.bondedTokens, lang), + quorum: formatPercentage(proposal.quorum, lang), + })} {formatCrypto(voteSummary.amount, crypto.name, lang)} - {voteSummary.description} + {t('voted over bonded', { + voted: formatCrypto(voteSummary.amount, crypto.name, lang, true, true), + bonded: formatCrypto(proposal.bondedTokens, crypto.name, lang, false, true), + })} diff --git a/components/ProposalDetail/VoteTable.tsx b/components/ProposalDetail/VoteTable.tsx index c237aa86..8fc901c6 100644 --- a/components/ProposalDetail/VoteTable.tsx +++ b/components/ProposalDetail/VoteTable.tsx @@ -18,7 +18,7 @@ import { useGetStyles } from './styles' import { VoteDetail } from './index' import { useGeneralContext } from '../../contexts/GeneralContext' import TablePagination from '../TablePagination' -// import { formatPercentage, formatCrypto } from '../../misc/utils' +import { formatCrypto, formatPercentage } from '../../misc/utils' interface DepositTableProps { voteDetails: VoteDetail[] @@ -47,18 +47,18 @@ const VoteTable: React.FC = ({ voteDetails, crypto }) => { { label: 'voter', }, - // { - // label: 'voting power', - // alignRight: true, - // }, - // { - // label: 'voting power percentage', - // alignRight: true, - // }, - // { - // label: 'voting power override', - // alignRight: true, - // }, + { + label: 'voting power', + alignRight: true, + }, + { + label: 'voting power percentage', + alignRight: true, + }, + { + label: 'voting power override', + alignRight: true, + }, { label: 'answer', alignRight: true, @@ -115,7 +115,7 @@ const VoteTable: React.FC = ({ voteDetails, crypto }) => { - {/* + {formatCrypto(v.votingPower, crypto.name, lang)} @@ -129,7 +129,7 @@ const VoteTable: React.FC = ({ voteDetails, crypto }) => { {formatPercentage(v.votingPowerOverride, lang)} - */} + {t(v.answer)} diff --git a/components/ProposalDetail/index.tsx b/components/ProposalDetail/index.tsx index 233ad72e..9c827ad7 100644 --- a/components/ProposalDetail/index.tsx +++ b/components/ProposalDetail/index.tsx @@ -28,9 +28,9 @@ export interface VoteDetail { image: string address: string } - // votingPower: number - // votingPowerPercentage: number - // votingPowerOverride: number + votingPower: number + votingPowerPercentage: number + votingPowerOverride: number answer: string } @@ -133,7 +133,7 @@ const ProposalDetail: React.FC = ({ - + diff --git a/components/VoteDialog/ConfirmAnswer.tsx b/components/VoteDialog/ConfirmAnswer.tsx deleted file mode 100644 index e963a9e8..00000000 --- a/components/VoteDialog/ConfirmAnswer.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { Button, DialogActions, DialogContent, Divider, Typography, Box } from '@material-ui/core' -import useTranslation from 'next-translate/useTranslation' -import React from 'react' -import dynamic from 'next/dynamic' -import { useGeneralContext } from '../../contexts/GeneralContext' -import useStyles from './styles' -import { formatTokenAmount } from '../../misc/utils' - -const ReactJson = dynamic(() => import('react-json-view'), { ssr: false }) - -interface ConfirmWithdrawProps { - account: Account - proposal: Proposal - gasFee: TokenAmount - memo: string - answer: { id: string; name: string } - onConfirm( - account: Account, - proposal: Proposal, - memo: string, - answer: { id: string; name: string } - ): void - rawTransactionData: any -} - -const ConfirmAnswer: React.FC = ({ - account, - gasFee, - proposal, - memo, - onConfirm, - answer, - rawTransactionData, -}) => { - const { t, lang } = useTranslation('common') - const classes = useStyles() - const { theme: themeSetting } = useGeneralContext() - const [viewData, setViewData] = React.useState(false) - const toggleViewData = () => { - setViewData(!viewData) - } - - return ( - <> - - - - - {t('address')} - - {account.address} - - - - - {`${t('vote proposal')} #${proposal.id}`} - - {answer.name} - - - - - {t('memo')} - - {memo || 'N/A'} - - - - - {t('fee')} - - {formatTokenAmount(gasFee, account.crypto, lang)} - - - - - - - {viewData ? ( - - - - ) : null} - - - - - - ) -} - -export default ConfirmAnswer diff --git a/custom.d.ts b/custom.d.ts index e47e3ce1..924ea515 100644 --- a/custom.d.ts +++ b/custom.d.ts @@ -190,6 +190,16 @@ interface Proposal { voteDetails?: VoteDetail[] totalDeposits?: TokenAmount minDeposit: TokenAmount + quorum: number + bondedTokens: number +} + +interface Profile { + bio: string + coverPic: string + dtag: string + nickname: string + profilePic: string } interface TokenUnit { @@ -358,6 +368,18 @@ interface TransactionMsgDeposit { } } +interface TransactionMsgSaveProfile { + typeUrl: '/desmos.profiles.v1beta1.MsgSaveProfile' + value: { + dtag: string + nickname: string + bio: string + profilePicture: string + coverPicture: string + creator: string + } +} + type TransactionMsg = | TransactionMsgDelegate | TransactionMsgUndelegate @@ -368,6 +390,7 @@ type TransactionMsg = | TransactionMsgSubmitProposal | TransactionMsgVote | TransactionMsgDeposit + | TransactionMsgSaveProfile interface Transaction { msgs: TransactionMsg[] diff --git a/graphql/queries/profile.ts b/graphql/queries/profile.ts new file mode 100644 index 00000000..d6af1534 --- /dev/null +++ b/graphql/queries/profile.ts @@ -0,0 +1,11 @@ +export const getProfile = (crypto: string): string => ` + subscription Profile($address: String!) { + profile(where: {address: {_eq: $address}}) { + bio + cover_pic + dtag + nickname + profile_pic + } + } +` diff --git a/graphql/queries/proposals.ts b/graphql/queries/proposals.ts index 962342d1..be6a6f39 100644 --- a/graphql/queries/proposals.ts +++ b/graphql/queries/proposals.ts @@ -31,6 +31,7 @@ export const getDepositParams = (crypto: string): string => ` subscription DepositParams { gov_params { deposit_params + tally_params } } ` @@ -62,60 +63,28 @@ subscription Proposal($id: Int!) { status proposal_deposits { amount + block { + timestamp + } depositor { address validator_infos { - validator { - validator_descriptions { - moniker - avatar_url + validator { + validator_descriptions { + moniker + avatar_url + } } } - } } } + staking_pool_snapshot { + bonded_tokens + } } } ` -// export const getProposers = (crypto: string): string => ` -// query Account { -// account(where: {validator_infos: {operator_address: {_neq: "null"}}}) { -// address -// validator_infos { -// operator_address -// validator { -// validator_descriptions { -// avatar_url -// identity -// moniker -// validator_address -// } -// } -// } -// } -// } -// ` - -// export const getProposer = (crypto: string): string => ` -// query Account($address: String!) { -// account(where: {address: {_eq: $address}}) { -// address -// validator_infos { -// operator_address -// validator { -// validator_descriptions { -// avatar_url -// identity -// moniker -// validator_address -// } -// } -// } -// } -// } -// ` - export const getProposalResult = (crypto: string): string => ` subscription ProposalResult($id: Int!) { proposal_tally_result(where: {proposal_id: {_eq: $id}}) { @@ -136,15 +105,21 @@ subscription VoteDetail($id: Int!) { proposal_id option account { + account_balance_histories(limit: 1, order_by: {timestamp: desc}) { + delegated + } validator_infos { validator { validator_descriptions { avatar_url moniker + } + validator_voting_powers { + voting_power + } } } } - } } } ` diff --git a/graphql/queries/rewards.ts b/graphql/queries/rewards.ts deleted file mode 100644 index 529bf151..00000000 --- a/graphql/queries/rewards.ts +++ /dev/null @@ -1,29 +0,0 @@ -export const getRewards = (crypto: string): string => ` - subscription Redelegations($address: String!) { - redelegations: redelegation(where: { delegator_address: {_eq: $address} }, distinct_on: [height] ,order_by: { height: desc }) { - height - amount - completion_timestamp: completion_time - from_validator: validator { - info: validator_info { - operator_address - } - description: validator_descriptions(limit: 1, order_by: { height: desc }) { - moniker - avatar_url - height - } - } - to_validator: validatorByDstValidatorAddress { - info: validator_info { - operator_address - } - description: validator_descriptions(limit: 1, order_by: { height: desc }) { - moniker - avatar_url - height - } - } - } - } -` diff --git a/locales/en/common.json b/locales/en/common.json index ea301764..91655383 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -258,7 +258,8 @@ "description": "Description", "depositor": "Depositor", "time": "Time", - "voted": "Voted", + "voted and quorum percentage": "Voted / Quorum ({{voted}} / {{quorum}})", + "voted over bonded": "(~{{voted}} of ~{{bonded}})", "yes": "Yes", "no": "No", "veto": "No With Veto", @@ -380,5 +381,15 @@ "deposit to": "Deposit to", "deposit time": "Deposit Time: {{from}} to {{to}}", "proposal with id": "# proposal {{id}}", - "target delegation amount": "Target delegation amount" + "target delegation amount": "Target delegation amount", + "edit profile": "Edit Profile", + "create profile": "Create Profile", + "save profile": "Save Profile", + "nickname": "Nickname", + "nickname placeholder": "Nickname", + "dtag": "DTag", + "dtag placeholder": "@", + "bio": "Bio", + "bio placeholder": "Add a bio to your profile", + "profile was saved": "Your profile was saved" } diff --git a/misc/cryptocurrencies.dev.ts b/misc/cryptocurrencies.dev.ts index c2442479..5550b94c 100644 --- a/misc/cryptocurrencies.dev.ts +++ b/misc/cryptocurrencies.dev.ts @@ -6,7 +6,7 @@ const cryptocurrencies: { [key: string]: Cryptocurrency } = { ecosystem: 'cosmos', chainId: 'morpheus-apollo-2', chainName: 'Desmos', - image: '/static/images/cryptocurrencies/dsm.png', + image: '/static/images/cryptocurrencies/dsm.svg', coinType: 852, graphqlHttpUrl: 'https://gql.morpheus.desmos.network/v1/graphql', graphqlWsUrl: 'wss://gql.morpheus.desmos.network/v1/graphql', @@ -37,6 +37,7 @@ const cryptocurrencies: { [key: string]: Cryptocurrency } = { '/cosmos.gov.v1beta1.MsgSubmitProposal': '400000', '/cosmos.gov.v1beta1.MsgDeposit': '400000', '/cosmos.gov.v1beta1.MsgVote': '400000', + '/desmos.profiles.v1beta1.MsgSaveProfile': '400000', }, }, }, diff --git a/misc/cryptocurrencies.prod.ts b/misc/cryptocurrencies.prod.ts index 7a6e149d..07aea155 100644 --- a/misc/cryptocurrencies.prod.ts +++ b/misc/cryptocurrencies.prod.ts @@ -37,6 +37,7 @@ const cryptocurrencies: { [key: string]: Cryptocurrency } = { '/cosmos.gov.v1beta1.MsgSubmitProposal': '400000', '/cosmos.gov.v1beta1.MsgDeposit': '400000', '/cosmos.gov.v1beta1.MsgVote': '400000', + '/desmos.profiles.v1beta1.MsgSaveProfile': '400000', }, }, }, diff --git a/misc/cryptocurrencies.ts b/misc/cryptocurrencies.ts index 7a6e149d..07aea155 100644 --- a/misc/cryptocurrencies.ts +++ b/misc/cryptocurrencies.ts @@ -37,6 +37,7 @@ const cryptocurrencies: { [key: string]: Cryptocurrency } = { '/cosmos.gov.v1beta1.MsgSubmitProposal': '400000', '/cosmos.gov.v1beta1.MsgDeposit': '400000', '/cosmos.gov.v1beta1.MsgVote': '400000', + '/desmos.profiles.v1beta1.MsgSaveProfile': '400000', }, }, }, diff --git a/misc/utils.ts b/misc/utils.ts index 7ead1e04..dca314a9 100644 --- a/misc/utils.ts +++ b/misc/utils.ts @@ -18,11 +18,13 @@ export const formatCrypto = ( amount: number, unit: string, lang: string, - hideUnit?: boolean + hideUnit?: boolean, + compact?: boolean ): string => `${new Intl.NumberFormat(lang, { signDisplay: 'never', - maximumFractionDigits: 6, + maximumFractionDigits: compact ? 2 : 6, + notation: compact ? 'compact' : undefined, }).format(amount || 0)}${hideUnit ? '' : ` ${(unit || '').toUpperCase()}`}` export const formatCurrency = ( @@ -613,7 +615,7 @@ export const transformProposal = ( address: get(x, 'depositor.address'), }, amount: getTokenAmountFromDenoms(get(x, 'amount'), tokensPrices), - // time: `${format(new Date(x.block.timestamp), 'dd MMM yyyy HH:mm')} UTC`, + time: `${format(new Date(get(x, 'block.timestamp')), 'dd MMM yyyy HH:mm')} UTC`, } }), totalDeposits: getTokenAmountFromDenoms(totalDepositsList, tokensPrices), @@ -623,6 +625,8 @@ export const transformProposal = ( tokensPrices ) : null, + quorum: get(depositParams, 'gov_params[0].tally_params.quorum', 0), + bondedTokens: get(p, 'staking_pool_snapshot.bonded_tokens', 0) / 10 ** 6, } } @@ -685,18 +689,28 @@ export const getVoteAnswer = (answer: string) => { return 'veto' } -export const transformVoteDetail = (voteDetail: any): any => { - return get(voteDetail, 'proposal_vote', []).map((d) => ({ - voter: { - name: get(d, 'account.validator_infos[0].validator.validator_descriptions[0].moniker'), - image: get(d, 'account.validator_infos[0].validator.validator_descriptions[0].avatar_url'), - address: get(d, 'voter_address'), - }, - // votingPower: 0, - // votingPowerPercentage: 0.1, - // votingPowerOverride: 0.1, - answer: getVoteAnswer(get(d, 'option')), - })) +export const transformVoteDetail = (voteDetail: any, proposal: Proposal): any => { + return get(voteDetail, 'proposal_vote', []).map((d) => { + const isValidator = !!get(d, 'account.validator_infos', []).length + const votingPower = isValidator + ? get(d, 'account.validator_infos[0].validator.validator_voting_powers[0].voting_power', 0) + : Number(get(d, 'account.account_balance_histories[0].delegated[0].amount', '0')) / 10 ** 6 + return { + voter: { + name: get(d, 'account.validator_infos[0].validator.validator_descriptions[0].moniker', ''), + image: get( + d, + 'account.validator_infos[0].validator.validator_descriptions[0].avatar_url', + '' + ), + address: get(d, 'voter_address', ''), + }, + votingPower, + votingPowerPercentage: votingPower / proposal.bondedTokens, + votingPowerOverride: isValidator ? 0 : votingPower / proposal.bondedTokens, + answer: getVoteAnswer(get(d, 'option')), + } + }) } export const isAddressValid = (prefix: string, address: string): boolean => { @@ -727,3 +741,13 @@ export const getValidatorConditionClass = (condition: number) => { return conditionClass } + +export const transformProfile = (data: any): Profile => { + return { + bio: get(data, 'profile[0].bio', ''), + coverPic: get(data, 'profile[0].cover_pic', ''), + dtag: get(data, 'profile[0].dtag', ''), + nickname: get(data, 'profile[0].nickname', ''), + profilePic: get(data, 'profile[0].profile_pic', ''), + } +} diff --git a/package.json b/package.json index ddbfeea6..030cfebf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "forbole-x", - "version": "0.4.10", + "version": "0.5.0", "private": true, "scripts": { "dev": "next dev", diff --git a/pages/account/[address].tsx b/pages/account/[address].tsx index 0b74cedc..7a82ae7f 100644 --- a/pages/account/[address].tsx +++ b/pages/account/[address].tsx @@ -16,6 +16,7 @@ import cryptocurrencies from '../../misc/cryptocurrencies' import { getValidators } from '../../graphql/queries/validators' import { transformGqlAcountBalance, + transformProfile, transformRedelegations, transformTransactions, transformUnbonding, @@ -26,6 +27,9 @@ import { getRedelegations } from '../../graphql/queries/redelegations' import { getTransactions } from '../../graphql/queries/transactions' import AccountBalanceCard from '../../components/AccountBalanceCard' import IBCTransferDialog from '../../components/IBCTransferDialog' +import ProfileCard from '../../components/ProfileCard' +import { getProfile } from '../../graphql/queries/profile' +import ProfileDialog from '../../components/ProfileDialog' const Account: React.FC = () => { const router = useRouter() @@ -71,6 +75,14 @@ const Account: React.FC = () => { } ) + const { data } = useSubscription( + gql` + ${getProfile(crypto.name)} + `, + { variables: { address: account ? account.address : '' } } + ) + const profile = transformProfile(data) + const validators = transformValidatorsWithTokenAmount(validatorsData, balanceData) const unbondings = transformUnbonding(validatorsData, balanceData) const redelegations = transformRedelegations(redelegationsData, balanceData) @@ -94,6 +106,7 @@ const Account: React.FC = () => { ) const [isIBCDialogOpen, setIsIBCDialogOpen] = React.useState(false) + const [isProfileDialogOpen, setIsProfileDialogOpen] = React.useState(false) return ( { ) : null } > + {profile.dtag ? ( + setIsProfileDialogOpen(true)} /> + ) : null} {account ? ( setIsProfileDialogOpen(true)} wallet={wallet} account={account} validators={validators} @@ -152,6 +170,12 @@ const Account: React.FC = () => { onClose={() => setIsIBCDialogOpen(false)} /> ) : null} + setIsProfileDialogOpen(false)} + /> ) } diff --git a/pages/proposals/[crypto]/[id].tsx b/pages/proposals/[crypto]/[id].tsx index e271c501..09b169d3 100644 --- a/pages/proposals/[crypto]/[id].tsx +++ b/pages/proposals/[crypto]/[id].tsx @@ -2,11 +2,10 @@ import { Breadcrumbs, Link as MLink, Typography } from '@material-ui/core' import useTranslation from 'next-translate/useTranslation' import Link from 'next/link' import { useRouter } from 'next/router' -import { gql, useSubscription, useQuery } from '@apollo/client' +import { gql, useSubscription } from '@apollo/client' import React from 'react' import get from 'lodash/get' import Layout from '../../../components/Layout' -import { useWalletsContext } from '../../../contexts/WalletsContext' import cryptocurrencies from '../../../misc/cryptocurrencies' import ProposalDetail from '../../../components/ProposalDetail' import { @@ -62,7 +61,7 @@ const Proposal: React.FC = () => { const proposal = transformProposal(proposalData, balanceData, depositParamsData) const voteSummary = transformVoteSummary(proporslReaultData) - const voteDetail = transformVoteDetail(voteDetailData) + const voteDetail = transformVoteDetail(voteDetailData, proposal) return ( ({ dtag, nickname, bio, profile_picture: profilePicture, cover_picture: coverPicture, creator }), ++ fromAmino: ({ dtag, nickname, bio, profile_picture, cover_picture, creator }) => ({ dtag, nickname, bio, profilePicture: profile_picture, coverPicture: cover_picture, creator }), ++ }, + }; + } + /** +diff --git a/node_modules/@cosmjs/stargate/build/signingstargateclient.js b/node_modules/@cosmjs/stargate/build/signingstargateclient.js +index 26f0c9f..c171737 100644 +--- a/node_modules/@cosmjs/stargate/build/signingstargateclient.js ++++ b/node_modules/@cosmjs/stargate/build/signingstargateclient.js +@@ -56,6 +56,7 @@ exports.defaultRegistryTypes = [ + ["/ibc.core.connection.v1.MsgConnectionOpenAck", tx_9.MsgConnectionOpenAck], + ["/ibc.core.connection.v1.MsgConnectionOpenConfirm", tx_9.MsgConnectionOpenConfirm], + ["/ibc.applications.transfer.v1.MsgTransfer", tx_6.MsgTransfer], ++ ["/desmos.profiles.v1beta1.MsgSaveProfile", tx_6.MsgSaveProfile] + ]; + function createDefaultRegistry() { + return new proto_signing_1.Registry(exports.defaultRegistryTypes); diff --git a/patches/cosmjs-types+0.2.0.patch b/patches/cosmjs-types+0.2.0.patch index e1060caa..9346942e 100644 --- a/patches/cosmjs-types+0.2.0.patch +++ b/patches/cosmjs-types+0.2.0.patch @@ -86,3 +86,186 @@ index 0673e21..41ebbd1 100644 +}; //# sourceMappingURL=auth.js.map \ No newline at end of file +diff --git a/node_modules/cosmjs-types/ibc/applications/transfer/v1/tx.d.ts b/node_modules/cosmjs-types/ibc/applications/transfer/v1/tx.d.ts +index 4a69d0d..259fb72 100644 +--- a/node_modules/cosmjs-types/ibc/applications/transfer/v1/tx.d.ts ++++ b/node_modules/cosmjs-types/ibc/applications/transfer/v1/tx.d.ts +@@ -65,3 +65,20 @@ export declare type DeepPartial = T extends Builtin ? T : T extends Array; + } : Partial; + export {}; ++ ++export interface MsgSaveProfile { ++ dtag: string ++ nickname: string ++ bio: string ++ profilePicture: string ++ coverPicture: string ++ creator: string ++ } ++ ++export declare const MsgSaveProfile: { ++ encode(message: MsgSaveProfile, writer?: _m0.Writer): _m0.Writer; ++ decode(input: _m0.Reader | Uint8Array, length?: number | undefined): MsgSaveProfile; ++ fromJSON(object: any): MsgSaveProfile; ++ toJSON(message: MsgSaveProfile): unknown; ++ fromPartial(object: DeepPartial): MsgSaveProfile; ++}; +\ No newline at end of file +diff --git a/node_modules/cosmjs-types/ibc/applications/transfer/v1/tx.js b/node_modules/cosmjs-types/ibc/applications/transfer/v1/tx.js +index 017ce51..ec28dec 100644 +--- a/node_modules/cosmjs-types/ibc/applications/transfer/v1/tx.js ++++ b/node_modules/cosmjs-types/ibc/applications/transfer/v1/tx.js +@@ -232,3 +232,152 @@ if (minimal_1.default.util.Long !== long_1.default) { + minimal_1.default.configure(); + } + //# sourceMappingURL=tx.js.map ++ ++// Desmos ++const baseMsgSaveProfile = { ++ dtag: '', ++ nickname: '', ++ bio: '', ++ profilePicture: '', ++ coverPicture: '', ++ creator: '', ++ } ++exports.MsgSaveProfile = { ++ encode(message, writer = minimal_1.default.Writer.create()) { ++ if (message.dtag !== '') { ++ writer.uint32(10).string(message.dtag) ++ } ++ if (message.nickname !== '') { ++ writer.uint32(18).string(message.nickname) ++ } ++ if (message.bio !== '') { ++ writer.uint32(26).string(message.bio) ++ } ++ if (message.profilePicture !== '') { ++ writer.uint32(34).string(message.profilePicture) ++ } ++ if (message.coverPicture !== '') { ++ writer.uint32(42).string(message.coverPicture) ++ } ++ if (message.creator !== '') { ++ writer.uint32(50).string(message.creator) ++ } ++ return writer ++ }, ++ ++ decode(input, length) { ++ const reader = input instanceof minimal_1.default.Reader ? input : new minimal_1.default.Reader(input); ++ let end = length === undefined ? reader.len : reader.pos + length ++ const message = { ...baseMsgSaveProfile } ++ while (reader.pos < end) { ++ const tag = reader.uint32() ++ switch (tag >>> 3) { ++ case 1: ++ message.dtag = reader.string() ++ break ++ case 2: ++ message.nickname = reader.string() ++ break ++ case 3: ++ message.bio = reader.string() ++ break ++ case 4: ++ message.profilePicture = reader.string() ++ break ++ case 5: ++ message.coverPicture = reader.string() ++ break ++ case 6: ++ message.creator = reader.string() ++ break ++ default: ++ reader.skipType(tag & 7) ++ break ++ } ++ } ++ return message ++ }, ++ ++ fromJSON(object) { ++ const message = { ...baseMsgSaveProfile } ++ if (object.dtag !== undefined && object.dtag !== null) { ++ message.dtag = String(object.dtag) ++ } else { ++ message.dtag = '' ++ } ++ if (object.nickname !== undefined && object.nickname !== null) { ++ message.nickname = String(object.nickname) ++ } else { ++ message.nickname = '' ++ } ++ if (object.bio !== undefined && object.bio !== null) { ++ message.bio = String(object.bio) ++ } else { ++ message.bio = '' ++ } ++ if (object.profilePicture !== undefined && object.profilePicture !== null) { ++ message.profilePicture = String(object.profilePicture) ++ } else { ++ message.profilePicture = '' ++ } ++ if (object.coverPicture !== undefined && object.coverPicture !== null) { ++ message.coverPicture = String(object.coverPicture) ++ } else { ++ message.coverPicture = '' ++ } ++ if (object.creator !== undefined && object.creator !== null) { ++ message.creator = String(object.creator) ++ } else { ++ message.creator = '' ++ } ++ return message ++ }, ++ ++ toJSON(message) { ++ const obj = {} ++ message.dtag !== undefined && (obj.dtag = message.dtag) ++ message.nickname !== undefined && (obj.nickname = message.nickname) ++ message.bio !== undefined && (obj.bio = message.bio) ++ message.profilePicture !== undefined && ++ (obj.profilePicture = message.profilePicture) ++ message.coverPicture !== undefined && ++ (obj.coverPicture = message.coverPicture) ++ message.creator !== undefined && (obj.creator = message.creator) ++ return obj ++ }, ++ ++ fromPartial(object) { ++ const message = { ...baseMsgSaveProfile } ++ if (object.dtag !== undefined && object.dtag !== null) { ++ message.dtag = object.dtag ++ } else { ++ message.dtag = '' ++ } ++ if (object.nickname !== undefined && object.nickname !== null) { ++ message.nickname = object.nickname ++ } else { ++ message.nickname = '' ++ } ++ if (object.bio !== undefined && object.bio !== null) { ++ message.bio = object.bio ++ } else { ++ message.bio = '' ++ } ++ if (object.profilePicture !== undefined && object.profilePicture !== null) { ++ message.profilePicture = object.profilePicture ++ } else { ++ message.profilePicture = '' ++ } ++ if (object.coverPicture !== undefined && object.coverPicture !== null) { ++ message.coverPicture = object.coverPicture ++ } else { ++ message.coverPicture = '' ++ } ++ if (object.creator !== undefined && object.creator !== null) { ++ message.creator = object.creator ++ } else { ++ message.creator = '' ++ } ++ return message ++ }, ++ } diff --git a/static/images/default_cover_image_dark.png b/static/images/default_cover_image_dark.png new file mode 100644 index 00000000..64edc90c Binary files /dev/null and b/static/images/default_cover_image_dark.png differ diff --git a/static/images/default_cover_image_light.png b/static/images/default_cover_image_light.png new file mode 100644 index 00000000..e03c3eb7 Binary files /dev/null and b/static/images/default_cover_image_light.png differ diff --git a/static/images/default_profile_pic_dark.png b/static/images/default_profile_pic_dark.png new file mode 100644 index 00000000..3f370c25 Binary files /dev/null and b/static/images/default_profile_pic_dark.png differ diff --git a/static/images/default_profile_pic_light.png b/static/images/default_profile_pic_light.png new file mode 100644 index 00000000..7bd41616 Binary files /dev/null and b/static/images/default_profile_pic_light.png differ diff --git a/tests/components/Layout/__snapshots__/LeftMenu.test.tsx.snap b/tests/components/Layout/__snapshots__/LeftMenu.test.tsx.snap index a08d889e..ddd42fb0 100644 --- a/tests/components/Layout/__snapshots__/LeftMenu.test.tsx.snap +++ b/tests/components/Layout/__snapshots__/LeftMenu.test.tsx.snap @@ -321,11 +321,13 @@ exports[`component: Layout - LeftMenu renders activeItem "/" correctly 1`] = ` className="MuiButtonBase-root MuiButton-root MuiButton-contained HookGlobalStyles-starButton-8 MuiButton-containedPrimary" href="/wallets" onBlur={[Function]} + onClick={[Function]} onDragLeave={[Function]} onFocus={[Function]} onKeyDown={[Function]} onKeyUp={[Function]} onMouseDown={[Function]} + onMouseEnter={[Function]} onMouseLeave={[Function]} onMouseUp={[Function]} onTouchEnd={[Function]} @@ -655,11 +657,13 @@ exports[`component: Layout - LeftMenu renders no matching activeItem correctly 1 className="MuiButtonBase-root MuiButton-root MuiButton-contained HookGlobalStyles-starButton-8 MuiButton-containedPrimary" href="/wallets" onBlur={[Function]} + onClick={[Function]} onDragLeave={[Function]} onFocus={[Function]} onKeyDown={[Function]} onKeyUp={[Function]} onMouseDown={[Function]} + onMouseEnter={[Function]} onMouseLeave={[Function]} onMouseUp={[Function]} onTouchEnd={[Function]}