Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multichain erc20 vote weight + Native ETH voting #10049

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion libs/model/src/community/CreateTopic.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,14 @@ export function CreateTopic(): Command<typeof schemas.CreateTopic> {
throw new InvalidState(Errors.StakeNotAllowed);
}

// new path: stake or ERC20
if (payload.weighted_voting) {
options = {
...options,
weighted_voting: payload.weighted_voting,
token_address: payload.token_address || undefined,
token_symbol: payload.token_symbol || undefined,
vote_weight_multiplier: payload.vote_weight_multiplier || undefined,
chain_node_id: payload.chain_node_id || undefined,
};
}

Expand Down
46 changes: 27 additions & 19 deletions libs/model/src/community/GetTopics.query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as schemas from '@hicommonwealth/schemas';
import { QueryTypes } from 'sequelize';
import { z } from 'zod';
import { models } from '../database';
import { buildChainNodeUrl } from '../utils';

const includeContestManagersQuery = `
SELECT td.*,
Expand Down Expand Up @@ -58,31 +59,35 @@ export function GetTopics(): Query<typeof schemas.GetTopics> {
: 'AND archived_at IS NULL';

const sql = `
WITH topic_data AS (SELECT id,
name,
community_id,
description,
telegram,
featured_in_sidebar,
featured_in_new_post,
default_offchain_template,
"order",
channel_id,
group_ids,
weighted_voting,
token_symbol,
vote_weight_multiplier,
token_address,
created_at::text AS created_at,
updated_at::text AS updated_at,
deleted_at::text AS deleted_at,
archived_at::text AS archived_at,
WITH topic_data AS (SELECT t.id,
t.name,
t.community_id,
t.description,
t.telegram,
t.featured_in_sidebar,
t.featured_in_new_post,
t.default_offchain_template,
t."order",
t.channel_id,
t.group_ids,
t.weighted_voting,
t.token_symbol,
t.vote_weight_multiplier,
t.token_address,
cn.url as chain_node_url,
cn.eth_chain_id as eth_chain_id,
t.created_at::text AS created_at,
t.updated_at::text AS updated_at,
t.deleted_at::text AS deleted_at,
t.archived_at::text AS archived_at,
(SELECT count(*)::int
FROM "Threads"
WHERE community_id = :community_id
AND topic_id = t.id
AND deleted_at IS NULL) AS total_threads
FROM "Topics" t
LEFT JOIN "ChainNodes" cn
ON t.chain_node_id = cn.id
WHERE t.community_id = :community_id
AND t.deleted_at IS NULL ${archivedTopicsQuery})
${contest_managers}
Expand All @@ -102,6 +107,9 @@ export function GetTopics(): Query<typeof schemas.GetTopics> {
c.voting_power = BigNumber.from(c.voting_power).toString();
});
});
if (r.chain_node_url) {
r.chain_node_url = buildChainNodeUrl(r.chain_node_url, 'public');
}
});

return results;
Expand Down
3 changes: 2 additions & 1 deletion libs/model/src/models/associations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ export const buildAssociations = (db: DB) => {

db.ChainNode.withMany(db.Community)
.withMany(db.EvmEventSource)
.withOne(db.LastProcessedEvmBlock);
.withOne(db.LastProcessedEvmBlock)
.withMany(db.Topic);

db.ContractAbi.withMany(db.EvmEventSource, { foreignKey: 'abi_id' });

Expand Down
3 changes: 3 additions & 0 deletions libs/model/src/models/topic.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Topic } from '@hicommonwealth/schemas';
import Sequelize from 'sequelize';
import { z } from 'zod';
import { ChainNodeAttributes } from './chain_node';
import type { CommunityAttributes } from './community';
import type { ThreadAttributes } from './thread';
import type { ModelInstance } from './types';
Expand All @@ -9,6 +10,7 @@ export type TopicAttributes = z.infer<typeof Topic> & {
// associations
community?: CommunityAttributes;
threads?: ThreadAttributes[];
ChainNode?: ChainNodeAttributes;
};
export type TopicInstance = ModelInstance<TopicAttributes>;

