Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

add points UI #125

Merged
merged 15 commits into from
Sep 15, 2024
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
NEXT_PUBLIC_MEMPOOL_API=https://mempool.space
NEXT_PUBLIC_API_URL=https://staking-api.testnet.babylonchain.io
NEXT_PUBLIC_POINTS_API_URL=https://points.testnet.babylonchain.io
NEXT_PUBLIC_NETWORK=signet
NEXT_PUBLIC_DISPLAY_TESTING_MESSAGES=true
1 change: 1 addition & 0 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ jobs:
build-args: |
NEXT_PUBLIC_MEMPOOL_API=${{ vars.NEXT_PUBLIC_MEMPOOL_API }}
NEXT_PUBLIC_API_URL=${{ vars.NEXT_PUBLIC_API_URL }}
NEXT_PUBLIC_POINTS_API_URL=${{ vars.NEXT_PUBLIC_POINTS_API_URL }}
NEXT_PUBLIC_NETWORK=${{ vars.NEXT_PUBLIC_NETWORK }}
NEXT_PUBLIC_DISPLAY_TESTING_MESSAGES=${{ vars.NEXT_PUBLIC_DISPLAY_TESTING_MESSAGES }}

Expand Down
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ ENV NEXT_PUBLIC_MEMPOOL_API=${NEXT_PUBLIC_MEMPOOL_API}
ARG NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}

ARG NEXT_PUBLIC_POINTS_API_URL
ENV NEXT_PUBLIC_POINTS_API_URL=${NEXT_PUBLIC_POINTS_API_URL}

ARG NEXT_PUBLIC_NETWORK
ENV NEXT_PUBLIC_NETWORK=${NEXT_PUBLIC_NETWORK}

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ where,
node queries
- `NEXT_PUBLIC_API_URL` specifies the back-end API to use for the staking
system queries
- `NEXT_PUBLIC_POINTS_API_URL` specifies the Points API to use for the points
system
- `NEXT_PUBLIC_NETWORK` specifies the BTC network environment
- `NEXT_PUBLIC_DISPLAY_TESTING_MESSAGES` boolean value to indicate whether display
testing network related message. Default to true
Expand Down
4 changes: 2 additions & 2 deletions src/app/api/apiWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import axios from "axios";

