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
Merged
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
5 changes: 3 additions & 2 deletions src/app/api/apiWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import axios from "axios";

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

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

import { apiWrapper } from "./apiWrapper";

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[];
}

// Get delegation points by staker BTC public key
export const getDelegationPointsByStakerBtcPk = async (
jeremy-babylonlabs marked this conversation as resolved.
Show resolved Hide resolved
stakerBtcPk: string,
paginationKey?: string,
): Promise<PaginatedDelegationsPoints> => {
jeremy-babylonlabs marked this conversation as resolved.
Show resolved Hide resolved
const params: Record<string, string> = {
staker_btc_pk: encode(stakerBtcPk),
};

if (paginationKey && paginationKey !== "") {
params.pagination_key = encode(paginationKey);
}

const response = await apiWrapper(
"GET",
process.env.NEXT_PUBLIC_POINTS_API_URL || "",
"/v1/points/staker/delegations",
"Error getting delegation points by staker BTC public key",
params,
);

return {
data: response.data.data,
pagination: response.data.pagination,
};
};

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

return {
data: response.data,
};
};
1 change: 1 addition & 0 deletions src/app/api/getDelegations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export const getDelegations = async (

const response = await apiWrapper(
"GET",
process.env.NEXT_PUBLIC_API_URL || "",
jeremy-babylonlabs marked this conversation as resolved.
Show resolved Hide resolved
"/v1/staker/delegations",
"Error getting delegations",
params,
Expand Down
1 change: 1 addition & 0 deletions src/app/api/getFinalityProviders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const getFinalityProviders = async (

const response = await apiWrapper(
"GET",
process.env.NEXT_PUBLIC_API_URL || "",
"/v1/finality-providers",
"Error getting finality providers",
params,
Expand Down
1 change: 1 addition & 0 deletions src/app/api/getGlobalParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ interface GlobalParamsDataResponse {
export const getGlobalParams = async (): Promise<GlobalParamsVersion[]> => {
const { data } = (await apiWrapper(
"GET",
process.env.NEXT_PUBLIC_API_URL || "",
"/v1/global-params",
"Error getting global params",
)) as AxiosResponse<{ data: GlobalParamsDataResponse }>;
Expand Down
1 change: 1 addition & 0 deletions src/app/api/getStakers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const getStakers = async (): Promise<Stakers> => {

const response = await apiWrapper(
"GET",
process.env.NEXT_PUBLIC_API_URL || "",
"/v1/stats/staker",
"Error getting stakers",
params,
Expand Down
29 changes: 29 additions & 0 deletions src/app/api/getStakersPoints.ts
jeremy-babylonlabs marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { encode } from "url-safe-base64";

import { apiWrapper } from "./apiWrapper";

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

export const getStakersPoints = async (
stakerBtcPk: string[],
): Promise<StakerPoints[]> => {
const params: Record<string, string> = {};

params.staker_btc_pk =
stakerBtcPk.length > 1
? stakerBtcPk.map(encode).join(",")
: encode(stakerBtcPk[0]);

const response = await apiWrapper(
jeremy-babylonlabs marked this conversation as resolved.
Show resolved Hide resolved
"GET",
process.env.NEXT_PUBLIC_POINTS_API_URL || "",
"/v1/points/stakers",
"Error getting staker points",
params,
);

return response.data;
};
7 changes: 6 additions & 1 deletion src/app/api/getStats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ interface StatsAPI {
}

export const getStats = async (): Promise<StakingStats> => {
const response = await apiWrapper("GET", "/v1/stats", "Error getting stats");
const response = await apiWrapper(
"GET",
process.env.NEXT_PUBLIC_API_URL || "",
"/v1/stats",
"Error getting stats",
);
const statsAPIResponse: StatsAPIResponse = response.data;
const statsAPI: StatsAPI = statsAPIResponse.data;

Expand Down
1 change: 1 addition & 0 deletions src/app/api/getUnbondingEligibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const getUnbondingEligibility = async (txID: string) => {

const response = await apiWrapper(
"GET",
process.env.NEXT_PUBLIC_API_URL || "",
"/v1/unbonding/eligibility",
"Error checking unbonding eligibility",
params,
Expand Down
1 change: 1 addition & 0 deletions src/app/api/postFilterOrdinals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const postVerifyUtxoOrdinals = async (
utxoChunks.map((chunk) =>
apiWrapper(
"POST",
process.env.NEXT_PUBLIC_API_URL || "",
"/v1/ordinals/verify-utxos",
"Error verifying utxos ordinals",
{
Expand Down
1 change: 1 addition & 0 deletions src/app/api/postUnbonding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const postUnbonding = async (

const response = await apiWrapper(
"POST",
process.env.NEXT_PUBLIC_API_URL || "",
"/v1/unbonding",
"Error submitting unbonding request",
payload,
Expand Down
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
Loading