Skip to content

Commit d25517d

Browse files
committed
feat: add rootstock staking
1 parent 95d82db commit d25517d

31 files changed

Lines changed: 1374 additions & 146 deletions

File tree

src/assets/pic/rootstock.png

113 KB
Loading

src/components/account-select/index.vue

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
<template>
22
<div class="account-select" v-if="account">
33
<a ref="toggle" class="account-select__block" @click="toggleAction" href="javascript:void(0)">
4-
<img :src="wallet.wallet.value?.adapter.icon" />
4+
<img :src="walletIcon" />
55
<span>{{ $filters.replaceWithEllipsis(account.address, 4, 4) }}</span>
66
<arrow-down />
77
</a>
88
<div v-show="isOpen" ref="dropdown" class="account-select__dropdown">
99
<div class="account-select__info">
10-
<img :src="wallet.wallet.value?.adapter.icon" />
10+
<img :src="walletIcon" />
1111
<span>{{ $filters.replaceWithEllipsis(account.address, 4, 4) }}</span>
1212
<a class="account-select__info-action" @click="onCopyClicked" href="javascript:void(0)">
1313
<copy-icon />
@@ -17,7 +17,7 @@
1717
</a>
1818
</div>
1919
<div class="account-select__amount">
20-
{{ $filters.cryptoCurrencyFormat(walletBalance) }} <span>sol</span>
20+
{{ $filters.cryptoCurrencyFormat(walletBalance) }} <span>{{ balanceSymbol }}</span>
2121
</div>
2222
<a @click="disconnectAction" class="account-select__disconnect" href="javascript:void(0)">
2323
<logout-icon />
@@ -37,25 +37,33 @@ import CopyIcon from "@/icons/common/copy-icon.vue";
3737
import LinkIcon from "@/icons/common/link-icon.vue";
3838
import LogoutIcon from "@/icons/common/logout-icon.vue";
3939
import { SharedTypes } from "@/store/shared/consts";
40-
import { copyToClipboard, openSolscanExplorerAddress } from "@/utils/browser";
40+
import { copyToClipboard, openExplorerAddress } from "@/utils/browser";
4141
import { useWallet } from "solana-wallets-vue";
42+
import { Chains } from "@/core/interfaces";
43+
import { BASE_TOKENS } from "@/core/constants/index";
4244
4345
const wallet = useWallet();
4446
const isOpen = ref<boolean>(false);
4547
const dropdown = ref(null);
4648
const toggle = ref(null);
4749
const store = useStore();
4850
49-
const walletBalance = computed(() => store.getters[SharedTypes.WALLET_BALANCE_GETTER]);
50-
const network = computed(() => store.getters[SharedTypes.NETWORK_GETTER]);
51-
5251
const props = defineProps({
5352
account: {
5453
type: Object as PropType<Account>,
5554
default: null,
5655
},
5756
});
5857
58+
const walletBalance = computed(() => store.getters[SharedTypes.WALLET_BALANCE_GETTER]);
59+
const network = computed(() => store.getters[SharedTypes.NETWORK_GETTER]);
60+
const activeChain = computed(() => store.getters[SharedTypes.CHAIN_GETTER]);
61+
const walletIcon = computed(() => props.account?.image ?? wallet.wallet.value?.adapter.icon ?? "");
62+
const balanceSymbol = computed(() => {
63+
const chain = activeChain.value as Chains;
64+
return BASE_TOKENS[chain]?.symbol?.toUpperCase?.() ?? "";
65+
});
66+
5967
const emit = defineEmits(["disconnect"]);
6068
6169
const toggleAction = () => {
@@ -73,7 +81,7 @@ const onCopyClicked = () => {
7381
}
7482
7583
const onLinkClicked = () => {
76-
openSolscanExplorerAddress(props.account.address, network.value);
84+
openExplorerAddress(props.account.address, activeChain.value as Chains, network.value);
7785
}
7886
7987
onClickOutside(

src/components/amount-input/index.vue

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
:small="true"
1111
/>
1212
<div class="amount-input__balance" :class="{ error: !hasEnoughBalance }">
13-
Balance: {{ maxValue }}
13+
Balance: {{ maxValueDisplay }}
1414
</div>
1515
<div class="amount-input__wrapper">
1616
<input
@@ -124,6 +124,12 @@ const amountValue = computed({
124124
125125
const prices = computed(() => store.getters[SharedTypes.PRICE_GETTER]);
126126
const activeChain = computed(() => store.getters[SharedTypes.CHAIN_GETTER]);
127+
const maxValueDisplay = computed(() => {
128+
if (activeChain.value === "rootstock") {
129+
return Number(props.maxValue || 0).toFixed(2);
130+
}
131+
return props.maxValue;
132+
});
127133
const amountUsd = computed(() => {
128134
const value = parseFloat(amountValue.value || "0");
129135
const price = prices.value?.[BASE_TOKENS[activeChain.value].symbol] || 0;

src/components/app-header/index.vue

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,21 @@
1717
<span>Get Enkrypt</span>
1818
</a>
1919

20-
<!--
2120
<select-list
2221
v-if="!isToggleMenu"
23-
:select="network"
24-
:items="[chainsData[Chains.SOLANA]]"
22+
:select="currentChainOption"
23+
:items="chainOptions"
2524
:is-minify="true"
2625
:is-list-image="true"
2726
@update:select="selectNetworkAction"
2827
/>
29-
-->
3028

3129
<account-select
32-
v-if="wallet.connected.value && !isToggleMenu"
30+
v-if="isWalletConnected && !isToggleMenu"
3331
:account="walletAccount"
3432
@disconnect="disconnectWallet"
3533
></account-select>
36-
<a v-else-if="!isToggleMenu && !wallet.connected.value" class="header__connect" @click="openWalletModal" href="javascript:void(0)">
34+
<a v-else-if="!isToggleMenu" class="header__connect" @click="connectWallet" href="javascript:void(0)">
3735
Connect
3836
</a>
3937
</div>
@@ -70,13 +68,22 @@ const store = useStore();
7068
const router = useRouter();
7169
const isScroll = ref<boolean>(false);
7270
const wallet = useWallet();
73-
const network = ref<ChainDataItem>(chainsData[Chains.SOLANA]);
7471
const walletAccount = computed(() => store.getters[SharedTypes.WALLET_ACCOUNT_GETTER]);
7572
const isWalletModalOpen = computed(() => store.getters[SharedTypes.IS_CONNECT_MODAL_VISIBLE_GETTER]);
73+
const activeChain = computed(() => store.getters[SharedTypes.CHAIN_GETTER]);
74+
const isWalletConnected = computed(() => store.getters[SharedTypes.IS_WALLET_CONNECTED_GETTER]);
75+
const chainOptions = computed<ChainDataItem[]>(() => [
76+
chainsData[Chains.SOLANA],
77+
chainsData[Chains.ROOTSTOCK],
78+
]);
79+
const currentChainOption = computed<ChainDataItem>(() => chainsData[activeChain.value]);
7680
7781
watch(
7882
() => wallet.publicKey?.value,
7983
(newVal, oldVal) => {
84+
if (activeChain.value !== Chains.SOLANA) {
85+
return;
86+
}
8087
if (newVal) {
8188
if (oldVal && newVal.toString() !== oldVal.toString()) {
8289
store.dispatch(SharedTypes.DISCONNECT_WALLET_ACTION);
@@ -89,9 +96,13 @@ watch(
8996
{ immediate: true }
9097
);
9198
92-
const openWalletModal = () => {
99+
const connectWallet = async () => {
93100
trackButtonsEvents(ButtonsActionEventType.MainScreenConnectButtonClicked);
94-
store.dispatch(SharedTypes.CONNECT_MODAL_ACTION, true);
101+
if (activeChain.value === Chains.SOLANA) {
102+
store.dispatch(SharedTypes.CONNECT_MODAL_ACTION, true);
103+
} else {
104+
await store.dispatch(SharedTypes.CONNECT_EVM_WALLET_ACTION);
105+
}
95106
};
96107
97108
const updateWalletModalVisibility = (visible: boolean) => {
@@ -101,10 +112,13 @@ const updateWalletModalVisibility = (visible: boolean) => {
101112
const disconnectWallet = async () => {
102113
trackButtonsEvents(ButtonsActionEventType.MainScreenDisconnectButtonClicked);
103114
try {
104-
if (wallet.connected.value) {
115+
if (activeChain.value === Chains.SOLANA && wallet.connected.value) {
105116
await store.dispatch(SharedTypes.DISCONNECT_WALLET_ACTION);
106117
await wallet.disconnect();
107118
router.push('/');
119+
} else if (activeChain.value === Chains.ROOTSTOCK) {
120+
await store.dispatch(SharedTypes.DISCONNECT_WALLET_ACTION);
121+
router.push('/');
108122
}
109123
} catch (err) {
110124
console.error('Failed to disconnect:', err);
@@ -140,8 +154,17 @@ const onScroll = () => {
140154
}
141155
};
142156
143-
const selectNetworkAction = (item: ChainDataItem) => {
144-
network.value = item;
157+
const selectNetworkAction = async (item: ChainDataItem) => {
158+
if (item.id === activeChain.value) {
159+
return;
160+
}
161+
162+
if (activeChain.value === Chains.SOLANA && wallet.connected.value) {
163+
await store.dispatch(SharedTypes.DISCONNECT_WALLET_ACTION);
164+
await wallet.disconnect();
165+
}
166+
167+
await store.dispatch(SharedTypes.SWITCH_CHAIN_ACTION, item.id as Chains);
145168
};
146169
147170
const toggleMenu = () => {

src/components/app-menu/index.vue

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,19 @@
1818
>
1919
<span>Stake Solana</span>
2020
</router-link>
21+
<router-link
22+
:to="{ name: 'stake' }"
23+
class="stake"
24+
:class="{ disabled: !isStakeRootstockEnabled }"
25+
:disabled="!isStakeRootstockEnabled"
26+
>
27+
<span>Stake Rootstock</span>
28+
</router-link>
2129
<router-link
2230
:to="{ name: 'portfolio' }"
2331
class="portfolio"
24-
:class="{ disabled: !wallet.connected.value }"
25-
:disabled="!wallet.connected.value"
32+
:class="{ disabled: !isPortfolioEnabled }"
33+
:disabled="!isPortfolioEnabled"
2634
>
2735
<portfolio-icon />
2836
<span>My staking portfolio</span>
@@ -61,10 +69,53 @@
6169
import HomeIcon from "@/icons/menu/home-icon.vue";
6270
import PortfolioIcon from "@/icons/menu/portfolio-icon.vue";
6371
import BuyIcon from "@/icons/menu/buy-icon.vue";
72+
import { computed, ref, watch } from "vue";
73+
import { useStore } from "vuex";
6474
import { useWallet } from "solana-wallets-vue";
6575
import { openContactSupport } from "@/utils/browser";
76+
import { SharedTypes } from "@/store/shared/consts";
77+
import { Chains } from "@/core/interfaces";
78+
import { ROOTSTOCK_STRIF_TOKEN_ADDRESS, STRIF_TOKEN_ABI } from "@/core/constants";
79+
import EvmWalletService from "@/core/services/evmWalletService";
80+
import { ethers } from "ethers";
6681
6782
const wallet = useWallet();
83+
const store = useStore();
84+
const evmWalletService = EvmWalletService.getInstance();
85+
const rootstockStakedBalance = ref(0);
86+
const chain = computed(() => store.getters[SharedTypes.CHAIN_GETTER]);
87+
const account = computed(() => store.getters[SharedTypes.WALLET_ACCOUNT_GETTER]);
88+
const rootstockBalance = computed(() => store.getters[SharedTypes.WALLET_BALANCE_GETTER]);
89+
const isPortfolioEnabled = computed(() => {
90+
if (chain.value === Chains.ROOTSTOCK) {
91+
return rootstockStakedBalance.value > 0;
92+
}
93+
return wallet.connected.value;
94+
});
95+
const isStakeRootstockEnabled = computed(() => {
96+
return chain.value === Chains.ROOTSTOCK && !!account.value?.address && rootstockBalance.value > 0;
97+
});
98+
99+
watch([chain, account], async () => {
100+
if (chain.value !== Chains.ROOTSTOCK || !account.value?.address) {
101+
rootstockStakedBalance.value = 0;
102+
return;
103+
}
104+
105+
try {
106+
const provider = evmWalletService.getRpcProvider();
107+
if (!provider) {
108+
rootstockStakedBalance.value = 0;
109+
return;
110+
}
111+
112+
const stRifContract = new ethers.Contract(ROOTSTOCK_STRIF_TOKEN_ADDRESS, STRIF_TOKEN_ABI, provider);
113+
const balance = await stRifContract.balanceOf(account.value.address);
114+
rootstockStakedBalance.value = Number(ethers.utils.formatEther(balance));
115+
} catch {
116+
rootstockStakedBalance.value = 0;
117+
}
118+
}, { immediate: true });
68119
69120
defineProps({
70121
isToggleMenu: {

src/core/api/price.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import { makeRequest } from "@/utils/request";
2-
import { SolanaPriceResponse, SolanaPriceRequest } from "../interfaces/price";
2+
import { TokenMarketDataResponse, TokenMarketDataRequest } from "../interfaces/price";
33

4-
export async function getSolanaPrice(): Promise<SolanaPriceResponse> {
5-
return makeRequest<SolanaPriceRequest, SolanaPriceResponse>({
4+
export async function getTokenMarketData(tokenId: string): Promise<TokenMarketDataResponse> {
5+
return makeRequest<TokenMarketDataRequest, TokenMarketDataResponse>({
66
route: "",
77
externalLink: `https://api-v3.ethvm.dev/`,
88
method: "POST",
99
body: {
1010
operationName: null,
1111
variables: {},
1212
query:
13-
'{\n getCoinGeckoTokenMarketDataByIds(coinGeckoTokenIds: ["solana"]) {\n id\n symbol\n name\n image\n market_cap\n market_cap_rank\n high_24h\n low_24h\n price_change_24h\n price_change_percentage_24h\n sparkline_in_24h {\n price\n }\n price_change_percentage_24h_in_currency\n current_price\n }\n}\n',
13+
`{\n getCoinGeckoTokenMarketDataByIds(coinGeckoTokenIds: ["${tokenId}"]) {\n id\n symbol\n name\n image\n market_cap\n market_cap_rank\n high_24h\n low_24h\n price_change_24h\n price_change_percentage_24h\n sparkline_in_24h {\n price\n }\n price_change_percentage_24h_in_currency\n current_price\n }\n}\n`,
1414
},
1515
});
1616
}

src/core/api/staking.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
GetValidatorSummaryResponse,
1616
GetDelegatorApyResponse,
1717
getDelegatorRewardsResponse,
18+
RootstockAbiResponse,
1819
} from "@/core/interfaces";
1920
import { makeRequest } from "@/utils/request";
2021
import { P2P_VALIDATOR } from "@/core/constants"
@@ -142,3 +143,11 @@ export async function getDelegatorApy(
142143
method: "GET",
143144
});
144145
};
146+
147+
export async function getRootstockAbi(): Promise<RootstockAbiResponse> {
148+
return makeRequest<void, RootstockAbiResponse>({
149+
route: ``,
150+
externalLink: "https://metrics-api.rootstockcollective.xyz/api/rc-abi",
151+
method: "GET",
152+
});
153+
};

0 commit comments

Comments
 (0)