export const apiWrapper = async (
method: "GET" | "POST",
url: string,
path: string,
generalErrorMessage: string,
params?: any,
timeout?: number,
Expand All @@ -23,7 +23,7 @@ export const apiWrapper = async (
try {
// destructure params in case of post request
response = await handler(
`${process.env.NEXT_PUBLIC_API_URL}${url}`,
`${process.env.NEXT_PUBLIC_API_URL}${path}`,
method === "POST"
? { ...params }
: {
Expand Down
70 changes: 70 additions & 0 deletions src/app/api/getPoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { encode } from "url-safe-base64";

import { Pagination } from "../types/api";

import { pointsApiWrapper } from "./pointsApiWrapper";

export interface StakerPoints {
staker_btc_pk: string;
points: number;
}

export interface DelegationPoints {
staking_tx_hash_hex: string;
staker: {
pk: string;
points: number;
};
finality_provider: {
pk: string;
points: number;
};
staking_height: number;
unbonding_height: number | null;
expiry_height: number;
}

export interface PaginatedDelegationsPoints {
data: DelegationPoints[];
pagination: Pagination;
}

export interface DelegationsPoints {
data: DelegationPoints[];
}

export const getStakersPoints = async (
jeremy-babylonlabs marked this conversation as resolved.
Show resolved Hide resolved
stakerBtcPk: string[],
jeremy-babylonlabs marked this conversation as resolved.
Show resolved Hide resolved
): Promise<StakerPoints[]> => {
const params: Record<string, string> = {};

params.staker_btc_pk =
jeremy-babylonlabs marked this conversation as resolved.
Show resolved Hide resolved
stakerBtcPk.length > 1
? stakerBtcPk.map(encode).join(",")
: encode(stakerBtcPk[0]);

const response = await pointsApiWrapper(
"GET",
"/v1/points/stakers",
"Error getting staker points",
params,
);

return response.data;
};

// Get delegation points by staking transaction hash hex
export const getDelegationPointsByStakingTxHashHexes = async (
stakingTxHashHexes: string[],
): Promise<DelegationsPoints> => {
const response = await pointsApiWrapper(
"POST",
"/v1/points/delegations",
"Error getting delegation points by staking transaction hashes",
{ staking_tx_hash_hex: stakingTxHashHexes },
);

return {
data: response.data,
};
};
45 changes: 45 additions & 0 deletions src/app/api/pointsApiWrapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import axios from "axios";

export const pointsApiWrapper = async (
method: "GET" | "POST",
path: string,
generalErrorMessage: string,
params?: any,
timeout?: number,
) => {
let response;
let handler;
switch (method) {
case "GET":
handler = axios.get;
break;
case "POST":
handler = axios.post;
break;
default:
throw new Error("Invalid method");
}

try {
// destructure params in case of post request
response = await handler(
`${process.env.NEXT_PUBLIC_POINTS_API_URL}${path}`,
method === "POST"
? { ...params }
: {
params,
},
{
timeout: timeout || 0, // 0 is no timeout
},
);
} catch (error) {
if (axios.isAxiosError(error)) {
const message = error?.response?.data?.message;
throw new Error(message || generalErrorMessage);
} else {
throw new Error(generalErrorMessage);
}
}
return response;
};
15 changes: 11 additions & 4 deletions src/app/components/Delegations/Delegation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { FaBitcoin } from "react-icons/fa";
import { IoIosWarning } from "react-icons/io";
import { Tooltip } from "react-tooltip";

import { useHealthCheck } from "@/app/hooks/useHealthCheck";
import { DelegationState, StakingTx } from "@/app/types/delegations";
import { GlobalParamsVersion } from "@/app/types/globalParams";
import { getNetworkConfig } from "@/config/network.config";
Expand All @@ -13,6 +14,8 @@ import { getState, getStateTooltip } from "@/utils/getState";
import { maxDecimals } from "@/utils/maxDecimals";
import { trim } from "@/utils/trim";

import { DelegationPoints } from "../Points/DelegationPoints";

interface DelegationProps {
stakingTx: StakingTx;
stakingValueSat: number;
Expand Down Expand Up @@ -41,6 +44,7 @@ export const Delegation: React.FC<DelegationProps> = ({
}) => {
const { startTimestamp } = stakingTx;
const [currentTime, setCurrentTime] = useState(Date.now());
const { isApiNormal, isGeoBlocked } = useHealthCheck();

useEffect(() => {
const timerId = setInterval(() => {
Expand Down Expand Up @@ -122,7 +126,7 @@ export const Delegation: React.FC<DelegationProps> = ({
<p>overflow</p>
</div>
)}
<div className="grid grid-flow-col grid-cols-2 grid-rows-3 items-center gap-2 lg:grid-flow-row lg:grid-cols-5 lg:grid-rows-1">
<div className="grid grid-flow-col grid-cols-2 grid-rows-3 items-center gap-2 lg:grid-flow-row lg:grid-cols-6 lg:grid-rows-1">
<div className="flex gap-1 items-center order-1">
<FaBitcoin className="text-primary" />
<p>
Expand All @@ -142,13 +146,11 @@ export const Delegation: React.FC<DelegationProps> = ({
{trim(stakingTxHash)}
</a>
</div>
{/* Future data placeholder */}
<div className="order-5 lg:hidden" />
{/*
we need to center the text without the tooltip
add its size 12px and gap 4px, 16/2 = 8px
*/}
<div className="relative flex justify-end lg:left-[8px] lg:justify-center order-4">
<div className="relative flex justify-end lg:justify-center order-4">
<div className="flex items-center gap-1">
<p>{renderState()}</p>
<span
Expand All @@ -162,6 +164,11 @@ export const Delegation: React.FC<DelegationProps> = ({
<Tooltip id={`tooltip-${stakingTxHash}`} className="tooltip-wrap" />
</div>
</div>
{isApiNormal && !isGeoBlocked && (
jrwbabylonlab marked this conversation as resolved.
Show resolved Hide resolved
<div className="relative flex justify-end lg:justify-center order-5">
<DelegationPoints stakingTxHash={stakingTxHash} />
</div>
)}
<div className="order-6">{generateActionButton()}</div>
</div>
</div>
Expand Down
49 changes: 46 additions & 3 deletions src/app/components/Delegations/Delegations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { useLocalStorage } from "usehooks-ts";

import { SignPsbtTransaction } from "@/app/common/utils/psbt";
import { LoadingTableList } from "@/app/components/Loading/Loading";
import { DelegationsPointsProvider } from "@/app/context/api/DelegationsPointsProvider";
import { useError } from "@/app/context/Error/ErrorContext";
import { useHealthCheck } from "@/app/hooks/useHealthCheck";
import { QueryMeta } from "@/app/types/api";
import {
Delegation as DelegationInterface,
Expand Down Expand Up @@ -39,24 +41,62 @@ interface DelegationsProps {
pushTx: WalletProvider["pushTx"];
queryMeta: QueryMeta;
getNetworkFees: WalletProvider["getNetworkFees"];
isWalletConnected: boolean;
}

export const Delegations: React.FC<DelegationsProps> = ({
delegationsAPI,
delegationsLocalStorage,
globalParamsVersion,
publicKeyNoCoord,
btcWalletNetwork,
signPsbtTx,
pushTx,
queryMeta,
getNetworkFees,
address,
btcWalletNetwork,
publicKeyNoCoord,
isWalletConnected,
}) => {
return (
<DelegationsPointsProvider
publicKeyNoCoord={publicKeyNoCoord}
delegationsAPI={delegationsAPI}
isWalletConnected={isWalletConnected}
>
<DelegationsContent
delegationsAPI={delegationsAPI}
delegationsLocalStorage={delegationsLocalStorage}
globalParamsVersion={globalParamsVersion}
signPsbtTx={signPsbtTx}
pushTx={pushTx}
queryMeta={queryMeta}
getNetworkFees={getNetworkFees}
address={address}
btcWalletNetwork={btcWalletNetwork}
publicKeyNoCoord={publicKeyNoCoord}
isWalletConnected={isWalletConnected}
/>
</DelegationsPointsProvider>
);
};

const DelegationsContent: React.FC<DelegationsProps> = ({
delegationsAPI,
delegationsLocalStorage,
globalParamsVersion,
signPsbtTx,
pushTx,
queryMeta,
getNetworkFees,
address,
btcWalletNetwork,
publicKeyNoCoord,
}) => {
const [modalOpen, setModalOpen] = useState(false);
const [txID, setTxID] = useState("");
const [modalMode, setModalMode] = useState<MODE>();
const { showError } = useError();
const { isApiNormal, isGeoBlocked } = useHealthCheck();

// Local storage state for intermediate delegations (withdrawing, unbonding)
const intermediateDelegationsLocalStorageKey =
Expand Down Expand Up @@ -233,11 +273,14 @@ export const Delegations: React.FC<DelegationsProps> = ({
</div>
) : (
<>
<div className="hidden grid-cols-5 gap-2 px-4 lg:grid">
<div className="hidden grid-cols-6 gap-2 px-4 lg:grid">
<p>Amount</p>
<p>Inception</p>
<p className="text-center">Transaction hash</p>
<p className="text-center">Status</p>
{isApiNormal && !isGeoBlocked && (
jeremy-babylonlabs marked this conversation as resolved.
Show resolved Hide resolved
<p className="text-center">Points</p>
)}
<p>Action</p>
</div>
<div
Expand Down
4 changes: 4 additions & 0 deletions src/app/components/FAQ/data/questions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ export const questions = (
title: "Are hardware wallets supported?",
content: `<p>Keystone via QR code is the only hardware wallet supporting Bitcoin Staking. Using any other hardware wallet through any means (such as connection to a software/extension/mobile wallet) can lead to permanent inability to withdraw the stake.</p>`,
},
{
title: "What are the points?",
content: `<p>We use points to track staking activity. Points are not blockchain tokens. Points do not, and may never, convert to, accrue to, be used as a basis to calculate, or become tokens, other digital assets, or distributions thereof. Points are virtual calculations with no monetary value. Points do not constitute any currency or property of any type and are not redeemable, refundable, or transferable.</p>`,
},
];
if (shouldDisplayTestingMsg()) {
questionList.push({
Expand Down
37 changes: 37 additions & 0 deletions src/app/components/Points/DelegationPoints.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from "react";
import { useMediaQuery } from "usehooks-ts";

import { useDelegationsPoints } from "@/app/context/api/DelegationsPointsProvider";

interface DelegationPointsProps {
stakingTxHash: string;
}

export const DelegationPoints: React.FC<DelegationPointsProps> = ({
stakingTxHash,
}) => {
const { delegationPoints, isLoading } = useDelegationsPoints();
const isMobile = useMediaQuery("(max-width:1000px)");
jeremy-babylonlabs marked this conversation as resolved.
Show resolved Hide resolved

const points = delegationPoints.get(stakingTxHash);

if (isLoading) {
jeremy-babylonlabs marked this conversation as resolved.
Show resolved Hide resolved
return (
<div className="flex items-center justify-end gap-1">
<div className="h-5 w-12 animate-pulse rounded bg-gray-300 dark:bg-gray-700"></div>
</div>
);
}

return (
<div className="flex items-center justify-end gap-1">
<p className="whitespace-nowrap">
{isMobile
? `Points: ${points !== undefined ? points : 0}`
: points !== undefined
? points
: 0}
</p>
</div>
);
};
Loading