From 9937856ed591428bb7379baa4c2598a281d4451e Mon Sep 17 00:00:00 2001 From: "Carina.Akaia.io" Date: Fri, 29 Nov 2024 09:18:00 +0400 Subject: [PATCH] mpDAO features progress & bugfixes (#263) --- .vscode/settings.json | 1 + package.json | 2 + src/common/_config/index.ts | 48 +- src/common/_config/production.env-config.ts | 88 +- src/common/_config/staging.env-config.ts | 88 +- src/common/_config/test.env-config.ts | 88 +- src/common/constants.ts | 3 + src/common/contracts/core/voting/abi.json | 1166 +++++++++++++++++ src/common/contracts/core/voting/client.ts | 6 + src/common/contracts/core/voting/index.ts | 5 + src/common/contracts/metapool/index.ts | 2 + .../metapool/liquid-staking/client.ts | 12 + .../metapool/liquid-staking/index.ts | 3 + .../contracts/metapool/vote/constants.ts | 1 + src/common/contracts/metapool/vote/index.ts | 2 + .../contracts/metapool/vote/interface.d.ts | 30 + src/common/lib/converters.ts | 23 +- src/common/services/ft/constants.ts | 4 + src/common/services/ft/models.ts | 139 +- src/common/types.ts | 25 +- .../ui/components/molecules/checklist.tsx | 60 +- src/modules/access-control/index.ts | 1 + src/modules/access-control/types.ts | 5 + .../donation/components/DonationSuccess.tsx | 8 +- src/modules/pot/components/PotCard.tsx | 2 +- src/modules/pot/components/PotHero.tsx | 26 +- src/modules/pot/components/PotTimeline.tsx | 9 +- src/modules/pot/hooks/clearance.ts | 128 +- src/modules/pot/hooks/index.ts | 2 +- src/modules/pot/hooks/lifecycle.ts | 12 + src/modules/pot/hooks/useNearAndUsdByPot.ts | 20 +- src/modules/pot/hooks/voting.ts | 55 + .../token/components/TokenTotalValue.tsx | 4 +- 33 files changed, 1763 insertions(+), 305 deletions(-) create mode 100644 src/common/contracts/core/voting/abi.json create mode 100644 src/common/contracts/core/voting/client.ts create mode 100644 src/common/contracts/core/voting/index.ts create mode 100644 src/common/contracts/metapool/index.ts create mode 100644 src/common/contracts/metapool/liquid-staking/client.ts create mode 100644 src/common/contracts/metapool/liquid-staking/index.ts create mode 100644 src/common/contracts/metapool/vote/constants.ts create mode 100644 src/common/contracts/metapool/vote/index.ts create mode 100644 src/common/contracts/metapool/vote/interface.d.ts create mode 100644 src/common/services/ft/constants.ts create mode 100644 src/modules/access-control/types.ts create mode 100644 src/modules/pot/hooks/voting.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 6cba4b35..346c8b8c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -44,6 +44,7 @@ "shadcn", "socialdb", "SOURCECODE", + "stnear", "svgr", "typecheck", "unocss", diff --git a/package.json b/package.json index c517b1f0..f232578d 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,8 @@ "lint": "next lint", "format": "yarn lint --fix ./", "typecheck": "tsc --noEmit", + "generate:core-voting-contract-client": "near2ts ./src/common/contracts/core/voting/abi.json", + "generate:contract-clients": "concurrently 'yarn generate:core-voting-contract-client'", "generate:api-clients": "orval --config api.config.cjs", "generate:clients": "concurrently 'yarn generate:api-clients'", "generate:css": "unocss 'src/**/*.tsx' --out-file=src/common/ui/styles/uno.generated.css --write-transformed", diff --git a/src/common/_config/index.ts b/src/common/_config/index.ts index ea9ce5ca..1a9f6b0d 100644 --- a/src/common/_config/index.ts +++ b/src/common/_config/index.ts @@ -9,28 +9,31 @@ export const { contractMetadata: { version: CONTRACT_SOURCECODE_VERSION, repoUrl: CONTRACT_SOURCECODE_REPO_URL }, - features: FEATURE_REGISTRY, + core: { + campaigns: { + contract: { accountId: CAMPAIGNS_CONTRACT_ACCOUNT_ID }, + }, - campaigns: { - contract: { accountId: CAMPAIGNS_CONTRACT_ACCOUNT_ID }, - }, + donation: { + contract: { accountId: DONATION_CONTRACT_ACCOUNT_ID }, + }, - deFi: { - refFinance: { - exchangeContract: { accountId: REF_EXCHANGE_CONTRACT_ACCOUNT_ID }, + lists: { + contract: { accountId: LISTS_CONTRACT_ACCOUNT_ID }, }, - }, - donation: { - contract: { accountId: DONATION_CONTRACT_ACCOUNT_ID }, - }, + potFactory: { + contract: { accountId: POT_FACTORY_CONTRACT_ACCOUNT_ID }, + }, - lists: { - contract: { accountId: LISTS_CONTRACT_ACCOUNT_ID }, - }, + sybil: { + app: { url: SYBIL_APP_LINK_URL }, + contract: { accountId: SYBIL_CONTRACT_ACCOUNT_ID }, + }, - potFactory: { - contract: { accountId: POT_FACTORY_CONTRACT_ACCOUNT_ID }, + voting: { + contract: { accountId: VOTING_CONTRACT_ACCOUNT_ID }, + }, }, social: { @@ -38,10 +41,17 @@ export const { contract: { accountId: SOCIAL_CONTRACT_ACCOUNT_ID }, }, - sybil: { - app: { url: SYBIL_APP_LINK_URL }, - contract: { accountId: SYBIL_CONTRACT_ACCOUNT_ID }, + deFi: { + metapool: { + liquidStakingContract: { accountId: METAPOOL_LIQUID_STAKING_CONTRACT_ACCOUNT_ID }, + }, + + refFinance: { + exchangeContract: { accountId: REF_EXCHANGE_CONTRACT_ACCOUNT_ID }, + }, }, + + features: FEATURE_REGISTRY, } = getEnvConfig(); export const BLOCKCHAIN_EXPLORER_TX_ENDPOINT_URL = diff --git a/src/common/_config/production.env-config.ts b/src/common/_config/production.env-config.ts index 016a4ed2..ecb94ebb 100644 --- a/src/common/_config/production.env-config.ts +++ b/src/common/_config/production.env-config.ts @@ -8,51 +8,35 @@ export const envConfig: EnvConfig = { repoUrl: "https://github.com/PotLock/core", }, - features: { - [FeatureId.DirectFtDonation]: { - id: FeatureId.DirectFtDonation, - name: "Direct FT donation", + indexer: { + api: { endpointUrl: "https://api.potlock.io" }, + }, - /** - * The implementation is not finished yet - */ - isEnabled: false, + core: { + campaigns: { + contract: { accountId: "campaigns.staging.potlock.near" }, }, - [FeatureId.DirectNativeTokenDonation]: { - id: FeatureId.DirectNativeTokenDonation, - name: "Direct native token donation", - isEnabled: true, + donation: { + contract: { accountId: "donate.potlock.near" }, }, - }, - deFi: { - refFinance: { - exchangeContract: { - accountId: "v2.ref-finance.near", - }, + lists: { + contract: { accountId: "lists.potlock.near" }, }, - }, - campaigns: { - contract: { accountId: "campaigns.staging.potlock.near" }, - }, - - donation: { - contract: { accountId: "donate.potlock.near" }, - }, - - lists: { - contract: { accountId: "lists.potlock.near" }, - }, + potFactory: { + contract: { accountId: "v1.potfactory.potlock.near" }, + }, - potFactory: { - contract: { accountId: "v1.potfactory.potlock.near" }, - }, + sybil: { + app: { url: "https://app.nada.bot" }, + contract: { accountId: "v1.nadabot.near" }, + }, - sybil: { - app: { url: "https://app.nada.bot" }, - contract: { accountId: "v1.nadabot.near" }, + voting: { + contract: { accountId: "v1.voting.potlock.near" }, + }, }, social: { @@ -60,7 +44,35 @@ export const envConfig: EnvConfig = { contract: { accountId: "social.near" }, }, - indexer: { - api: { endpointUrl: "https://api.potlock.io" }, + deFi: { + metapool: { + liquidStakingContract: { + accountId: "meta-pool.near", + }, + }, + + refFinance: { + exchangeContract: { + accountId: "v2.ref-finance.near", + }, + }, + }, + + features: { + [FeatureId.DirectFtDonation]: { + id: FeatureId.DirectFtDonation, + name: "Direct FT donation", + + /** + * The implementation is not finished yet + */ + isEnabled: false, + }, + + [FeatureId.DirectNativeTokenDonation]: { + id: FeatureId.DirectNativeTokenDonation, + name: "Direct native token donation", + isEnabled: true, + }, }, }; diff --git a/src/common/_config/staging.env-config.ts b/src/common/_config/staging.env-config.ts index a696546f..c9c59fd9 100644 --- a/src/common/_config/staging.env-config.ts +++ b/src/common/_config/staging.env-config.ts @@ -8,51 +8,35 @@ export const envConfig: EnvConfig = { repoUrl: "https://github.com/PotLock/core", }, - features: { - [FeatureId.DirectFtDonation]: { - id: FeatureId.DirectFtDonation, - name: "Direct FT donation", + indexer: { + api: { endpointUrl: "https://dev.potlock.io" }, + }, - /** - * The implementation is not finished yet - */ - isEnabled: false, + core: { + donation: { + contract: { accountId: "donate.staging.potlock.near" }, }, - [FeatureId.DirectNativeTokenDonation]: { - id: FeatureId.DirectNativeTokenDonation, - name: "Direct native token donation", - isEnabled: true, + campaigns: { + contract: { accountId: "campaigns.staging.potlock.near" }, }, - }, - deFi: { - refFinance: { - exchangeContract: { - accountId: "v2.ref-finance.near", - }, + lists: { + contract: { accountId: "lists.staging.potlock.near" }, }, - }, - donation: { - contract: { accountId: "donate.staging.potlock.near" }, - }, - - campaigns: { - contract: { accountId: "campaigns.staging.potlock.near" }, - }, - - lists: { - contract: { accountId: "lists.staging.potlock.near" }, - }, + potFactory: { + contract: { accountId: "potfactory.staging.potlock.near" }, + }, - potFactory: { - contract: { accountId: "potfactory.staging.potlock.near" }, - }, + sybil: { + app: { url: "https://staging.nada.bot" }, + contract: { accountId: "v2new.staging.nadabot.near" }, + }, - sybil: { - app: { url: "https://staging.nada.bot" }, - contract: { accountId: "v2new.staging.nadabot.near" }, + voting: { + contract: { accountId: "v1.voting.potlock.near" }, + }, }, social: { @@ -60,7 +44,35 @@ export const envConfig: EnvConfig = { contract: { accountId: "social.near" }, }, - indexer: { - api: { endpointUrl: "https://dev.potlock.io" }, + deFi: { + metapool: { + liquidStakingContract: { + accountId: "meta-pool.near", + }, + }, + + refFinance: { + exchangeContract: { + accountId: "v2.ref-finance.near", + }, + }, + }, + + features: { + [FeatureId.DirectFtDonation]: { + id: FeatureId.DirectFtDonation, + name: "Direct FT donation", + + /** + * The implementation is not finished yet + */ + isEnabled: false, + }, + + [FeatureId.DirectNativeTokenDonation]: { + id: FeatureId.DirectNativeTokenDonation, + name: "Direct native token donation", + isEnabled: true, + }, }, }; diff --git a/src/common/_config/test.env-config.ts b/src/common/_config/test.env-config.ts index b0763f2d..4f6fd7ec 100644 --- a/src/common/_config/test.env-config.ts +++ b/src/common/_config/test.env-config.ts @@ -8,51 +8,35 @@ export const envConfig: EnvConfig = { repoUrl: "https://github.com/PotLock/core", }, - features: { - [FeatureId.DirectFtDonation]: { - id: FeatureId.DirectFtDonation, - name: "Direct FT donation", + indexer: { + api: { endpointUrl: "https://test-dev.potlock.io" }, + }, - /** - * The implementation is not finished yet - */ - isEnabled: false, + core: { + donation: { + contract: { accountId: "donate.potlock.testnet" }, }, - [FeatureId.DirectNativeTokenDonation]: { - id: FeatureId.DirectNativeTokenDonation, - name: "Direct native token donation", - isEnabled: true, + campaigns: { + contract: { accountId: "campaignstest2.potlock.testnet" }, }, - }, - deFi: { - refFinance: { - exchangeContract: { - accountId: "ref-finance-101.testnet", - }, + lists: { + contract: { accountId: "lists.potlock.testnet" }, }, - }, - donation: { - contract: { accountId: "donate.potlock.testnet" }, - }, - - campaigns: { - contract: { accountId: "campaignstest2.potlock.testnet" }, - }, - - lists: { - contract: { accountId: "lists.potlock.testnet" }, - }, + potFactory: { + contract: { accountId: "v1.potfactory.potlock.testnet" }, + }, - potFactory: { - contract: { accountId: "v1.potfactory.potlock.testnet" }, - }, + sybil: { + app: { url: "https://testnet.nada.bot" }, + contract: { accountId: "v1.nadabot.testnet" }, + }, - sybil: { - app: { url: "https://testnet.nada.bot" }, - contract: { accountId: "v1.nadabot.testnet" }, + voting: { + contract: { accountId: "v1.voting.potlock.testnet" }, + }, }, social: { @@ -60,7 +44,35 @@ export const envConfig: EnvConfig = { contract: { accountId: "v1.social08.testnet" }, }, - indexer: { - api: { endpointUrl: "https://test-dev.potlock.io" }, + deFi: { + metapool: { + liquidStakingContract: { + accountId: "meta-v2.pool.testnet", + }, + }, + + refFinance: { + exchangeContract: { + accountId: "ref-finance-101.testnet", + }, + }, + }, + + features: { + [FeatureId.DirectFtDonation]: { + id: FeatureId.DirectFtDonation, + name: "Direct FT donation", + + /** + * The implementation is not finished yet + */ + isEnabled: false, + }, + + [FeatureId.DirectNativeTokenDonation]: { + id: FeatureId.DirectNativeTokenDonation, + name: "Direct native token donation", + isEnabled: true, + }, }, }; diff --git a/src/common/constants.ts b/src/common/constants.ts index afdd9dab..89e87f45 100644 --- a/src/common/constants.ts +++ b/src/common/constants.ts @@ -47,6 +47,9 @@ export const TOP_LEVEL_ROOT_ACCOUNT_ID = NETWORK === "mainnet" ? "near" : "testn export const NATIVE_TOKEN_ID = "near"; export const NATIVE_TOKEN_DECIMALS = 24; +export const MPDAO_TOKEN_CONTRACT_ACCOUNT_ID = + NETWORK === "mainnet" ? "mpdao-token.near" : "mpdao-token.testnet"; + // List ID of PotLock Public Goods Registry export const PUBLIC_GOODS_REGISTRY_LIST_ID = 1; diff --git a/src/common/contracts/core/voting/abi.json b/src/common/contracts/core/voting/abi.json new file mode 100644 index 00000000..d45ac1b4 --- /dev/null +++ b/src/common/contracts/core/voting/abi.json @@ -0,0 +1,1166 @@ +{ + "schema_version": "0.4.0", + "metadata": { + "name": "voting-contract", + "version": "0.1.0", + "build": { + "compiler": "rustc 1.82.0", + "builder": "cargo-near cargo-near-build 0.3.2" + }, + "wasm_hash": "J4SFdHE3unkD7PxBHxs9bvhSn4JonKxyqsYJRj1XFjpC" + }, + "body": { + "functions": [ + { + "name": "apply", + "kind": "call", + "modifiers": [ + "payable" + ], + "params": { + "serialization_type": "json", + "args": [ + { + "name": "election_id", + "type_schema": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + ] + } + }, + { + "name": "contract_source_metadata", + "kind": "view" + }, + { + "name": "create_election", + "kind": "call", + "modifiers": [ + "payable" + ], + "params": { + "serialization_type": "json", + "args": [ + { + "name": "title", + "type_schema": { + "type": "string" + } + }, + { + "name": "description", + "type_schema": { + "type": "string" + } + }, + { + "name": "start_date", + "type_schema": { + "type": "string" + } + }, + { + "name": "end_date", + "type_schema": { + "type": "string" + } + }, + { + "name": "votes_per_voter", + "type_schema": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + { + "name": "voter_eligibility", + "type_schema": { + "$ref": "#/definitions/EligibilityType" + } + }, + { + "name": "voting_type", + "type_schema": { + "$ref": "#/definitions/VotingType" + } + }, + { + "name": "election_type", + "type_schema": { + "$ref": "#/definitions/ElectionType" + } + }, + { + "name": "candidates", + "type_schema": { + "type": "array", + "items": { + "$ref": "#/definitions/AccountId" + } + } + } + ] + }, + "result": { + "serialization_type": "json", + "type_schema": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + }, + { + "name": "eligible_voting_callback", + "kind": "call", + "modifiers": [ + "private" + ], + "params": { + "serialization_type": "json", + "args": [ + { + "name": "voter", + "type_schema": { + "$ref": "#/definitions/AccountId" + } + }, + { + "name": "election", + "type_schema": { + "$ref": "#/definitions/Election" + } + }, + { + "name": "votes", + "type_schema": { + "type": "array", + "items": [ + { + "$ref": "#/definitions/AccountId" + }, + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + ], + "maxItems": 2, + "minItems": 2 + } + } + ] + }, + "callbacks": [ + { + "serialization_type": "json", + "type_schema": { + "type": "boolean" + } + } + ], + "result": { + "serialization_type": "json", + "type_schema": { + "type": "boolean" + } + } + }, + { + "name": "get_active_elections", + "doc": " Returns all active elections (in nomination or voting period)", + "kind": "view", + "result": { + "serialization_type": "json", + "type_schema": { + "type": "array", + "items": { + "type": "array", + "items": [ + { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + { + "$ref": "#/definitions/Election" + } + ], + "maxItems": 2, + "minItems": 2 + } + } + } + }, + { + "name": "get_candidate_vote_count", + "kind": "view", + "params": { + "serialization_type": "json", + "args": [ + { + "name": "election_id", + "type_schema": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + { + "name": "candidate_id", + "type_schema": { + "$ref": "#/definitions/AccountId" + } + } + ] + }, + "result": { + "serialization_type": "json", + "type_schema": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + } + }, + { + "name": "get_candidate_vote_weight", + "doc": " Returns the total weight/points received by a candidate", + "kind": "view", + "params": { + "serialization_type": "json", + "args": [ + { + "name": "election_id", + "type_schema": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + { + "name": "candidate_id", + "type_schema": { + "$ref": "#/definitions/AccountId" + } + } + ] + }, + "result": { + "serialization_type": "json", + "type_schema": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + }, + { + "name": "get_candidate_votes", + "kind": "view", + "params": { + "serialization_type": "json", + "args": [ + { + "name": "election_id", + "type_schema": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + { + "name": "candidate_id", + "type_schema": { + "$ref": "#/definitions/AccountId" + } + } + ] + }, + "result": { + "serialization_type": "json", + "type_schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Vote" + } + } + } + }, + { + "name": "get_election", + "kind": "view", + "params": { + "serialization_type": "json", + "args": [ + { + "name": "election_id", + "type_schema": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + ] + }, + "result": { + "serialization_type": "json", + "type_schema": { + "anyOf": [ + { + "$ref": "#/definitions/Election" + }, + { + "type": "null" + } + ] + } + } + }, + { + "name": "get_election_candidates", + "kind": "view", + "params": { + "serialization_type": "json", + "args": [ + { + "name": "election_id", + "type_schema": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + ] + }, + "result": { + "serialization_type": "json", + "type_schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Candidate" + } + } + } + }, + { + "name": "get_election_phase", + "doc": " Returns the current phase of an election", + "kind": "view", + "params": { + "serialization_type": "json", + "args": [ + { + "name": "election_id", + "type_schema": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + ] + }, + "result": { + "serialization_type": "json", + "type_schema": { + "anyOf": [ + { + "$ref": "#/definitions/ElectionPhase" + }, + { + "type": "null" + } + ] + } + } + }, + { + "name": "get_election_results", + "kind": "view", + "params": { + "serialization_type": "json", + "args": [ + { + "name": "election_id", + "type_schema": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + ] + }, + "result": { + "serialization_type": "json", + "type_schema": { + "type": "array", + "items": { + "type": "array", + "items": [ + { + "$ref": "#/definitions/AccountId" + }, + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + ], + "maxItems": 2, + "minItems": 2 + } + } + } + }, + { + "name": "get_election_vote_count", + "doc": " Returns the total number of votes cast in an election", + "kind": "view", + "params": { + "serialization_type": "json", + "args": [ + { + "name": "election_id", + "type_schema": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + ] + }, + "result": { + "serialization_type": "json", + "type_schema": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + }, + { + "name": "get_election_votes", + "kind": "view", + "params": { + "serialization_type": "json", + "args": [ + { + "name": "election_id", + "type_schema": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + ] + }, + "result": { + "serialization_type": "json", + "type_schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Vote" + } + } + } + }, + { + "name": "get_elections", + "kind": "view", + "params": { + "serialization_type": "json", + "args": [ + { + "name": "from_index", + "type_schema": { + "type": [ + "integer", + "null" + ], + "format": "uint128", + "minimum": 0.0 + } + }, + { + "name": "limit", + "type_schema": { + "type": [ + "integer", + "null" + ], + "format": "uint128", + "minimum": 0.0 + } + } + ] + }, + "result": { + "serialization_type": "json", + "type_schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Election" + } + } + } + }, + { + "name": "get_elections_by_creator", + "doc": " Returns all elections created by a specific account", + "kind": "view", + "params": { + "serialization_type": "json", + "args": [ + { + "name": "creator", + "type_schema": { + "$ref": "#/definitions/AccountId" + } + } + ] + }, + "result": { + "serialization_type": "json", + "type_schema": { + "type": "array", + "items": { + "type": "array", + "items": [ + { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + { + "$ref": "#/definitions/Election" + } + ], + "maxItems": 2, + "minItems": 2 + } + } + } + }, + { + "name": "get_time_remaining", + "doc": " Returns time remaining (in nanoseconds) in the current phase", + "kind": "view", + "params": { + "serialization_type": "json", + "args": [ + { + "name": "election_id", + "type_schema": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + ] + }, + "result": { + "serialization_type": "json", + "type_schema": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + } + }, + { + "name": "get_voter_remaining_capacity", + "doc": " Returns the remaining votes/points available for a voter in an election", + "kind": "view", + "params": { + "serialization_type": "json", + "args": [ + { + "name": "election_id", + "type_schema": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + { + "name": "voter", + "type_schema": { + "$ref": "#/definitions/AccountId" + } + } + ] + }, + "result": { + "serialization_type": "json", + "type_schema": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + } + }, + { + "name": "get_voter_votes", + "doc": " Returns all votes cast by a specific voter in a given election", + "kind": "view", + "params": { + "serialization_type": "json", + "args": [ + { + "name": "election_id", + "type_schema": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + { + "name": "voter", + "type_schema": { + "$ref": "#/definitions/AccountId" + } + } + ] + }, + "result": { + "serialization_type": "json", + "type_schema": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Vote" + } + } + } + }, + { + "name": "handle_voting", + "kind": "call", + "params": { + "serialization_type": "json", + "args": [ + { + "name": "voter", + "type_schema": { + "$ref": "#/definitions/AccountId" + } + }, + { + "name": "election", + "type_schema": { + "$ref": "#/definitions/Election" + } + }, + { + "name": "votes", + "type_schema": { + "type": "array", + "items": [ + { + "$ref": "#/definitions/AccountId" + }, + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + ], + "maxItems": 2, + "minItems": 2 + } + } + ] + }, + "result": { + "serialization_type": "json", + "type_schema": { + "type": "boolean" + } + } + }, + { + "name": "has_voter_participated", + "doc": " Returns whether a voter has participated in an election", + "kind": "view", + "params": { + "serialization_type": "json", + "args": [ + { + "name": "election_id", + "type_schema": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + { + "name": "voter", + "type_schema": { + "$ref": "#/definitions/AccountId" + } + } + ] + }, + "result": { + "serialization_type": "json", + "type_schema": { + "type": "boolean" + } + } + }, + { + "name": "is_election_ended", + "doc": " Returns whether an election has ended", + "kind": "view", + "params": { + "serialization_type": "json", + "args": [ + { + "name": "election_id", + "type_schema": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + ] + }, + "result": { + "serialization_type": "json", + "type_schema": { + "type": "boolean" + } + } + }, + { + "name": "is_voting_period", + "doc": " Returns whether an election is currently in the voting period", + "kind": "view", + "params": { + "serialization_type": "json", + "args": [ + { + "name": "election_id", + "type_schema": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + ] + }, + "result": { + "serialization_type": "json", + "type_schema": { + "type": "boolean" + } + } + }, + { + "name": "new", + "kind": "call", + "modifiers": [ + "init" + ], + "params": { + "serialization_type": "json", + "args": [ + { + "name": "owner_id", + "type_schema": { + "$ref": "#/definitions/AccountId" + } + } + ] + } + }, + { + "name": "pause", + "kind": "call", + "modifiers": [ + "payable" + ] + }, + { + "name": "unpause", + "kind": "call", + "modifiers": [ + "payable" + ] + }, + { + "name": "vote", + "kind": "call", + "modifiers": [ + "payable" + ], + "params": { + "serialization_type": "json", + "args": [ + { + "name": "election_id", + "type_schema": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + { + "name": "vote", + "type_schema": { + "type": "array", + "items": [ + { + "$ref": "#/definitions/AccountId" + }, + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + ], + "maxItems": 2, + "minItems": 2 + } + } + ] + }, + "result": { + "serialization_type": "json", + "type_schema": { + "$ref": "#/definitions/PromiseOrValueBoolean" + } + } + } + ], + "root_schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "String", + "type": "string", + "definitions": { + "AccountId": { + "description": "NEAR Account Identifier.\n\nThis is a unique, syntactically valid, human-readable account identifier on the NEAR network.\n\n[See the crate-level docs for information about validation.](index.html#account-id-rules)\n\nAlso see [Error kind precedence](AccountId#error-kind-precedence).\n\n## Examples\n\n``` use near_account_id::AccountId;\n\nlet alice: AccountId = \"alice.near\".parse().unwrap();\n\nassert!(\"ƒelicia.near\".parse::().is_err()); // (ƒ is not f) ```", + "type": "string" + }, + "ApplicationStatus": { + "type": "string", + "enum": [ + "Pending", + "Approved", + "Rejected" + ] + }, + "Candidate": { + "type": "object", + "required": [ + "account_id", + "application_date", + "status", + "votes_received" + ], + "properties": { + "account_id": { + "$ref": "#/definitions/AccountId" + }, + "application_date": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/ApplicationStatus" + }, + "votes_received": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + }, + "Election": { + "type": "object", + "required": [ + "creating_project", + "description", + "election_type", + "end_date", + "id", + "owner", + "start_date", + "status", + "title", + "voter_eligibility", + "votes_per_voter", + "voting_type", + "winner_ids" + ], + "properties": { + "challenge_period_end": { + "type": [ + "string", + "null" + ] + }, + "creating_project": { + "$ref": "#/definitions/AccountId" + }, + "description": { + "type": "string" + }, + "election_type": { + "$ref": "#/definitions/ElectionType" + }, + "end_date": { + "type": "string" + }, + "id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "owner": { + "$ref": "#/definitions/AccountId" + }, + "start_date": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/ElectionStatus" + }, + "title": { + "type": "string" + }, + "voter_eligibility": { + "$ref": "#/definitions/EligibilityType" + }, + "votes_per_voter": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "voting_type": { + "$ref": "#/definitions/VotingType" + }, + "winner_ids": { + "type": "array", + "items": { + "$ref": "#/definitions/AccountId" + } + } + } + }, + "ElectionPhase": { + "type": "string", + "enum": [ + "Pending", + "Nomination", + "Voting", + "Ended" + ] + }, + "ElectionStatus": { + "type": "string", + "enum": [ + "Pending", + "NominationPeriod", + "VotingPeriod", + "ChallengePeriod", + "Completed", + "Cancelled" + ] + }, + "ElectionType": { + "oneOf": [ + { + "type": "string", + "enum": [ + "GeneralElection" + ] + }, + { + "type": "object", + "required": [ + "ProjectProposal" + ], + "properties": { + "ProjectProposal": { + "$ref": "#/definitions/AccountId" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "Pot" + ], + "properties": { + "Pot": { + "$ref": "#/definitions/AccountId" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "Custom" + ], + "properties": { + "Custom": { + "type": "array", + "items": [ + { + "type": "string" + }, + { + "anyOf": [ + { + "$ref": "#/definitions/AccountId" + }, + { + "type": "null" + } + ] + } + ], + "maxItems": 2, + "minItems": 2 + } + }, + "additionalProperties": false + } + ] + }, + "EligibilityType": { + "oneOf": [ + { + "type": "string", + "enum": [ + "Open" + ] + }, + { + "type": "object", + "required": [ + "ListBased" + ], + "properties": { + "ListBased": { + "type": "array", + "items": [ + { + "$ref": "#/definitions/AccountId" + }, + { + "type": "string" + } + ], + "maxItems": 2, + "minItems": 2 + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "TokenBased" + ], + "properties": { + "TokenBased": { + "type": "array", + "items": [ + { + "$ref": "#/definitions/AccountId" + }, + { + "type": "string" + } + ], + "maxItems": 2, + "minItems": 2 + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "Custom" + ], + "properties": { + "Custom": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + }, + "PromiseOrValueBoolean": { + "type": "boolean" + }, + "Vote": { + "type": "object", + "required": [ + "candidate_id", + "timestamp", + "voter", + "weight" + ], + "properties": { + "candidate_id": { + "$ref": "#/definitions/AccountId" + }, + "timestamp": { + "type": "string" + }, + "voter": { + "$ref": "#/definitions/AccountId" + }, + "weight": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + } + }, + "VotingType": { + "oneOf": [ + { + "type": "string", + "enum": [ + "Simple" + ] + }, + { + "type": "object", + "required": [ + "Weighted" + ], + "properties": { + "Weighted": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/src/common/contracts/core/voting/client.ts b/src/common/contracts/core/voting/client.ts new file mode 100644 index 00000000..8597d7f6 --- /dev/null +++ b/src/common/contracts/core/voting/client.ts @@ -0,0 +1,6 @@ +import { VOTING_CONTRACT_ACCOUNT_ID } from "@/common/_config"; +import { naxiosInstance } from "@/common/api/near"; + +export const contractApi = naxiosInstance.contractApi({ + contractId: VOTING_CONTRACT_ACCOUNT_ID, +}); diff --git a/src/common/contracts/core/voting/index.ts b/src/common/contracts/core/voting/index.ts new file mode 100644 index 00000000..50a4a61e --- /dev/null +++ b/src/common/contracts/core/voting/index.ts @@ -0,0 +1,5 @@ +import * as votingClient from "./client"; + +// export * from "./interface.d"; + +export { votingClient }; diff --git a/src/common/contracts/metapool/index.ts b/src/common/contracts/metapool/index.ts new file mode 100644 index 00000000..48179c04 --- /dev/null +++ b/src/common/contracts/metapool/index.ts @@ -0,0 +1,2 @@ +export * from "./liquid-staking"; +export * from "./vote"; diff --git a/src/common/contracts/metapool/liquid-staking/client.ts b/src/common/contracts/metapool/liquid-staking/client.ts new file mode 100644 index 00000000..cb3c074a --- /dev/null +++ b/src/common/contracts/metapool/liquid-staking/client.ts @@ -0,0 +1,12 @@ +import { MemoryCache } from "@wpdas/naxios"; + +import { METAPOOL_LIQUID_STAKING_CONTRACT_ACCOUNT_ID } from "@/common/_config"; +import { naxiosInstance } from "@/common/api/near"; +import { U128String } from "@/common/types"; + +export const contractApi = naxiosInstance.contractApi({ + contractId: METAPOOL_LIQUID_STAKING_CONTRACT_ACCOUNT_ID, + cache: new MemoryCache({ expirationTime: 600 }), +}); + +export const get_stnear_price = () => contractApi.view<{}, U128String>("get_stnear_price"); diff --git a/src/common/contracts/metapool/liquid-staking/index.ts b/src/common/contracts/metapool/liquid-staking/index.ts new file mode 100644 index 00000000..da2e87da --- /dev/null +++ b/src/common/contracts/metapool/liquid-staking/index.ts @@ -0,0 +1,3 @@ +import * as liquidStakingClient from "./client"; + +export { liquidStakingClient }; diff --git a/src/common/contracts/metapool/vote/constants.ts b/src/common/contracts/metapool/vote/constants.ts new file mode 100644 index 00000000..807ea9a2 --- /dev/null +++ b/src/common/contracts/metapool/vote/constants.ts @@ -0,0 +1 @@ +export const METAPOOL_MPDAO_VOTING_POWER_DECIMALS = 24; diff --git a/src/common/contracts/metapool/vote/index.ts b/src/common/contracts/metapool/vote/index.ts new file mode 100644 index 00000000..8e1cf660 --- /dev/null +++ b/src/common/contracts/metapool/vote/index.ts @@ -0,0 +1,2 @@ +export * from "./constants"; +export * from "./interface.d"; diff --git a/src/common/contracts/metapool/vote/interface.d.ts b/src/common/contracts/metapool/vote/interface.d.ts new file mode 100644 index 00000000..7f28e221 --- /dev/null +++ b/src/common/contracts/metapool/vote/interface.d.ts @@ -0,0 +1,30 @@ +import { AccountId, U128String } from "@/common/types"; + +/** + * u128 with 24 decimals (NEAR standard) + */ +export type VotingPower = U128String; + +export type Voter = { + voter_id: AccountId; + balance_in_contract: U128String; + + locking_positions: { + index: number; + amount: string; + locking_period: number; + voting_power: VotingPower; + unlocking_started_at: number | null; + is_unlocked: boolean; + is_unlocking: boolean; + is_locked: boolean; + }[]; + + voting_power: VotingPower; + + vote_positions: { + votable_address: string; + votable_object_id: string; + voting_power: VotingPower; + }[]; +}; diff --git a/src/common/lib/converters.ts b/src/common/lib/converters.ts index 0a2fcb78..62d1fce0 100644 --- a/src/common/lib/converters.ts +++ b/src/common/lib/converters.ts @@ -1,26 +1,19 @@ -import Big from "big.js"; +import { Big } from "big.js"; -import formatWithCommas from "./formatWithCommas"; import { NATIVE_TOKEN_DECIMALS } from "../constants"; +import { U128String } from "../types"; -/** - * @deprecated Use `yoctoNearToFloat` - */ -export const yoctosToNear = (amountYoctoNear: string, abbreviate?: boolean) => { - return formatWithCommas(Big(amountYoctoNear).div(1e24).toFixed(2)) + (abbreviate ? "N" : " NEAR"); -}; - -export const bigNumFromString = (amount: string, decimals: number) => +export const u128StringToBigNum = (amount: U128String, decimals: number) => Big(amount).div(Big(10).pow(decimals)); -export const bigStringToFloat = (amount: string, decimals: number) => - parseFloat(bigNumFromString(amount, decimals).toFixed(2)); +export const u128StringToFloat = (amount: U128String, decimals: number) => + parseFloat(u128StringToBigNum(amount, decimals).toFixed(2)); export const floatToBigNum = (amount: number, decimals: number) => Big(amount).mul(Big(10).pow(decimals)); -export const yoctoNearToFloat = (amountYoctoNear: string) => - bigStringToFloat(amountYoctoNear, NATIVE_TOKEN_DECIMALS); +export const yoctoNearToFloat = (amountYoctoNear: U128String) => + u128StringToFloat(amountYoctoNear, NATIVE_TOKEN_DECIMALS); -export const floatToYoctoNear = (amountFloat: number) => +export const floatToYoctoNear = (amountFloat: number): U128String => floatToBigNum(amountFloat, NATIVE_TOKEN_DECIMALS).toFixed().toString(); diff --git a/src/common/services/ft/constants.ts b/src/common/services/ft/constants.ts new file mode 100644 index 00000000..8e19731b --- /dev/null +++ b/src/common/services/ft/constants.ts @@ -0,0 +1,4 @@ +import { MPDAO_TOKEN_CONTRACT_ACCOUNT_ID } from "@/common/constants"; +import { AccountId } from "@/common/types"; + +export const MANUALLY_LISTED_ACCOUNT_IDS: AccountId[] = [MPDAO_TOKEN_CONTRACT_ACCOUNT_ID]; diff --git a/src/common/services/ft/models.ts b/src/common/services/ft/models.ts index 45e172c2..2bd6f2f0 100644 --- a/src/common/services/ft/models.ts +++ b/src/common/services/ft/models.ts @@ -5,7 +5,6 @@ import { filter, fromEntries, isError, isNonNull, merge, piped } from "remeda"; import { create } from "zustand"; import { persist } from "zustand/middleware"; -import { NETWORK } from "@/common/_config"; import { coingeckoClient } from "@/common/api/coingecko"; import { naxiosInstance, nearRpc, walletApi } from "@/common/api/near"; import { PRICES_REQUEST_CONFIG, pricesClient } from "@/common/api/prices"; @@ -16,15 +15,18 @@ import { TOP_LEVEL_ROOT_ACCOUNT_ID, } from "@/common/constants"; import { refExchangeClient } from "@/common/contracts/ref-finance"; -import { bigStringToFloat, isNetworkAccountId } from "@/common/lib"; +import { isNetworkAccountId, u128StringToBigNum, u128StringToFloat } from "@/common/lib"; import { AccountId, FungibleTokenMetadata, TokenId } from "@/common/types"; +import { MANUALLY_LISTED_ACCOUNT_IDS } from "./constants"; + export type FtRegistryEntry = { contract_account_id: TokenId; metadata: FungibleTokenMetadata; balance?: Big.Big; balanceFloat?: number; - balanceUsdApproximation?: string | null; + balanceUsd?: Big.Big; + balanceUsdStringApproximation?: string; usdPrice?: Big.Big; }; @@ -79,7 +81,7 @@ export const useFtRegistryStore = create()( finality: "final", }) .then(async ({ amount }) => { - const balanceFloat = bigStringToFloat( + const balanceFloat = u128StringToFloat( amount, NATIVE_TOKEN_PSEUDO_FT_REGISTRY_ENTRY.metadata.decimals, ); @@ -98,73 +100,82 @@ export const useFtRegistryStore = create()( ...NATIVE_TOKEN_PSEUDO_FT_REGISTRY_ENTRY, balance, balanceFloat, - balanceUsdApproximation: usdPrice?.mul(balance).toFixed(2), + balanceUsdStringApproximation: usdPrice?.mul(balance).toFixed(2), usdPrice, }, ] as [TokenId, FtRegistryEntry]; }), - ...tokenContractAccountIds.map(async (contract_account_id) => { - const ftClient = naxiosInstance.contractApi({ - contractId: contract_account_id, - cache: new MemoryCache({ expirationTime: 600 }), - }); - - const metadata = await ftClient - .view<{}, FungibleTokenMetadata>("ft_metadata") - .catch(() => undefined); - - const [balanceRaw, usdPrice] = - metadata === undefined - ? [undefined, undefined] - : await Promise.all([ - ftClient - .view<{ account_id: AccountId }, string>( - "ft_balance_of", - - { - args: { - account_id: walletApi.accountId ?? "unknown", + ...MANUALLY_LISTED_ACCOUNT_IDS.concat(tokenContractAccountIds).map( + async (contract_account_id) => { + const ftClient = naxiosInstance.contractApi({ + contractId: contract_account_id, + cache: new MemoryCache({ expirationTime: 600 }), + }); + + const metadata = await ftClient + .view<{}, FungibleTokenMetadata>("ft_metadata") + .catch(() => undefined); + + const [balanceRaw, usdPrice] = + metadata === undefined + ? [undefined, undefined] + : await Promise.all([ + ftClient + .view<{ account_id: AccountId }, string>( + "ft_balance_of", + + { + args: { + account_id: walletApi.accountId ?? "unknown", + }, }, - }, - ) - .catch(() => undefined), - - pricesClient - .getSuperPrecisePrice( - { token_id: contract_account_id }, - PRICES_REQUEST_CONFIG.axios, - ) - .then(({ data }) => Big(data)) - .catch(() => undefined), - ]); - - const balanceFloat = - metadata === undefined || balanceRaw === undefined - ? undefined - : bigStringToFloat(balanceRaw, metadata.decimals); - - const balance = balanceFloat ? Big(balanceFloat) : undefined; - - return metadata === undefined - ? null - : ([ - contract_account_id, - { + ) + .catch(() => undefined), + + pricesClient + .getSuperPrecisePrice( + { token_id: contract_account_id }, + PRICES_REQUEST_CONFIG.axios, + ) + .then(({ data }) => Big(data)) + .catch(() => undefined), + ]); + + const balance = + metadata === undefined || balanceRaw === undefined + ? undefined + : u128StringToBigNum(balanceRaw, metadata.decimals); + + const balanceFloat = + metadata === undefined || balanceRaw === undefined + ? undefined + : u128StringToFloat(balanceRaw, metadata.decimals); + + const balanceUsd = + balance?.gt(0) && usdPrice?.gt(0) ? balance?.mul(usdPrice) : Big(0); + + return metadata === undefined + ? null + : ([ contract_account_id, - metadata, - balance, - balanceFloat, - - balanceUsdApproximation: - balance?.gt(0) && usdPrice?.gt(0) - ? `~$ ${usdPrice.mul(balance).toFixed(2)}` - : null, - - usdPrice, - }, - ] as [TokenId, FtRegistryEntry]); - }), + { + contract_account_id, + metadata, + balance, + balanceFloat, + balanceUsd, + + balanceUsdStringApproximation: + balance?.gt(0) && usdPrice?.gt(0) + ? `~$ ${usdPrice.mul(balance).toFixed(2)}` + : "$ 0", + + usdPrice, + }, + ] as [TokenId, FtRegistryEntry]); + }, + ), ]).then( piped( filter(isNonNull), diff --git a/src/common/types.ts b/src/common/types.ts index 8ff62be2..a8868703 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -39,26 +39,37 @@ export type EnvConfig = { network: Network; contractMetadata: { version: string; repoUrl: string }; indexer: { api: { endpointUrl: string } }; - features: FeatureRegistry; + + core: { + campaigns: { contract: { accountId: string } }; + donation: { contract: ContractConfig }; + lists: { contract: ContractConfig }; + potFactory: { contract: ContractConfig }; + sybil: { app: { url: string }; contract: ContractConfig }; + voting: { contract: ContractConfig }; + }; + + social: { app: { url: string }; contract: ContractConfig }; deFi: { + metapool: { + liquidStakingContract: ContractConfig; + }; + refFinance: { exchangeContract: ContractConfig; }; }; - campaigns: { contract: { accountId: string } }; - donation: { contract: ContractConfig }; - lists: { contract: ContractConfig }; - potFactory: { contract: ContractConfig }; - sybil: { app: { url: string }; contract: ContractConfig }; - social: { app: { url: string }; contract: ContractConfig }; + features: FeatureRegistry; }; export type { infer as FromSchema } from "zod"; export type UnionFromStringList = ListOfMembers[number]; +export type U128String = string; + export type ClientConfig = { swr?: SWRConfiguration }; export interface ConditionalExecution { diff --git a/src/common/ui/components/molecules/checklist.tsx b/src/common/ui/components/molecules/checklist.tsx index 426eb8b5..b0a7386f 100644 --- a/src/common/ui/components/molecules/checklist.tsx +++ b/src/common/ui/components/molecules/checklist.tsx @@ -11,11 +11,17 @@ import { cn } from "../../utils"; export type ChecklistProps = { title: string; - breakdown: BasicRequirement[]; + requirements: BasicRequirement[]; isFinalized?: boolean; + error?: Error; }; -export const Checklist: React.FC = ({ title, breakdown, isFinalized = true }) => ( +export const Checklist: React.FC = ({ + title, + requirements, + isFinalized = true, + error, +}) => (
= ({ title, breakdown, isFinali
-
- {breakdown.map(({ title, isSatisfied }) => ( -
- {isSatisfied ? ( - - ) : ( - <> - {isFinalized ? ( - - ) : ( - - )} - - )} + {error ? ( + {error.message} + ) : ( +
    + {requirements.map(({ title, isSatisfied }) => ( +
  • + {isSatisfied ? ( + + ) : ( + <> + {isFinalized ? ( + + ) : ( + + )} + + )} - {title} -
- ))} -
+ {title} + + ))} + + )} ); diff --git a/src/modules/access-control/index.ts b/src/modules/access-control/index.ts index 1465f0dc..8f17729f 100644 --- a/src/modules/access-control/index.ts +++ b/src/modules/access-control/index.ts @@ -1 +1,2 @@ export * from "./components/AccessControlList"; +export * from "./types"; diff --git a/src/modules/access-control/types.ts b/src/modules/access-control/types.ts new file mode 100644 index 00000000..b63c9fd4 --- /dev/null +++ b/src/modules/access-control/types.ts @@ -0,0 +1,5 @@ +import { BasicRequirement } from "@/common/types"; + +export type AccessControlClearanceCheckResult = + | { requirements: BasicRequirement[]; isEveryRequirementSatisfied: boolean; error: null } + | { requirements: null; isEveryRequirementSatisfied: false; error: Error }; diff --git a/src/modules/donation/components/DonationSuccess.tsx b/src/modules/donation/components/DonationSuccess.tsx index c8c82953..386672a3 100644 --- a/src/modules/donation/components/DonationSuccess.tsx +++ b/src/modules/donation/components/DonationSuccess.tsx @@ -13,7 +13,7 @@ import { POTLOCK_TWITTER_ACCOUNT_ID, } from "@/common/constants"; import { DirectDonation, PotDonation } from "@/common/contracts/core"; -import { bigStringToFloat, truncate } from "@/common/lib"; +import { truncate, u128StringToFloat } from "@/common/lib"; import { ftService } from "@/common/services"; import { Button, @@ -59,17 +59,17 @@ export const DonationSuccess = ({ form, transactionHash, closeModal }: DonationS const isLoading = isResultLoading || recipient === undefined || token === undefined; - const totalAmountFloat = bigStringToFloat( + const totalAmountFloat = u128StringToFloat( finalOutcome?.total_amount ?? "0", token?.metadata.decimals ?? NATIVE_TOKEN_DECIMALS, ); - const protocolFeeAmountFloat = bigStringToFloat( + const protocolFeeAmountFloat = u128StringToFloat( finalOutcome?.protocol_fee ?? "0", token?.metadata.decimals ?? NATIVE_TOKEN_DECIMALS, ); - const referralFeeFinalAmountFloat = bigStringToFloat( + const referralFeeFinalAmountFloat = u128StringToFloat( finalOutcome?.referrer_fee ?? "0", token?.metadata.decimals ?? NATIVE_TOKEN_DECIMALS, ); diff --git a/src/modules/pot/components/PotCard.tsx b/src/modules/pot/components/PotCard.tsx index abf8b92b..c3626fb7 100644 --- a/src/modules/pot/components/PotCard.tsx +++ b/src/modules/pot/components/PotCard.tsx @@ -5,7 +5,7 @@ import routesPath from "@/modules/core/routes"; import Indicator from "./Indicator"; import { PotTag } from "./PotTag"; -import useNearAndUsdByPot from "../hooks/useNearAndUsdByPot"; +import { useNearAndUsdByPot } from "../hooks/useNearAndUsdByPot"; import getPotTags from "../utils/getPotTags"; export type PotCardProps = { diff --git a/src/modules/pot/components/PotHero.tsx b/src/modules/pot/components/PotHero.tsx index 985b1cf0..b6d9ac7c 100644 --- a/src/modules/pot/components/PotHero.tsx +++ b/src/modules/pot/components/PotHero.tsx @@ -21,22 +21,20 @@ import NewApplicationModal from "./NewApplicationModal"; import { PotStats } from "./PotStats"; import { PotTimeline } from "./PotTimeline"; import { - usePotUserApplicationRequirements, + usePotUserApplicationClearance, usePotUserPermissions, - usePotUserVotingRequirements, + usePotUserVotingClearance, } from "../hooks/clearance"; import { isPotVotingBased } from "../utils/voting"; export type PotHeroProps = ByPotId & {}; export const PotHero: React.FC = ({ potId }) => { - const router = useRouter(); - const isOnVotingPage = router.pathname.includes("votes"); const { data: pot } = indexer.usePot({ potId }); const isVotingBasedPot = isPotVotingBased({ potId }); const { isSignedIn, accountId } = useAuthSession(); - const applicationClearanceBreakdown = usePotUserApplicationRequirements(); - const votingClearanceBreakdown = usePotUserVotingRequirements(); + const applicationClearance = usePotUserApplicationClearance({ potId }); + const votingClearance = usePotUserVotingClearance({ potId }); const { canApply, canDonate, canFund, canChallengePayouts, existingChallengeForUser } = usePotUserPermissions({ potId }); @@ -62,6 +60,13 @@ export const PotHero: React.FC = ({ potId }) => { } else return [pot?.description ?? null, null]; }, [pot?.description]); + // TODO: Implement proper voting stage check + const isVotingRoundOngoing = useMemo(() => { + if (!pot) return false; + + return false; + }, [pot]); + return ( <> {pot && ( @@ -156,12 +161,15 @@ export const PotHero: React.FC = ({ potId }) => {
{isVotingBasedPot ? ( <> - {isOnVotingPage ? ( - + {isVotingRoundOngoing ? ( + ) : ( )} diff --git a/src/modules/pot/components/PotTimeline.tsx b/src/modules/pot/components/PotTimeline.tsx index ec5eb61b..51f44e06 100644 --- a/src/modules/pot/components/PotTimeline.tsx +++ b/src/modules/pot/components/PotTimeline.tsx @@ -9,11 +9,12 @@ import { cn } from "@/common/ui/utils"; import { PotTimelineFragment } from "./PotTimelineFragment"; import { usePotLifecycle } from "../hooks/lifecycle"; +const containerHeight = 181; + /** * @deprecated convert to div with Tailwind classes */ const Container = styled.div<{ - containerHeight: number; showActiveState: number; }>` display: flex; @@ -31,7 +32,7 @@ const Container = styled.div<{ @media only screen and (max-width: 1280px) { justify-content: left; - height: ${(props) => props.containerHeight / 4}px; + height: ${containerHeight / 4}px; overflow: hidden; .mobile-selected { @@ -89,7 +90,6 @@ export const PotTimeline: React.FC = ({ potId, classNames }) = return index; }; - const containerHeight = 181; const showActiveState = getIndexOfActive() * (containerHeight / 4); return ( @@ -102,9 +102,8 @@ export const PotTimeline: React.FC = ({ potId, classNames }) = )} >
{ const { isSignedIn, accountId } = useAuthSession(); const { data: pot } = indexer.usePot({ potId }); @@ -40,14 +49,17 @@ export const usePotUserPermissions = ({ potId }: ByPotId) => { const canDonate = useMemo(() => publicRoundOpen && accountId, [publicRoundOpen, accountId]); const canFund = useMemo(() => pot && now < getDateTime(pot.matching_round_end), [pot, now]); - const userIsAdminOrGreater = useMemo( - () => pot?.admins.find((adm) => adm.id === accountId) || pot?.owner.id === accountId, + const isAdminOrGreater = useMemo( + () => + pot?.admins.find(({ id: adminAccountId }) => adminAccountId === accountId) || + pot?.owner.id === accountId, + [pot, accountId], ); - const userIsChefOrGreater = useMemo( - () => userIsAdminOrGreater || pot?.chef?.id === accountId, - [userIsAdminOrGreater, pot, accountId], + const isChefOrGreater = useMemo( + () => isAdminOrGreater || pot?.chef?.id === accountId, + [isAdminOrGreater, pot, accountId], ); const applicationOpen = useMemo( @@ -58,8 +70,8 @@ export const usePotUserPermissions = ({ potId }: ByPotId) => { ); const canApply = useMemo( - () => applicationOpen && existingApplication === undefined && !userIsChefOrGreater, - [applicationOpen, existingApplication, userIsChefOrGreater], + () => applicationOpen && existingApplication === undefined && !isChefOrGreater, + [applicationOpen, existingApplication, isChefOrGreater], ); const canChallengePayouts = useMemo( @@ -91,38 +103,90 @@ export const usePotUserPermissions = ({ potId }: ByPotId) => { // TODO: refactor to support multi-mechanism for the V2 milestone /** - *! Heads up! At the moment, this hook only covers one specific use case, - *! as it's built for the mpDAO milestone. + * Heads up! At the moment, this hook only covers one specific use case, + * as it's built for the mpDAO milestone. */ -export const usePotUserApplicationRequirements = (): BasicRequirement[] => { - const { accountId, isVerifiedPublicGoodsProvider } = useAuthSession(); +export const usePotUserApplicationClearance = ({ + potId, +}: ByPotId): AccessControlClearanceCheckResult => { + const { accountId: _, isVerifiedPublicGoodsProvider } = useAuthSession(); + const isVotingBasedPot = isPotVotingBased({ potId }); - // TODO!: calculate this for fox sake - const metaPoolDaoRpgfScore = 0; + const { data: stNear } = ftService.useRegisteredToken({ + tokenId: METAPOOL_LIQUID_STAKING_CONTRACT_ACCOUNT_ID, + }); - return [ - { title: "Verified Project on Potlock", isSatisfied: isVerifiedPublicGoodsProvider }, - { title: "A minimum stake of 500 USD in Meta Pool", isSatisfied: false }, - { title: "A minimum of 50,000 votes", isSatisfied: false }, + // TODO: Get voting power from the snapshot + const votingPowerU128StringMock = "0"; - { - title: "A total of 25 points accumulated for the RPGF score", - isSatisfied: metaPoolDaoRpgfScore >= 25, - }, - ]; + // TODO: calculate this + const metaPoolDaoRpgfScore = 0; + + return useMemo(() => { + const requirements = [ + { title: "Verified Project on Potlock", isSatisfied: isVerifiedPublicGoodsProvider }, + + ...(isVotingBasedPot + ? [ + { + title: "An equivalent of 25 USD staked in NEAR on Meta Pool", + isSatisfied: stNear?.balanceUsd?.gte(25) ?? false, + }, + + { + title: "Voting power 5000 or more", + isSatisfied: u128StringToBigNum( + votingPowerU128StringMock, + METAPOOL_MPDAO_VOTING_POWER_DECIMALS, + ).gte(5000), + }, + + { + title: "A total of 10 points accumulated for the RPGF score", + isSatisfied: metaPoolDaoRpgfScore >= 10, + }, + ] + : []), + ]; + + return { + requirements, + isEveryRequirementSatisfied: requirements.every(prop("isSatisfied")), + error: null, + }; + }, [isVerifiedPublicGoodsProvider, isVotingBasedPot, stNear?.balanceUsd]); }; // TODO: refactor to support multi-mechanism for the V2 milestone /** - *! Heads up! At the moment, this hook only covers one specific use case, - *! as it's built for the mpDAO milestone. + * Heads up! At the moment, this hook only covers one specific use case, + * as it's built for the mpDAO milestone. */ -export const usePotUserVotingRequirements = (): BasicRequirement[] => { - const { accountId, account } = useAuthSession(); +export const usePotUserVotingClearance = ({ + potId, +}: ByPotId): AccessControlClearanceCheckResult => { + const { accountId, isVerifiedPublicGoodsProvider } = useAuthSession(); const { nadaBotVerified: isHuman } = useIsHuman(accountId); - - return [ - { title: "Must have an account on Potlock.", isSatisfied: account !== undefined }, - { title: "Must have human verification.", isSatisfied: isHuman }, - ]; + const isVotingBasedPot = isPotVotingBased({ potId }); + + return useMemo(() => { + if (!isVotingBasedPot) { + return { + requirements: null, + isEveryRequirementSatisfied: false, + error: new Error("This pot doesn't support voting mechanisms."), + }; + } else { + const requirements = [ + { title: "Must have an account on Potlock.", isSatisfied: isVerifiedPublicGoodsProvider }, + { title: "Must have human verification.", isSatisfied: isHuman }, + ]; + + return { + requirements, + isEveryRequirementSatisfied: requirements.every(prop("isSatisfied")), + error: null, + }; + } + }, [isHuman, isVerifiedPublicGoodsProvider, isVotingBasedPot]); }; diff --git a/src/modules/pot/hooks/index.ts b/src/modules/pot/hooks/index.ts index 7fa1274f..4672df97 100644 --- a/src/modules/pot/hooks/index.ts +++ b/src/modules/pot/hooks/index.ts @@ -1,5 +1,5 @@ export * from "./forms"; export { default as useFilteredPots } from "./useFilteredPots"; -export { default as useNearAndUsdByPot } from "./useNearAndUsdByPot"; +export { useNearAndUsdByPot } from "./useNearAndUsdByPot"; export * from "./useOrderedDonations"; export { useProtocolConfig } from "./useProtocolConfig"; diff --git a/src/modules/pot/hooks/lifecycle.ts b/src/modules/pot/hooks/lifecycle.ts index 47b76dd5..8a7dd316 100644 --- a/src/modules/pot/hooks/lifecycle.ts +++ b/src/modules/pot/hooks/lifecycle.ts @@ -72,5 +72,17 @@ export const usePotLifecycle = ({ potId }: PotLifecycleCalculationInputs) => { } else return []; }, [isVotingEnabled, now, pot]); + // const getActiveStageIndex = () => { + // let index = 0; + // + // stages.forEach((status, idx) => { + // if (status.started && !status.completed) { + // index = idx; + // } + // }); + // + // return index === null ? 3 : index; + // }; + return stages; }; diff --git a/src/modules/pot/hooks/useNearAndUsdByPot.ts b/src/modules/pot/hooks/useNearAndUsdByPot.ts index 45cc4cce..99e9a578 100644 --- a/src/modules/pot/hooks/useNearAndUsdByPot.ts +++ b/src/modules/pot/hooks/useNearAndUsdByPot.ts @@ -1,24 +1,32 @@ import { useEffect, useState } from "react"; +import { Big } from "big.js"; import { pipe } from "remeda"; import { Pot } from "@/common/api/indexer"; -import { yoctosToNear } from "@/common/lib"; +import { formatWithCommas } from "@/common/lib"; import { yoctosToUsdWithFallback } from "@/modules/core"; /** - * @deprecated use `ftService` + * @deprecated Use `yoctoNearToFloat` */ -const useNearAndUsdByPot = ({ pot }: { pot?: Pot }) => { +const yoctoNearToNear = (amountYoctoNear: string, abbreviate?: boolean) => { + return formatWithCommas(Big(amountYoctoNear).div(1e24).toFixed(2)) + (abbreviate ? "N" : " NEAR"); +}; + +/** + * @deprecated use `ftService` capabilities. + */ +export const useNearAndUsdByPot = ({ pot }: { pot?: Pot }) => { const [amountNear, setAmountNear] = useState( - pot ? yoctosToNear(pot.matching_pool_balance, true) : undefined, + pot ? yoctoNearToNear(pot.matching_pool_balance, true) : undefined, ); const [amountUsd, setAmountUsd] = useState("-"); useEffect(() => { if (pot) { pipe(pot.matching_pool_balance, yoctosToUsdWithFallback, setAmountUsd); - setAmountNear(yoctosToNear(pot.matching_pool_balance, true)); + setAmountNear(yoctoNearToNear(pot.matching_pool_balance, true)); } }, [pot]); @@ -27,5 +35,3 @@ const useNearAndUsdByPot = ({ pot }: { pot?: Pot }) => { amountUsd, }; }; - -export default useNearAndUsdByPot; diff --git a/src/modules/pot/hooks/voting.ts b/src/modules/pot/hooks/voting.ts new file mode 100644 index 00000000..ed1a1b44 --- /dev/null +++ b/src/modules/pot/hooks/voting.ts @@ -0,0 +1,55 @@ +import { ByPotId } from "@/common/api/indexer"; +import { METAPOOL_MPDAO_VOTING_POWER_DECIMALS, Voter } from "@/common/contracts/metapool"; +import { useAuthSession } from "@/modules/auth"; + +import { isPotVotingBased } from "../utils/voting"; + +export const POT_MPDAO_VOTER_MOCK: Voter = { + voter_id: "lucascasp.near", + balance_in_contract: "0", + + locking_positions: [ + { + index: 0, + amount: "1447929400", + locking_period: 300, + voting_power: "7239647000000000000000000000", + unlocking_started_at: null, + is_unlocked: false, + is_unlocking: false, + is_locked: true, + }, + ], + + voting_power: "0", + + vote_positions: [ + { + votable_address: "metastaking.app", + votable_object_id: "luganodes.pool.near", + voting_power: "7239647000000000000000000000", + }, + ], +}; + +/** + * Heads up! At the moment, this hook only covers one specific use case, + * as it's built for the mpDAO milestone. + * + * - Human-verified Users: Votes are weighted at 10% for verified users [KYC]. + * + * - mpDAO Governance Participants: Users with at least 10,000 votes in mpDAO governance + * receive an additional 25% vote weight. Those with 25,000 votes gain another 25%. + * + * - stNEAR Stakeholders: Users with 2 stNEAR staked in Meta Pool receive a 10% boost, + * and those with 10 stNEAR staked gain a 30% vote weight increase. + * + * https://docs.google.com/document/d/1P5iSBBSuh7nep29r7N3S-g4Y1bDbF4xLU_3v7XHmJR8 + */ +export const usePotUserVoteWeight = ({ potId }: ByPotId) => { + const { accountId } = useAuthSession(); + const isVotingBasedPot = isPotVotingBased({ potId }); + + // TODO: calculate voting amplifiers + METAPOOL_MPDAO_VOTING_POWER_DECIMALS; +}; diff --git a/src/modules/token/components/TokenTotalValue.tsx b/src/modules/token/components/TokenTotalValue.tsx index d62cc1dd..1ee1d549 100644 --- a/src/modules/token/components/TokenTotalValue.tsx +++ b/src/modules/token/components/TokenTotalValue.tsx @@ -1,5 +1,5 @@ import { NATIVE_TOKEN_DECIMALS } from "@/common/constants"; -import { bigStringToFloat } from "@/common/lib"; +import { u128StringToFloat } from "@/common/lib"; import { ftService } from "@/common/services"; import { ByTokenId } from "@/common/types"; import { cn } from "@/common/ui/utils"; @@ -23,7 +23,7 @@ export const TokenTotalValue: React.FC = ({ const amount = "amountFloat" in props ? props.amountFloat - : bigStringToFloat(props.amountBigString, token?.metadata.decimals ?? NATIVE_TOKEN_DECIMALS); + : u128StringToFloat(props.amountBigString, token?.metadata.decimals ?? NATIVE_TOKEN_DECIMALS); const amountUsd = token?.usdPrice?.gt(0) ? token?.usdPrice?.mul(amount).toFixed(2) : null;