Skip to content

Commit

Permalink
Get transaction details (request) (#101)
Browse files Browse the repository at this point in the history
  • Loading branch information
avkos authored Jan 21, 2025
1 parent cfd673b commit b282d5e
Show file tree
Hide file tree
Showing 28 changed files with 511 additions and 499 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import type {
CreateTransactionInput,
GetTransactionInput,
} from '@circle-fin/developer-controlled-wallets';
import type { CreateTransactionInput } from '@circle-fin/developer-controlled-wallets';
import type { Meta, StoryObj } from '@storybook/react';

import { Blockchain } from '~/lib/constants';
Expand Down Expand Up @@ -40,7 +37,5 @@ export const Default: Story = {
},
onSendTransaction: (data: CreateTransactionInput) =>
Promise.resolve(data as unknown as Transaction),
onGetTransaction: (data: GetTransactionInput) =>
Promise.resolve({ transaction: data as unknown as Transaction }),
},
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import {
CreateTransactionInput,
GetTransactionInput,
} from '@circle-fin/developer-controlled-wallets';
import { CreateTransactionInput } from '@circle-fin/developer-controlled-wallets';
import { zodResolver } from '@hookform/resolvers/zod';
import { LoaderCircle } from 'lucide-react';
import { useState } from 'react';
Expand All @@ -28,9 +25,8 @@ export interface SendTransactionFormProps {
wallet: Wallet;
balances: WalletTokenBalance[];
onSendTransaction: (data: CreateTransactionInput) => Promise<Transaction | CircleError>;
onGetTransaction: (data: GetTransactionInput) => Promise<{ transaction: Transaction }>;
onScreenAddress?: (address: string) => Promise<ScreenAddressResult>;
onConfirmed?: (data: Transaction) => Promise<void>;
onSent?: (data: Transaction) => void;
}

