-
+
- No website connected
+ {connectionText}
diff --git a/packages/browser-wallet/src/popup/popupX/page-layouts/MainLayout/Header/components/MenuTiles.tsx b/packages/browser-wallet/src/popup/popupX/page-layouts/MainLayout/Header/components/MenuTiles.tsx
index df9f6e9d9..a89d38dd5 100644
--- a/packages/browser-wallet/src/popup/popupX/page-layouts/MainLayout/Header/components/MenuTiles.tsx
+++ b/packages/browser-wallet/src/popup/popupX/page-layouts/MainLayout/Header/components/MenuTiles.tsx
@@ -4,7 +4,7 @@ import Browsers from '@assets/svgX/browsers.svg';
import TextColumns from '@assets/svgX/text-columns.svg';
import Password from '@assets/svgX/password.svg';
import WebId from '@assets/svgX/web-id.svg';
-import Plant from '@assets/svgX/plant.svg';
+import Percent from '@assets/svgX/percent.svg';
import LinkSimple from '@assets/svgX/link-simple-horizontal.svg';
import Info from '@assets/svgX/info2.svg';
import Restore from '@assets/svgX/arrow-counter-clock.svg';
@@ -69,7 +69,7 @@ export default function MenuTiles({ menuOpen, setMenuOpen }: MenuTilesProps) {
-
+
{t('earn')}
diff --git a/packages/browser-wallet/src/popup/popupX/page-layouts/MainLayout/Header/i18n/en.ts b/packages/browser-wallet/src/popup/popupX/page-layouts/MainLayout/Header/i18n/en.ts
index 09cbbd344..45c4f9e96 100644
--- a/packages/browser-wallet/src/popup/popupX/page-layouts/MainLayout/Header/i18n/en.ts
+++ b/packages/browser-wallet/src/popup/popupX/page-layouts/MainLayout/Header/i18n/en.ts
@@ -17,6 +17,10 @@ const t = {
sortDesc: 'Sort Z-A',
searchBy: 'Search by name or address',
},
+ connection: {
+ siteNotConnected: 'No website connected',
+ waiting: 'Waiting',
+ },
};
export default t;
diff --git a/packages/browser-wallet/src/popup/popupX/pages/MainPage/MainPage.scss b/packages/browser-wallet/src/popup/popupX/pages/MainPage/MainPage.scss
index 677fc32ba..62b5ce2d0 100644
--- a/packages/browser-wallet/src/popup/popupX/pages/MainPage/MainPage.scss
+++ b/packages/browser-wallet/src/popup/popupX/pages/MainPage/MainPage.scss
@@ -7,6 +7,7 @@
&__balance {
display: flex;
flex-direction: column;
+ align-items: flex-start;
}
&__action-buttons {
@@ -71,6 +72,9 @@
&__amount,
&__exchange-rate {
display: flex;
+ width: 100%;
+ justify-content: flex-end;
+ align-items: center;
}
.capture__main_small {
@@ -81,14 +85,15 @@
color: $color-white;
}
- .label__main:last-child,
- .capture__main_small:last-child {
+ .balance-rate {
+ display: flex;
+ flex-direction: column;
+ gap: rem(8px);
margin-left: auto;
}
svg {
margin-left: rem(8px);
- margin-top: rem(-2px);
opacity: 0.5;
width: rem(16px);
height: rem(16px);
diff --git a/packages/browser-wallet/src/popup/popupX/pages/MainPage/MainPage.tsx b/packages/browser-wallet/src/popup/popupX/pages/MainPage/MainPage.tsx
index 55c8eed00..00815fb1b 100644
--- a/packages/browser-wallet/src/popup/popupX/pages/MainPage/MainPage.tsx
+++ b/packages/browser-wallet/src/popup/popupX/pages/MainPage/MainPage.tsx
@@ -17,9 +17,9 @@ import Text from '@popup/popupX/shared/Text';
import Button from '@popup/popupX/shared/Button';
import { withSelectedCredential } from '@popup/popupX/shared/utils/hoc';
import Arrow from '@assets/svgX/arrow-right.svg';
-import FileText from '@assets/svgX/file-text.svg';
+import Clock from '@assets/svgX/clock.svg';
import ConcordiumLogo from '@assets/svgX/concordium-logo.svg';
-import Plant from '@assets/svgX/plant.svg';
+import Percent from '@assets/svgX/percent.svg';
import Gear from '@assets/svgX/gear.svg';
import { formatTokenAmount } from '@popup/popupX/shared/utils/helpers';
@@ -59,8 +59,7 @@ function displayCcdAsEur(microCcdPerEur: Ratio, microCcd: bigint, decimals: numb
}
function Balance({ credential }: { credential: WalletCredential }) {
- const chainParameters = useBlockChainParameters();
- const microCcdPerEur = chainParameters?.microGTUPerEuro;
+ const { t } = useTranslation('x', { keyPrefix: 'mainPage' });
const accountInfo = useAccountInfo(credential);
if (!accountInfo) {
@@ -68,13 +67,16 @@ function Balance({ credential }: { credential: WalletCredential }) {
}
const ccdBalance = displayAsCcd(accountInfo.accountAmount.microCcdAmount, false, true);
- const eurBalance =
- microCcdPerEur && displayCcdAsEur(microCcdPerEur, accountInfo.accountAmount.microCcdAmount, 2, true);
+ const ccdAvailableBalance = displayAsCcd(accountInfo.accountAvailableBalance.microCcdAmount, false, true);
return (
- {microCcdPerEur ? eurBalance : ccdBalance}
- {microCcdPerEur ? ccdBalance : ''}
+
+ {ccdBalance}
+
+
+ {ccdAvailableBalance} {t('atDisposal')}
+
);
}
@@ -98,15 +100,16 @@ function TokenItem({ thumbnail, symbol, balance, balanceBase, staked, microCcdPe
{symbol}
- {staked &&
}
-
{balance}
+ {staked &&
}
+
+ {balance}
+ {isNoExchange ? null : (
+
+ {displayCcdAsEur(microCcdPerEur, balanceBase, 2)}
+
+ )}
+
- {isNoExchange ? null : (
-
- {displayCcdAsEur(microCcdPerEur, 1000000n, 6)}
- {displayCcdAsEur(microCcdPerEur, balanceBase, 2)}
-
- )}
);
@@ -120,6 +123,7 @@ function MainPageConfirmedAccount({ credential }: MainPageConfirmedAccountProps)
const nav = useNavigate();
const navToSend = () => nav(generatePath(absoluteRoutes.home.sendFunds.path, { account: credential.address }));
const navToReceive = () => nav(relativeRoutes.home.receive.path);
+ const navToEarn = () => nav(absoluteRoutes.settings.earn.path);
const navToTransactionLog = () =>
nav(relativeRoutes.home.transactionLog.path.replace(':account', credential.address));
const navToTokenDetails = (contractIndex: string) =>
@@ -144,7 +148,8 @@ function MainPageConfirmedAccount({ credential }: MainPageConfirmedAccountProps)
} label={t('receive')} onClick={navToReceive} className="receive" />
} label={t('send')} onClick={navToSend} className="send" />
- } label={t('transactions')} onClick={navToTransactionLog} />
+ } label={t('earn')} onClick={navToEarn} />
+ } label={t('activity')} onClick={navToTransactionLog} />
@@ -192,7 +197,8 @@ function MainPagePendingAccount() {
} label={t('receive')} disabled className="receive" />
} label={t('send')} disabled className="send" />
- } label={t('transactions')} disabled />
+ } label={t('earn')} disabled />
+ } label={t('activity')} disabled />
diff --git a/packages/browser-wallet/src/popup/popupX/pages/MainPage/i18n/en.ts b/packages/browser-wallet/src/popup/popupX/pages/MainPage/i18n/en.ts
index 12665aea0..2b3f0e4c1 100644
--- a/packages/browser-wallet/src/popup/popupX/pages/MainPage/i18n/en.ts
+++ b/packages/browser-wallet/src/popup/popupX/pages/MainPage/i18n/en.ts
@@ -1,10 +1,12 @@
const t = {
receive: 'Receive',
send: 'Send',
- transactions: 'Transactions',
+ earn: 'Earn',
+ activity: 'Activity',
manageTokenList: 'Manage token list',
pendingAccount: 'Creating account',
pendingSubText: 'Ready within a few minutes',
+ atDisposal: 'at disposal',
};
export default t;
diff --git a/packages/browser-wallet/src/popup/popupX/pages/TokenDetails/TokenDetails.scss b/packages/browser-wallet/src/popup/popupX/pages/TokenDetails/TokenDetails.scss
index dcbd201eb..ff306e9da 100644
--- a/packages/browser-wallet/src/popup/popupX/pages/TokenDetails/TokenDetails.scss
+++ b/packages/browser-wallet/src/popup/popupX/pages/TokenDetails/TokenDetails.scss
@@ -1,15 +1,15 @@
.token-details-x {
&__stake {
display: flex;
- justify-content: space-between;
+ flex-direction: column;
margin-top: rem(12px);
margin-bottom: rem(16px);
padding-top: rem(12px);
- border-top: 1px solid rgba($color-white, 0.1);
&_group {
display: flex;
- flex-direction: column;
+ flex-direction: row;
+ justify-content: space-between;
.capture__additional_small {
color: $color-white;
@@ -33,6 +33,19 @@
}
}
+ &__token {
+ display: flex;
+ align-items: center;
+ margin-bottom: rem(8px);
+
+ .token-icon,
+ svg {
+ width: rem(20px);
+ height: rem(20px);
+ margin-right: rem(8px);
+ }
+ }
+
.card-x {
margin-top: rem(16px);
}
diff --git a/packages/browser-wallet/src/popup/popupX/pages/TokenDetails/TokenDetails.tsx b/packages/browser-wallet/src/popup/popupX/pages/TokenDetails/TokenDetails.tsx
index bacdaeee8..ea451f365 100644
--- a/packages/browser-wallet/src/popup/popupX/pages/TokenDetails/TokenDetails.tsx
+++ b/packages/browser-wallet/src/popup/popupX/pages/TokenDetails/TokenDetails.tsx
@@ -15,10 +15,11 @@ import { getMetadataDecimals, trunctateSymbol } from '@shared/utils/token-helper
import { useUpdateAtom } from 'jotai/utils';
import { WalletCredential } from '@shared/storage/types';
import Arrow from '@assets/svgX/arrow-right.svg';
-import FileText from '@assets/svgX/file-text.svg';
+import Clock from '@assets/svgX/clock.svg';
import Notebook from '@assets/svgX/notebook.svg';
import Eye from '@assets/svgX/eye-slash.svg';
import { AccountAddress, ContractAddress } from '@concordium/web-sdk';
+import Img from '@popup/shared/Img';
import { SendFundsLocationState } from '../SendFunds/SendFunds';
const SUB_INDEX = '0';
@@ -57,9 +58,9 @@ function TokenDetails({ credential }: { credential: WalletCredential }) {
return (
-
+
{renderBalance(balance)} {trunctateSymbol(metadata.symbol || '')}
-
+
}
@@ -68,13 +69,18 @@ function TokenDetails({ credential }: { credential: WalletCredential }) {
className="receive"
/>
} label={t('send')} onClick={() => navToSend()} className="send" />
- }
- label={t('transactions')}
- onClick={() => navToTransactionLog()}
- />
+ } label={t('activity')} onClick={() => navToTransactionLog()} />
+
+
+
{metadata.name}
+
diff --git a/packages/browser-wallet/src/popup/popupX/pages/TokenDetails/TokenDetailsCcd.tsx b/packages/browser-wallet/src/popup/popupX/pages/TokenDetails/TokenDetailsCcd.tsx
index a851b9269..eb0363b8e 100644
--- a/packages/browser-wallet/src/popup/popupX/pages/TokenDetails/TokenDetailsCcd.tsx
+++ b/packages/browser-wallet/src/popup/popupX/pages/TokenDetails/TokenDetailsCcd.tsx
@@ -1,7 +1,7 @@
import React, { useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
-import { AccountAddress, AccountInfoType } from '@concordium/web-sdk';
+import { AccountAddress } from '@concordium/web-sdk';
import { relativeRoutes, absoluteRoutes, sendFundsRoute } from '@popup/popupX/constants/routes';
import Page from '@popup/popupX/shared/Page';
import Text from '@popup/popupX/shared/Text';
@@ -12,8 +12,9 @@ import { displayAsCcd, getPublicAccountAmounts, PublicAccountAmounts } from 'wal
import { WalletCredential } from '@shared/storage/types';
import { withSelectedCredential } from '@popup/popupX/shared/utils/hoc';
import Arrow from '@assets/svgX/arrow-right.svg';
-import FileText from '@assets/svgX/file-text.svg';
-import Plant from '@assets/svgX/plant.svg';
+import Clock from '@assets/svgX/clock.svg';
+import Percent from '@assets/svgX/percent.svg';
+import ConcordiumLogo from '@assets/svgX/concordium-logo.svg';
import { TokenPickerVariant } from '@popup/popupX/shared/Form/TokenAmount/View';
const zeroBalance: Omit = {
@@ -24,7 +25,6 @@ const zeroBalance: Omit = {
};
function useCcdInfo(credential: WalletCredential) {
- const { t } = useTranslation('x', { keyPrefix: 'tokenDetails' });
const [balances, setBalances] = useState>(zeroBalance);
const accountInfo = useAccountInfo(credential);
@@ -36,21 +36,13 @@ function useCcdInfo(credential: WalletCredential) {
}
}, [accountInfo]);
- type AccountTypeMap = { [TYPE in AccountInfoType]: string };
-
const tokenDetails = useMemo(() => {
if (accountInfo) {
- const type: AccountTypeMap = {
- [AccountInfoType.Delegator]: t('delegated'),
- [AccountInfoType.Baker]: t('validated'),
- [AccountInfoType.Simple]: '',
- };
return {
total: displayAsCcd(balances.total, false, true),
atDisposal: displayAsCcd(balances.atDisposal, false, true),
staked: displayAsCcd(balances.staked, false, true),
cooldown: displayAsCcd(balances.cooldown, false, true),
- type: type[accountInfo.type],
};
}
return { total: null, atDisposal: null };
@@ -76,22 +68,22 @@ function TokenDetailsCcd({ credential }: { credential: WalletCredential }) {
return (
- {tokenDetails.total}
+
+ {tokenDetails.total}
+
- {t('atDisposal')}
- {tokenDetails.atDisposal}
+ {t('earning')}
+ {tokenDetails.staked}
- {tokenDetails.type && (
-
- {tokenDetails.type}
- {tokenDetails.staked}
-
- )}
{t('cooldown')}
{tokenDetails.cooldown}
+
+ {t('atDisposal')}
+ {tokenDetails.atDisposal}
+
} label={t('send')} onClick={() => navToSend()} className="send" />
- }
- label={t('transactions')}
- onClick={() => navToTransactionLog()}
- />
- } label={t('earn')} onClick={() => navToEarn()} />
+ } label={t('earn')} onClick={() => navToEarn()} />
+ } label={t('activity')} onClick={() => navToTransactionLog()} />
+
+
+ CCD
+
diff --git a/packages/browser-wallet/src/popup/popupX/pages/TokenDetails/i18n/en.ts b/packages/browser-wallet/src/popup/popupX/pages/TokenDetails/i18n/en.ts
index 3d48c69d3..4875f6848 100644
--- a/packages/browser-wallet/src/popup/popupX/pages/TokenDetails/i18n/en.ts
+++ b/packages/browser-wallet/src/popup/popupX/pages/TokenDetails/i18n/en.ts
@@ -1,8 +1,9 @@
const t = {
receive: 'Receive',
send: 'Send',
- transactions: 'Transactions',
+ activity: 'Activity',
earn: 'Earn',
+ earning: 'Earning',
description: 'Description',
decimals: 'Decimals',
indexSubindex: 'Contract index, subindex',
diff --git a/packages/browser-wallet/src/popup/popupX/pages/prompts/SendTransaction/DisplayTransactionPayload.tsx b/packages/browser-wallet/src/popup/popupX/pages/prompts/SendTransaction/DisplayTransactionPayload.tsx
new file mode 100644
index 000000000..84678750a
--- /dev/null
+++ b/packages/browser-wallet/src/popup/popupX/pages/prompts/SendTransaction/DisplayTransactionPayload.tsx
@@ -0,0 +1,159 @@
+import React, { useEffect, useMemo, useState } from 'react';
+import {
+ AccountTransactionPayload,
+ AccountTransactionType,
+ CcdAmount,
+ DeployModulePayload,
+ InitContractPayload,
+ RegisterDataPayload,
+ sha256,
+ SimpleTransferPayload,
+ UpdateContractPayload,
+} from '@concordium/web-sdk';
+import { SmartContractParameters } from '@concordium/browser-wallet-api-helpers';
+import { useTranslation } from 'react-i18next';
+import { chunkString, displayAsCcd } from 'wallet-common-helpers';
+import * as JSONBig from 'json-bigint';
+import { decode } from 'cbor';
+import Card from '@popup/popupX/shared/Card';
+import Parameter from '@popup/popupX/shared/Parameter';
+
+export function DisplayParameters({ parameters }: { parameters?: SmartContractParameters }) {
+ const hasParameters = parameters !== undefined && parameters !== null;
+ if (!hasParameters) return null;
+ return ;
+}
+
+/**
+ * Displays an overview of a simple transfer.
+ */
+function DisplaySimpleTransfer({ payload }: { payload: SimpleTransferPayload }) {
+ const { t } = useTranslation('x', { keyPrefix: 'prompts.sendTransactionX.payload' });
+ return (
+ <>
+
+
+ >
+ );
+}
+
+/**
+ * Displays an overview of a update contract transaction.
+ */
+function DisplayUpdateContract({ payload }: { payload: Omit }) {
+ const { t } = useTranslation('x', { keyPrefix: 'prompts.sendTransactionX.payload' });
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+
+/**
+ * Displays an overview of a init contract transaction.
+ */
+function DisplayInitContract({ payload }: { payload: Omit }) {
+ const { t } = useTranslation('x', { keyPrefix: 'prompts.sendTransactionX.payload' });
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+
+/**
+ * Displays an overview of a register data.
+ */
+function DisplayRegisterData({ payload }: { payload: RegisterDataPayload }) {
+ const { t } = useTranslation('x', { keyPrefix: 'prompts.sendTransactionX.payload' });
+ const [decoded, setDecoded] = useState();
+
+ useEffect(() => {
+ try {
+ setDecoded(decode(payload.data.data));
+ } catch {
+ // display raw if unable to decode
+ }
+ }, []);
+
+ const title = `${t('data')}${!decoded ? t('rawData') : ''}`;
+ const value = decoded || payload.data.toJSON();
+
+ return ;
+}
+
+/**
+ * Displays an overview of a deploy module transaction.
+ */
+function DisplayDeployModule({ payload }: { payload: DeployModulePayload }) {
+ const { t } = useTranslation('x', { keyPrefix: 'prompts.sendTransactionX.payload' });
+ const hash = useMemo(() => sha256([payload.source]).toString('hex'), []);
+ const { version } = payload;
+
+ return (
+ <>
+ {version && }
+
+ >
+ );
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function displayValue(value: any) {
+ if (CcdAmount.instanceOf(value)) {
+ return displayAsCcd(value.microCcdAmount);
+ }
+ return value.toString();
+}
+
+/**
+ * Displays an overview of any transaction payload.
+ */
+function DisplayGenericPayload({ payload }: { payload: AccountTransactionPayload }) {
+ return (
+ <>
+ {Object.entries(payload).map(([key, value]) => (
+
+ ))}
+ >
+ );
+}
+
+export default function DisplayTransactionPayload({
+ payload,
+ type,
+}: {
+ type: AccountTransactionType;
+ payload: AccountTransactionPayload;
+}) {
+ switch (type) {
+ case AccountTransactionType.Transfer:
+ return ;
+ case AccountTransactionType.Update:
+ return ;
+ case AccountTransactionType.InitContract:
+ return ;
+ case AccountTransactionType.RegisterData:
+ return ;
+ case AccountTransactionType.DeployModule:
+ return ;
+ default:
+ return ;
+ }
+}
diff --git a/packages/browser-wallet/src/popup/popupX/pages/prompts/SendTransaction/SendTransaction.scss b/packages/browser-wallet/src/popup/popupX/pages/prompts/SendTransaction/SendTransaction.scss
new file mode 100644
index 000000000..3e86d406f
--- /dev/null
+++ b/packages/browser-wallet/src/popup/popupX/pages/prompts/SendTransaction/SendTransaction.scss
@@ -0,0 +1,22 @@
+.send-transaction-x {
+ .text__main {
+ color: $color-mineral-2;
+
+ &_medium {
+ color: $color-white;
+ }
+
+ .white {
+ color: $color-white;
+ }
+ }
+
+ .card-x.grey {
+ margin-top: rem(16px);
+ margin-bottom: rem(32px);
+
+ .row.details:has(+ .parameter-x) {
+ border: unset;
+ }
+ }
+}
diff --git a/packages/browser-wallet/src/popup/popupX/pages/prompts/SendTransaction/SendTransaction.tsx b/packages/browser-wallet/src/popup/popupX/pages/prompts/SendTransaction/SendTransaction.tsx
new file mode 100644
index 000000000..72d925429
--- /dev/null
+++ b/packages/browser-wallet/src/popup/popupX/pages/prompts/SendTransaction/SendTransaction.tsx
@@ -0,0 +1,162 @@
+import React, { useCallback, useContext, useEffect, useMemo } from 'react';
+import { BackgroundSendTransactionPayload } from '@shared/utils/types';
+import { useLocation } from 'react-router-dom';
+import { Trans, useTranslation } from 'react-i18next';
+import { useAtomValue, useSetAtom } from 'jotai';
+import { addToastAtom } from '@popup/state';
+import { grpcClientAtom } from '@popup/store/settings';
+import { fullscreenPromptContext } from '@popup/popupX/page-layouts/FullscreenPromptLayout';
+import { useUpdateAtom } from 'jotai/utils';
+import { addPendingTransactionAtom } from '@popup/store/transactions';
+import { useBlockChainParameters } from '@popup/shared/BlockChainParametersProvider';
+import { usePrivateKey } from '@popup/shared/utils/account-helpers';
+import { parsePayload } from '@shared/utils/payload-helpers';
+import * as JSONBig from 'json-bigint';
+import { SmartContractParameters } from '@concordium/browser-wallet-api-helpers';
+import { convertEnergyToMicroCcd, getEnergyCost } from '@shared/utils/energy-helpers';
+import { AccountAddress } from '@concordium/web-sdk';
+import { displayAsCcd, getPublicAccountAmounts } from 'wallet-common-helpers';
+import {
+ createPendingTransactionFromAccountTransaction,
+ getDefaultExpiry,
+ getTransactionAmount,
+ getTransactionTypeName,
+ sendTransaction,
+} from '@popup/shared/utils/transaction-helpers';
+import Page from '@popup/popupX/shared/Page';
+import Text from '@popup/popupX/shared/Text';
+import { displayUrl } from '@popup/shared/utils/string-helpers';
+import Button from '@popup/popupX/shared/Button';
+import Card from '@popup/popupX/shared/Card';
+import DisplayTransactionPayload, {
+ DisplayParameters,
+} from '@popup/popupX/pages/prompts/SendTransaction/DisplayTransactionPayload';
+
+interface Location {
+ state: {
+ payload: BackgroundSendTransactionPayload;
+ };
+}
+
+interface Props {
+ onSubmit(hash: string): void;
+ onReject(): void;
+}
+
+export default function SendTransaction({ onSubmit, onReject }: Props) {
+ const { state } = useLocation() as Location;
+ const { t } = useTranslation('x', { keyPrefix: 'prompts.sendTransactionX' });
+ const addToast = useSetAtom(addToastAtom);
+ const client = useAtomValue(grpcClientAtom);
+ const { withClose, onClose } = useContext(fullscreenPromptContext);
+ const addPendingTransaction = useUpdateAtom(addPendingTransactionAtom);
+ const chainParameters = useBlockChainParameters();
+
+ const { accountAddress, url } = state.payload;
+ const key = usePrivateKey(accountAddress);
+
+ const { type: transactionType, payload } = useMemo(
+ () =>
+ parsePayload(
+ state.payload.type,
+ state.payload.payload,
+ state.payload.parameters,
+ state.payload.schema,
+ state.payload.schemaVersion
+ ),
+ [JSON.stringify(state.payload)]
+ );
+ const parameters = useMemo(
+ () =>
+ state.payload.parameters === undefined
+ ? undefined
+ : (JSONBig.parse(state.payload.parameters) as SmartContractParameters),
+ [state.payload.parameters]
+ );
+
+ const cost = useMemo(() => {
+ if (chainParameters) {
+ const energy = getEnergyCost(transactionType, payload);
+ return convertEnergyToMicroCcd(energy, chainParameters);
+ }
+ return undefined;
+ }, [transactionType, chainParameters]);
+
+ useEffect(() => onClose(onReject), [onClose, onReject]);
+
+ const handleSubmit = useCallback(async () => {
+ if (!accountAddress) {
+ throw new Error(t('errors.missingAccount'));
+ }
+ if (!key) {
+ throw new Error(t('errors.missingKey'));
+ }
+
+ const sender = AccountAddress.fromBase58(accountAddress);
+ const accountInfo = await client.getAccountInfo(sender);
+ if (
+ getPublicAccountAmounts(accountInfo).atDisposal <
+ getTransactionAmount(transactionType, payload) + (cost || 0n)
+ ) {
+ throw new Error(t('errors.insufficientFunds'));
+ }
+
+ const nonce = await client.getNextAccountNonce(sender);
+
+ if (!nonce) {
+ throw new Error(t('errors.missingNonce'));
+ }
+
+ const header = {
+ expiry: getDefaultExpiry(),
+ sender,
+ nonce: nonce.nonce,
+ };
+ const transaction = { payload, header, type: transactionType };
+
+ const hash = await sendTransaction(client, transaction, key);
+ const pending = createPendingTransactionFromAccountTransaction(transaction, hash, cost);
+ await addPendingTransaction(pending);
+
+ return hash;
+ }, [payload, key, cost]);
+
+ return (
+
+
+
+
+ }}
+ values={{ dApp: displayUrl(url) }}
+ />
+
+
+
+ {getTransactionTypeName(transactionType)}
+
+
+
+
+
+
+
+
+
+ {
+ handleSubmit()
+ .then(withClose(onSubmit))
+ .catch((e) => addToast(e.message));
+ }}
+ />
+
+
+ );
+}
diff --git a/packages/browser-wallet/src/popup/popupX/pages/prompts/SendTransaction/i18n/en.ts b/packages/browser-wallet/src/popup/popupX/pages/prompts/SendTransaction/i18n/en.ts
new file mode 100644
index 000000000..7cbc8d215
--- /dev/null
+++ b/packages/browser-wallet/src/popup/popupX/pages/prompts/SendTransaction/i18n/en.ts
@@ -0,0 +1,31 @@
+const t = {
+ reject: 'Reject',
+ sign: 'Sign & Submit',
+ signTransaction: '<1>{{dApp}}1> suggests a transaction',
+ signRequest: 'Signature request',
+ errors: {
+ missingAccount: 'Missing account address',
+ missingKey: 'Missing key for the chosen address',
+ insufficientFunds: 'Account has insufficient funds for the transaction',
+ missingNonce: 'No nonce was found for the chosen account',
+ },
+ payload: {
+ amount: 'Amount',
+ receiver: 'Receiver',
+ contractIndex: 'Contract index (subindex)',
+ receiveName: 'Contract and function name',
+ maxEnergy: 'Max energy allowed',
+ nrg: 'NRG',
+ noParameter: 'No parameters',
+ sender: 'Sender account',
+ cost: 'Estimated transaction fee',
+ unknown: 'Unknown',
+ moduleReference: 'Module reference',
+ contractName: 'Contract name',
+ data: 'Data',
+ rawData: ': (Unable to be decoded)',
+ version: 'Version',
+ },
+};
+
+export default t;
diff --git a/packages/browser-wallet/src/popup/popupX/pages/prompts/SendTransaction/index.ts b/packages/browser-wallet/src/popup/popupX/pages/prompts/SendTransaction/index.ts
new file mode 100644
index 000000000..152d81622
--- /dev/null
+++ b/packages/browser-wallet/src/popup/popupX/pages/prompts/SendTransaction/index.ts
@@ -0,0 +1 @@
+export { default } from './SendTransaction';
diff --git a/packages/browser-wallet/src/popup/popupX/pages/prompts/SignCis3Message/SignCis3Message.scss b/packages/browser-wallet/src/popup/popupX/pages/prompts/SignCis3Message/SignCis3Message.scss
new file mode 100644
index 000000000..1ae85a917
--- /dev/null
+++ b/packages/browser-wallet/src/popup/popupX/pages/prompts/SignCis3Message/SignCis3Message.scss
@@ -0,0 +1,25 @@
+.sign-cis3-x {
+ .page__main {
+ .text__main {
+ margin-bottom: rem(24px);
+ }
+
+ .text__main,
+ .capture__main_small {
+ color: $color-mineral-2;
+
+ .white {
+ color: $color-white;
+ }
+ }
+
+ .card-x {
+ margin-top: rem(16px);
+ margin-bottom: rem(32px);
+
+ .row.details:has(+ .parameter-x) {
+ border: unset;
+ }
+ }
+ }
+}
diff --git a/packages/browser-wallet/src/popup/popupX/pages/prompts/SignCis3Message/SignCis3Message.tsx b/packages/browser-wallet/src/popup/popupX/pages/prompts/SignCis3Message/SignCis3Message.tsx
new file mode 100644
index 000000000..60da6e618
--- /dev/null
+++ b/packages/browser-wallet/src/popup/popupX/pages/prompts/SignCis3Message/SignCis3Message.tsx
@@ -0,0 +1,169 @@
+import React, { useCallback, useContext, useEffect, useState } from 'react';
+import { Trans, useTranslation } from 'react-i18next';
+import { useSetAtom } from 'jotai';
+import Page from '@popup/popupX/shared/Page';
+import Text from '@popup/popupX/shared/Text';
+import Button from '@popup/popupX/shared/Button';
+import { usePrivateKey } from '@popup/shared/utils/account-helpers';
+import { fullscreenPromptContext } from '@popup/popupX/page-layouts/FullscreenPromptLayout';
+import Card from '@popup/popupX/shared/Card';
+import {
+ AccountAddress,
+ AccountTransactionSignature,
+ buildBasicAccountSigner,
+ ContractAddress,
+ ContractName,
+ deserializeTypeValue,
+ EntrypointName,
+ serializeTypeValue,
+ signMessage,
+} from '@concordium/web-sdk';
+import { addToastAtom } from '@popup/state';
+import { useLocation } from 'react-router-dom';
+import { SignMessageObject } from '@concordium/browser-wallet-api-helpers';
+import { Buffer } from 'buffer';
+import { stringify } from 'json-bigint';
+import Parameter from '@popup/popupX/shared/Parameter';
+import { displayUrl } from '@popup/shared/utils/string-helpers';
+
+const SERIALIZATION_HELPER_SCHEMA =
+ 'FAAFAAAAEAAAAGNvbnRyYWN0X2FkZHJlc3MMBQAAAG5vbmNlBQkAAAB0aW1lc3RhbXANCwAAAGVudHJ5X3BvaW50FgEHAAAAcGF5bG9hZBABAg==';
+
+async function parseMessage(message: SignMessageObject) {
+ return stringify(
+ deserializeTypeValue(Buffer.from(message.data, 'hex'), Buffer.from(message.schema, 'base64')),
+ undefined,
+ 2
+ );
+}
+
+function useMessageDetails({
+ payloadMessage,
+ cis3ContractDetails,
+}: {
+ payloadMessage: SignMessageObject;
+ cis3ContractDetails: Cis3ContractDetailsObject;
+}) {
+ const { t } = useTranslation('x', { keyPrefix: 'prompts.signCis3' });
+ const { contractAddress, contractName, entrypointName, nonce, expiryTimeSignature } = cis3ContractDetails;
+ const [parsedMessage, setParsedMessage] = useState('');
+ const expiry = new Date(expiryTimeSignature).toString();
+
+ useEffect(() => {
+ parseMessage(payloadMessage)
+ .then((m) => setParsedMessage(m))
+ .catch(() => setParsedMessage(t('unableToDeserialize')));
+ }, []);
+
+ return {
+ mainDetails: [
+ [t('contractIndex'), `${contractAddress.index.toString()} (${contractAddress.subindex.toString()})`],
+ [t('receiveName'), `${contractName.value.toString()}.${entrypointName.value.toString()}`],
+ [t('nonce'), nonce.toString()],
+ [t('expiry'), expiry],
+ ],
+ parsedMessage,
+ };
+}
+
+function serializeMessage(payloadMessage: SignMessageObject, cis3ContractDetails: Cis3ContractDetailsObject) {
+ const { contractAddress, entrypointName, nonce, expiryTimeSignature } = cis3ContractDetails;
+ const message = {
+ contract_address: {
+ index: Number(contractAddress.index),
+ subindex: Number(contractAddress.subindex),
+ },
+ nonce: Number(nonce),
+ timestamp: expiryTimeSignature,
+ entry_point: EntrypointName.toString(entrypointName),
+ payload: Array.from(Buffer.from(payloadMessage.data, 'hex')),
+ };
+
+ return serializeTypeValue(message, Buffer.from(SERIALIZATION_HELPER_SCHEMA, 'base64'));
+}
+
+interface Location {
+ state: {
+ payload: {
+ accountAddress: string;
+ message: SignMessageObject;
+ url: string;
+ cis3ContractDetails: Cis3ContractDetailsObject;
+ };
+ };
+}
+
+type Cis3ContractDetailsObject = {
+ contractAddress: ContractAddress.Type;
+ contractName: ContractName.Type;
+ entrypointName: EntrypointName.Type;
+ nonce: bigint | number;
+ expiryTimeSignature: string;
+};
+
+type Props = {
+ onSubmit(signature: AccountTransactionSignature): void;
+ onReject(): void;
+};
+
+export default function SignCis3Message({ onSubmit, onReject }: Props) {
+ const { t } = useTranslation('x', { keyPrefix: 'prompts.signCis3' });
+ const { state } = useLocation() as Location;
+ const { withClose } = useContext(fullscreenPromptContext);
+ const { accountAddress, message, url, cis3ContractDetails } = state.payload;
+ const { mainDetails, parsedMessage } = useMessageDetails({ payloadMessage: message, cis3ContractDetails });
+ const key = usePrivateKey(accountAddress);
+ const addToast = useSetAtom(addToastAtom);
+ const onClick = useCallback(async () => {
+ if (!key) {
+ throw new Error('Missing key for the chosen address');
+ }
+
+ return signMessage(
+ AccountAddress.fromBase58(accountAddress),
+ serializeMessage(message, cis3ContractDetails).buffer,
+ buildBasicAccountSigner(key)
+ );
+ }, [state.payload.message, state.payload.accountAddress, key]);
+
+ return (
+
+
+
+
+ }}
+ values={{ dApp: displayUrl(url) }}
+ />
+
+
+ }}
+ values={{ dApp: displayUrl(url) }}
+ />
+
+
+ {mainDetails.map(([title, value]) => (
+
+ ))}
+
+
+
+
+
+ {
+ onClick()
+ .then(withClose(onSubmit))
+ .catch((e) => addToast(e.message));
+ }}
+ />
+
+
+ );
+}
diff --git a/packages/browser-wallet/src/popup/popupX/pages/prompts/SignCis3Message/i18n/en.ts b/packages/browser-wallet/src/popup/popupX/pages/prompts/SignCis3Message/i18n/en.ts
new file mode 100644
index 000000000..d660cc96d
--- /dev/null
+++ b/packages/browser-wallet/src/popup/popupX/pages/prompts/SignCis3Message/i18n/en.ts
@@ -0,0 +1,16 @@
+const t = {
+ signRequest: 'Signature request',
+ signTransaction: '<1>{{dApp}}1> requests a signature on a message',
+ connectionDetails:
+ "<1>{{dApp}}1> has provided the raw message and a schema to render it. We've rendered the message but you should only sign it if you trust <1>{{dApp}}1>",
+ unableToDeserialize: 'Unable to render message',
+ contractIndex: 'Contract index (subindex)',
+ receiveName: 'Contract and function name',
+ parameter: 'Parameter',
+ nonce: 'Nonce',
+ expiry: 'Expiry time',
+ reject: 'Reject',
+ sign: 'Sign & Submit',
+};
+
+export default t;
diff --git a/packages/browser-wallet/src/popup/popupX/pages/prompts/SignCis3Message/index.ts b/packages/browser-wallet/src/popup/popupX/pages/prompts/SignCis3Message/index.ts
new file mode 100644
index 000000000..68f93d061
--- /dev/null
+++ b/packages/browser-wallet/src/popup/popupX/pages/prompts/SignCis3Message/index.ts
@@ -0,0 +1 @@
+export { default } from './SignCis3Message';
diff --git a/packages/browser-wallet/src/popup/popupX/pages/prompts/SignMessage/SignMessage.scss b/packages/browser-wallet/src/popup/popupX/pages/prompts/SignMessage/SignMessage.scss
new file mode 100644
index 000000000..26f6f75c5
--- /dev/null
+++ b/packages/browser-wallet/src/popup/popupX/pages/prompts/SignMessage/SignMessage.scss
@@ -0,0 +1,14 @@
+.sign-message-x {
+ .text__main {
+ color: $color-mineral-2;
+ margin-bottom: rem(24px);
+
+ .white {
+ color: $color-white;
+ }
+ }
+
+ .binary-display-x {
+ margin-bottom: rem(24px);
+ }
+}
diff --git a/packages/browser-wallet/src/popup/popupX/pages/prompts/SignMessage/SignMessage.tsx b/packages/browser-wallet/src/popup/popupX/pages/prompts/SignMessage/SignMessage.tsx
new file mode 100644
index 000000000..9e3487fbd
--- /dev/null
+++ b/packages/browser-wallet/src/popup/popupX/pages/prompts/SignMessage/SignMessage.tsx
@@ -0,0 +1,86 @@
+import { Trans, useTranslation } from 'react-i18next';
+import { useLocation } from 'react-router-dom';
+import React, { useCallback, useContext } from 'react';
+import { fullscreenPromptContext } from '@popup/popupX/page-layouts/FullscreenPromptLayout';
+import { usePrivateKey } from '@popup/shared/utils/account-helpers';
+import { useSetAtom } from 'jotai';
+import { addToastAtom } from '@popup/state';
+import { AccountAddress, AccountTransactionSignature, buildBasicAccountSigner, signMessage } from '@concordium/web-sdk';
+import Page from '@popup/popupX/shared/Page';
+import Text from '@popup/popupX/shared/Text';
+import Button from '@popup/popupX/shared/Button';
+import { Buffer } from 'buffer';
+import { displayUrl } from '@popup/shared/utils/string-helpers';
+import BinaryDisplay from '@popup/popupX/shared/BinaryDisplay';
+
+type Props = {
+ onSubmit(signature: AccountTransactionSignature): void;
+ onReject(): void;
+};
+
+interface Location {
+ state: {
+ payload: {
+ accountAddress: string;
+ message: string | MessageObject;
+ url: string;
+ };
+ };
+}
+
+type MessageObject = {
+ schema: string;
+ data: string;
+};
+
+export default function SignMessage({ onSubmit, onReject }: Props) {
+ const { state } = useLocation() as Location;
+ const { t } = useTranslation('x', { keyPrefix: 'prompts.signMessageX' });
+ const { withClose } = useContext(fullscreenPromptContext);
+ const { accountAddress, url } = state.payload;
+ const key = usePrivateKey(accountAddress);
+ const addToast = useSetAtom(addToastAtom);
+ const { message } = state.payload;
+ const messageIsAString = typeof message === 'string';
+
+ const onClick = useCallback(async () => {
+ if (!key) {
+ throw new Error('Missing key for the chosen address');
+ }
+
+ return signMessage(
+ AccountAddress.fromBase58(accountAddress),
+ messageIsAString ? message : Buffer.from(message.data, 'hex'),
+ buildBasicAccountSigner(key)
+ );
+ }, [state.payload.message, state.payload.accountAddress, key]);
+
+ return (
+
+
+
+
+ }}
+ values={{ dApp: displayUrl(url) }}
+ />
+
+
+
+
+
+
+ {
+ onClick()
+ .then(withClose(onSubmit))
+ .catch((e) => addToast(e.message));
+ }}
+ />
+
+
+ );
+}
diff --git a/packages/browser-wallet/src/popup/popupX/pages/prompts/SignMessage/i18n/en.ts b/packages/browser-wallet/src/popup/popupX/pages/prompts/SignMessage/i18n/en.ts
new file mode 100644
index 000000000..d660cc96d
--- /dev/null
+++ b/packages/browser-wallet/src/popup/popupX/pages/prompts/SignMessage/i18n/en.ts
@@ -0,0 +1,16 @@
+const t = {
+ signRequest: 'Signature request',
+ signTransaction: '<1>{{dApp}}1> requests a signature on a message',
+ connectionDetails:
+ "<1>{{dApp}}1> has provided the raw message and a schema to render it. We've rendered the message but you should only sign it if you trust <1>{{dApp}}1>",
+ unableToDeserialize: 'Unable to render message',
+ contractIndex: 'Contract index (subindex)',
+ receiveName: 'Contract and function name',
+ parameter: 'Parameter',
+ nonce: 'Nonce',
+ expiry: 'Expiry time',
+ reject: 'Reject',
+ sign: 'Sign & Submit',
+};
+
+export default t;
diff --git a/packages/browser-wallet/src/popup/popupX/pages/prompts/SignMessage/index.ts b/packages/browser-wallet/src/popup/popupX/pages/prompts/SignMessage/index.ts
new file mode 100644
index 000000000..e6f80df6f
--- /dev/null
+++ b/packages/browser-wallet/src/popup/popupX/pages/prompts/SignMessage/index.ts
@@ -0,0 +1 @@
+export { default } from './SignMessage';
diff --git a/packages/browser-wallet/src/popup/popupX/shared/BinaryDisplay/BinaryDisplay.scss b/packages/browser-wallet/src/popup/popupX/shared/BinaryDisplay/BinaryDisplay.scss
new file mode 100644
index 000000000..091e86e7c
--- /dev/null
+++ b/packages/browser-wallet/src/popup/popupX/shared/BinaryDisplay/BinaryDisplay.scss
@@ -0,0 +1,23 @@
+.binary-display-x {
+ .tab-bar {
+ margin-top: rem(16px);
+ }
+
+ .white {
+ color: $color-white;
+ }
+
+ .sign-message {
+ &__binary-text-area {
+ border: 1px solid $color-input-border;
+ border-top: unset;
+ border-radius: 0 0 rem(12px) rem(12px);
+ background: $color-transaction-bg;
+
+ .form-input__area {
+ min-height: rem(165px);
+ font-size: rem(12px);
+ }
+ }
+ }
+}
diff --git a/packages/browser-wallet/src/popup/popupX/shared/BinaryDisplay/BinaryDisplay.tsx b/packages/browser-wallet/src/popup/popupX/shared/BinaryDisplay/BinaryDisplay.tsx
new file mode 100644
index 000000000..28d2abf1a
--- /dev/null
+++ b/packages/browser-wallet/src/popup/popupX/shared/BinaryDisplay/BinaryDisplay.tsx
@@ -0,0 +1,83 @@
+import React, { useMemo, useState } from 'react';
+import Text from '@popup/popupX/shared/Text';
+import { Trans, useTranslation } from 'react-i18next';
+import { TextArea } from '@popup/popupX/shared/Form/TextArea';
+import { stringify } from 'json-bigint';
+import { deserializeTypeValue } from '@concordium/web-sdk';
+import { Buffer } from 'buffer';
+import { displayUrl } from '@popup/shared/utils/string-helpers';
+import TabBar from '@popup/popupX/shared/TabBar';
+import clsx from 'clsx';
+import Button from '@popup/popupX/shared/Button';
+
+type MessageObject = {
+ schema: string;
+ data: string;
+};
+
+function StringRender({ message }: { message: string }) {
+ return (
+
+
+
+ );
+}
+
+function BinaryRender({ message, url }: { message: MessageObject; url: string }) {
+ const { t } = useTranslation('x', { keyPrefix: 'sharedX.binaryDisplay' });
+ const [displayDeserialized, setDisplayDeserialized] = useState(true);
+
+ const parsedMessage = useMemo(() => {
+ try {
+ return stringify(
+ deserializeTypeValue(Buffer.from(message.data, 'hex'), Buffer.from(message.schema, 'base64')),
+ undefined,
+ 2
+ );
+ } catch (e) {
+ return t('unableToDeserialize');
+ }
+ }, []);
+
+ const display = useMemo(() => (displayDeserialized ? parsedMessage : message.data), [displayDeserialized]);
+
+ return (
+
+
+ }}
+ values={{ dApp: displayUrl(url) }}
+ />
+
+
+ setDisplayDeserialized(true)}
+ >
+ {t('deserializedDisplay')}
+
+ setDisplayDeserialized(false)}
+ >
+ {t('rawDisplay')}
+
+
+
+
+ );
+}
+
+export default function BinaryDisplay({ message, url }: { message: MessageObject | string; url: string }) {
+ const messageIsAString = typeof message === 'string';
+
+ if (messageIsAString) {
+ return ;
+ }
+
+ return ;
+}
diff --git a/packages/browser-wallet/src/popup/popupX/shared/BinaryDisplay/index.ts b/packages/browser-wallet/src/popup/popupX/shared/BinaryDisplay/index.ts
new file mode 100644
index 000000000..d177ebf86
--- /dev/null
+++ b/packages/browser-wallet/src/popup/popupX/shared/BinaryDisplay/index.ts
@@ -0,0 +1 @@
+export { default } from './BinaryDisplay';
diff --git a/packages/browser-wallet/src/popup/popupX/shared/Card/Card.scss b/packages/browser-wallet/src/popup/popupX/shared/Card/Card.scss
index 4fd24c999..25888bd11 100644
--- a/packages/browser-wallet/src/popup/popupX/shared/Card/Card.scss
+++ b/packages/browser-wallet/src/popup/popupX/shared/Card/Card.scss
@@ -20,7 +20,7 @@
.row {
display: flex;
- &:not(:last-child) {
+ &:not(:last-of-type) {
padding-bottom: rem(8px);
margin-bottom: rem(8px);
border-bottom: 1px solid rgba($color-white, 0.1);
diff --git a/packages/browser-wallet/src/popup/popupX/shared/Form/Form.scss b/packages/browser-wallet/src/popup/popupX/shared/Form/Form.scss
index 0e1f1d0e5..883547cf0 100644
--- a/packages/browser-wallet/src/popup/popupX/shared/Form/Form.scss
+++ b/packages/browser-wallet/src/popup/popupX/shared/Form/Form.scss
@@ -47,6 +47,12 @@
}
}
+ &__area {
+ background-color: transparent;
+ color: $color-white;
+ border: unset;
+ }
+
&__checkbox {
display: flex;
flex-direction: row;
@@ -165,7 +171,6 @@
transform: unset;
svg {
-
g,
path {
fill: $color-mineral-3;
@@ -195,11 +200,11 @@ $handle-size: rem(20px);
.form-toggle-x {
&__root {
- input:checked+.form-toggle-x__slider {
+ input:checked + .form-toggle-x__slider {
background-color: $color-green-toggle;
}
- input:checked+.form-toggle-x__slider::before {
+ input:checked + .form-toggle-x__slider::before {
transform: translateX(rem(24px));
}
}
@@ -279,11 +284,11 @@ $handle-size: rem(20px);
}
}
- &:hover input~.checkmark {
+ &:hover input ~ .checkmark {
background-color: $color-grey-3;
}
- input:checked~.checkmark::after {
+ input:checked ~ .checkmark::after {
display: block;
}
diff --git a/packages/browser-wallet/src/popup/popupX/shared/Form/TextArea/TextArea.tsx b/packages/browser-wallet/src/popup/popupX/shared/Form/TextArea/TextArea.tsx
new file mode 100644
index 000000000..87d605fab
--- /dev/null
+++ b/packages/browser-wallet/src/popup/popupX/shared/Form/TextArea/TextArea.tsx
@@ -0,0 +1,53 @@
+/* eslint-disable react/prop-types */
+import clsx from 'clsx';
+import React, { forwardRef, TextareaHTMLAttributes } from 'react';
+
+import { CommonFieldProps, RequiredUncontrolledFieldProps } from '../common/types';
+import { makeUncontrolled } from '../common/utils';
+import ErrorMessage from '../ErrorMessage';
+
+type Props = Pick<
+ TextareaHTMLAttributes,
+ 'className' | 'value' | 'onChange' | 'onBlur' | 'readOnly' | 'onPaste'
+> &
+ RequiredUncontrolledFieldProps &
+ CommonFieldProps;
+
+/**
+ * @description
+ * Use as a normal \