diff --git a/packages/apps/dev-wallet/src/App/Layout/Layout.tsx b/packages/apps/dev-wallet/src/App/Layout/Layout.tsx index 22e9022b34..7dcdff5586 100644 --- a/packages/apps/dev-wallet/src/App/Layout/Layout.tsx +++ b/packages/apps/dev-wallet/src/App/Layout/Layout.tsx @@ -15,7 +15,8 @@ import { } from '@kadena/kode-ui/patterns'; import classNames from 'classnames'; import { FC, useMemo } from 'react'; -import { Link, Outlet, useLocation, useNavigate } from 'react-router-dom'; +import { Link, Outlet, useLocation } from 'react-router-dom'; +import { usePatchedNavigate } from '../../utils/usePatchedNavigate'; import { SideBar } from './SideBar'; import { isExpandedMainClass, @@ -26,7 +27,7 @@ import { export const Layout: FC = () => { const { theme, setTheme } = useTheme(); const location = useLocation(); - const navigate = useNavigate(); + const navigate = usePatchedNavigate(); const { isExpanded } = useLayout(); const innerLocation = useMemo( diff --git a/packages/apps/dev-wallet/src/App/Layout/SideBar.tsx b/packages/apps/dev-wallet/src/App/Layout/SideBar.tsx index c1aa8001b4..d3ddbb58f5 100644 --- a/packages/apps/dev-wallet/src/App/Layout/SideBar.tsx +++ b/packages/apps/dev-wallet/src/App/Layout/SideBar.tsx @@ -39,7 +39,8 @@ import { useLayout, } from '@kadena/kode-ui/patterns'; import { FC } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; +import { Link } from 'react-router-dom'; +import { usePatchedNavigate } from '../../utils/usePatchedNavigate'; import { BetaHeader } from '../BetaHeader'; import { KLogo } from './KLogo'; @@ -47,7 +48,7 @@ export const SideBar: FC = () => { const { theme, setTheme } = useTheme(); const { isExpanded } = useLayout(); const { lockProfile, profileList, unlockProfile, profile } = useWallet(); - const navigate = useNavigate(); + const navigate = usePatchedNavigate(); const toggleTheme = (): void => { const newTheme = theme === Themes.dark ? Themes.light : Themes.dark; diff --git a/packages/apps/dev-wallet/src/App/routes.tsx b/packages/apps/dev-wallet/src/App/routes.tsx index 9a2bddca4f..0ae58ef806 100644 --- a/packages/apps/dev-wallet/src/App/routes.tsx +++ b/packages/apps/dev-wallet/src/App/routes.tsx @@ -1,12 +1,12 @@ import { FC, PropsWithChildren, useEffect, useState } from 'react'; import { + createBrowserRouter, + createMemoryRouter, + createRoutesFromElements, Navigate, Outlet, Route, RouterProvider, - createBrowserRouter, - createMemoryRouter, - createRoutesFromElements, useLocation, } from 'react-router-dom'; @@ -39,8 +39,8 @@ import { HomePage } from '../pages/home/home-page'; import { SelectProfile } from '../pages/select-profile/select-profile'; import { UnlockProfile } from '../pages/unlock-profile/unlock-profile'; import { getScriptType } from '../utils/window'; -import { Layout } from './Layout/Layout'; import { LayoutMini } from './layout-mini'; +import { Layout } from './Layout/Layout'; const Redirect: FC< PropsWithChildren<{ diff --git a/packages/apps/dev-wallet/src/App/session.tsx b/packages/apps/dev-wallet/src/App/session.tsx index e707ec3cf1..8fdf4592bd 100644 --- a/packages/apps/dev-wallet/src/App/session.tsx +++ b/packages/apps/dev-wallet/src/App/session.tsx @@ -25,8 +25,10 @@ export const SessionProvider: FC = ({ children }) => { const [loaded, setLoaded] = useState(false); useLayoutEffect(() => { const events = ['visibilitychange', 'touchstart', 'keydown', 'click']; + let removeListener = () => {}; const run = async () => { await Session.load(); + removeListener = Session.ListenToExternalChanges(); // console.log('Session is loaded', Session.get('profileId')); events.forEach((event) => { document.addEventListener(event, Session.renew); @@ -39,6 +41,7 @@ export const SessionProvider: FC = ({ children }) => { events.forEach((event) => { document.removeEventListener(event, Session.renew); }); + removeListener(); }; }, []); diff --git a/packages/apps/dev-wallet/src/Components/AccountBalanceDistribution/AccountBalanceDistribution.tsx b/packages/apps/dev-wallet/src/Components/AccountBalanceDistribution/AccountBalanceDistribution.tsx index 28066235f8..ba901791df 100644 --- a/packages/apps/dev-wallet/src/Components/AccountBalanceDistribution/AccountBalanceDistribution.tsx +++ b/packages/apps/dev-wallet/src/Components/AccountBalanceDistribution/AccountBalanceDistribution.tsx @@ -138,6 +138,7 @@ export const AccountBalanceDistribution: FC = ({ network: activeNetwork!, redistribution, mapKeys, + creationTime: Math.round(Date.now() / 1000), }); onRedistribution(groupId); diff --git a/packages/apps/dev-wallet/src/Components/NetworkSelector/NetworkSelector.tsx b/packages/apps/dev-wallet/src/Components/NetworkSelector/NetworkSelector.tsx index bce558510e..b3bfc5d23a 100644 --- a/packages/apps/dev-wallet/src/Components/NetworkSelector/NetworkSelector.tsx +++ b/packages/apps/dev-wallet/src/Components/NetworkSelector/NetworkSelector.tsx @@ -1,4 +1,5 @@ import { useWallet } from '@/modules/wallet/wallet.hook'; +import { usePatchedNavigate } from '@/utils/usePatchedNavigate'; import { MonoCheck, MonoSettings, @@ -12,7 +13,6 @@ import { IButtonProps, } from '@kadena/kode-ui'; import { FC } from 'react'; -import { useNavigate } from 'react-router-dom'; export const NetworkSelector: FC<{ showLabel?: boolean; @@ -20,7 +20,7 @@ export const NetworkSelector: FC<{ isCompact?: IButtonProps['isCompact']; }> = ({ showLabel = true, variant, isCompact = false }) => { const { networks, activeNetwork, setActiveNetwork } = useWallet(); - const navigate = useNavigate(); + const navigate = usePatchedNavigate(); const handleNetworkUpdate = (uuid: string) => { const network = networks.find((network) => network.uuid === uuid); diff --git a/packages/apps/dev-wallet/src/Components/ProfileChanger/ProfileChanger.tsx b/packages/apps/dev-wallet/src/Components/ProfileChanger/ProfileChanger.tsx index 80679aafb3..fd362ef1a9 100644 --- a/packages/apps/dev-wallet/src/Components/ProfileChanger/ProfileChanger.tsx +++ b/packages/apps/dev-wallet/src/Components/ProfileChanger/ProfileChanger.tsx @@ -1,6 +1,7 @@ import { useWallet } from '@/modules/wallet/wallet.hook'; import { IProfile } from '@/modules/wallet/wallet.repository'; import { getWebAuthnPass } from '@/modules/wallet/wallet.service'; +import { usePatchedNavigate } from '@/utils/usePatchedNavigate'; import { MonoMoreHoriz } from '@kadena/kode-icons/system'; import { Button, @@ -10,7 +11,6 @@ import { Stack, } from '@kadena/kode-ui'; import { FC } from 'react'; -import { useNavigate } from 'react-router-dom'; import { Profile } from './components/Profile'; import { profileClass, profileListClass } from './components/style.css'; @@ -25,7 +25,7 @@ export const ProfileChanger: FC = () => { unlockProfile, lockProfile, } = useWallet(); - const navigate = useNavigate(); + const navigate = usePatchedNavigate(); const handleSelect = async (profile: Pick) => { if (profile.options.authMode === 'WEB_AUTHN') { diff --git a/packages/apps/dev-wallet/src/Components/UnlockPrompt/UnlockPrompt.tsx b/packages/apps/dev-wallet/src/Components/UnlockPrompt/UnlockPrompt.tsx index f1e1a560bb..8de06df638 100644 --- a/packages/apps/dev-wallet/src/Components/UnlockPrompt/UnlockPrompt.tsx +++ b/packages/apps/dev-wallet/src/Components/UnlockPrompt/UnlockPrompt.tsx @@ -1,15 +1,17 @@ import { IProfile } from '@/modules/wallet/wallet.repository.ts'; +import { getErrorMessage } from '@/utils/getErrorMessage.ts'; import { Button, Dialog, Heading, + Notification, Radio, RadioGroup, Stack, Text, TextField, } from '@kadena/kode-ui'; -import React from 'react'; +import React, { useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { unlockPrompt } from './style.css.ts'; @@ -23,7 +25,7 @@ export const UnlockPrompt: React.FC<{ }: { password: string; keepOpen: 'session' | 'short-time' | 'never'; - }) => void; + }) => Promise; reject: (reason: any) => void; storePassword?: boolean; }> = ({ @@ -34,6 +36,7 @@ export const UnlockPrompt: React.FC<{ profile, storePassword = true, }) => { + const [error, setError] = useState(); const { control, register, handleSubmit } = useForm({ defaultValues: { keepOpen: rememberPassword || 'session', @@ -49,7 +52,9 @@ export const UnlockPrompt: React.FC<{ >
{ - resolve(data); + resolve(data).catch((e) => { + setError(getErrorMessage(e, 'Password in not correct')); + }); })} > @@ -93,6 +98,14 @@ export const UnlockPrompt: React.FC<{ )} /> )} + {error && ( + + + {error} + Please try again! + + + )} + ). + + + Are you sure about this action? + + + } + onDelete={() => resolve(true)} + onCancel={reject} + deleteText={`Delete ${profile?.name} Profile`} + /> + )); + if (answer) { + deleteProfile(profile.uuid) + .then(() => { + lockProfile(); + }) + .catch((e) => { + setError(getErrorMessage(e, 'Failed to delete profile')); + }); + } + }} + > + Delete Profile + + {error && ( + + {error} + + )} ); } diff --git a/packages/apps/dev-wallet/src/pages/signature-builder/signature-builder.tsx b/packages/apps/dev-wallet/src/pages/signature-builder/signature-builder.tsx index 6034394895..e33407a72a 100644 --- a/packages/apps/dev-wallet/src/pages/signature-builder/signature-builder.tsx +++ b/packages/apps/dev-wallet/src/pages/signature-builder/signature-builder.tsx @@ -17,6 +17,7 @@ import { RequestScheme, signingRequestToPactCommand, } from '@/utils/transaction-scheme'; +import { usePatchedNavigate } from '@/utils/usePatchedNavigate'; import { base64UrlDecodeArr } from '@kadena/cryptography-utils'; import { MonoDashboardCustomize } from '@kadena/kode-icons/system'; import { @@ -32,12 +33,15 @@ import { execCodeParser } from '@kadena/pactjs-generator'; import classNames from 'classnames'; import yaml from 'js-yaml'; import { useEffect, useMemo, useState } from 'react'; -import { useNavigate, useSearchParams } from 'react-router-dom'; +import { useSearchParams } from 'react-router-dom'; import { codeArea } from './style.css'; +const getTxFromUrlHash = () => + window.location.hash ? window.location.hash.substring(1) : undefined; + export function SignatureBuilder() { const [searchParams] = useSearchParams(); - const urlTransaction = searchParams.get('transaction'); + const urlTransaction = searchParams.get('transaction') || getTxFromUrlHash(); const [error, setError] = useState(); const [schema, setSchema] = useState(); const [input, setInput] = useState(''); @@ -48,7 +52,7 @@ export function SignatureBuilder() { ISigningRequest['caps'] >([]); const { profile, activeNetwork, networks, setActiveNetwork } = useWallet(); - const navigate = useNavigate(); + const navigate = usePatchedNavigate(); const exec = pactCommand && pactCommand.payload && 'exec' in pactCommand.payload diff --git a/packages/apps/dev-wallet/src/pages/transaction/components/CommandView.tsx b/packages/apps/dev-wallet/src/pages/transaction/components/CommandView.tsx index 3ca5f23e14..3524a5b284 100644 --- a/packages/apps/dev-wallet/src/pages/transaction/components/CommandView.tsx +++ b/packages/apps/dev-wallet/src/pages/transaction/components/CommandView.tsx @@ -1,6 +1,6 @@ import { CopyButton } from '@/Components/CopyButton/CopyButton'; import { ITransaction } from '@/modules/transaction/transaction.repository'; -import { shorten } from '@/utils/helpers'; +import { shorten, toISOLocalDateTime } from '@/utils/helpers'; import { shortenPactCode } from '@/utils/parsedCodeToPact'; import { IPactCommand } from '@kadena/client'; import { MonoTextSnippet } from '@kadena/kode-icons/system'; @@ -114,16 +114,16 @@ export function CommandView({ {command.meta.creationTime} ( - {new Date(command.meta.creationTime! * 1000).toLocaleString()}) + {toISOLocalDateTime(command.meta.creationTime! * 1000)}) {command.meta.ttl} ( - {new Date( + {toISOLocalDateTime( (command.meta.ttl! + command.meta.creationTime!) * 1000, - ).toLocaleString()} + )} ) @@ -154,7 +154,11 @@ export function CommandView({ - + ); } diff --git a/packages/apps/dev-wallet/src/pages/transaction/components/ExpandedTransaction.tsx b/packages/apps/dev-wallet/src/pages/transaction/components/ExpandedTransaction.tsx index 2900c9cd08..d21eefad65 100644 --- a/packages/apps/dev-wallet/src/pages/transaction/components/ExpandedTransaction.tsx +++ b/packages/apps/dev-wallet/src/pages/transaction/components/ExpandedTransaction.tsx @@ -22,8 +22,13 @@ import { useWallet } from '@/modules/wallet/wallet.hook.tsx'; import { panelClass } from '@/pages/home/style.css.ts'; import { shorten } from '@/utils/helpers.ts'; +import { normalizeTx } from '@/utils/normalizeSigs.ts'; import { base64UrlEncodeArr } from '@kadena/cryptography-utils'; -import { MonoMoreVert, MonoShare } from '@kadena/kode-icons/system'; +import { + MonoContentCopy, + MonoMoreVert, + MonoShare, +} from '@kadena/kode-icons/system'; import { useState } from 'react'; import { CommandView } from './CommandView.tsx'; import { statusPassed, TxPipeLine } from './TxPipeLine.tsx'; @@ -40,7 +45,7 @@ export function ExpandedTransaction({ transaction: ITransaction; contTx?: ITransaction; onSign: (sig: ITransaction['sigs']) => void; - onSubmit: () => Promise; + onSubmit: (skipPreflight?: boolean) => Promise; sendDisabled?: boolean; showTitle?: boolean; isDialog?: boolean; @@ -48,22 +53,25 @@ export function ExpandedTransaction({ const { sign } = useWallet(); const [showShareTooltip, setShowShareTooltip] = useState(false); - const copyTransactionAs = (format: 'json' | 'yaml') => () => { - const transactionData = { - hash: transaction.hash, - cmd: transaction.cmd, - sigs: transaction.sigs, - }; + const copyTransactionAs = + (format: 'json' | 'yaml', legacySig = false) => + () => { + const tx = { + hash: transaction.hash, + cmd: transaction.cmd, + sigs: transaction.sigs, + }; + const transactionData = legacySig ? normalizeTx(tx) : tx; - let formattedData: string; - if (format === 'json') { - formattedData = JSON.stringify(transactionData, null, 2); - } else { - formattedData = yaml.dump(transactionData); - } + let formattedData: string; + if (format === 'json') { + formattedData = JSON.stringify(transactionData, null, 2); + } else { + formattedData = yaml.dump(transactionData); + } - navigator.clipboard.writeText(formattedData); - }; + navigator.clipboard.writeText(formattedData); + }; const signAll = async () => { const signedTx = (await sign(transaction)) as IUnsignedCommand | ICommand; @@ -159,7 +167,7 @@ export function ExpandedTransaction({ ); const baseUrl = `${window.location.protocol}//${window.location.host}`; navigator.clipboard.writeText( - `${baseUrl}/sig-builder?transaction=${encodedTx}`, + `${baseUrl}/sig-builder#${encodedTx}`, ); setShowShareTooltip(true); setTimeout(() => setShowShareTooltip(false), 5000); @@ -179,13 +187,25 @@ export function ExpandedTransaction({ } > } onClick={copyTransactionAs('json')} /> } onClick={copyTransactionAs('yaml')} /> + } + onClick={copyTransactionAs('json', true)} + /> + } + onClick={copyTransactionAs('yaml', true)} + /> diff --git a/packages/apps/dev-wallet/src/pages/transaction/components/ReviewTransaction.tsx b/packages/apps/dev-wallet/src/pages/transaction/components/ReviewTransaction.tsx index dacaa8cf41..fbe1f4bc05 100644 --- a/packages/apps/dev-wallet/src/pages/transaction/components/ReviewTransaction.tsx +++ b/packages/apps/dev-wallet/src/pages/transaction/components/ReviewTransaction.tsx @@ -20,9 +20,11 @@ import { Label, Value } from './helpers.tsx'; export function ReviewTransaction({ transaction, + transactionStatus, onSign, }: { transaction: IUnsignedCommand; + transactionStatus: ITransaction['status']; onSign: (sig: ITransaction['sigs']) => void; }) { const { sign } = useWallet(); @@ -161,7 +163,11 @@ export function ReviewTransaction({ - + diff --git a/packages/apps/dev-wallet/src/pages/transaction/components/Signers.tsx b/packages/apps/dev-wallet/src/pages/transaction/components/Signers.tsx index f399482aee..710cf25bf4 100644 --- a/packages/apps/dev-wallet/src/pages/transaction/components/Signers.tsx +++ b/packages/apps/dev-wallet/src/pages/transaction/components/Signers.tsx @@ -18,9 +18,12 @@ import { import { ITransaction } from '@/modules/transaction/transaction.repository.ts'; import { useWallet } from '@/modules/wallet/wallet.hook.tsx'; import { normalizeSigs } from '@/utils/normalizeSigs.ts'; -import { MonoContentCopy } from '@kadena/kode-icons/system'; +import { MonoContentCopy, MonoDelete } from '@kadena/kode-icons/system'; import classNames from 'classnames'; +import yaml from 'js-yaml'; +import { statusPassed } from './TxPipeLine.tsx'; + const Value: FC> = ({ children, className, @@ -32,9 +35,11 @@ const Value: FC> = ({ export function Signers({ transaction, + transactionStatus, onSign, }: { transaction: IUnsignedCommand; + transactionStatus: ITransaction['status']; onSign: (sig: ITransaction['sigs']) => void; }) { const { sign } = useWallet(); @@ -127,7 +132,12 @@ export function Signers({ | { pubKey: string; sig?: string; - } = JSON.parse(signature); + } = yaml.load(signature) as + | IUnsignedCommand + | { + pubKey: string; + sig?: string; + }; let sigs: Array<{ sig?: string; @@ -187,19 +197,39 @@ export function Signers({ gap={'sm'} > Signature - + + + + )} ), showAfterCont && statusPassed(tx.status, 'submitted') && ( - - + + - + {tx.request ? : } Send + {variant === 'expanded' && !tx.request && ( + + + + )} ), showAfterCont && diff --git a/packages/apps/dev-wallet/src/pages/transfer-v2/Components/AccountSearchBox.tsx b/packages/apps/dev-wallet/src/pages/transfer-v2/Components/AccountSearchBox.tsx index 0d2224f56f..d6fbd96ad3 100644 --- a/packages/apps/dev-wallet/src/pages/transfer-v2/Components/AccountSearchBox.tsx +++ b/packages/apps/dev-wallet/src/pages/transfer-v2/Components/AccountSearchBox.tsx @@ -19,18 +19,13 @@ import { ISigner } from '@kadena/client'; import { MonoClose, MonoInfo } from '@kadena/kode-icons/system'; import { Button, Divider, Heading, Stack, Text } from '@kadena/kode-ui'; import { useCallback, useEffect, useRef, useState } from 'react'; -import { labelClass } from '../Steps/style.css'; + import { discoverReceiver, IReceiverAccount } from '../utils'; import { AccountItem } from './AccountItem'; import { Keyset } from './keyset'; +import { Label } from './Label'; import { createAccountBoxClass, popoverClass } from './style.css'; -const Label = ({ children }: { children: React.ReactNode }) => ( - - {children} - -); - const discover = withRaceGuard(debounce(discoverReceiver, 500)); export function AccountSearchBox({ diff --git a/packages/apps/dev-wallet/src/pages/transfer-v2/Components/CreationTime.tsx b/packages/apps/dev-wallet/src/pages/transfer-v2/Components/CreationTime.tsx new file mode 100644 index 0000000000..20de1a0d75 --- /dev/null +++ b/packages/apps/dev-wallet/src/pages/transfer-v2/Components/CreationTime.tsx @@ -0,0 +1,42 @@ +import { toISOLocalDateTime } from '@/utils/helpers'; +import { TextField } from '@kadena/kode-ui'; +import { useEffect, useState } from 'react'; +import { Label } from './Label'; +import { Seconds } from './TTLSelect'; + +export function CreationTime({ + value, + onChange, +}: { + value?: Seconds; + onChange: (value: Seconds) => void; +}) { + const [defaultTime, setDefaultTime] = useState(Date.now()); + useEffect(() => { + const timer = setInterval(() => { + setDefaultTime(Date.now()); + }, 1000); + return () => { + clearInterval(timer); + }; + }, []); + return ( + Valid From} + placeholder="When to start the transaction" + value={ + value + ? toISOLocalDateTime(value * 1000) + : toISOLocalDateTime(defaultTime) + } + defaultValue={toISOLocalDateTime(defaultTime)} + onChange={(e) => { + console.log('e.target.value', new Date(e.target.value)); + onChange(Math.round(new Date(e.target.value).getTime() / 1000)); + }} + type="datetime-local" + size="sm" + /> + ); +} diff --git a/packages/apps/dev-wallet/src/pages/transfer-v2/Components/Label.tsx b/packages/apps/dev-wallet/src/pages/transfer-v2/Components/Label.tsx new file mode 100644 index 0000000000..6ac4febc73 --- /dev/null +++ b/packages/apps/dev-wallet/src/pages/transfer-v2/Components/Label.tsx @@ -0,0 +1,8 @@ +import { Text } from '@kadena/kode-ui'; +import { labelClass } from './style.css'; + +export const Label = ({ children }: { children: React.ReactNode }) => ( + + {children} + +); diff --git a/packages/apps/dev-wallet/src/pages/transfer-v2/Components/TTLSelect.tsx b/packages/apps/dev-wallet/src/pages/transfer-v2/Components/TTLSelect.tsx new file mode 100644 index 0000000000..6f99f0b88b --- /dev/null +++ b/packages/apps/dev-wallet/src/pages/transfer-v2/Components/TTLSelect.tsx @@ -0,0 +1,91 @@ +import { Badge, Select, SelectItem, Stack, TextField } from '@kadena/kode-ui'; +import { Label } from './Label'; + +export type Seconds = number & { _brand?: 'Seconds' }; + +export function TTLSelect({ + value, + onChange, +}: { + value: Seconds; + onChange: (ttl: Seconds) => void; +}) { + return ( + + + + + + + Seconds + + } + placeholder="Enter TTL (Timer to live)" + value={value.toString()} + defaultValue={value.toString()} + onChange={(e) => { + onChange(+e.target.value); + }} + type="number" + size="sm" + /> + + + ); +} + +type TTLOptions = '30min' | '2hours' | '6hours' | '1day' | '2days' | 'custom'; + +function getTTlKey(ttl: Seconds): TTLOptions { + if (ttl === 30 * 60) { + return '30min'; + } + if (ttl === 2 * 60 * 60) { + return '2hours'; + } + if (ttl === 6 * 60 * 60) { + return '6hours'; + } + if (ttl === 24 * 60 * 60) { + return '1day'; + } + if (ttl === 48 * 60 * 60) { + return '2days'; + } + return 'custom'; +} + +function getTTlValue(key: TTLOptions): Seconds { + switch (key) { + case '30min': + return 30 * 60; + case '2hours': + return 2 * 60 * 60; + case '6hours': + return 6 * 60 * 60; + case '1day': + return 24 * 60 * 60; + case '2days': + return 48 * 60 * 60; + default: + return 0; + } +} diff --git a/packages/apps/dev-wallet/src/pages/transfer-v2/Components/style.css.ts b/packages/apps/dev-wallet/src/pages/transfer-v2/Components/style.css.ts index 46cb807d80..409d23ceda 100644 --- a/packages/apps/dev-wallet/src/pages/transfer-v2/Components/style.css.ts +++ b/packages/apps/dev-wallet/src/pages/transfer-v2/Components/style.css.ts @@ -16,3 +16,12 @@ export const createAccountBoxClass = style({ color: tokens.kda.foundation.color.text.semantic.warning.default, borderLeft: 'solid 4px', }); + +export const labelClass = style({ + minWidth: '90px', + display: 'flex', + background: tokens.kda.foundation.color.background.surface.default, + padding: '8px', + marginLeft: '-12px', + fontWeight: '700', +}); diff --git a/packages/apps/dev-wallet/src/pages/transfer-v2/Steps/TransferForm.tsx b/packages/apps/dev-wallet/src/pages/transfer-v2/Steps/TransferForm.tsx index 6bbd40b7f3..08e62d1a21 100644 --- a/packages/apps/dev-wallet/src/pages/transfer-v2/Steps/TransferForm.tsx +++ b/packages/apps/dev-wallet/src/pages/transfer-v2/Steps/TransferForm.tsx @@ -37,9 +37,11 @@ import { linkClass } from '../../transfer/style.css'; import { AccountItem } from '../Components/AccountItem'; import { Keyset } from '../Components/keyset'; import { CHAINS, IReceiver, IReceiverAccount, getTransfers } from '../utils'; -import { labelClass } from './style.css'; import { AccountSearchBox } from '../Components/AccountSearchBox'; +import { CreationTime } from '../Components/CreationTime'; +import { Label } from '../Components/Label'; +import { TTLSelect } from '../Components/TTLSelect'; export interface Transfer { fungible: string; @@ -50,9 +52,10 @@ export interface Transfer { gasPrice: string; gasLimit: string; type: 'safeTransfer' | 'normalTransfer'; - ttl: string; + ttl: number; senderAccount?: IAccount; totalAmount: number; + creationTime?: number; } export type Redistribution = { @@ -72,12 +75,6 @@ export interface TrG { txs: ITransaction[]; } -const Label = ({ children }: { children: React.ReactNode }) => ( - - {children} - -); - export function TransferForm({ accountId, onSubmit, @@ -131,7 +128,7 @@ export function TransferForm({ gasPrice: '1e-8', gasLimit: '2500', type: 'normalTransfer', - ttl: (2 * 60 * 60).toString(), + ttl: 2 * 60 * 60, totalAmount: 0, }, }); @@ -183,6 +180,7 @@ export function TransferForm({ gasLimit: activity.data.transferData.gasLimit, type: activity.data.transferData.type, ttl: activity.data.transferData.ttl, + creationTime: activity.data.transferData.creationTime, totalAmount: 0, }); evaluateTransactions(); @@ -842,21 +840,27 @@ export function TransferForm({ Meta Data + ( + { + field.onChange(sec); + }} + /> + )} + /> ( - TTL:} - placeholder="Enter TTL (Timer to live)" + { - field.onChange(+e.target.value); + onChange={(value) => { + field.onChange(value); }} - type="number" - size="sm" /> )} /> diff --git a/packages/apps/dev-wallet/src/pages/transfer-v2/Steps/style.css.ts b/packages/apps/dev-wallet/src/pages/transfer-v2/Steps/style.css.ts deleted file mode 100644 index e52045df43..0000000000 --- a/packages/apps/dev-wallet/src/pages/transfer-v2/Steps/style.css.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { tokens } from '@kadena/kode-ui/styles'; -import { style } from '@vanilla-extract/css'; - -export const labelClass = style({ - minWidth: '90px', - display: 'flex', - background: tokens.kda.foundation.color.background.surface.default, - padding: '8px', - marginLeft: '-12px', - fontWeight: '700', -}); diff --git a/packages/apps/dev-wallet/src/pages/transfer-v2/transfer-v2.tsx b/packages/apps/dev-wallet/src/pages/transfer-v2/transfer-v2.tsx index afad9f1f2e..d41b2ca00d 100644 --- a/packages/apps/dev-wallet/src/pages/transfer-v2/transfer-v2.tsx +++ b/packages/apps/dev-wallet/src/pages/transfer-v2/transfer-v2.tsx @@ -12,8 +12,9 @@ import { ITransaction, transactionRepository, } from '@/modules/transaction/transaction.repository'; +import { usePatchedNavigate } from '@/utils/usePatchedNavigate'; import { SideBarBreadcrumbsItem } from '@kadena/kode-ui/patterns'; -import { useNavigate, useSearchParams } from 'react-router-dom'; +import { useSearchParams } from 'react-router-dom'; import { ReviewTransaction } from '../transaction/components/ReviewTransaction'; import { TxList } from '../transaction/components/TxList'; import { statusPassed } from '../transaction/components/TxPipeLine'; @@ -33,7 +34,7 @@ export function TransferV2() { profile, } = useWallet(); - const navigate = useNavigate(); + const navigate = usePatchedNavigate(); const [searchParams] = useSearchParams(); const accountId = searchParams.get('accountId'); const urlActivityId = searchParams.get('activityId'); @@ -97,6 +98,7 @@ export function TransferV2() { network: activeNetwork!, profileId: profile.uuid, mapKeys, + creationTime: data.creationTime, }); } @@ -165,6 +167,7 @@ export function TransferV2() { gasPrice: +formData.gasPrice, network: activeNetwork!, mapKeys, + creationTime: formData.creationTime, }); } } @@ -179,6 +182,7 @@ export function TransferV2() { return ( { const updated = { ...selectedTx, @@ -291,7 +295,12 @@ export function TransferV2() { (acc) => acc.uuid === data.accountId, ); if (!senderAccount?.keyset?.uuid) return; - const formData = { ...data, senderAccount }; + const formData = { + ...data, + senderAccount, + creationTime: + data.creationTime ?? Math.round(Date.now() / 1000), + }; const getEmpty = () => ['', []] as [string, ITransaction[]]; let redistributionGroup = getEmpty(); diff --git a/packages/apps/dev-wallet/src/pages/transfer-v2/utils.ts b/packages/apps/dev-wallet/src/pages/transfer-v2/utils.ts index d20f9a6071..cccc23c185 100644 --- a/packages/apps/dev-wallet/src/pages/transfer-v2/utils.ts +++ b/packages/apps/dev-wallet/src/pages/transfer-v2/utils.ts @@ -362,6 +362,7 @@ export const createTransactions = async ({ mapKeys, gasPrice, gasLimit, + creationTime, }: { account: IAccount; receivers: IReceiver[]; @@ -372,6 +373,7 @@ export const createTransactions = async ({ mapKeys: (key: ISigner) => ISigner; gasPrice: number; gasLimit: number; + creationTime: number; }) => { if (!account || +account.overallBalance < 0 || !network || !profileId) { throw new Error('INVALID_INPUTs'); @@ -432,6 +434,7 @@ export const createTransactions = async ({ networkId: network.networkId, meta: { chainId, + creationTime, }, }, ); @@ -478,6 +481,7 @@ export const createTransactions = async ({ chainId: optimal.chainId, gasLimit: gasLimit, gasPrice: gasPrice, + creationTime, }, }, )(); @@ -527,6 +531,7 @@ export async function createRedistributionTxs({ network, gasLimit, gasPrice, + creationTime, }: { redistribution: Array<{ source: ChainId; target: ChainId; amount: string }>; account: IAccount; @@ -534,6 +539,7 @@ export async function createRedistributionTxs({ network: INetwork; gasLimit: number; gasPrice: number; + creationTime: number; }) { const groupId = crypto.randomUUID(); const txs = redistribution.map(async ({ source, target, amount }) => { @@ -558,6 +564,7 @@ export async function createRedistributionTxs({ chainId: source, gasLimit: gasLimit, gasPrice: gasPrice, + creationTime, }, }, ); diff --git a/packages/apps/dev-wallet/src/pages/transfer/transfer.tsx b/packages/apps/dev-wallet/src/pages/transfer/transfer.tsx index a0ea18c029..687ae342b5 100644 --- a/packages/apps/dev-wallet/src/pages/transfer/transfer.tsx +++ b/packages/apps/dev-wallet/src/pages/transfer/transfer.tsx @@ -2,6 +2,7 @@ import { DiscoverdAccounts } from '@/Components/AccountInput/DiscoverdAccounts'; import { accountRepository } from '@/modules/account/account.repository'; import * as transactionService from '@/modules/transaction/transaction.service'; import { useWallet } from '@/modules/wallet/wallet.hook'; +import { usePatchedNavigate } from '@/utils/usePatchedNavigate'; import { ChainId, createTransaction, @@ -32,7 +33,7 @@ import { import classNames from 'classnames'; import { useCallback, useEffect, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; -import { useNavigate, useSearchParams } from 'react-router-dom'; +import { useSearchParams } from 'react-router-dom'; import { getAccount, IReceiverAccount } from '../transfer-v2/utils'; import { card, disabledItemClass, linkClass } from './style.css'; import { IOptimalTransfer, simpleOptimalTransfer } from './utils'; @@ -58,7 +59,7 @@ export function Transfer() { const [optimalTransfers, setOptimalTransfers] = useState< Array >([]); - const navigate = useNavigate(); + const navigate = usePatchedNavigate(); const mapKeys = useCallback( (key: ISigner) => { diff --git a/packages/apps/dev-wallet/src/pages/unlock-profile/unlock-profile.tsx b/packages/apps/dev-wallet/src/pages/unlock-profile/unlock-profile.tsx index a4bb0a4f70..1bbc9d5ba7 100644 --- a/packages/apps/dev-wallet/src/pages/unlock-profile/unlock-profile.tsx +++ b/packages/apps/dev-wallet/src/pages/unlock-profile/unlock-profile.tsx @@ -1,4 +1,5 @@ import { AuthCard } from '@/Components/AuthCard/AuthCard'; +import { usePatchedNavigate } from '@/utils/usePatchedNavigate.tsx'; import { Button, Heading, @@ -8,7 +9,7 @@ import { Link as UiLink, } from '@kadena/kode-ui'; import { useForm } from 'react-hook-form'; -import { Link, Navigate, useNavigate, useParams } from 'react-router-dom'; +import { Link, Navigate, useParams } from 'react-router-dom'; import { useWallet } from '../../modules/wallet/wallet.hook'; import InitialsAvatar from '../select-profile/initials.tsx'; import { passwordContainer, profileContainer } from './styles.css.ts'; @@ -21,7 +22,7 @@ export function UnlockProfile({ origin }: { origin: string }) { formState: { isValid, errors }, } = useForm<{ password: string }>(); const { profileId } = useParams(); - const navigate = useNavigate(); + const navigate = usePatchedNavigate(); const { profileList, unlockProfile, isUnlocked } = useWallet(); const profile = profileList.find((p) => p.uuid === profileId); const incorrectPasswordMsg = 'Password is incorrect'; diff --git a/packages/apps/dev-wallet/src/pages/wallet-recovery/Components/RecoveredV3.tsx b/packages/apps/dev-wallet/src/pages/wallet-recovery/Components/RecoveredV3.tsx index 087001330d..a19185dffc 100644 --- a/packages/apps/dev-wallet/src/pages/wallet-recovery/Components/RecoveredV3.tsx +++ b/packages/apps/dev-wallet/src/pages/wallet-recovery/Components/RecoveredV3.tsx @@ -6,6 +6,7 @@ import { dbService } from '@/modules/db/db.service'; import { useWallet } from '@/modules/wallet/wallet.hook'; import { IProfile } from '@/modules/wallet/wallet.repository'; import InitialsAvatar from '@/pages/select-profile/initials'; +import { usePatchedNavigate } from '@/utils/usePatchedNavigate'; import { MonoConstruction, MonoDoDisturb } from '@kadena/kode-icons/system'; import { Box, @@ -22,7 +23,6 @@ import { CardFooterGroup, } from '@kadena/kode-ui/patterns'; import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; export interface IV3Backup { scheme: 'v3'; @@ -37,7 +37,7 @@ export function RecoveredV3({ cancel: () => void; }) { const [error, setError] = useState(); - const navigate = useNavigate(); + const navigate = usePatchedNavigate(); const prompt = usePrompt(); const { profileList: walletProfiles } = useWallet(); const [bypassAvailableCheck, setBypassAvailableCheck] = useState(false); diff --git a/packages/apps/dev-wallet/src/utils/getErrorMessage.ts b/packages/apps/dev-wallet/src/utils/getErrorMessage.ts index cefdf82330..12cc797577 100644 --- a/packages/apps/dev-wallet/src/utils/getErrorMessage.ts +++ b/packages/apps/dev-wallet/src/utils/getErrorMessage.ts @@ -1,7 +1,7 @@ -export const getErrorMessage = (e: any) => { +export const getErrorMessage = (e: any, fallback: string = 'UNKNOWN ERROR') => { const message = 'message' in e ? e.message : JSON.stringify(e); if (message) { return typeof message === 'string' ? message : JSON.stringify(message); } - return 'Unknown error'; + return fallback; }; diff --git a/packages/apps/dev-wallet/src/utils/helpers.ts b/packages/apps/dev-wallet/src/utils/helpers.ts index 445c69a8e2..eca59a21ac 100644 --- a/packages/apps/dev-wallet/src/utils/helpers.ts +++ b/packages/apps/dev-wallet/src/utils/helpers.ts @@ -57,3 +57,57 @@ export const formatChainIds = (chainIds: ChainId[]) => { const chains = chainIds.map((id) => +id); return formatList(chains); }; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const throttle = any>( + fn: T, + delay: number, +) => { + let lastCall = 0; + let lastResult: ReturnType; + return (...args: Parameters): ReturnType => { + const now = Date.now(); + if (now - lastCall < delay) return lastResult; + lastCall = now; + lastResult = fn(...args); + return lastResult; + }; +}; + +export function createEventEmitter< + E extends Record, + T extends string | number | symbol = keyof E, + P = any, +>() { + const listeners: Record = {} as any; + return { + emit: (event: S, payload: E[S]) => { + for (const cb of listeners[event] || []) { + (cb as (payload: E[T]) => void)(payload); + } + for (const cb of listeners['*'] || []) { + (cb as (event: T, payload: P) => void)(event, payload); + } + }, + subscribe: ( + event: S, + cb: S extends '*' + ? (event: T, payload: E[T]) => void + : (payload: E[S]) => void, + ) => { + listeners[event] = listeners[event] || []; + const listenersForEvent = listeners[event]; + listenersForEvent.push(cb); + return () => { + const index = listenersForEvent.indexOf(cb); + if (index > -1) { + listenersForEvent.splice(index, 1); + } + }; + }, + }; +} + +export function toISOLocalDateTime(time: number) { + return new Date(time).toISOString().slice(0, 16); +} diff --git a/packages/apps/dev-wallet/src/utils/normalizeSigs.ts b/packages/apps/dev-wallet/src/utils/normalizeSigs.ts index b585b12b4b..7a587085d5 100644 --- a/packages/apps/dev-wallet/src/utils/normalizeSigs.ts +++ b/packages/apps/dev-wallet/src/utils/normalizeSigs.ts @@ -14,15 +14,37 @@ type CommandJson = Array< | undefined >; +type CommandJsonNew = Array; + const sigScheme = ( - sigs: SigData | CommandSigData | CommandJson, -): 'SigData' | 'CommandSigData' | 'CommandJson' | 'unknown' => { + sigs: SigData | CommandSigData | CommandJson | CommandJsonNew, +): + | 'SigData' + | 'CommandSigData' + | 'CommandJson' + | 'CommandJsonNew' + | 'unknown' => { if (Array.isArray(sigs)) { - if (sigs.every((item) => item && 'pubKey' in item)) { + if ( + sigs.every((item) => item && typeof item === 'object' && 'pubKey' in item) + ) { return 'CommandSigData'; } if ( - sigs.every((item) => item === null || item === undefined || 'sig' in item) + sigs.every( + (item) => + item === null || item === undefined || typeof item === 'string', + ) + ) { + return 'CommandJsonNew'; + } + if ( + sigs.every( + (item) => + item === null || + item === undefined || + (typeof item === 'object' && 'sig' in item), + ) ) { return 'CommandJson'; } @@ -57,6 +79,14 @@ export function normalizeSigs( }); return normalizedSigs; } + if (scheme === 'CommandJsonNew') { + const sigs = tx.sigs as unknown as CommandJsonNew; + const normalizedSigs = cmd.signers.map(({ pubKey }, index) => { + const item = sigs[index]; + return item ? { sig: item, pubKey } : { pubKey }; + }); + return normalizedSigs; + } if (scheme === 'CommandJson') { return cmd.signers.map(({ pubKey }, index) => { const sig = (tx.sigs as CommandJson)[index]?.sig; diff --git a/packages/apps/dev-wallet/src/utils/session.ts b/packages/apps/dev-wallet/src/utils/session.ts index fc553d5274..a7d1d2140e 100644 --- a/packages/apps/dev-wallet/src/utils/session.ts +++ b/packages/apps/dev-wallet/src/utils/session.ts @@ -1,144 +1,104 @@ import { config } from '@/config'; -import { kadenaDecrypt, kadenaEncrypt } from '@kadena/hd-wallet'; - -export async function encryptRecord( - sessionValue: Record | null, -) { - if (!sessionValue) return; - const encrypted: { key: string; value: string }[] = []; - for (const [key, value] of Object.entries(sessionValue)) { - encrypted.push({ - key: await kadenaEncrypt('key', key), - value: await kadenaEncrypt(key, value), - } as EncryptedRecord[number]); - } - return encrypted; -} - -export type EncryptedRecord = { - key: string; - value: string; -}[]; - -export async function decryptRecord(encrypted: EncryptedRecord) { - const decrypted: Record = {}; - for (const { key, value } of encrypted) { - const decryptedKey = new TextDecoder().decode( - await kadenaDecrypt('key', key), - ); - decrypted[decryptedKey] = await kadenaDecrypt(decryptedKey, value); - } - return decrypted; -} - -const SESSION_PASS = new TextEncoder().encode('7b_ksKD_M4D0jnd7_ZM'); - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const throttle = any>( - fn: T, - delay: number, -) => { - let lastCall = 0; - let lastResult: ReturnType; - return (...args: Parameters): ReturnType => { - const now = Date.now(); - if (now - lastCall < delay) return lastResult; - lastCall = now; - lastResult = fn(...args); - return lastResult; - }; -}; +import { getErrorMessage } from './getErrorMessage'; +import { createEventEmitter, throttle } from './helpers'; type SessionValue = { expiration?: string; creationDate?: string } & Record< string, unknown >; -interface ISessionSerialization { - serialize: (session: SessionValue) => Promise; - deserialize: (session: string) => Promise; -} - -const encryptSession: ISessionSerialization = { - serialize: (session: SessionValue): Promise => - kadenaEncrypt(SESSION_PASS, JSON.stringify(session)), - deserialize: async (session: string) => - JSON.parse( - new TextDecoder().decode(await kadenaDecrypt(SESSION_PASS, session)), - ) as SessionValue, -}; - -const serializeSession: ISessionSerialization = { +const serialization = { serialize: async (session: SessionValue) => JSON.stringify(session), deserialize: async (session: string) => JSON.parse(session) as SessionValue, }; -export function createSession( - key: string = 'session', - serialization = config.SESSION.ENCRYPT_SESSION - ? encryptSession - : serializeSession, -) { +const isExpired = (session: SessionValue) => + Date.now() >= Number(session.expiration); + +export function createSession(key: string = 'session') { let loaded = false; let session: SessionValue = { creationDate: `${Date.now()}`, expiration: `${Date.now() + config.SESSION.TTL}`, }; - const isExpired = () => Date.now() >= Number(session.expiration); - const listeners = [] as Array< - ( - event: 'loaded' | 'renewed' | 'expired' | 'cleared', - session?: SessionValue, - ) => void - >; + + const eventEmitter = createEventEmitter<{ + loaded: SessionValue; + renewed: SessionValue; + expired: undefined; + cleared: undefined; + }>(); + let expireTimeout: NodeJS.Timeout | null = null; + const renewData = async () => { session.expiration = `${Date.now() + config.SESSION.TTL}`; localStorage.setItem('session', await serialization.serialize(session)); - listeners.forEach((cb) => cb('renewed', session)); + eventEmitter.emit('renewed', session); }; + const renew = async () => { // console.log('Renewing session', session); if (expireTimeout) { clearTimeout(expireTimeout); } - if (isExpired()) { + if (isExpired(session)) { localStorage.removeItem(key); - listeners.forEach((cb) => cb('expired')); - expireTimeout = null; + eventEmitter.emit('expired', undefined); + return; } - renewData(); + expireTimeout = setTimeout(async () => { - if (isExpired()) { + if (isExpired(session)) { localStorage.removeItem(key); - listeners.forEach((cb) => cb('expired')); - renewData(); + eventEmitter.emit('expired', undefined); } }, config.SESSION.TTL); + + renewData(); }; + return { + ListenToExternalChanges: () => { + const listener = async (event: StorageEvent) => { + if (event.key === key) { + if (event.newValue) { + session = await serialization.deserialize(event.newValue); + loaded = true; + } else { + session = {}; + loaded = false; + eventEmitter.emit('cleared', undefined); + } + } + }; + // Add a storage event listener + window.addEventListener('storage', listener); + return () => window.removeEventListener('storage', listener); + }, load: async () => { const current = localStorage.getItem(key); if (current) { try { session = await serialization.deserialize(current); - // console.log('Loaded session', session); - if (isExpired()) { + if (isExpired(session)) { throw new Error('Session expired!'); } } catch (e) { console.log( - e && typeof e === 'object' && `message` in e - ? e.message - : 'Error loading session', + getErrorMessage(e, 'Error loading session from local storage'), ); - console.log('Creating new session'); localStorage.removeItem(key); + console.log('Resetting session'); + session = { + creationDate: `${Date.now()}`, + expiration: `${Date.now() + config.SESSION.TTL}`, + }; } } await renew(); - listeners.forEach((cb) => cb('loaded', session)); + eventEmitter.emit('loaded', session); loaded = true; }, renew: throttle(renew, 1000 * 1), // 1 minute @@ -151,7 +111,7 @@ export function createSession( localStorage.removeItem('session'); session = {}; loaded = false; - listeners.forEach((cb) => cb('cleared')); + eventEmitter.emit('cleared', undefined); }, reset: () => { session = { @@ -160,20 +120,7 @@ export function createSession( return renew(); }, isLoaded: () => loaded, - subscribe: ( - cb: ( - event: 'loaded' | 'renewed' | 'expired' | 'cleared', - session?: SessionValue, - ) => void, - ) => { - listeners.push(cb); - return () => { - const index = listeners.indexOf(cb); - if (index > -1) { - listeners.splice(index, 1); - } - }; - }, + subscribe: eventEmitter.subscribe, }; } diff --git a/packages/apps/dev-wallet/src/utils/usePatchedNavigate.tsx b/packages/apps/dev-wallet/src/utils/usePatchedNavigate.tsx new file mode 100644 index 0000000000..ff95bed9af --- /dev/null +++ b/packages/apps/dev-wallet/src/utils/usePatchedNavigate.tsx @@ -0,0 +1,13 @@ +import { useCallback, useRef } from 'react'; +import { NavigateFunction, useNavigate } from 'react-router-dom'; + +export const usePatchedNavigate = () => { + const navigate = useNavigate(); + const navigateRef = useRef(navigate); + navigateRef.current = navigate; + const fixedNavigate = useCallback( + ((to, options) => navigateRef.current(to, options)) as NavigateFunction, + [], + ); + return fixedNavigate; +};