From ea032bd24ee295fadb88a7328e091936737d574f Mon Sep 17 00:00:00 2001 From: Noah Saso Date: Wed, 6 Mar 2024 23:39:57 -0800 Subject: [PATCH 1/4] fixed oraichain cw20-stake queries --- .../recoil/selectors/contracts/Cw20Stake.ts | 176 ++++++++++++++---- 1 file changed, 144 insertions(+), 32 deletions(-) diff --git a/packages/state/recoil/selectors/contracts/Cw20Stake.ts b/packages/state/recoil/selectors/contracts/Cw20Stake.ts index 2a7962e747..cc16fb21f4 100644 --- a/packages/state/recoil/selectors/contracts/Cw20Stake.ts +++ b/packages/state/recoil/selectors/contracts/Cw20Stake.ts @@ -23,6 +23,8 @@ import { } from '../../atoms' import { cosmWasmClientForChainSelector } from '../chain' import { queryContractIndexerSelector } from '../indexer' +import { objectMatchesStructure } from '@dao-dao/utils' +import { toUtf8 } from '@cosmjs/encoding' type QueryClientParams = WithChainId<{ contractAddress: string @@ -72,21 +74,26 @@ export const stakedBalanceAtHeightSelector = selectorFamily< async ({ get }) => { const id = get(refreshWalletBalancesIdAtom(params[0].address)) - const balance = get( - queryContractIndexerSelector({ - ...queryClientParams, - formula: 'cw20Stake/stakedBalance', - args: { - address: params[0].address, - }, - block: params[0].height ? { height: params[0].height } : undefined, - id, - }) + const isOraichainProxy = get( + isOraichainProxySnapshotContractSelector(queryClientParams) ) - if (balance && !isNaN(balance)) { - return { - balance, - height: params[0].height, + if (!isOraichainProxy) { + const balance = get( + queryContractIndexerSelector({ + ...queryClientParams, + formula: 'cw20Stake/stakedBalance', + args: { + address: params[0].address, + }, + block: params[0].height ? { height: params[0].height } : undefined, + id, + }) + ) + if (balance && !isNaN(balance)) { + return { + balance, + height: params[0].height, + } } } @@ -107,18 +114,23 @@ export const totalStakedAtHeightSelector = selectorFamily< async ({ get }) => { const id = get(refreshWalletBalancesIdAtom(undefined)) - const total = get( - queryContractIndexerSelector({ - ...queryClientParams, - formula: 'cw20Stake/totalStaked', - block: params[0].height ? { height: params[0].height } : undefined, - id, - }) + const isOraichainProxy = get( + isOraichainProxySnapshotContractSelector(queryClientParams) ) - if (total && !isNaN(total)) { - return { - total, - height: params[0].height, + if (!isOraichainProxy) { + const total = get( + queryContractIndexerSelector({ + ...queryClientParams, + formula: 'cw20Stake/totalStaked', + block: params[0].height ? { height: params[0].height } : undefined, + id, + }) + ) + if (total && !isNaN(total)) { + return { + total, + height: params[0].height, + } } } @@ -139,6 +151,21 @@ export const stakedValueSelector = selectorFamily< async ({ get }) => { const id = get(refreshWalletBalancesIdAtom(params[0].address)) + const isOraichainProxy = get( + isOraichainProxySnapshotContractSelector(queryClientParams) + ) + if (isOraichainProxy) { + const { balance } = get( + stakedBalanceAtHeightSelector({ + ...queryClientParams, + params, + }) + ) + return { + value: balance, + } + } + const value = get( queryContractIndexerSelector({ ...queryClientParams, @@ -168,6 +195,21 @@ export const totalValueSelector = selectorFamily< async ({ get }) => { const id = get(refreshWalletBalancesIdAtom(undefined)) + const isOraichainProxy = get( + isOraichainProxySnapshotContractSelector(queryClientParams) + ) + if (isOraichainProxy) { + const { total } = get( + totalStakedAtHeightSelector({ + ...queryClientParams, + params: [{}], + }) + ) + return { + total, + } + } + const total = get( queryContractIndexerSelector({ ...queryClientParams, @@ -194,14 +236,25 @@ export const getConfigSelector = selectorFamily< get: ({ params, ...queryClientParams }) => async ({ get }) => { - const config = get( - queryContractIndexerSelector({ - ...queryClientParams, - formula: 'cw20Stake/config', - }) + const isOraichainProxy = get( + isOraichainProxySnapshotContractSelector(queryClientParams) ) - if (config) { - return config + + // The Oraichain cw20-staking proxy-snapshot contract is used as the + // staking contract for their custom staking solution. If this is the + // case, ignore the indexer response and use the contract query since it + // executes a passthrough query and returns the correct config. + if (!isOraichainProxy) { + const config = get( + queryContractIndexerSelector({ + ...queryClientParams, + formula: 'cw20Stake/config', + }) + ) + + if (config) { + return config + } } // If indexer query fails, fallback to contract query. @@ -221,6 +274,14 @@ export const claimsSelector = selectorFamily< async ({ get }) => { const id = get(refreshClaimsIdAtom(params[0].address)) + // Oraichain has their own interface. + const isOraichainProxy = get( + isOraichainProxySnapshotContractSelector(queryClientParams) + ) + if (isOraichainProxy) { + return { claims: [] } + } + const claims = get( queryContractIndexerSelector({ ...queryClientParams, @@ -262,6 +323,14 @@ export const listStakersSelector = selectorFamily< get: ({ params, ...queryClientParams }) => async ({ get }) => { + // Oraichain has their own interface. + const isOraichainProxy = get( + isOraichainProxySnapshotContractSelector(queryClientParams) + ) + if (isOraichainProxy) { + return { stakers: [] } + } + const list = get( queryContractIndexerSelector({ ...queryClientParams, @@ -303,3 +372,46 @@ export const topStakersSelector = selectorFamily< }) ) ?? undefined, }) + +/** + * The Oraichain cw20-staking proxy-snapshot contract is used as the staking + * contract for their custom staking solution. This selector returns whether or + * not this is a proxy-snapshot contract. + */ +export const isOraichainProxySnapshotContractSelector = selectorFamily< + boolean, + QueryClientParams +>({ + key: 'cw20StakeIsOraichainProxySnapshotContract', + get: + (queryClientParams) => + async ({ get }) => { + let config = get( + queryContractIndexerSelector({ + ...queryClientParams, + formula: 'cw20Stake/config', + }) + ) + + if (!config) { + // If indexer fails, fallback to querying chain. + const client = get( + cosmWasmClientForChainSelector(queryClientParams.chainId) + ) + config = await client.queryContractRaw( + queryClientParams.contractAddress, + toUtf8('config') + ) + } + + if (!config) { + throw new Error('No config found') + } + + return objectMatchesStructure(config, { + owner: {}, + asset_key: {}, + staking_contract: {}, + }) + }, +}) From beeb7fc93ed07768f227d370b5c41016e4b72e12 Mon Sep 17 00:00:00 2001 From: Noah Saso Date: Thu, 7 Mar 2024 01:47:18 -0800 Subject: [PATCH 2/4] support oraichain custom staking contracts --- .../state/contracts/OraichainCw20Staking.ts | 565 ++++++++++++++++++ packages/state/contracts/index.ts | 4 + .../recoil/selectors/contracts/Cw20Stake.ts | 216 ++++--- .../contracts/OraichainCw20Staking.ts | 260 ++++++++ .../state/recoil/selectors/contracts/index.ts | 1 + .../hooks/contracts/OraichainCw20Staking.ts | 50 ++ packages/stateful/hooks/contracts/index.ts | 1 + .../components/ProfileCardMemberInfo.tsx | 60 +- .../components/StakingModal.tsx | 100 +++- .../ProfileCardMemberInfoTokens.tsx | 7 +- .../components/token/UnstakingModal.tsx | 117 ++-- .../components/token/UnstakingStatus.tsx | 2 +- .../types/contracts/OraichainCw20Staking.ts | 183 ++++++ .../OraichainCw20StakingProxySnapshot.ts | 63 ++ packages/utils/constants/contracts.ts | 2 + 15 files changed, 1471 insertions(+), 160 deletions(-) create mode 100644 packages/state/contracts/OraichainCw20Staking.ts create mode 100644 packages/state/recoil/selectors/contracts/OraichainCw20Staking.ts create mode 100644 packages/stateful/hooks/contracts/OraichainCw20Staking.ts create mode 100644 packages/types/contracts/OraichainCw20Staking.ts create mode 100644 packages/types/contracts/OraichainCw20StakingProxySnapshot.ts diff --git a/packages/state/contracts/OraichainCw20Staking.ts b/packages/state/contracts/OraichainCw20Staking.ts new file mode 100644 index 0000000000..a39b899be8 --- /dev/null +++ b/packages/state/contracts/OraichainCw20Staking.ts @@ -0,0 +1,565 @@ +/** + * This file was automatically generated by @cosmwasm/ts-codegen@0.35.7. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run the @cosmwasm/ts-codegen generate command to regenerate this file. + */ + +import { Coin, StdFee } from '@cosmjs/amino' +import { + CosmWasmClient, + ExecuteResult, + SigningCosmWasmClient, +} from '@cosmjs/cosmwasm-stargate' + +import { + Addr, + ArrayOfQueryPoolInfoResponse, + ArrayOfRewardInfoResponse, + Asset, + Binary, + ConfigResponse, + LockInfosResponse, + PoolInfoResponse, + RewardInfoResponse, + RewardMsg, + RewardsPerSecResponse, + StakedBalanceAtHeightResponse, + TotalStakedAtHeightResponse, + Uint128, +} from '@dao-dao/types/contracts/OraichainCw20Staking' + +export interface OraichainCw20StakingReadOnlyInterface { + contractAddress: string + config: () => Promise + poolInfo: ({ + stakingToken, + }: { + stakingToken: Addr + }) => Promise + rewardsPerSec: ({ + stakingToken, + }: { + stakingToken: Addr + }) => Promise + rewardInfo: ({ + stakerAddr, + stakingToken, + }: { + stakerAddr: Addr + stakingToken?: Addr + }) => Promise + rewardInfos: ({ + limit, + order, + stakingToken, + startAfter, + }: { + limit?: number + order?: number + stakingToken: Addr + startAfter?: Addr + }) => Promise + getPoolsInformation: () => Promise + lockInfos: ({ + limit, + order, + stakerAddr, + stakingToken, + startAfter, + }: { + limit?: number + order?: number + stakerAddr: Addr + stakingToken: Addr + startAfter?: number + }) => Promise + stakedBalanceAtHeight: ({ + address, + assetKey, + height, + }: { + address: string + assetKey: Addr + height?: number + }) => Promise + totalStakedAtHeight: ({ + assetKey, + height, + }: { + assetKey: Addr + height?: number + }) => Promise +} +export class OraichainCw20StakingQueryClient + implements OraichainCw20StakingReadOnlyInterface +{ + client: CosmWasmClient + contractAddress: string + + constructor(client: CosmWasmClient, contractAddress: string) { + this.client = client + this.contractAddress = contractAddress + this.config = this.config.bind(this) + this.poolInfo = this.poolInfo.bind(this) + this.rewardsPerSec = this.rewardsPerSec.bind(this) + this.rewardInfo = this.rewardInfo.bind(this) + this.rewardInfos = this.rewardInfos.bind(this) + this.getPoolsInformation = this.getPoolsInformation.bind(this) + this.lockInfos = this.lockInfos.bind(this) + this.stakedBalanceAtHeight = this.stakedBalanceAtHeight.bind(this) + this.totalStakedAtHeight = this.totalStakedAtHeight.bind(this) + } + + config = async (): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + config: {}, + }) + } + poolInfo = async ({ + stakingToken, + }: { + stakingToken: Addr + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + pool_info: { + staking_token: stakingToken, + }, + }) + } + rewardsPerSec = async ({ + stakingToken, + }: { + stakingToken: Addr + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + rewards_per_sec: { + staking_token: stakingToken, + }, + }) + } + rewardInfo = async ({ + stakerAddr, + stakingToken, + }: { + stakerAddr: Addr + stakingToken?: Addr + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + reward_info: { + staker_addr: stakerAddr, + staking_token: stakingToken, + }, + }) + } + rewardInfos = async ({ + limit, + order, + stakingToken, + startAfter, + }: { + limit?: number + order?: number + stakingToken: Addr + startAfter?: Addr + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + reward_infos: { + limit, + order, + staking_token: stakingToken, + start_after: startAfter, + }, + }) + } + getPoolsInformation = async (): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + get_pools_information: {}, + }) + } + lockInfos = async ({ + limit, + order, + stakerAddr, + stakingToken, + startAfter, + }: { + limit?: number + order?: number + stakerAddr: Addr + stakingToken: Addr + startAfter?: number + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + lock_infos: { + limit, + order, + staker_addr: stakerAddr, + staking_token: stakingToken, + start_after: startAfter, + }, + }) + } + stakedBalanceAtHeight = async ({ + address, + assetKey, + height, + }: { + address: string + assetKey: Addr + height?: number + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + staked_balance_at_height: { + address, + asset_key: assetKey, + height, + }, + }) + } + totalStakedAtHeight = async ({ + assetKey, + height, + }: { + assetKey: Addr + height?: number + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + total_staked_at_height: { + asset_key: assetKey, + height, + }, + }) + } +} +export interface OraichainCw20StakingInterface + extends OraichainCw20StakingReadOnlyInterface { + contractAddress: string + sender: string + receive: ( + { + amount, + msg, + sender, + }: { + amount: Uint128 + msg: Binary + sender: string + }, + fee?: number | StdFee | 'auto', + memo?: string, + _funds?: Coin[] + ) => Promise + updateConfig: ( + { + owner, + rewarder, + }: { + owner?: Addr + rewarder?: Addr + }, + fee?: number | StdFee | 'auto', + memo?: string, + _funds?: Coin[] + ) => Promise + registerAsset: ( + { + stakingToken, + unbondingPeriod, + }: { + stakingToken: Addr + unbondingPeriod?: number + }, + fee?: number | StdFee | 'auto', + memo?: string, + _funds?: Coin[] + ) => Promise + updateRewardsPerSec: ( + { + assets, + stakingToken, + }: { + assets: Asset[] + stakingToken: Addr + }, + fee?: number | StdFee | 'auto', + memo?: string, + _funds?: Coin[] + ) => Promise + depositReward: ( + { + rewards, + }: { + rewards: RewardMsg[] + }, + fee?: number | StdFee | 'auto', + memo?: string, + _funds?: Coin[] + ) => Promise + unbond: ( + { + amount, + stakingToken, + }: { + amount: Uint128 + stakingToken: Addr + }, + fee?: number | StdFee | 'auto', + memo?: string, + _funds?: Coin[] + ) => Promise + withdraw: ( + { + stakingToken, + }: { + stakingToken?: Addr + }, + fee?: number | StdFee | 'auto', + memo?: string, + _funds?: Coin[] + ) => Promise + withdrawOthers: ( + { + stakerAddrs, + stakingToken, + }: { + stakerAddrs: Addr[] + stakingToken?: Addr + }, + fee?: number | StdFee | 'auto', + memo?: string, + _funds?: Coin[] + ) => Promise +} +export class OraichainCw20StakingClient + extends OraichainCw20StakingQueryClient + implements OraichainCw20StakingInterface +{ + client: SigningCosmWasmClient + sender: string + contractAddress: string + + constructor( + client: SigningCosmWasmClient, + sender: string, + contractAddress: string + ) { + super(client, contractAddress) + this.client = client + this.sender = sender + this.contractAddress = contractAddress + this.receive = this.receive.bind(this) + this.updateConfig = this.updateConfig.bind(this) + this.registerAsset = this.registerAsset.bind(this) + this.updateRewardsPerSec = this.updateRewardsPerSec.bind(this) + this.depositReward = this.depositReward.bind(this) + this.unbond = this.unbond.bind(this) + this.withdraw = this.withdraw.bind(this) + this.withdrawOthers = this.withdrawOthers.bind(this) + } + + receive = async ( + { + amount, + msg, + sender, + }: { + amount: Uint128 + msg: Binary + sender: string + }, + fee: number | StdFee | 'auto' = 'auto', + memo?: string, + _funds?: Coin[] + ): Promise => { + return await this.client.execute( + this.sender, + this.contractAddress, + { + receive: { + amount, + msg, + sender, + }, + }, + fee, + memo, + _funds + ) + } + updateConfig = async ( + { + owner, + rewarder, + }: { + owner?: Addr + rewarder?: Addr + }, + fee: number | StdFee | 'auto' = 'auto', + memo?: string, + _funds?: Coin[] + ): Promise => { + return await this.client.execute( + this.sender, + this.contractAddress, + { + update_config: { + owner, + rewarder, + }, + }, + fee, + memo, + _funds + ) + } + registerAsset = async ( + { + stakingToken, + unbondingPeriod, + }: { + stakingToken: Addr + unbondingPeriod?: number + }, + fee: number | StdFee | 'auto' = 'auto', + memo?: string, + _funds?: Coin[] + ): Promise => { + return await this.client.execute( + this.sender, + this.contractAddress, + { + register_asset: { + staking_token: stakingToken, + unbonding_period: unbondingPeriod, + }, + }, + fee, + memo, + _funds + ) + } + updateRewardsPerSec = async ( + { + assets, + stakingToken, + }: { + assets: Asset[] + stakingToken: Addr + }, + fee: number | StdFee | 'auto' = 'auto', + memo?: string, + _funds?: Coin[] + ): Promise => { + return await this.client.execute( + this.sender, + this.contractAddress, + { + update_rewards_per_sec: { + assets, + staking_token: stakingToken, + }, + }, + fee, + memo, + _funds + ) + } + depositReward = async ( + { + rewards, + }: { + rewards: RewardMsg[] + }, + fee: number | StdFee | 'auto' = 'auto', + memo?: string, + _funds?: Coin[] + ): Promise => { + return await this.client.execute( + this.sender, + this.contractAddress, + { + deposit_reward: { + rewards, + }, + }, + fee, + memo, + _funds + ) + } + unbond = async ( + { + amount, + stakingToken, + }: { + amount: Uint128 + stakingToken: Addr + }, + fee: number | StdFee | 'auto' = 'auto', + memo?: string, + _funds?: Coin[] + ): Promise => { + return await this.client.execute( + this.sender, + this.contractAddress, + { + unbond: { + amount, + staking_token: stakingToken, + }, + }, + fee, + memo, + _funds + ) + } + withdraw = async ( + { + stakingToken, + }: { + stakingToken?: Addr + }, + fee: number | StdFee | 'auto' = 'auto', + memo?: string, + _funds?: Coin[] + ): Promise => { + return await this.client.execute( + this.sender, + this.contractAddress, + { + withdraw: { + staking_token: stakingToken, + }, + }, + fee, + memo, + _funds + ) + } + withdrawOthers = async ( + { + stakerAddrs, + stakingToken, + }: { + stakerAddrs: Addr[] + stakingToken?: Addr + }, + fee: number | StdFee | 'auto' = 'auto', + memo?: string, + _funds?: Coin[] + ): Promise => { + return await this.client.execute( + this.sender, + this.contractAddress, + { + withdraw_others: { + staker_addrs: stakerAddrs, + staking_token: stakingToken, + }, + }, + fee, + memo, + _funds + ) + } +} diff --git a/packages/state/contracts/index.ts b/packages/state/contracts/index.ts index b65183560d..769157de0c 100644 --- a/packages/state/contracts/index.ts +++ b/packages/state/contracts/index.ts @@ -70,6 +70,10 @@ export { NeutronVotingRegistryClient, NeutronVotingRegistryQueryClient, } from './NeutronVotingRegistry' +export { + OraichainCw20StakingClient, + OraichainCw20StakingQueryClient, +} from './OraichainCw20Staking' export { NeutronCwdPreProposeSingleOverruleClient, NeutronCwdPreProposeSingleOverruleQueryClient, diff --git a/packages/state/recoil/selectors/contracts/Cw20Stake.ts b/packages/state/recoil/selectors/contracts/Cw20Stake.ts index cc16fb21f4..ea24cd4992 100644 --- a/packages/state/recoil/selectors/contracts/Cw20Stake.ts +++ b/packages/state/recoil/selectors/contracts/Cw20Stake.ts @@ -2,6 +2,7 @@ import { selectorFamily } from 'recoil' import { WithChainId } from '@dao-dao/types' import { + Claim, ClaimsResponse, GetConfigResponse, GetHooksResponse, @@ -11,6 +12,8 @@ import { TotalStakedAtHeightResponse, TotalValueResponse, } from '@dao-dao/types/contracts/Cw20Stake' +import { ConfigResponse as OraichainCw20StakingProxySnapshotConfigResponse } from '@dao-dao/types/contracts/OraichainCw20StakingProxySnapshot' +import { ContractName } from '@dao-dao/utils' import { Cw20StakeClient, @@ -22,9 +25,9 @@ import { signingCosmWasmClientAtom, } from '../../atoms' import { cosmWasmClientForChainSelector } from '../chain' +import { isContractSelector } from '../contract' import { queryContractIndexerSelector } from '../indexer' -import { objectMatchesStructure } from '@dao-dao/utils' -import { toUtf8 } from '@cosmjs/encoding' +import { allLockInfosSelector } from './OraichainCw20Staking' type QueryClientParams = WithChainId<{ contractAddress: string @@ -74,26 +77,33 @@ export const stakedBalanceAtHeightSelector = selectorFamily< async ({ get }) => { const id = get(refreshWalletBalancesIdAtom(params[0].address)) + // If Oraichain proxy, get staking token and pass to indexer query. + let oraichainStakingToken: string | undefined const isOraichainProxy = get( isOraichainProxySnapshotContractSelector(queryClientParams) ) - if (!isOraichainProxy) { - const balance = get( - queryContractIndexerSelector({ - ...queryClientParams, - formula: 'cw20Stake/stakedBalance', - args: { - address: params[0].address, - }, - block: params[0].height ? { height: params[0].height } : undefined, - id, - }) - ) - if (balance && !isNaN(balance)) { - return { - balance, - height: params[0].height, - } + if (isOraichainProxy) { + oraichainStakingToken = get( + oraichainProxySnapshotConfigSelector(queryClientParams) + ).asset_key + } + + const balance = get( + queryContractIndexerSelector({ + ...queryClientParams, + formula: 'cw20Stake/stakedBalance', + args: { + address: params[0].address, + oraichainStakingToken, + }, + block: params[0].height ? { height: params[0].height } : undefined, + id, + }) + ) + if (balance && !isNaN(balance)) { + return { + balance, + height: params[0].height, } } @@ -114,23 +124,32 @@ export const totalStakedAtHeightSelector = selectorFamily< async ({ get }) => { const id = get(refreshWalletBalancesIdAtom(undefined)) + // If Oraichain proxy, get staking token and pass to indexer query. + let oraichainStakingToken: string | undefined const isOraichainProxy = get( isOraichainProxySnapshotContractSelector(queryClientParams) ) - if (!isOraichainProxy) { - const total = get( - queryContractIndexerSelector({ - ...queryClientParams, - formula: 'cw20Stake/totalStaked', - block: params[0].height ? { height: params[0].height } : undefined, - id, - }) - ) - if (total && !isNaN(total)) { - return { - total, - height: params[0].height, - } + if (isOraichainProxy) { + oraichainStakingToken = get( + oraichainProxySnapshotConfigSelector(queryClientParams) + ).asset_key + } + + const total = get( + queryContractIndexerSelector({ + ...queryClientParams, + formula: 'cw20Stake/totalStaked', + block: params[0].height ? { height: params[0].height } : undefined, + id, + args: { + oraichainStakingToken, + }, + }) + ) + if (total && !isNaN(total)) { + return { + total, + height: params[0].height, } } @@ -151,6 +170,7 @@ export const stakedValueSelector = selectorFamily< async ({ get }) => { const id = get(refreshWalletBalancesIdAtom(params[0].address)) + // Oraichain proxy handles passing the query through. const isOraichainProxy = get( isOraichainProxySnapshotContractSelector(queryClientParams) ) @@ -195,6 +215,7 @@ export const totalValueSelector = selectorFamily< async ({ get }) => { const id = get(refreshWalletBalancesIdAtom(undefined)) + // This query does not exist on Oraichain's proxy-snapshot. const isOraichainProxy = get( isOraichainProxySnapshotContractSelector(queryClientParams) ) @@ -240,10 +261,7 @@ export const getConfigSelector = selectorFamily< isOraichainProxySnapshotContractSelector(queryClientParams) ) - // The Oraichain cw20-staking proxy-snapshot contract is used as the - // staking contract for their custom staking solution. If this is the - // case, ignore the indexer response and use the contract query since it - // executes a passthrough query and returns the correct config. + // Oraichain proxy handles passing the query through. if (!isOraichainProxy) { const config = get( queryContractIndexerSelector({ @@ -274,12 +292,34 @@ export const claimsSelector = selectorFamily< async ({ get }) => { const id = get(refreshClaimsIdAtom(params[0].address)) - // Oraichain has their own interface. + // Convert Oraichain lock infos to claims. const isOraichainProxy = get( isOraichainProxySnapshotContractSelector(queryClientParams) ) if (isOraichainProxy) { - return { claims: [] } + const { asset_key, staking_contract } = get( + oraichainProxySnapshotConfigSelector(queryClientParams) + ) + const { lock_infos } = get( + allLockInfosSelector({ + chainId: queryClientParams.chainId, + contractAddress: staking_contract, + stakerAddr: params[0].address, + stakingToken: asset_key, + }) + ) + + return { + claims: lock_infos.map( + ({ amount, unlock_time }): Claim => ({ + amount, + release_at: { + // Convert seconds to nanoseconds. + at_time: (BigInt(unlock_time) * BigInt(1e9)).toString(), + }, + }) + ), + } } const claims = get( @@ -361,22 +401,37 @@ export const topStakersSelector = selectorFamily< key: 'cw20StakeTopStakers', get: ({ limit, ...queryClientParams }) => - ({ get }) => - get( - queryContractIndexerSelector({ - ...queryClientParams, - formula: 'cw20Stake/topStakers', - args: { - limit, - }, - }) - ) ?? undefined, + ({ get }) => { + // If Oraichain proxy, get staking token and pass to indexer query. + let oraichainStakingToken: string | undefined + const isOraichainProxy = get( + isOraichainProxySnapshotContractSelector(queryClientParams) + ) + if (isOraichainProxy) { + oraichainStakingToken = get( + oraichainProxySnapshotConfigSelector(queryClientParams) + ).asset_key + } + + return ( + get( + queryContractIndexerSelector({ + ...queryClientParams, + formula: 'cw20Stake/topStakers', + args: { + limit, + oraichainStakingToken, + }, + }) + ) ?? undefined + ) + }, }) /** - * The Oraichain cw20-staking proxy-snapshot contract is used as the staking + * The Oraichain cw20-staking-proxy-snapshot contract is used as the staking * contract for their custom staking solution. This selector returns whether or - * not this is a proxy-snapshot contract. + * not this is a cw20-staking-proxy-snapshot contract. */ export const isOraichainProxySnapshotContractSelector = selectorFamily< boolean, @@ -385,33 +440,52 @@ export const isOraichainProxySnapshotContractSelector = selectorFamily< key: 'cw20StakeIsOraichainProxySnapshotContract', get: (queryClientParams) => - async ({ get }) => { - let config = get( - queryContractIndexerSelector({ + ({ get }) => + get( + isContractSelector({ ...queryClientParams, - formula: 'cw20Stake/config', + name: ContractName.OraichainCw20StakingProxySnapshot, }) - ) + ), +}) - if (!config) { - // If indexer fails, fallback to querying chain. - const client = get( - cosmWasmClientForChainSelector(queryClientParams.chainId) - ) - config = await client.queryContractRaw( - queryClientParams.contractAddress, - toUtf8('config') +/** + * Get config for Oraichain's cw20-staking-proxy-snapshot contract. + */ +export const oraichainProxySnapshotConfigSelector = selectorFamily< + OraichainCw20StakingProxySnapshotConfigResponse, + QueryClientParams +>({ + key: 'cw20StakeOraichainProxySnapshotConfig', + get: + (queryClientParams) => + async ({ get }) => { + if (!get(isOraichainProxySnapshotContractSelector(queryClientParams))) { + throw new Error( + 'Contract is not an Oraichain cw20-staking proxy-snapshot contract' ) } - if (!config) { - throw new Error('No config found') + let config = get( + queryContractIndexerSelector({ + ...queryClientParams, + formula: 'item', + args: { + key: 'config', + }, + }) + ) + if (config) { + return config } - return objectMatchesStructure(config, { - owner: {}, - asset_key: {}, - staking_contract: {}, - }) + // If indexer fails, fallback to querying chain. + const client = get( + cosmWasmClientForChainSelector(queryClientParams.chainId) + ) + return await client.queryContractSmart( + queryClientParams.contractAddress, + { config: {} } + ) }, }) diff --git a/packages/state/recoil/selectors/contracts/OraichainCw20Staking.ts b/packages/state/recoil/selectors/contracts/OraichainCw20Staking.ts new file mode 100644 index 0000000000..85e7def933 --- /dev/null +++ b/packages/state/recoil/selectors/contracts/OraichainCw20Staking.ts @@ -0,0 +1,260 @@ +/** + * This file was automatically generated by @cosmwasm/ts-codegen@0.35.7. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run the @cosmwasm/ts-codegen generate command to regenerate this file. + */ + +import { selectorFamily } from 'recoil' + +import { WithChainId } from '@dao-dao/types' +import { + ArrayOfQueryPoolInfoResponse, + ArrayOfRewardInfoResponse, + ConfigResponse, + LockInfosResponse, + PoolInfoResponse, + RewardInfoResponse, + RewardsPerSecResponse, + StakedBalanceAtHeightResponse, + TotalStakedAtHeightResponse, +} from '@dao-dao/types/contracts/OraichainCw20Staking' + +import { + OraichainCw20StakingClient, + OraichainCw20StakingQueryClient, +} from '../../../contracts/OraichainCw20Staking' +import { + refreshClaimsIdAtom, + refreshWalletBalancesIdAtom, + signingCosmWasmClientAtom, +} from '../../atoms' +import { cosmWasmClientForChainSelector } from '../chain' + +type QueryClientParams = WithChainId<{ + contractAddress: string +}> + +export const queryClient = selectorFamily< + OraichainCw20StakingQueryClient, + QueryClientParams +>({ + key: 'oraichainCw20StakingQueryClient', + get: + ({ contractAddress, chainId }) => + ({ get }) => { + const client = get(cosmWasmClientForChainSelector(chainId)) + return new OraichainCw20StakingQueryClient(client, contractAddress) + }, + dangerouslyAllowMutability: true, +}) + +export type ExecuteClientParams = WithChainId<{ + contractAddress: string + sender: string +}> + +export const executeClient = selectorFamily< + OraichainCw20StakingClient | undefined, + ExecuteClientParams +>({ + key: 'oraichainCw20StakingExecuteClient', + get: + ({ chainId, contractAddress, sender }) => + ({ get }) => { + const client = get(signingCosmWasmClientAtom({ chainId })) + if (!client) return + + return new OraichainCw20StakingClient(client, sender, contractAddress) + }, + dangerouslyAllowMutability: true, +}) +export const configSelector = selectorFamily< + ConfigResponse, + QueryClientParams & { + params: Parameters + } +>({ + key: 'oraichainCw20StakingConfig', + get: + ({ params, ...queryClientParams }) => + async ({ get }) => { + const client = get(queryClient(queryClientParams)) + return await client.config(...params) + }, +}) +export const poolInfoSelector = selectorFamily< + PoolInfoResponse, + QueryClientParams & { + params: Parameters + } +>({ + key: 'oraichainCw20StakingPoolInfo', + get: + ({ params, ...queryClientParams }) => + async ({ get }) => { + const client = get(queryClient(queryClientParams)) + return await client.poolInfo(...params) + }, +}) +export const rewardsPerSecSelector = selectorFamily< + RewardsPerSecResponse, + QueryClientParams & { + params: Parameters + } +>({ + key: 'oraichainCw20StakingRewardsPerSec', + get: + ({ params, ...queryClientParams }) => + async ({ get }) => { + const client = get(queryClient(queryClientParams)) + return await client.rewardsPerSec(...params) + }, +}) +export const rewardInfoSelector = selectorFamily< + RewardInfoResponse, + QueryClientParams & { + params: Parameters + } +>({ + key: 'oraichainCw20StakingRewardInfo', + get: + ({ params, ...queryClientParams }) => + async ({ get }) => { + const client = get(queryClient(queryClientParams)) + return await client.rewardInfo(...params) + }, +}) +export const rewardInfosSelector = selectorFamily< + ArrayOfRewardInfoResponse, + QueryClientParams & { + params: Parameters + } +>({ + key: 'oraichainCw20StakingRewardInfos', + get: + ({ params, ...queryClientParams }) => + async ({ get }) => { + const client = get(queryClient(queryClientParams)) + return await client.rewardInfos(...params) + }, +}) +export const getPoolsInformationSelector = selectorFamily< + ArrayOfQueryPoolInfoResponse, + QueryClientParams & { + params: Parameters + } +>({ + key: 'oraichainCw20StakingGetPoolsInformation', + get: + ({ params, ...queryClientParams }) => + async ({ get }) => { + const client = get(queryClient(queryClientParams)) + return await client.getPoolsInformation(...params) + }, +}) +export const lockInfosSelector = selectorFamily< + LockInfosResponse, + QueryClientParams & { + params: Parameters + } +>({ + key: 'oraichainCw20StakingLockInfos', + get: + ({ params, ...queryClientParams }) => + async ({ get }) => { + get(refreshClaimsIdAtom(params[0].stakerAddr)) + + const client = get(queryClient(queryClientParams)) + return await client.lockInfos(...params) + }, +}) +export const stakedBalanceAtHeightSelector = selectorFamily< + StakedBalanceAtHeightResponse, + QueryClientParams & { + params: Parameters + } +>({ + key: 'oraichainCw20StakingStakedBalanceAtHeight', + get: + ({ params, ...queryClientParams }) => + async ({ get }) => { + get(refreshWalletBalancesIdAtom(params[0].address)) + + const client = get(queryClient(queryClientParams)) + return await client.stakedBalanceAtHeight(...params) + }, +}) +export const totalStakedAtHeightSelector = selectorFamily< + TotalStakedAtHeightResponse, + QueryClientParams & { + params: Parameters + } +>({ + key: 'oraichainCw20StakingTotalStakedAtHeight', + get: + ({ params, ...queryClientParams }) => + async ({ get }) => { + get(refreshWalletBalancesIdAtom(undefined)) + + const client = get(queryClient(queryClientParams)) + return await client.totalStakedAtHeight(...params) + }, +}) + +// Custom + +const LOCK_INFOS_LIMIT = 30 +export const allLockInfosSelector = selectorFamily< + LockInfosResponse, + QueryClientParams & + Pick< + Parameters[0], + 'stakerAddr' | 'stakingToken' + > +>({ + key: 'oraichainCw20StakingAllLockInfos', + get: + ({ stakerAddr, stakingToken, ...queryClientParams }) => + async ({ get }) => { + const response: LockInfosResponse = { + lock_infos: [], + staker_addr: '', + staking_token: '', + } + + while (true) { + const page = await get( + lockInfosSelector({ + ...queryClientParams, + params: [ + { + stakerAddr, + stakingToken, + order: 1, // descending + startAfter: + response.lock_infos[response.lock_infos.length - 1] + ?.unlock_time, + limit: LOCK_INFOS_LIMIT, + }, + ], + }) + ) + + if (!page.staker_addr) { + response.staker_addr = page.staker_addr + } + if (!page.staking_token) { + response.staking_token = page.staking_token + } + + response.lock_infos.push(...page.lock_infos) + + // If we have less than the limit of items, we've exhausted them. + if (response.lock_infos.length < LOCK_INFOS_LIMIT) { + break + } + } + + return response + }, +}) diff --git a/packages/state/recoil/selectors/contracts/index.ts b/packages/state/recoil/selectors/contracts/index.ts index 2b658fc7b2..397eb9dbb8 100644 --- a/packages/state/recoil/selectors/contracts/index.ts +++ b/packages/state/recoil/selectors/contracts/index.ts @@ -31,6 +31,7 @@ export * as NeutronCwdSubdaoPreProposeSingleSelectors from './NeutronCwdSubdaoPr export * as NeutronCwdSubdaoTimelockSingleSelectors from './NeutronCwdSubdaoTimelockSingle' export * as NeutronVaultSelectors from './NeutronVault' export * as NeutronVotingRegistrySelectors from './NeutronVotingRegistry' +export * as OraichainCw20StakingSelectors from './OraichainCw20Staking' export * as PolytoneListenerSelectors from './PolytoneListener' export * as PolytoneNoteSelectors from './PolytoneNote' export * as PolytoneProxySelectors from './PolytoneProxy' diff --git a/packages/stateful/hooks/contracts/OraichainCw20Staking.ts b/packages/stateful/hooks/contracts/OraichainCw20Staking.ts new file mode 100644 index 0000000000..93755f8812 --- /dev/null +++ b/packages/stateful/hooks/contracts/OraichainCw20Staking.ts @@ -0,0 +1,50 @@ +/* eslint-disable react-hooks/rules-of-hooks */ + +import { ExecuteResult } from '@cosmjs/cosmwasm-stargate' +import { useCallback } from 'react' +import { useRecoilValueLoadable } from 'recoil' + +import { OraichainCw20StakingClient as ExecuteClient } from '@dao-dao/state/contracts/OraichainCw20Staking' +import { + ExecuteClientParams, + executeClient, +} from '@dao-dao/state/recoil/selectors/contracts/OraichainCw20Staking' +import { useChain } from '@dao-dao/stateless' +import { FunctionKeyOf } from '@dao-dao/types' + +import { useSyncWalletSigner } from '../useSyncWalletSigner' + +// This hook wrapper lets us easily make hooks out of all execution functions on +// the contract clients, without having to fetch the `executeClient` selector as +// a loadable and add `useCallback` hooks in all the components. +const wrapExecuteHook = + >(fn: T) => + (params: Omit) => { + // Make sure we have the signing client for this chain and wallet. + useSyncWalletSigner() + + const { chain_id: chainId } = useChain() + const clientLoadable = useRecoilValueLoadable( + executeClient({ + ...params, + chainId, + }) + ) + const client = + clientLoadable.state === 'hasValue' ? clientLoadable.contents : undefined + + return useCallback( + (...args: Parameters) => { + if (client) + return ( + client[fn] as ( + ...args: Parameters + ) => Promise + )(...args) + throw new Error('Wallet signer not set up.') + }, + [client] + ) + } + +export const useUnbond = wrapExecuteHook('unbond') diff --git a/packages/stateful/hooks/contracts/index.ts b/packages/stateful/hooks/contracts/index.ts index f8f47af635..eb5abfb6a2 100644 --- a/packages/stateful/hooks/contracts/index.ts +++ b/packages/stateful/hooks/contracts/index.ts @@ -12,3 +12,4 @@ export * as DaoVotingCw721StakedHooks from './DaoVotingCw721Staked' export * as DaoVotingNativeStakedHooks from './DaoVotingNativeStaked' export * as DaoVotingTokenStakedHooks from './DaoVotingTokenStaked' export * as NeutronVaultHooks from './NeutronVault' +export * as OraichainCw20StakingHooks from './OraichainCw20Staking' diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/components/ProfileCardMemberInfo.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/components/ProfileCardMemberInfo.tsx index 4abe282308..cceb0302bb 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/components/ProfileCardMemberInfo.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/components/ProfileCardMemberInfo.tsx @@ -1,9 +1,10 @@ import { useCallback, useState } from 'react' import toast from 'react-hot-toast' import { useTranslation } from 'react-i18next' -import { useRecoilValue } from 'recoil' +import { constSelector, useRecoilValue } from 'recoil' import { + Cw20StakeSelectors, blockHeightSelector, blocksPerYearSelector, stakingLoadingAtom, @@ -27,6 +28,7 @@ import { import { Cw20StakeHooks, + OraichainCw20StakingHooks, useAwaitNextBlock, useWallet, useWalletBalances, @@ -72,8 +74,35 @@ export const ProfileCardMemberInfo = ({ fetchTotalStakedValue: true, }) + const isOraichainCustomStaking = useRecoilValue( + Cw20StakeSelectors.isOraichainProxySnapshotContractSelector({ + chainId, + contractAddress: stakingContractAddress, + }) + ) + + const oraichainCw20StakingConfig = useRecoilValue( + isOraichainCustomStaking + ? Cw20StakeSelectors.oraichainProxySnapshotConfigSelector({ + chainId, + contractAddress: stakingContractAddress, + }) + : constSelector(undefined) + ) + + // Support Oraichain custom cw20-staking contract. + const stakingContractToExecute = isOraichainCustomStaking + ? // If Oraichain proxy snapshot, fallback to empty string so it errors if + // trying to stake anything. This should never happen. + oraichainCw20StakingConfig?.staking_contract || '' + : stakingContractAddress + const doClaim = Cw20StakeHooks.useClaim({ - contractAddress: stakingContractAddress, + contractAddress: stakingContractToExecute, + sender: walletAddress ?? '', + }) + const doOraichainUnbond = OraichainCw20StakingHooks.useUnbond({ + contractAddress: stakingContractToExecute, sender: walletAddress ?? '', }) @@ -88,7 +117,15 @@ export const ProfileCardMemberInfo = ({ setClaimingLoading(true) try { - await doClaim() + if (isOraichainCustomStaking) { + // Oraichain claiming is an unbond with zero amount. + await doOraichainUnbond({ + amount: '0', + stakingToken: governanceToken.denomOrAddress, + }) + } else { + await doClaim() + } // New balances will not appear until the next block. await awaitNextBlock() @@ -112,16 +149,19 @@ export const ProfileCardMemberInfo = ({ setClaimingLoading(false) } }, [ - awaitNextBlock, isWalletConnected, - doClaim, - governanceToken.decimals, - governanceToken.symbol, - refreshBalances, - refreshClaims, - refreshTotals, sumClaimsAvailable, t, + isOraichainCustomStaking, + awaitNextBlock, + refreshBalances, + refreshTotals, + refreshClaims, + governanceToken.decimals, + governanceToken.symbol, + governanceToken.denomOrAddress, + doOraichainUnbond, + doClaim, ]) const blockHeightLoadable = useCachedLoadable( diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/components/StakingModal.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/components/StakingModal.tsx index 31e610e0a0..0180c5a502 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/components/StakingModal.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw20Staked/components/StakingModal.tsx @@ -6,6 +6,7 @@ import { useRecoilState, useRecoilValue, useSetRecoilState, + waitForAll, } from 'recoil' import { @@ -22,8 +23,10 @@ import { } from '@dao-dao/stateless' import { BaseStakingModalProps } from '@dao-dao/types' import { + convertDenomToMicroDenomStringWithDecimals, convertDenomToMicroDenomWithDecimals, convertMicroDenomToDenomWithDecimals, + encodeMessageAsBase64, processError, } from '@dao-dao/utils' @@ -31,6 +34,7 @@ import { SuspenseLoader } from '../../../../components' import { Cw20BaseHooks, Cw20StakeHooks, + OraichainCw20StakingHooks, useAwaitNextBlock, useWallet, useWalletBalances, @@ -77,14 +81,42 @@ const InnerStakingModal = ({ fetchWalletStakedValue: true, }) - const totalStakedBalance = useRecoilValue( - Cw20StakeSelectors.totalStakedAtHeightSelector({ - chainId, - contractAddress: stakingContractAddress, - params: [{}], - }) + const [isOraichainCustomStaking, totalStakedBalance, totalValue] = + useRecoilValue( + waitForAll([ + Cw20StakeSelectors.isOraichainProxySnapshotContractSelector({ + chainId, + contractAddress: stakingContractAddress, + }), + Cw20StakeSelectors.totalStakedAtHeightSelector({ + chainId, + contractAddress: stakingContractAddress, + params: [{}], + }), + Cw20StakeSelectors.totalValueSelector({ + chainId, + contractAddress: stakingContractAddress, + params: [], + }), + ]) + ) + + const oraichainCw20StakingConfig = useRecoilValue( + isOraichainCustomStaking + ? Cw20StakeSelectors.oraichainProxySnapshotConfigSelector({ + chainId, + contractAddress: stakingContractAddress, + }) + : constSelector(undefined) ) + // Support Oraichain custom cw20-staking contract. + const stakingContractToExecute = isOraichainCustomStaking + ? // If Oraichain proxy snapshot, fallback to empty string so it errors if + // trying to stake anything. This should never happen. + oraichainCw20StakingConfig?.staking_contract || '' + : stakingContractAddress + const walletStakedBalanceLoadable = useCachedLoadable( walletAddress ? Cw20StakeSelectors.stakedBalanceAtHeightSelector({ @@ -100,26 +132,22 @@ const InnerStakingModal = ({ ? Number(walletStakedBalanceLoadable.contents.balance) : undefined - const totalValue = useRecoilValue( - Cw20StakeSelectors.totalValueSelector({ - chainId, - contractAddress: stakingContractAddress, - params: [], - }) - ) - const [amount, setAmount] = useState(0) - const doStake = Cw20BaseHooks.useSend({ + const doCw20SendAndExecute = Cw20BaseHooks.useSend({ contractAddress: governanceToken.denomOrAddress, sender: walletAddress ?? '', }) const doUnstake = Cw20StakeHooks.useUnstake({ - contractAddress: stakingContractAddress, + contractAddress: stakingContractToExecute, + sender: walletAddress ?? '', + }) + const doOraichainUnbond = OraichainCw20StakingHooks.useUnbond({ + contractAddress: stakingContractToExecute, sender: walletAddress ?? '', }) const doClaim = Cw20StakeHooks.useClaim({ - contractAddress: stakingContractAddress, + contractAddress: stakingContractToExecute, sender: walletAddress ?? '', }) @@ -146,13 +174,15 @@ const InnerStakingModal = ({ setStakingLoading(true) try { - await doStake({ + await doCw20SendAndExecute({ amount: convertDenomToMicroDenomWithDecimals( amount, governanceToken.decimals ).toString(), - contract: stakingContractAddress, - msg: btoa('{"stake": {}}'), + contract: stakingContractToExecute, + msg: encodeMessageAsBase64({ + [isOraichainCustomStaking ? 'bond' : 'stake']: {}, + }), }) // New balances will not appear until the next block. @@ -218,12 +248,20 @@ const InnerStakingModal = ({ } try { - await doUnstake({ - amount: convertDenomToMicroDenomWithDecimals( - amountToUnstake, - governanceToken.decimals - ).toString(), - }) + const convertedAmount = convertDenomToMicroDenomStringWithDecimals( + amountToUnstake, + governanceToken.decimals + ) + if (isOraichainCustomStaking) { + await doOraichainUnbond({ + amount: convertedAmount, + stakingToken: governanceToken.denomOrAddress, + }) + } else { + await doUnstake({ + amount: convertedAmount, + }) + } // New balances will not appear until the next block. await awaitNextBlock() @@ -258,7 +296,15 @@ const InnerStakingModal = ({ setStakingLoading(true) try { - await doClaim() + if (isOraichainCustomStaking) { + // Oraichain claiming is an unbond with zero amount. + await doOraichainUnbond({ + amount: '0', + stakingToken: governanceToken.denomOrAddress, + }) + } else { + await doClaim() + } // New balances will not appear until the next block. await awaitNextBlock() diff --git a/packages/stateful/voting-module-adapter/components/ProfileCardMemberInfoTokens.tsx b/packages/stateful/voting-module-adapter/components/ProfileCardMemberInfoTokens.tsx index 2a595fcaf7..89a74c94dc 100644 --- a/packages/stateful/voting-module-adapter/components/ProfileCardMemberInfoTokens.tsx +++ b/packages/stateful/voting-module-adapter/components/ProfileCardMemberInfoTokens.tsx @@ -266,8 +266,8 @@ export const ProfileCardMemberInfoTokens = ({ onClick={onClaim} size="lg" variant={ - // If stake button below is primary, don't make this primary. - canBeMemberButIsnt ? 'secondary' : 'primary' + // If stake button below is brand, don't make this brand. + canBeMemberButIsnt ? 'secondary' : 'brand' } > {loadingTokens.loading || !onlyOneToken @@ -287,7 +287,7 @@ export const ProfileCardMemberInfoTokens = ({ loading={stakingLoading} onClick={onStake} size="lg" - variant={canBeMemberButIsnt ? 'primary' : 'secondary'} + variant={canBeMemberButIsnt ? 'brand' : 'secondary'} > {loadingTokens.loading || !hasStaked ? onlyOneToken @@ -301,6 +301,7 @@ export const ProfileCardMemberInfoTokens = ({ {!hideUnstaking && ( setShowUnstakingTokens(false)} refresh={refreshUnstakingTasks} diff --git a/packages/stateless/components/token/UnstakingModal.tsx b/packages/stateless/components/token/UnstakingModal.tsx index 0f89b59b12..70b06cebbe 100644 --- a/packages/stateless/components/token/UnstakingModal.tsx +++ b/packages/stateless/components/token/UnstakingModal.tsx @@ -1,4 +1,4 @@ -import { ArrowDropDown } from '@mui/icons-material' +import { ArrowDropDown, WarningRounded } from '@mui/icons-material' import clsx from 'clsx' import { useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' @@ -7,10 +7,12 @@ import { UnstakingTask } from '@dao-dao/types' import { Button } from '../buttons/Button' import { Modal, ModalProps } from '../modals/Modal' +import { NoContent } from '../NoContent' import { UnstakingLine } from './UnstakingLine' import { UnstakingTaskStatus } from './UnstakingStatus' export interface UnstakingModalProps extends Omit { + claimingLoading?: boolean unstakingDuration?: string tasks: UnstakingTask[] onClaim?: () => void @@ -18,6 +20,7 @@ export interface UnstakingModalProps extends Omit { } export const UnstakingModal = ({ + claimingLoading, unstakingDuration, tasks, containerClassName, @@ -115,62 +118,80 @@ export const UnstakingModal = ({ containerClassName )} > - {/* Only show if something is ready to claim. */} - {readyToClaim.length > 0 && ( - <> -
- {readyToClaim.map((task, index) => ( - {t('button.claim')} - ) - } - task={task} - /> - ))} -
- - )} - -
- - -

{t('title.numTasks', { count: unstaking.length })}

-
- - {unstaking.length > 0 && ( -
- {unstaking.map((task, index) => ( - - ))} -
- )} - - {claimed.length > 0 && ( + ) : ( <> -
+ {/* Only show if something is ready to claim. */} + {readyToClaim.length > 0 && ( + <> +
+ {readyToClaim.map((task, index) => ( + + {t('button.claim')} + + ) + } + task={task} + /> + ))} +
+ + )} + +
-

- {claimed.length === 0 ? t('title.noHistory') : t('title.history')} -

+

{t('title.numTasks', { count: unstaking.length })}

-
- {claimed.map((task, index) => ( - - ))} -
+ {unstaking.length > 0 && ( +
+ {unstaking.map((task, index) => ( + + ))} +
+ )} + + {claimed.length > 0 && ( + <> +
+ + +

+ {claimed.length === 0 + ? t('title.noHistory') + : t('title.history')} +

+
+ +
+ {claimed.map((task, index) => ( + + ))} +
+ + )} )} diff --git a/packages/stateless/components/token/UnstakingStatus.tsx b/packages/stateless/components/token/UnstakingStatus.tsx index 5f74d3acde..3e20152d75 100644 --- a/packages/stateless/components/token/UnstakingStatus.tsx +++ b/packages/stateless/components/token/UnstakingStatus.tsx @@ -22,7 +22,7 @@ export const UnstakingStatus = ({ status }: UnstakingStatusProps) => { Icon={Icon} iconClassName={clsx('!h-[19px] !w-[19px]', iconClassName)} label={t(`info.unstakingStatus.${status}`)} - labelClassName={clsx('w-10', textClassName)} + labelClassName={clsx('', textClassName)} /> ) } diff --git a/packages/types/contracts/OraichainCw20Staking.ts b/packages/types/contracts/OraichainCw20Staking.ts new file mode 100644 index 0000000000..38a9353292 --- /dev/null +++ b/packages/types/contracts/OraichainCw20Staking.ts @@ -0,0 +1,183 @@ +/** + * This file was automatically generated by @cosmwasm/ts-codegen@0.35.7. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run the @cosmwasm/ts-codegen generate command to regenerate this file. + */ + +export type Addr = string +export interface InstantiateMsg { + owner?: Addr | null + rewarder: Addr +} +export type ExecuteMsg = + | { + receive: Cw20ReceiveMsg + } + | { + update_config: { + owner?: Addr | null + rewarder?: Addr | null + } + } + | { + register_asset: { + staking_token: Addr + unbonding_period?: number | null + } + } + | { + update_rewards_per_sec: { + assets: Asset[] + staking_token: Addr + } + } + | { + deposit_reward: { + rewards: RewardMsg[] + } + } + | { + unbond: { + amount: Uint128 + staking_token: Addr + } + } + | { + withdraw: { + staking_token?: Addr | null + } + } + | { + withdraw_others: { + staker_addrs: Addr[] + staking_token?: Addr | null + } + } +export type Uint128 = string +export type Binary = string +export type AssetInfo = + | { + token: { + contract_addr: Addr + } + } + | { + native_token: { + denom: string + } + } +export interface Cw20ReceiveMsg { + amount: Uint128 + msg: Binary + sender: string +} +export interface Asset { + amount: Uint128 + info: AssetInfo +} +export interface RewardMsg { + staking_token: Addr + total_accumulation_amount: Uint128 +} +export type QueryMsg = + | { + config: {} + } + | { + pool_info: { + staking_token: Addr + } + } + | { + rewards_per_sec: { + staking_token: Addr + } + } + | { + reward_info: { + staker_addr: Addr + staking_token?: Addr | null + } + } + | { + reward_infos: { + limit?: number | null + order?: number | null + staking_token: Addr + start_after?: Addr | null + } + } + | { + get_pools_information: {} + } + | { + lock_infos: { + limit?: number | null + order?: number | null + staker_addr: Addr + staking_token: Addr + start_after?: number | null + } + } + | { + staked_balance_at_height: { + address: string + asset_key: Addr + height?: number | null + } + } + | { + total_staked_at_height: { + asset_key: Addr + height?: number | null + } + } +export interface MigrateMsg {} +export interface ConfigResponse { + owner: Addr + rewarder: Addr +} +export type Decimal = string +export type ArrayOfQueryPoolInfoResponse = QueryPoolInfoResponse[] +export interface QueryPoolInfoResponse { + asset_key: string + pool_info: PoolInfoResponse +} +export interface PoolInfoResponse { + pending_reward: Uint128 + reward_index: Decimal + staking_token: Addr + total_bond_amount: Uint128 + unbonding_period?: number | null +} +export interface LockInfosResponse { + lock_infos: LockInfoResponse[] + staker_addr: Addr + staking_token: Addr +} +export interface LockInfoResponse { + amount: Uint128 + unlock_time: number +} +export interface RewardInfoResponse { + reward_infos: RewardInfoResponseItem[] + staker_addr: Addr +} +export interface RewardInfoResponseItem { + bond_amount: Uint128 + pending_reward: Uint128 + pending_withdraw: Asset[] + staking_token: Addr +} +export type ArrayOfRewardInfoResponse = RewardInfoResponse[] +export interface RewardsPerSecResponse { + assets: Asset[] +} +export interface StakedBalanceAtHeightResponse { + balance: Uint128 + height: number +} +export interface TotalStakedAtHeightResponse { + height: number + total: Uint128 +} diff --git a/packages/types/contracts/OraichainCw20StakingProxySnapshot.ts b/packages/types/contracts/OraichainCw20StakingProxySnapshot.ts new file mode 100644 index 0000000000..d7161cde35 --- /dev/null +++ b/packages/types/contracts/OraichainCw20StakingProxySnapshot.ts @@ -0,0 +1,63 @@ +/** + * This file was automatically generated by @cosmwasm/ts-codegen@0.35.7. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run the @cosmwasm/ts-codegen generate command to regenerate this file. + */ + +export type Addr = string +export interface InstantiateMsg { + asset_key: Addr + owner?: Addr | null + staking_contract: Addr +} +export type ExecuteMsg = { + update_config: { + asset_key?: Addr | null + owner?: Addr | null + staking_contract?: Addr | null + } +} +export type QueryMsg = + | { + get_config: {} + } + | { + config: {} + } + | { + staked_balance_at_height: { + address: string + height?: number | null + } + } + | { + total_staked_at_height: { + height?: number | null + } + } +export interface MigrateMsg {} +export interface ConfigResponse { + asset_key: Addr + owner: Addr + staking_contract: Addr +} +export type Duration = + | { + height: number + } + | { + time: number + } +export interface ConfigTokenStakingResponse { + token_address: Addr + unstaking_duration?: Duration | null +} +export type Uint128 = string +export interface StakedBalanceAtHeightResponse { + balance: Uint128 + height: number +} +export interface TotalStakedAtHeightResponse { + height: number + total: Uint128 +} diff --git a/packages/utils/constants/contracts.ts b/packages/utils/constants/contracts.ts index 67a9ff871a..5b44efc31a 100644 --- a/packages/utils/constants/contracts.ts +++ b/packages/utils/constants/contracts.ts @@ -13,6 +13,8 @@ export enum ContractName { NeutronCwdSubdaoPreProposeSingle = 'crates.io:cwd-subdao-pre-propose-single', NeutronCwdSubdaoTimelockSingle = 'crates.io:cwd-subdao-timelock-single', NeutronCwdPreProposeSingleOverrule = 'crates.io:cwd-pre-propose-single-overrule', + // https://github.com/oraichain/cw20-staking/tree/master/contracts/proxy-snapshot + OraichainCw20StakingProxySnapshot = 'cw20-staking-proxy-snapshot', } export const NEUTRON_SUBDAO_CORE_CONTRACT_NAMES = [ From 0597c6b0e92c43f69e33199e85181fd92e487e4b Mon Sep 17 00:00:00 2001 From: Noah Saso Date: Thu, 7 Mar 2024 04:19:30 -0800 Subject: [PATCH 3/4] refresh staker list on wallet balance change --- .../recoil/selectors/contracts/Cw20Stake.ts | 6 ++++ .../contracts/DaoVotingCw20Staked.ts | 28 ++++++++++++------- .../contracts/DaoVotingCw721Staked.ts | 4 ++- .../contracts/DaoVotingNativeStaked.ts | 22 ++++++++++----- .../contracts/DaoVotingTokenStaked.ts | 22 ++++++++++----- 5 files changed, 57 insertions(+), 25 deletions(-) diff --git a/packages/state/recoil/selectors/contracts/Cw20Stake.ts b/packages/state/recoil/selectors/contracts/Cw20Stake.ts index ea24cd4992..cfcb6100ed 100644 --- a/packages/state/recoil/selectors/contracts/Cw20Stake.ts +++ b/packages/state/recoil/selectors/contracts/Cw20Stake.ts @@ -21,6 +21,7 @@ import { } from '../../../contracts/Cw20Stake' import { refreshClaimsIdAtom, + refreshDaoVotingPowerAtom, refreshWalletBalancesIdAtom, signingCosmWasmClientAtom, } from '../../atoms' @@ -402,6 +403,10 @@ export const topStakersSelector = selectorFamily< get: ({ limit, ...queryClientParams }) => ({ get }) => { + const id = + get(refreshWalletBalancesIdAtom(undefined)) + + get(refreshDaoVotingPowerAtom(queryClientParams.contractAddress)) + // If Oraichain proxy, get staking token and pass to indexer query. let oraichainStakingToken: string | undefined const isOraichainProxy = get( @@ -422,6 +427,7 @@ export const topStakersSelector = selectorFamily< limit, oraichainStakingToken, }, + id, }) ) ?? undefined ) diff --git a/packages/state/recoil/selectors/contracts/DaoVotingCw20Staked.ts b/packages/state/recoil/selectors/contracts/DaoVotingCw20Staked.ts index cd2caf29e8..2807c42cf4 100644 --- a/packages/state/recoil/selectors/contracts/DaoVotingCw20Staked.ts +++ b/packages/state/recoil/selectors/contracts/DaoVotingCw20Staked.ts @@ -239,14 +239,22 @@ export const topStakersSelector = selectorFamily< key: 'daoVotingCw20StakedTopStakers', get: ({ limit, ...queryClientParams }) => - ({ get }) => - get( - queryContractIndexerSelector({ - ...queryClientParams, - formula: 'daoVotingCw20Staked/topStakers', - args: { - limit, - }, - }) - ) ?? undefined, + ({ get }) => { + const id = + get(refreshWalletBalancesIdAtom(undefined)) + + get(refreshDaoVotingPowerAtom(queryClientParams.contractAddress)) + + return ( + get( + queryContractIndexerSelector({ + ...queryClientParams, + formula: 'daoVotingCw20Staked/topStakers', + args: { + limit, + }, + id, + }) + ) ?? undefined + ) + }, }) diff --git a/packages/state/recoil/selectors/contracts/DaoVotingCw721Staked.ts b/packages/state/recoil/selectors/contracts/DaoVotingCw721Staked.ts index e135b9a314..a1d5966313 100644 --- a/packages/state/recoil/selectors/contracts/DaoVotingCw721Staked.ts +++ b/packages/state/recoil/selectors/contracts/DaoVotingCw721Staked.ts @@ -176,7 +176,9 @@ export const topStakersSelector = selectorFamily< get: ({ limit, ...queryClientParams }) => ({ get }) => { - const id = get(refreshWalletBalancesIdAtom(undefined)) + const id = + get(refreshWalletBalancesIdAtom(undefined)) + + get(refreshDaoVotingPowerAtom(queryClientParams.contractAddress)) return ( get( diff --git a/packages/state/recoil/selectors/contracts/DaoVotingNativeStaked.ts b/packages/state/recoil/selectors/contracts/DaoVotingNativeStaked.ts index a9ea44a891..5a6a766e45 100644 --- a/packages/state/recoil/selectors/contracts/DaoVotingNativeStaked.ts +++ b/packages/state/recoil/selectors/contracts/DaoVotingNativeStaked.ts @@ -253,11 +253,19 @@ export const topStakersSelector = selectorFamily< key: 'daoVotingNativeStakedTopStakers', get: (queryClientParams) => - ({ get }) => - get( - queryContractIndexerSelector({ - ...queryClientParams, - formula: 'daoVotingNativeStaked/topStakers', - }) - ) ?? undefined, + ({ get }) => { + const id = + get(refreshWalletBalancesIdAtom(undefined)) + + get(refreshDaoVotingPowerAtom(queryClientParams.contractAddress)) + + return ( + get( + queryContractIndexerSelector({ + ...queryClientParams, + formula: 'daoVotingNativeStaked/topStakers', + id, + }) + ) ?? undefined + ) + }, }) diff --git a/packages/state/recoil/selectors/contracts/DaoVotingTokenStaked.ts b/packages/state/recoil/selectors/contracts/DaoVotingTokenStaked.ts index 9eb04d501d..c00dedab16 100644 --- a/packages/state/recoil/selectors/contracts/DaoVotingTokenStaked.ts +++ b/packages/state/recoil/selectors/contracts/DaoVotingTokenStaked.ts @@ -350,13 +350,21 @@ export const topStakersSelector = selectorFamily< key: 'daoVotingTokenStakedTopStakers', get: (queryClientParams) => - ({ get }) => - get( - queryContractIndexerSelector({ - ...queryClientParams, - formula: 'daoVotingTokenStaked/topStakers', - }) - ) ?? undefined, + ({ get }) => { + const id = + get(refreshWalletBalancesIdAtom(undefined)) + + get(refreshDaoVotingPowerAtom(queryClientParams.contractAddress)) + + return ( + get( + queryContractIndexerSelector({ + ...queryClientParams, + formula: 'daoVotingTokenStaked/topStakers', + id, + }) + ) ?? undefined + ) + }, }) /** From 0cafdd718c9d1f1326bdd3e38448cc4de72c268d Mon Sep 17 00:00:00 2001 From: Noah Saso Date: Thu, 7 Mar 2024 04:23:33 -0800 Subject: [PATCH 4/4] clean up --- packages/stateless/components/token/UnstakingStatus.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stateless/components/token/UnstakingStatus.tsx b/packages/stateless/components/token/UnstakingStatus.tsx index 3e20152d75..59d1d6a521 100644 --- a/packages/stateless/components/token/UnstakingStatus.tsx +++ b/packages/stateless/components/token/UnstakingStatus.tsx @@ -22,7 +22,7 @@ export const UnstakingStatus = ({ status }: UnstakingStatusProps) => { Icon={Icon} iconClassName={clsx('!h-[19px] !w-[19px]', iconClassName)} label={t(`info.unstakingStatus.${status}`)} - labelClassName={clsx('', textClassName)} + labelClassName={textClassName} /> ) }