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

Transaction progress information #571

Merged
merged 14 commits into from
Nov 16, 2023
5 changes: 5 additions & 0 deletions packages/apps/public/images/finished.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions packages/apps/public/images/notification/progress.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion packages/apps/src/bridges/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export abstract class BaseBridge {
}

formatEstimateTime() {
return `${this.estimateTime.min}-${this.estimateTime.max} Minutes`;
return `${this.estimateTime.min}~${this.estimateTime.max} minutes`;
}

getTxGasLimit() {
Expand Down
6 changes: 1 addition & 5 deletions packages/apps/src/bridges/lnbridge-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,13 @@ export class LnBridgeBase extends BaseBridge {
symbol: "helix-symbol.svg",
};
this.name = "Helix LnBridge";
this.estimateTime = { min: 0, max: 1 };
this.estimateTime = { min: 1, max: 2 };
}

isLnBridge() {
return true;
}

formatEstimateTime(): string {
return "Within 1 minute";
}

async getFee(args?: { baseFee?: bigint; protocolFee?: bigint; liquidityFeeRate?: bigint; transferAmount?: bigint }) {
if (this.sourceToken) {
return {
Expand Down
77 changes: 77 additions & 0 deletions packages/apps/src/components/progress-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"use client";
import { Subscription, interval } from "rxjs";
import { CSSProperties, PropsWithChildren, forwardRef, useEffect, useRef } from "react";

interface Props {
percent?: number; // Value between 0 and 100
}

export default function ProgressIcon({ percent = 100 }: Props) {
const leftRef = useRef<HTMLSpanElement | null>(null);
const rightRef = useRef<HTMLSpanElement | null>(null);

useEffect(() => {
let sub$$: Subscription | undefined;

const leftAnimation = leftRef.current?.getAnimations().at(0);
const rightAnimation = rightRef.current?.getAnimations().at(0);

if (leftAnimation && rightAnimation) {
sub$$ = interval(300).subscribe(() => {
const leftProgress = (leftAnimation?.effect?.getComputedTiming().progress || 0) * 100 + 50;
const rightProgress = (rightAnimation?.effect?.getComputedTiming().progress || 0) * 100;

if (percent <= 50 && percent <= rightProgress) {
leftAnimation.pause();
rightAnimation.pause();
} else if (50 < percent && percent <= leftProgress) {
leftAnimation.pause();
rightAnimation.pause();
} else {
leftAnimation.play();
rightAnimation.play();
}
});
}

return () => sub$$?.unsubscribe();
}, [percent]);

return (
<div className="border-primary h-5 w-5 rounded-full border-2 p-[2px]">
<Ouro>
<span className="absolute left-0 h-full w-1/2 overflow-hidden">
<Anim
className="animate-progress-anim-left left-full rounded-l-none"
style={{ transformOrigin: "0 50% 0" }}
ref={leftRef}
/>
</span>
<span className="absolute left-1/2 h-full w-1/2 overflow-hidden">
<Anim
className="animate-progress-anim-right -left-full rounded-r-none"
style={{ transformOrigin: "100% 50% 0" }}
ref={rightRef}
/>
</span>
</Ouro>
</div>
);
}

function Ouro({ children }: PropsWithChildren<unknown>) {
return <div className={`relative h-full w-full`}>{children}</div>;
}

const Anim = forwardRef<HTMLSpanElement, { className?: string; style?: CSSProperties }>(function Anim(
{ className, style },
ref,
) {
return (
<span
ref={ref}
className={`bg-primary absolute top-0 h-full w-full rounded-full ${className}`}
style={style}
></span>
);
});
121 changes: 94 additions & 27 deletions packages/apps/src/components/transfer-modal.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import { BaseBridge } from "@/bridges/base";
import { ChainToken } from "@/types/misc";
import { RelayersResponseData } from "@/types/graphql";
import { RecordStatus, RelayersResponseData, TxProgressResponseData, TxProgressVariables } from "@/types/graphql";
import { formatBalance } from "@/utils/balance";
import { getChainLogoSrc } from "@/utils/misc";
import { ApolloQueryResult } from "@apollo/client";
import { ApolloQueryResult, useQuery } from "@apollo/client";
import Image from "next/image";
import { useCallback, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { TransferValue } from "./transfer-input";
import { notification } from "@/ui/notification";
import { parseUnits } from "viem";
import { Address, parseUnits } from "viem";
import { Token } from "@/types/token";
import { useTransfer } from "@/hooks/use-transfer";
import dynamic from "next/dynamic";
import ProgressIcon from "./progress-icon";
import { QUERY_TX_PROGRESS } from "@/config/gql";
import Link from "next/link";

const Modal = dynamic(() => import("@/ui/modal"), { ssr: false });
interface Props {
Expand All @@ -34,7 +37,15 @@ export default function TransferModal({
refetchRelayers,
}: Props) {
const { bridgeClient, sourceValue, targetValue, fee, transfer } = useTransfer();
const [txHash, setTxHash] = useState("");
const [busy, setBusy] = useState(false);
const [disabled, setDisabled] = useState(false);

const { data: txProgressData } = useQuery<TxProgressResponseData, TxProgressVariables>(QUERY_TX_PROGRESS, {
variables: { txHash },
pollInterval: txHash ? 300 : 0,
skip: !txHash,
});

const handleTransfer = useCallback(async () => {
if (sender && recipient && bridgeClient) {
Expand All @@ -59,6 +70,8 @@ export default function TransferModal({
});

if (receipt?.status === "success") {
setTxHash(receipt.transactionHash);
setDisabled(true);
onSuccess();
}
} catch (err) {
Expand All @@ -70,46 +83,71 @@ export default function TransferModal({
}
}, [bridgeClient, onSuccess, recipient, refetchRelayers, sender, transfer, transferValue]);

// Reset state
useEffect(() => {
if (isOpen) {
//
} else {
setTxHash("");
setBusy(false);
setDisabled(false);
}
}, [isOpen]);

return (
<Modal
title="Confirm Transfer"
title="Transfer Summary"
isOpen={isOpen}
className="w-full lg:w-[38rem]"
className="w-full lg:w-[34rem]"
okText="Confirm"
disabledCancel={busy}
disabledCancel={busy || disabled}
disabledOk={disabled}
busy={busy}
forceFooterHidden={txHash ? true : false}
onClose={onClose}
onCancel={onClose}
onOk={handleTransfer}
>
{/* from-to */}
<div className="gap-small flex flex-col">
<SourceTarget type="source" chainToken={sourceValue} transferValue={transferValue} />
<SourceTarget type="source" address={sender} chainToken={sourceValue} transferValue={transferValue} />
<div className="relative">
<div className="absolute bottom-0 left-0 right-0 top-0 flex items-center justify-center">
<Image width={36} height={36} alt="Transfer to" src="images/transfer-to.svg" className="shrink-0" />
</div>
</div>
<SourceTarget type="target" chainToken={targetValue} transferValue={transferValue} />
<SourceTarget type="target" address={recipient} chainToken={targetValue} transferValue={transferValue} />
</div>

{/* information */}
<div className="gap-middle flex flex-col">
<span className="text-sm font-normal text-white">Information</span>
<Information fee={fee} bridge={bridgeClient} sender={sender} recipient={recipient} />
<Information fee={fee} bridge={bridgeClient} />
</div>

{txHash ? (
<div className="px-middle bg-app-bg flex h-10 items-center rounded">
<Progress
confirmedBlocks={txProgressData?.historyRecordByTxHash?.confirmedBlocks}
result={txProgressData?.historyRecordByTxHash?.result}
id={txProgressData?.historyRecordByTxHash?.id}
/>
</div>
) : null}
</Modal>
);
}

function SourceTarget({
type,
address,
transferValue,
chainToken,
}: {
type: "source" | "target";
transferValue: TransferValue;
chainToken?: ChainToken | null;
address?: Address | null;
}) {
return chainToken ? (
<div className="bg-app-bg p-middle flex items-center justify-between rounded lg:p-5">
Expand All @@ -124,9 +162,7 @@ function SourceTarget({
/>
<div className="flex flex-col items-start">
<span className="text-base font-medium text-white">{chainToken.chain.name}</span>
<span className="text-sm font-medium text-white/40">
{type === "source" ? "Source Chain" : "Target Chain"}
</span>
<span className="text-sm font-medium text-white/50">{address}</span>
</div>
</div>

Expand All @@ -142,22 +178,9 @@ function SourceTarget({
) : null;
}

function Information({
fee,
bridge,
sender,
recipient,
}: {
fee?: { value: bigint; token: Token };
bridge?: BaseBridge | null;
sender?: string | null;
recipient?: string | null;
}) {
function Information({ fee, bridge }: { fee?: { value: bigint; token: Token }; bridge?: BaseBridge | null }) {
return (
<div className="p-middle bg-app-bg gap-small flex flex-col rounded">
<Item label="Bridge" value={bridge?.getName()} />
<Item label="From" value={sender} />
<Item label="To" value={recipient} />
<Item
label="Transaction Fee"
value={
Expand All @@ -179,3 +202,47 @@ function Item({ label, value }: { label: string; value?: string | null }) {
</div>
);
}

function Progress({
confirmedBlocks,
result,
id,
}: {
confirmedBlocks: string | null | undefined;
result: RecordStatus | null | undefined;
id: string | null | undefined;
}) {
const splited = confirmedBlocks?.split("/");
if (splited?.length === 2) {
const finished = Number(splited[0]);
const total = Number(splited[1]);

if (finished === total || result === RecordStatus.SUCCESS) {
return (
<div className="flex w-full items-center justify-between">
<div className="inline-flex">
<span className="text-sm font-medium">LnProvider relay finished. Go to&nbsp;</span>
<Link href={`/records/${id}`} className="text-primary text-sm font-medium hover:underline">
Detail
</Link>
</div>
<Image width={20} height={20} alt="Finished" src="/images/finished.svg" />
</div>
);
} else {
return (
<div className="flex w-full items-center justify-between">
<span className="text-sm font-medium">{`Waiting for LnProvider relay message(${confirmedBlocks})`}</span>
<ProgressIcon percent={(finished * 100) / total} />
</div>
);
}
} else {
return (
<div className="flex w-full items-center justify-between">
<span className="text-sm font-medium">Waiting for indexing...</span>
<ProgressIcon percent={10} />
</div>
);
}
}
1 change: 0 additions & 1 deletion packages/apps/src/components/transfer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,6 @@ export default function Transfer() {
onClose={setIsOpenFalse}
onSuccess={() => {
setTransferValue({ value: "", formatted: 0n });
setIsOpenFalse();
}}
refetchRelayers={refetchRelayers}
/>
Expand Down
10 changes: 10 additions & 0 deletions packages/apps/src/config/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,13 @@ export const QUERY_SPECIAL_RELAYER = gql`
}
}
`;

export const QUERY_TX_PROGRESS = gql`
query historyRecordByTxHash($txHash: String) {
historyRecordByTxHash(txHash: $txHash) {
confirmedBlocks
result
id
}
}
`;
8 changes: 8 additions & 0 deletions packages/apps/src/types/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,11 @@ export interface SpecialRelayerVariables {
bridge?: BridgeCategory;
relayer?: string;
}

export interface TxProgressResponseData {
historyRecordByTxHash: Pick<HistoryRecord, "confirmedBlocks" | "result" | "id"> | null;
}

export interface TxProgressVariables {
txHash: string;
}
Loading
Loading