Skip to content

Commit 3a9824e

Browse files
creed-victorgrinry
andauthored
SOV-5223: tx dialog (#14)
* wip: tx dialog * wip: sign message and typed data * chore: transaction confirmation flow * feat: add examples * fix: imports * fix: review comments * fix: review comments * fix: invalid handler --------- Co-authored-by: Rytis Grincevicius <rytis.grincevicius@gmail.com>
1 parent 0fc33ab commit 3a9824e

File tree

27 files changed

+2383
-273
lines changed

27 files changed

+2383
-273
lines changed

apps/web-app/package.json

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,24 @@
33
"private": true,
44
"type": "module",
55
"dependencies": {
6-
"@i18next-selector/vite-plugin": "^0.0.18",
6+
"@i18next-selector/vite-plugin": "0.0.18",
77
"@privy-io/react-auth": "3.0.1",
88
"@privy-io/wagmi": "2.0.0",
9-
"@radix-ui/react-checkbox": "^1.3.3",
10-
"@radix-ui/react-collapsible": "^1.1.12",
11-
"@radix-ui/react-dialog": "^1.1.15",
12-
"@radix-ui/react-dropdown-menu": "^2.1.16",
9+
"@radix-ui/react-checkbox": "1.3.3",
10+
"@radix-ui/react-collapsible": "1.1.12",
11+
"@radix-ui/react-dialog": "1.1.15",
12+
"@radix-ui/react-dropdown-menu": "2.1.16",
1313
"@radix-ui/react-label": "2.1.7",
1414
"@radix-ui/react-select": "2.2.6",
1515
"@radix-ui/react-slider": "1.3.6",
1616
"@radix-ui/react-slot": "1.2.3",
1717
"@radix-ui/react-switch": "1.2.6",
18-
"@radix-ui/react-tabs": "^1.1.13",
19-
"@radix-ui/react-tooltip": "^1.2.8",
18+
"@radix-ui/react-tabs": "1.1.13",
19+
"@radix-ui/react-tooltip": "1.2.8",
2020
"@sovryn/slayer-sdk": "workspace:*",
2121
"@sovryn/slayer-shared": "workspace:*",
2222
"@tailwindcss/vite": "4.1.13",
23-
"@tanstack/devtools-vite": "^0.3.3",
23+
"@tanstack/devtools-vite": "0.3.3",
2424
"@tanstack/react-devtools": "0.7.0",
2525
"@tanstack/react-form": "1.23.0",
2626
"@tanstack/react-query": "5.90.2",
@@ -30,27 +30,29 @@
3030
"@tanstack/router-plugin": "1.131.50",
3131
"class-variance-authority": "0.7.1",
3232
"clsx": "2.1.1",
33-
"debug": "^4.4.3",
33+
"debug": "4.4.3",
3434
"envalid": "8.1.0",
35-
"i18next": "^25.5.2",
36-
"i18next-browser-languagedetector": "^8.2.0",
37-
"i18next-http-backend": "^3.0.2",
35+
"i18next": "25.5.2",
36+
"i18next-browser-languagedetector": "8.2.0",
37+
"i18next-http-backend": "3.0.2",
3838
"lucide-react": "0.544.0",
39-
"next-themes": "^0.4.6",
39+
"next-themes": "0.4.6",
4040
"react": "19.1.1",
4141
"react-dom": "19.1.1",
42-
"react-i18next": "^15.7.3",
43-
"recharts": "^3.2.1",
44-
"sonner": "^2.0.7",
42+
"react-i18next": "15.7.3",
43+
"recharts": "3.2.1",
44+
"sonner": "2.0.7",
4545
"tailwind-merge": "3.3.1",
4646
"tailwindcss": "4.1.13",
4747
"tw-animate-css": "1.3.8",
48-
"viem": "2.37.8",
49-
"wagmi": "2.17.2",
50-
"zod": "4.1.11"
48+
"use-sync-external-store": "1.6.0",
49+
"viem": "2.38.6",
50+
"wagmi": "2.19.2",
51+
"zod": "4.1.11",
52+
"zustand": "5.0.8"
5153
},
5254
"devDependencies": {
53-
"@types/debug": "^4.1.12",
55+
"@types/debug": "4.1.12",
5456
"vite": "7.1.7",
5557
"vite-plugin-node-polyfills": "0.24.0",
5658
"web-vitals": "5.1.0"
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
{
22
"accept": "Accept",
33
"cancel": "Cancel",
4-
"close": "Close"
4+
"close": "Close",
5+
"confirm": "Confirm",
6+
"continue": "Continue",
7+
"abort": "Abort",
8+
"loading": "Loading..."
59
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"title": "Transaction Confirmation",
3+
"description": "Please review and confirm transactions in your wallet",
4+
"preparing": "Preparing transaction...",
5+
"connectWallet": "Connect your wallet to proceed.",
6+
"switchNetwork": "Switch to {{name}} network",
7+
"signMessage": "Sign Message",
8+
"signTypedData": "Sign Typed Data",
9+
"sendTransaction": "Send Transaction"
10+
}

apps/web-app/src/@types/i18next.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { resources as common } from 'public/locales/en/common';
22
import type { resources as glossary } from 'public/locales/en/glossary';
3+
import type { resources as tx } from 'public/locales/en/tx';
34
import type { resources as validation } from 'public/locales/en/validation';
45
import { defaultNS } from '../i18n';
56

@@ -11,6 +12,7 @@ declare module 'i18next' {
1112
common: typeof common;
1213
glossary: typeof glossary;
1314
validation: typeof validation;
15+
tx: typeof tx;
1416
};
1517
}
1618
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { useMemo } from 'react';
2+
import type { Address, Chain, Hash } from 'viem';
3+
import { useChains } from 'wagmi';
4+
5+
type ChainProps =
6+
| {
7+
chainId: number;
8+
}
9+
| {
10+
chain: Chain;
11+
};
12+
13+
type ValueProps =
14+
| {
15+
address: Address;
16+
}
17+
| {
18+
txHash: Hash;
19+
};
20+
21+
type LinkToExplorerProps = ValueProps &
22+
ChainProps & {
23+
className?: string;
24+
};
25+
26+
export const LinkToExplorer = (props: LinkToExplorerProps) => {
27+
const chains = useChains();
28+
29+
const chain = useMemo(() => {
30+
if ('chain' in props) {
31+
return props.chain;
32+
}
33+
return chains.find((c) => c.id === props.chainId);
34+
}, [props, chains]);
35+
36+
const path = useMemo(() => {
37+
if ('address' in props) {
38+
return `/address/${props.address}`;
39+
}
40+
if ('txHash' in props) {
41+
return `/tx/${props.txHash}`;
42+
}
43+
return '/';
44+
}, [props]);
45+
46+
const title = useMemo(() => {
47+
if ('address' in props) {
48+
return props.address;
49+
}
50+
if ('txHash' in props) {
51+
return props.txHash;
52+
}
53+
}, [props, chain]);
54+
55+
return (
56+
<a
57+
href={chain?.blockExplorers?.default.url + path}
58+
className={props.className}
59+
target="_blank"
60+
rel="noopener noreferrer"
61+
>
62+
{title?.slice(0, 6)}...{title?.slice(-4)}
63+
</a>
64+
);
65+
};

apps/web-app/src/components/MoneyMarket/components/BorrowAssetsList/components/AssetsTable/AssetsTable.tsx

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,8 @@ import { AmountRenderer } from '@/components/ui/amount-renderer';
1212
import { Button } from '@/components/ui/button';
1313
import { InfoButton } from '@/components/ui/info-button';
1414
import { sdk } from '@/lib/sdk';
15-
import {
16-
BorrowRateMode,
17-
type MoneyMarketPoolReserve,
18-
type Token,
19-
} from '@sovryn/slayer-sdk';
15+
import { useSlayerTx } from '@/lib/transactions';
16+
import { type MoneyMarketPoolReserve, type Token } from '@sovryn/slayer-sdk';
2017
import { Decimal } from '@sovryn/slayer-shared';
2118
import { useAccount, useWriteContract } from 'wagmi';
2219

@@ -35,22 +32,31 @@ export const AssetsTable: FC<AssetsTableProps> = ({ assets }) => {
3532

3633
const { writeContractAsync } = useWriteContract();
3734

35+
const { begin } = useSlayerTx();
36+
3837
const handleBorrow = async (token: Token) => {
39-
const msg = await sdk.moneyMarket.borrow(
40-
token,
41-
Decimal.from(1),
42-
BorrowRateMode.stable,
43-
{
38+
begin(async () => {
39+
const s = await sdk.moneyMarket.borrow(token, Decimal.from(1), 1, {
4440
account: address!,
45-
},
46-
);
47-
console.log('Transaction Request:', msg);
41+
});
42+
console.log('Transaction Request:', s);
43+
return s;
44+
});
45+
46+
// const msg = await sdk.moneyMarket.borrow(
47+
// token,
48+
// Decimal.from(1),
49+
// BorrowRateMode.stable,
50+
// {
51+
// account: address!,
52+
// },
53+
// );
54+
// console.log('Transaction Request:', msg);
4855

49-
if (msg.length) {
50-
const { chain, ...contractParams } = msg[0];
51-
const data = await writeContractAsync(contractParams);
52-
console.log('Transaction Response:', data);
53-
}
56+
// if (msg.length) {
57+
// // const data = await writeContractAsync<any>(msg[0]);
58+
// // console.log('Transaction Response:', data);
59+
// }
5460
// const d = await signMessageAsync(msg);
5561
// console.warn('Signature:', { data, d });
5662
};
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { txStore } from '@/lib/transactions/store';
2+
import clsx from 'clsx';
3+
import { Loader2Icon } from 'lucide-react';
4+
import { useTranslation } from 'react-i18next';
5+
import { useStoreWithEqualityFn } from 'zustand/traditional';
6+
import {
7+
Dialog,
8+
DialogContent,
9+
DialogDescription,
10+
DialogHeader,
11+
DialogTitle,
12+
} from '../ui/dialog';
13+
import { TxList } from './TxList';
14+
15+
export const TransactionDialogProvider = () => {
16+
const { t } = useTranslation('tx');
17+
18+
const [isOpen, isReady] = useStoreWithEqualityFn(
19+
txStore,
20+
(state) => [state.isFetching || state.isReady, state.isReady] as const,
21+
);
22+
23+
const onClose = (open: boolean) => {
24+
if (!open) {
25+
txStore.getState().reset();
26+
}
27+
};
28+
29+
const handleEscapes = (e: Event) => {
30+
if (!isReady) {
31+
txStore.getState().reset();
32+
return;
33+
}
34+
e.preventDefault();
35+
};
36+
37+
return (
38+
<Dialog open={isOpen} onOpenChange={onClose}>
39+
<DialogContent
40+
className={clsx(!isReady && 'w-64 h-64')}
41+
onInteractOutside={handleEscapes}
42+
onEscapeKeyDown={handleEscapes}
43+
>
44+
{isReady ? (
45+
<TxList />
46+
) : (
47+
<>
48+
<DialogHeader className="sr-only">
49+
<DialogTitle>{t(($) => $.title)}</DialogTitle>
50+
<DialogDescription>{t(($) => $.description)}</DialogDescription>
51+
</DialogHeader>
52+
<div className="flex flex-col justify-center items-center gap-4">
53+
<Loader2Icon className="mr-2 animate-spin" size={48} />
54+
<p className="text-sm">{t(($) => $.preparing)}</p>
55+
</div>
56+
</>
57+
)}
58+
</DialogContent>
59+
</Dialog>
60+
);
61+
};
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { TRANSACTION_STATE, type SlayerTx } from '@/lib/transactions/store';
2+
import { isTransactionRequest } from '@sovryn/slayer-sdk';
3+
import {
4+
CircleCheckBig,
5+
CircleDashed,
6+
CircleX,
7+
ExternalLink,
8+
Loader2Icon,
9+
} from 'lucide-react';
10+
import { useMemo, type FC } from 'react';
11+
import { useChains } from 'wagmi';
12+
import { LinkToExplorer } from '../LinkToExplorer/LinkToExplorer';
13+
14+
type TransactionItemProps = {
15+
index: number;
16+
item: SlayerTx;
17+
};
18+
19+
export const TransactionItem: FC<TransactionItemProps> = ({ item, index }) => {
20+
const isTx = isTransactionRequest(item);
21+
const chains = useChains();
22+
const chain = useMemo(() => {
23+
if (isTx) {
24+
const chainId = item.request.data.chain?.id;
25+
return chains.find((c) => c.id === chainId);
26+
}
27+
return undefined;
28+
}, [chains, isTx, item.request.data]);
29+
30+
return (
31+
<div className="flex flex-row justify-start items-start gap-4 mb-3">
32+
<div className="w-8 shrink-0 grow-0 text-center">
33+
<div className="flex flex-col items-center gap-1">
34+
{item.state === TRANSACTION_STATE.idle && <CircleDashed size={24} />}
35+
{item.state === TRANSACTION_STATE.pending && (
36+
<Loader2Icon className="animate-spin" size={24} />
37+
)}
38+
{item.state === TRANSACTION_STATE.success && (
39+
<CircleCheckBig size={24} className="text-green-500" />
40+
)}
41+
{item.state === TRANSACTION_STATE.error && (
42+
<CircleX size={24} className="text-red-500" />
43+
)}
44+
<div className="text-xs">#{index + 1}</div>
45+
</div>
46+
</div>
47+
<div className="grow">
48+
<p>{item.title}</p>
49+
<p className="text-sm">{item.description}</p>
50+
{isTx && chain && item.res?.transactionHash && (
51+
<p className="text-sm flex flex-row justify-start gap-2 items-center mt-2">
52+
<ExternalLink size={16} />
53+
<LinkToExplorer chain={chain} txHash={item.res.transactionHash} />
54+
</p>
55+
)}
56+
57+
{item.error && (
58+
<p className="mt-2 text-xs text-red-500">{item.error}</p>
59+
)}
60+
</div>
61+
</div>
62+
);
63+
};

0 commit comments

Comments
 (0)