Skip to content

Commit

Permalink
Implement reward heatmap
Browse files Browse the repository at this point in the history
Configure infura provider
  • Loading branch information
jmrossy committed Dec 26, 2023
1 parent 7de4c53 commit b984552
Show file tree
Hide file tree
Showing 18 changed files with 262 additions and 42 deletions.
2 changes: 1 addition & 1 deletion next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const securityHeaders = [
key: 'Content-Security-Policy',
value: `default-src 'self'; script-src 'self'${
isDev ? " 'unsafe-eval' 'unsafe-inline'" : ''
}; connect-src 'self' https://*.celo.org https://*.celoscan.io https://*.walletconnect.com wss://walletconnect.celo.org wss://*.walletconnect.com wss://*.walletconnect.org https://raw.githubusercontent.com; img-src 'self' data: https://raw.githubusercontent.com https://*.walletconnect.com; style-src 'self' 'unsafe-inline' https://*.googleapis.com; font-src 'self' data:; base-uri 'self'; form-action 'self'; frame-src 'self' https://*.walletconnect.com https://*.walletconnect.org;`,
}; connect-src 'self' https://*.celo.org https://*.celoscan.io https://*.walletconnect.com wss://walletconnect.celo.org wss://*.walletconnect.com wss://*.walletconnect.org https://raw.githubusercontent.com https://celo-mainnet.infura.io; img-src 'self' data: https://raw.githubusercontent.com https://*.walletconnect.com; style-src 'self' 'unsafe-inline' https://*.googleapis.com; font-src 'self' data:; base-uri 'self'; form-action 'self'; frame-src 'self' https://*.walletconnect.com https://*.walletconnect.org;`,
},
]

Expand Down
49 changes: 46 additions & 3 deletions src/app/staking/[address]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,30 @@ import { OutlineButton } from 'src/components/buttons/OutlineButton';
import { SolidButton } from 'src/components/buttons/SolidButton';
import { TabHeaderButton } from 'src/components/buttons/TabHeaderButton';
import { TextLink } from 'src/components/buttons/TextLink';
import { HeatmapSquares } from 'src/components/charts/Heatmap';
import { ArrowIcon } from 'src/components/icons/Arrow';
import { Circle } from 'src/components/icons/Circle';
import { Identicon } from 'src/components/icons/Identicon';
import { Section } from 'src/components/layout/Section';
import { Twitter } from 'src/components/logos/Twitter';
import { Web } from 'src/components/logos/Web';
import { formatNumberString } from 'src/components/numbers/Amount';
import { ZERO_ADDRESS } from 'src/config/consts';
import { EPOCH_DURATION_MS, ZERO_ADDRESS } from 'src/config/consts';
import { VALIDATOR_GROUPS } from 'src/config/validators';
import { ValidatorGroupLogo } from 'src/features/validators/ValidatorGroupLogo';
import { ValidatorGroup, ValidatorStatus } from 'src/features/validators/types';
import { useGroupRewardHistory } from 'src/features/validators/useGroupRewardHistory';
import { useValidatorGroups } from 'src/features/validators/useValidatorGroups';
import { useValidatorStakers } from 'src/features/validators/useValidatorStakers';
import { Color } from 'src/styles/Color';
import { useIsMobile } from 'src/styles/mediaQueries';
import { eqAddressSafe, shortenAddress } from 'src/utils/addresses';
import { fromWei, fromWeiRounded } from 'src/utils/amount';
import { useCopyHandler } from 'src/utils/clipboard';
import { objLength } from 'src/utils/objects';

const HEATMAP_SIZE = 100;

export const dynamicParams = true;