Expand Down Expand Up @@ -49,6 +51,7 @@ export default (
},
telegram: { type: Sequelize.STRING, allowNull: true },
weighted_voting: { type: Sequelize.STRING, allowNull: true },
chain_node_id: { type: Sequelize.INTEGER, allowNull: true },
token_address: { type: Sequelize.STRING, allowNull: true },
token_symbol: { type: Sequelize.STRING, allowNull: true },
vote_weight_multiplier: { type: Sequelize.FLOAT, allowNull: true },
Expand Down
20 changes: 13 additions & 7 deletions libs/model/src/services/stakeHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,21 @@ export async function getVotingWeight(
},
],
},
{
model: models.ChainNode.scope('withPrivateData'),
required: false,
},
],
});
mustExist('Topic', topic);

const { community } = topic;
mustExist('Community', community);

const chain_node = community.ChainNode;
const namespaceChainNode = community.ChainNode;

if (topic.weighted_voting === TopicWeightedVoting.Stake) {
mustExist('Chain Node Eth Chain Id', chain_node?.eth_chain_id);
mustExist('Chain Node Eth Chain Id', namespaceChainNode?.eth_chain_id);
mustExist('Community Namespace Address', community.namespace_address);

const stake = topic.community?.CommunityStakes?.at(0);
Expand All @@ -59,7 +63,7 @@ export async function getVotingWeight(
const stakeBalances = await contractHelpers.getNamespaceBalance(
community.namespace_address,
stake.stake_id,
chain_node.eth_chain_id,
namespaceChainNode.eth_chain_id,
[address],
);
const stakeBalance = stakeBalances[address];
Expand All @@ -68,16 +72,18 @@ export async function getVotingWeight(

return commonProtocol.calculateVoteWeight(stakeBalance, stake.vote_weight);
} else if (topic.weighted_voting === TopicWeightedVoting.ERC20) {
mustExist('Chain Node Eth Chain Id', chain_node?.eth_chain_id);
const chainNodeUrl = chain_node!.private_url! || chain_node!.url!;
const { eth_chain_id, private_url, url } = topic.ChainNode!;
mustExist('Chain Node Eth Chain Id', eth_chain_id);
const chainNodeUrl = private_url! || url!;
mustExist('Chain Node URL', chainNodeUrl);
mustExist('Topic Token Address', topic.token_address);

const balances = await tokenBalanceCache.getBalances({
balanceSourceType: BalanceSourceType.ERC20,
addresses: [address],
sourceOptions: {
evmChainId: chain_node.eth_chain_id,
contractAddress: topic.token_address!,
evmChainId: eth_chain_id,
contractAddress: topic.token_address,
},
cacheRefresh: true,
});
Expand Down
1 change: 1 addition & 0 deletions libs/schemas/src/commands/community.schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ export const CreateTopic = {
token_address: true,
token_symbol: true,
vote_weight_multiplier: true,
chain_node_id: true,
}),
),
output: z.object({
Expand Down
5 changes: 5 additions & 0 deletions libs/schemas/src/entities/topic.schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ export const Topic = z.object({
group_ids: z.array(PG_INT).default([]),
default_offchain_template_backup: z.string().nullish(),
weighted_voting: z.nativeEnum(TopicWeightedVoting).nullish(),
chain_node_id: z
.number()
.int()
.nullish()
.describe('token chain node ID, used for ERC20 topics'),
token_address: z
.string()
.nullish()
Expand Down
3 changes: 3 additions & 0 deletions libs/schemas/src/queries/community.schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,9 @@ export const TopicView = Topic.extend({
contest_topics: z.undefined(),
total_threads: z.number().default(0),
active_contest_managers: z.array(ConstestManagerView).optional(),
chain_node_id: z.number().nullish().optional(),
chain_node_url: z.string().nullish().optional(),
eth_chain_id: z.number().nullish().optional(),
});

export const GetTopics = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ const TokenBanner = ({
<Avatar url={avatarUrl} size={40} />
) : (
<div className="avatar-placeholder">
{(name || 'Token').charAt(0).toUpperCase()}
{(name || 'ETH').charAt(0).toUpperCase()}
</div>
)}
<CWText fontWeight="medium">{name}</CWText>
<CWText fontWeight="medium">{name || 'ETH'}</CWText>
<CWText fontWeight="medium" className="ticker">
{ticker}
</CWText>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ const TokenFinder = ({
onInput={(e) => setTokenValue(e.target.value.trim())}
customError={tokenError}
/>

{debouncedTokenValue && !tokenError && (
<TokenBanner
isLoading={tokenMetadataLoading}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ZERO_ADDRESS } from '@hicommonwealth/shared';
import { GetTokenMetadataResponse } from 'client/scripts/state/api/tokens/getTokenMetadata';
import { useState } from 'react';
import { useTokenMetadataQuery } from 'state/api/tokens';
import { useDebounce } from 'usehooks-ts';
Expand All @@ -20,7 +22,17 @@ const useTokenFinder = ({
nodeEthChainId,
});

const nativeTokenMetadata: GetTokenMetadataResponse = {
decimals: 18,
logo: '',
name: 'ETH', // TODO: get native eth name/symbol
rbennettcw marked this conversation as resolved.
Show resolved Hide resolved
symbol: 'ETH',
};

const getTokenError = (isOneOff?: boolean) => {
if (tokenValue === ZERO_ADDRESS) {
return null;
}
if (isOneOff && !tokenValue) {
return 'You must enter a token address';
}
Expand All @@ -34,7 +46,8 @@ const useTokenFinder = ({
tokenValue,
setTokenValue,
debouncedTokenValue,
tokenMetadata,
tokenMetadata:
tokenValue === ZERO_ADDRESS ? nativeTokenMetadata : tokenMetadata,
tokenMetadataLoading,
getTokenError,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { CWTextInput } from 'views/components/component_kit/new_designs/CWTextIn
import { CreateTopicStep } from '../utils';

import { TopicWeightedVoting } from '@hicommonwealth/schemas';
import { ZERO_ADDRESS } from '@hicommonwealth/shared';
import { CWCheckbox } from 'client/scripts/views/components/component_kit/cw_checkbox';
import { notifyError } from 'controllers/app/notifications';
import TokenFinder, { useTokenFinder } from 'views/components/TokenFinder';
import { HandleCreateTopicProps } from 'views/pages/CommunityManagement/Topics/Topics';
Expand Down Expand Up @@ -38,7 +40,10 @@ const WVERC20Details = ({ onStepChange, onCreateTopic }: WVConsentProps) => {
tokenMetadataLoading,
tokenValue,
} = useTokenFinder({
nodeEthChainId: app.chain.meta.ChainNode?.eth_chain_id || 0,
nodeEthChainId:
Number(selectedChain?.value) ||
app.chain.meta.ChainNode?.eth_chain_id ||
0,
});

const editMode = false;
Expand Down Expand Up @@ -77,7 +82,6 @@ const WVERC20Details = ({ onStepChange, onCreateTopic }: WVConsentProps) => {
selection is only available when the community is created
</CWText>
<CWSelectList
isDisabled
options={options}
value={selectedChain}
onChange={setSelectedChain}
Expand All @@ -96,10 +100,20 @@ const WVERC20Details = ({ onStepChange, onCreateTopic }: WVConsentProps) => {
setTokenValue={setTokenValue}
tokenValue={tokenValue}
containerClassName="token-input"
disabled={editMode}
disabled={editMode || tokenValue == ZERO_ADDRESS}
fullWidth
tokenError={getTokenError()}
/>
<CWCheckbox
rbennettcw marked this conversation as resolved.
Show resolved Hide resolved
label="Use native token"
onChange={() => {
if (tokenValue == ZERO_ADDRESS) {
setTokenValue('');
} else {
setTokenValue(ZERO_ADDRESS);
}
}}
/>

<CWText type="h5">Vote weight multiplier</CWText>

Expand Down
Loading
Loading