// @todo: use constant exported from sdk
Expand All @@ -50,8 +46,7 @@ export function SendTransactionForm({
wallet,
balances,
onSendTransaction,
onGetTransaction,
onConfirmed,
onSent,
onScreenAddress,
}: SendTransactionFormProps) {
const [screeningAddressResult, setScreeningAddressResult] =
Expand Down Expand Up @@ -90,21 +85,8 @@ export function SendTransactionForm({

const tx = res as Transaction;
setTransactionData({ state: tx.state } as Transaction);

if (tx.id) {
const interval = setInterval(() => {
const run = async () => {
const { transaction } = await onGetTransaction({ id: tx.id });
setTransactionData(transaction);
if (transaction && !isTransactionPending(transaction)) {
clearInterval(interval);
if (typeof onConfirmed === 'function') {
await onConfirmed(transaction);
}
}
};
run().catch(console.error);
}, 1000);
if (typeof onSent === 'function') {
onSent(tx);
}
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,62 +1,81 @@
import { ReactNode, useMemo } from 'react';

import { ChainLabel } from '~/components/ChainLabel';
import { Badge } from '~/components/ui/badge';
import { TokenItem } from '~/components/TokenItem';
import { TransactionStateText } from '~/components/TransactionStatusText';
import { useGetTransaction } from '~/hooks/useGetTransaction';
import { TransactionType } from '~/lib/constants';
import { formatDate, shortenAddress, shortenHash } from '~/lib/format';
import { Transaction } from '~/lib/types';
import { formatDate, shortenHash } from '~/lib/format';
import { TransactionWithToken } from '~/lib/types';

export interface TransactionDetailsProps {
/** The on-chain transaction */
transaction: Transaction;
transaction: TransactionWithToken;
}

const OneLine = ({ label, value }: { label: string; value: ReactNode | string }) => (
<div className="flex space-x-4 justify-between border-t py-2">
<div className="flex-1">{label}</div>
<div className="text-sm text-muted-foreground">{value}</div>
</div>
);

/** The details of an on-chain transaction */
export function TransactionDetails({ transaction }: TransactionDetailsProps) {
export function TransactionDetails(props: TransactionDetailsProps) {
const getTransactionFilter = useMemo(
() => ({ id: props.transaction?.id }),
[props.transaction],
);
const { data: transaction, reFetch } = useGetTransaction(
getTransactionFilter,
props.transaction,
);
const shortHash = useMemo(
() => (transaction?.txHash ? shortenHash(transaction.txHash) : ''),
[transaction],
);
if (!transaction) {
return null;
}

const isInbound = transaction.transactionType === TransactionType.Inbound;

return (
<div>
<div className="flex items-center gap-6 justify-between">
<Badge variant="outline">{transaction.operation}</Badge>

<div className="flex flex-col">
<p className="text-sm text-muted-foreground">
<strong>From:</strong> {shortenAddress(transaction.sourceAddress)}
</p>
<OneLine label="Hash" value={shortHash} />
<OneLine
label="Status"
value={
<TransactionStateText state={transaction.state} getTransaction={reFetch} />
}
/>
<OneLine label="From" value={transaction.sourceAddress} />
<OneLine label="To" value={transaction.destinationAddress} />
{transaction.token && (
<OneLine label="Token" value={<TokenItem token={transaction.token} />} />
)}

<p className="text-sm text-muted-foreground">
<strong>To:</strong> {shortenAddress(transaction.destinationAddress)}
</p>
</div>

<p className="text-sm text-muted-foreground">
<strong>Amount:</strong>{' '}
<OneLine
label="Amount"
value={
<span
className={`font-medium ${isInbound ? 'text-green-600' : 'text-destructive'}`}
className={`text-right font-medium ${
isInbound ? 'text-green-600' : 'text-destructive'
}`}
>
{isInbound ? '+' : '-'}
{transaction.amounts?.[0] ?? '0.00'}
{isInbound ? '+' : '-'} {transaction.amounts?.[0] ?? '0.00'}
</span>
</p>

{/* <p className="text-sm text-muted-foreground">
<strong>Fee:</strong> {transaction.networkFee}
</p> */}

<Badge variant="secondary">{transaction.state}</Badge>
</div>

<div className="flex items-center gap-6 justify-between">
<ChainLabel blockchain={transaction.blockchain} />

<p className="text-sm text-muted-foreground">
<strong>Date:</strong>{' '}
{formatDate(transaction.firstConfirmDate ?? transaction.createDate)}
</p>

<p className="text-sm text-muted-foreground" title={transaction.txHash}>
<strong>Hash:</strong> {shortenHash(transaction.txHash ?? '')}
</p>
</div>
}
/>
<OneLine
label="Date"
value={formatDate(transaction.firstConfirmDate ?? transaction.createDate)}
/>
<OneLine
label="Blockchain"
value={<ChainLabel blockchain={transaction.blockchain} />}
/>
<OneLine label="Note" value={transaction.refId} />
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { useEffect } from 'react';

import { Badge } from '~/components/ui/badge';
import { TransactionState } from '~/lib/constants';

export interface TransactionStateTextProps {
state: (typeof TransactionState)[keyof typeof TransactionState];
getTransaction?: () => Promise<boolean>;
}

const greenStates = [TransactionState.Complete, TransactionState.Confirmed];
Expand All @@ -16,17 +19,38 @@ const yellowStates = [
const capitalize = (str: string) =>
str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();

export function TransactionStateText({ state }: TransactionStateTextProps) {
export function TransactionStateText({
state,
getTransaction,
}: TransactionStateTextProps) {
useEffect(() => {
if (!greenStates.includes(state) && typeof getTransaction === 'function') {
const interval = setInterval(() => {
getTransaction().catch(console.error);
}, 1000);
return () => clearInterval(interval);
}
}, [state, getTransaction]);

return greenStates.includes(state) ? (
<Badge variant="accent" className="font-normal text-green-600 dark:text-green-500">
<Badge
variant="accent"
className="font-normal text-green-600 dark:text-green-500 whitespace-nowrap"
>
{capitalize(state)}
</Badge>
) : yellowStates.includes(state) ? (
<Badge variant="accent" className="font-normal text-yellow-500 dark:text-yellow-400">
<Badge
variant="accent"
className="font-normal text-yellow-500 dark:text-yellow-400 whitespace-nowrap"
>
{capitalize(state)}
</Badge>
) : (
<Badge variant="accent" className="font-normal text-red-500 dark:text-red-400">
<Badge
variant="accent"
className="font-normal text-red-500 dark:text-red-400 whitespace-nowrap"
>
{capitalize(state)}
</Badge>
);
Expand Down
45 changes: 18 additions & 27 deletions packages/circle-demo-webapp/app/hooks/useCreateWallet.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState } from 'react';
import { useCallback, useState } from 'react';

import { ErrorResponse } from '~/lib/responses';
import { callFetch } from '~/lib/utils';

interface CreateWalletArgs {
walletSetId: string;
Expand All @@ -19,32 +19,23 @@ export const useCreateWallet = (): UseCreateWalletResult => {
const [error, setError] = useState<Error | undefined>(undefined);
const [isLoading, setIsLoading] = useState<boolean>(false);

const createWallet = async (args: CreateWalletArgs) => {
setIsLoading(true);
setError(undefined);
try {
const response = await fetch('/api/createWallet', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(args),
});

if (response.status !== 200) {
const errorData = (await response.json()) as ErrorResponse;
throw new Error(errorData.error);
const createWallet = useCallback(
async (args: CreateWalletArgs) => {
setIsLoading(true);
setError(undefined);
try {
await callFetch('/api/createWallet', args);
return true;
} catch (err) {
setError(err as Error);

return false;
} finally {
setIsLoading(false);
}

return true;
} catch (err) {
setError(err as Error);

return false;
} finally {
setIsLoading(false);
}
};
},
[setError, setIsLoading],
);

return {
error,
Expand Down
45 changes: 18 additions & 27 deletions packages/circle-demo-webapp/app/hooks/useCreateWalletSet.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState } from 'react';
import { useCallback, useState } from 'react';

import { ErrorResponse } from '~/lib/responses';
import { callFetch } from '~/lib/utils';

interface CreateWalletSetArgs {
name: string;
Expand All @@ -16,32 +16,23 @@ export const useCreateWalletSet = (): UseCreateWalletSetResult => {
const [error, setError] = useState<Error | undefined>(undefined);
const [isLoading, setIsLoading] = useState<boolean>(false);

const createWalletSet = async (args: CreateWalletSetArgs) => {
setIsLoading(true);
setError(undefined);
try {
const response = await fetch('/api/createWalletSet', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(args),
});

if (response.status !== 200) {
const errorData = (await response.json()) as ErrorResponse;
throw new Error(errorData.error);
const createWalletSet = useCallback(
async (args: CreateWalletSetArgs) => {
setIsLoading(true);
setError(undefined);
try {
await callFetch('/api/createWalletSet', args);
return true;
} catch (err) {
setError(err as Error);

return false;
} finally {
setIsLoading(false);
}

return true;
} catch (err) {
setError(err as Error);

return false;
} finally {
setIsLoading(false);
}
};
},
[setError, setIsLoading],
);

return {
error,
Expand Down
Loading

0 comments on commit b282d5e

Please sign in to comment.