export default function Page({ params: { address } }: { params: { address: Address } }) {
Expand All @@ -47,8 +52,9 @@ export default function Page({ params: { address } }: { params: { address: Addre

return (
<Section>
<div className="space-y-12 px-2">
<div className="space-y-8 px-2">
<HeaderSection group={group} />
<HeatmapSection group={group} />
<DetailsSection group={group} />
</div>
</Section>
Expand All @@ -60,6 +66,8 @@ function HeaderSection({ group }: { group?: ValidatorGroup }) {
const webUrl = VALIDATOR_GROUPS[address]?.url;
const twitterUrl = VALIDATOR_GROUPS[address]?.twitter;

const onClickAddress = useCopyHandler(group?.address);

return (
<div>
<TextLink href="/staking" className="font-medium text-taupe-600">
Expand All @@ -74,7 +82,7 @@ function HeaderSection({ group }: { group?: ValidatorGroup }) {
<div>
<h1 className="font-serif text-4xl">{group?.name || '...'}</h1>
<div className=" mt-2 flex items-center space-x-3">
<OutlineButton className="all:border-black all:font-normal">
<OutlineButton className="all:border-black all:font-normal" onClick={onClickAddress}>
{shortenAddress(address)}
</OutlineButton>
{webUrl && (
Expand Down Expand Up @@ -102,6 +110,41 @@ function HeaderSection({ group }: { group?: ValidatorGroup }) {
);
}

function HeatmapSection({ group }: { group?: ValidatorGroup }) {
const { rewardHistory } = useGroupRewardHistory(group?.address, HEATMAP_SIZE);

const data = useMemo(() => {
const hasReward = Array(HEATMAP_SIZE).fill(false);
if (!rewardHistory?.length) return hasReward;
const startTimestamp = Date.now() - EPOCH_DURATION_MS * HEATMAP_SIZE;
for (let i = 0; i < rewardHistory.length; i++) {
if (rewardHistory[i].timestamp < startTimestamp) continue;
const epochIndex = Math.floor(
(rewardHistory[i].timestamp - startTimestamp) / EPOCH_DURATION_MS,
);
hasReward[epochIndex] = true;
}
return hasReward;
}, [rewardHistory]);

return (
<div className="space-y-2 border border-taupe-300 p-2">
<h3>Reward payments (last 100 days)</h3>
<HeatmapSquares data={data} rows={4} columns={25} />
<div className="ml-px flex space-x-10">
<div className="flex items-center">
<div className="bg-green-700 h-3 w-3"></div>
<label className="ml-2 text-sm">Reward Paid</label>
</div>
<div className="flex items-center">
<div className="h-3 w-3 bg-gray-400"></div>
<label className="ml-2 text-sm">No Reward</label>
</div>
</div>
</div>
);
}

function DetailsSection({ group }: { group?: ValidatorGroup }) {
const [tab, setTab] = useState<'members' | 'stakers'>('members');

Expand Down
32 changes: 32 additions & 0 deletions src/components/charts/Heatmap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import clsx from 'clsx';

// A simple grid of squares similar to Github's contribution graph
export function HeatmapSquares({
rows,
columns,
data,
}: {
rows: number;
columns: number;
data: any[];
}) {
return (
<div
style={{
gridTemplateRows: `repeat(${rows}, minmax(0, 1fr))`,
gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
}}
className="grid w-full gap-1"
>
{data.map((value, index) => (
<div
key={index}
className={clsx(
'h-3 w-3 rounded-[1px] border border-gray-300 transition-all duration-1000 sm:h-4 sm:w-4 md:h-5 md:w-5',
value ? 'bg-green-700' : 'bg-gray-400',
)}
/>
))}
</div>
);
}
9 changes: 8 additions & 1 deletion src/components/nav/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Image from 'next/image';
import { ExternalLink } from 'src/components/buttons/ExternalLink';
import { links } from 'src/config/links';
import Discord from 'src/images/logos/discord.svg';
import Github from 'src/images/logos/github.svg';
Expand All @@ -13,7 +14,13 @@ export function Footer() {
<FooterIconLink to={links.twitter} imgSrc={Twitter} alt="Twitter" />
<FooterIconLink to={links.discord} imgSrc={Discord} alt="Discord" />
</div>
<BlockNumber />
<div className="flex items-center space-x-1">
<div className="text-xs text-taupe-400">
Powered by <ExternalLink href={links.celoscan}>CeloScan</ExternalLink> and{' '}
<ExternalLink href="https://docs.celo.org/network/node/forno">Forno</ExternalLink> |
</div>
<BlockNumber />
</div>
</div>
);
}
Expand Down
3 changes: 3 additions & 0 deletions src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,21 @@ interface Config {
walletConnectProjectId: string;
fornoApiKey: string;
celoscanApiKey: string;
infuraApiKey: string;
}

const isDevMode = process?.env?.NODE_ENV === 'development';
const version = process?.env?.NEXT_PUBLIC_VERSION ?? null;
const walletConnectProjectId = process?.env?.NEXT_PUBLIC_WALLET_CONNECT_ID || '';
const fornoApiKey = process?.env?.NEXT_PUBLIC_FORNO_API_KEY || '';
const celoscanApiKey = process?.env?.NEXT_PUBLIC_CELOSCAN_API_KEY || '';
const infuraApiKey = process?.env?.NEXT_PUBLIC_INFURA_API_KEY || '';

export const config: Config = Object.freeze({
debug: isDevMode,
version,
walletConnectProjectId,
fornoApiKey,
celoscanApiKey,
infuraApiKey,
});
2 changes: 2 additions & 0 deletions src/config/consts.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
export const DEFAULT_DISPLAY_DECIMALS = 2;
export const DEFAULT_TOKEN_DECIMALS = 18;
export const AVG_BLOCK_TIMES_MS = 5_000; // 5 seconds
export const EPOCH_DURATION_MS = 86_400_000; // 1 day

// From the Election contract electableValidators config
export const MAX_NUM_ELECTABLE_VALIDATORS = 110;
Expand Down
10 changes: 7 additions & 3 deletions src/config/links.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
export const links = {
home: 'https://celostation.org',
celo: 'https://celo.org',
blockscout: 'https://explorer.celo.org',
celoscan: 'https://celoscan.io',
celoscanApi: 'https://api.celoscan.io',
discord: 'https://discord.gg/celo',
github: 'https://github.com/jmrossy/celo-station',
twitter: 'https://twitter.com/CeloOrg',
// RPCs
forno: 'https://forno.celo.org',
infura: 'https://celo-mainnet.infura.io/v3',
// Explorers
blockscout: 'https://explorer.celo.org',
celoscan: 'https://celoscan.io',
celoscanApi: 'https://api.celoscan.io',
};
21 changes: 13 additions & 8 deletions src/config/wagmi.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,33 @@ import {
trustWallet,
walletConnectWallet,
} from '@rainbow-me/rainbowkit/wallets';
import { infuraProvider } from '@wagmi/core/providers/infura';
import { jsonRpcProvider } from '@wagmi/core/providers/jsonRpc';
import 'react-toastify/dist/ReactToastify.css';
import { config } from 'src/config/config';
import { links } from 'src/config/links';
import { Color } from 'src/styles/Color';
import { WagmiConfig, configureChains, createConfig } from 'wagmi';

const fornoRpc = 'https://forno.celo.org?apikey=${config.fornoApiKey}';
const infuraRpc = 'https://mainnet.infura.io/v3';
export const fornoRpcUrl = `${links.forno}?apikey=${config.fornoApiKey}`;
export const infuraRpcUrl = `${links.infura}/${config.infuraApiKey}`;
const { chains, publicClient } = configureChains(
[
{
...Celo,
rpcUrls: {
default: { http: [fornoRpc] },
infura: { http: [infuraRpc] },
public: { http: [fornoRpc] },
default: { http: [fornoRpcUrl] },
infura: { http: [links.infura] },
public: { http: [fornoRpcUrl] },
},
},
],
[jsonRpcProvider({ rpc: (chain) => ({ http: chain.rpcUrls.default.http[0] }) })],
[
jsonRpcProvider({ rpc: (chain) => ({ http: chain.rpcUrls.default.http[0] }) }),
infuraProvider({ apiKey: config.infuraApiKey }),
],
{},
);

export const wagmiChains = chains;

const connectorConfig = {
Expand Down Expand Up @@ -65,7 +70,7 @@ export function WagmiContext({ children }: { children: React.ReactNode }) {
return (
<WagmiConfig config={wagmiConfig}>
<RainbowKitProvider
chains={wagmiChains}
chains={chains}
theme={lightTheme({
accentColor: Color.Fig,
borderRadius: 'small',
Expand Down
14 changes: 5 additions & 9 deletions src/features/staking/rewards/fetchStakeHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,20 @@ import { links } from 'src/config/links';
import { queryCeloscan } from 'src/features/explorers/celoscan';
import { TransactionLog } from 'src/features/explorers/types';
import { StakeEvent, StakeEventType } from 'src/features/staking/types';
import { ensure0x, eqAddress, isValidAddress, strip0x } from 'src/utils/addresses';
import { ensure0x, eqAddress, isValidAddress } from 'src/utils/addresses';
import { logger } from 'src/utils/logger';
import { isNullish } from 'src/utils/typeof';
import { decodeEventLog } from 'viem';
import { decodeEventLog, pad } from 'viem';

//ValidatorGroupVoteActivated(address,address,uint256,uint256)
// Keccak-256 ValidatorGroupVoteActivated(address,address,uint256,uint256)
const VOTE_ACTIVATED_TOPIC_0 = '0x45aac85f38083b18efe2d441a65b9c1ae177c78307cb5a5d4aec8f7dbcaeabfe';
//ValidatorGroupActiveVoteRevoked(address,address,uint256,uint256)
// Keccak-256 ValidatorGroupActiveVoteRevoked(address,address,uint256,uint256)
const VOTE_REVOKED_TOPIC_0 = '0xae7458f8697a680da6be36406ea0b8f40164915ac9cc40c0dad05a2ff6e8c6a8';

export async function fetchStakeEvents(accountAddress: Address, fromBlockNumber?: number) {
const electionAddress = Addresses.Election;
const fromBlock = fromBlockNumber ? fromBlockNumber : 100; // Not using block 0 here because of some explorers have issues with incorrect txs in low blocks
const topic1 = getPaddedAddress(accountAddress).toLowerCase();
const topic1 = pad(accountAddress).toLowerCase();
const baseUrl = `${links.celoscanApi}/api?module=logs&action=getLogs&fromBlock=${fromBlock}&toBlock=latest&address=${electionAddress}&topic1=${topic1}&topic0_1_opr=and`;

const activateLogsUrl = `${baseUrl}&topic0=${VOTE_ACTIVATED_TOPIC_0}`;
Expand Down Expand Up @@ -83,7 +83,3 @@ export function parseStakeLogs(
}
return stakeEvents;
}

function getPaddedAddress(address: Address) {
return ensure0x(strip0x(address).padStart(64, '0'));
}
1 change: 1 addition & 0 deletions src/features/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface AppState {
failUnconfirmedTransactions: () => void;
}

// TODO is a store needed?
export const useStore = create<AppState>()(
persist(
(set) => ({
Expand Down
Loading

0 comments on commit b984552

Please sign in to comment.