-
Delegation Id:
+
Delegation Id
{inputWithWithdraw.delegation_id}
-
Amount:
+
Amount
{inputWithWithdraw.amount.decimal}
@@ -476,9 +481,9 @@ const BridgeRequest = ({ transactionData }) => {
-
Token id:
+
Token id
{inputsWithTokens[0].utxo.value.token_id}
-
Amount:
+
Amount
{outputsWithTokens[0].value.amount.decimal}
@@ -489,8 +494,16 @@ const BridgeRequest = ({ transactionData }) => {
const SummaryView = ({ data }) => {
const { flags, transactionData } = SignTxHelpers.getTransactionDetails(data)
const { addresses } = useContext(AccountContext)
+ const { tokenMap } = useContext(MintlayerContext)
+ const { networkType } = useContext(SettingsContext)
const requiredAddresses = addresses.mlAddresses.mlChangeAddresses
+ const ownAddresses = {
+ receiving: addresses.mlAddresses.mlReceivingAddresses,
+ change: addresses.mlAddresses.mlChangeAddresses,
+ }
+ const coinTicker =
+ networkType === AppInfo.NETWORK_TYPES.TESTNET ? 'TML' : 'ML'
return (
@@ -551,6 +564,15 @@ const SummaryView = ({ data }) => {
requiredAddresses={requiredAddresses}
/>
)}
+
+ {flags.isUnknown && }
+
+
)
@@ -558,9 +580,11 @@ const SummaryView = ({ data }) => {
const InternalTransactionPreview = ({ data }) => {
return (
-
-
-
+
+
+
+
+
)
}
diff --git a/src/components/containers/SignTransaction/TransactionBreakdown/TransactionBreakdown.js b/src/components/containers/SignTransaction/TransactionBreakdown/TransactionBreakdown.js
new file mode 100644
index 00000000..22a5366c
--- /dev/null
+++ b/src/components/containers/SignTransaction/TransactionBreakdown/TransactionBreakdown.js
@@ -0,0 +1,301 @@
+import Decimal from 'decimal.js'
+
+import styles from './TransactionBreakdown.module.css'
+
+const COIN_KEY = 'Coin'
+const COIN_DECIMALS = 11
+
+// keeps room for the largest balances and never falls back to exponents
+const Amount = Decimal.clone({ precision: 40, toExpNeg: -30, toExpPos: 30 })
+
+const formatTotal = (total, isCoin) =>
+ (isCoin ? total.toDecimalPlaces(COIN_DECIMALS) : total).toFixed()
+
+const shortenId = (id) =>
+ id && id.length > 16 ? `${id.slice(0, 8)}…${id.slice(-6)}` : id
+
+const MAX_FIELD_DEPTH = 2
+const MAX_FIELD_LENGTH = 80
+
+const shortenData = (data) =>
+ data && data.length > MAX_FIELD_LENGTH
+ ? `${data.slice(0, MAX_FIELD_LENGTH)}…`
+ : data
+
+const getAmount = (source) =>
+ source?.value?.amount?.decimal ??
+ source?.amount?.decimal ??
+ (typeof source?.amount === 'string' ? source.amount : null)
+
+const getAsset = (source, tokenMap, coinTicker) => {
+ const value = source?.value || source
+ if (value?.type === 'Coin') {
+ return { key: COIN_KEY, label: coinTicker }
+ }
+
+ const tokenId = value?.token_id || source?.token_id
+ if (tokenId) {
+ return { key: tokenId, label: tokenMap[tokenId] || shortenId(tokenId) }
+ }
+
+ // account inputs carry a bare amount, and those are always coins
+ return getAmount(source) ? { key: COIN_KEY, label: coinTicker } : null
+}
+
+const getOwnership = (address, ownAddresses) => {
+ if (!address) return null
+ if (ownAddresses.receiving?.includes(address)) return 'Your address'
+ if (ownAddresses.change?.includes(address)) return 'Your change address'
+ return null
+}
+
+const DESCRIBED_FIELDS = [
+ 'type',
+ 'value',
+ 'destination',
+ 'amount',
+ 'input_type',
+ 'index',
+ 'source_id',
+ 'source_type',
+ 'command',
+ 'account_type',
+]
+
+const getExtraFields = (source) =>
+ Object.entries(source || {}).filter(
+ ([key, value]) => !DESCRIBED_FIELDS.includes(key) && value !== undefined,
+ )
+
+const LOCK_LABELS = {
+ ForBlockCount: (content) => `for ${content} blocks`,
+ UntilTime: (content) => `until ${content}`,
+ ForSeconds: (content) => `for ${content} seconds`,
+ UntilHeight: (content) => `until block ${content}`,
+}
+
+const formatLock = (lock) => {
+ const label = LOCK_LABELS[lock?.type]
+ return label ? label(lock.content) : null
+}
+
+const FieldValue = ({ value, depth = 0 }) => {
+ if (value === null) return 'null'
+
+ // the chain encodes text fields as a hex/string pair, only the text reads
+ if (value?.string !== undefined && value?.hex !== undefined) {
+ return
{shortenData(String(value.string))}
+ }
+
+ if (typeof value !== 'object') {
+ const text = String(value)
+ return
{shortenData(text)}
+ }
+
+ if (depth >= MAX_FIELD_DEPTH) {
+ const text = JSON.stringify(value)
+ return
{shortenData(text)}
+ }
+
+ return (
+
+ {Object.entries(value).map(([key, nested]) => (
+
+ {key}
+
+
+ ))}
+
+ )
+}
+
+const ExtraFields = ({ source }) => {
+ const fields = getExtraFields(source)
+ if (fields.length === 0) return null
+
+ return (
+
+ {fields.map(([key, value]) => {
+ const lock = key === 'lock' ? formatLock(value) : null
+
+ return (
+
+
- {key}
+ -
+ {lock || }
+
+
+ )
+ })}
+
+ )
+}
+
+const Entry = ({ title, amount, asset, address, ownership, source }) => (
+
+
+
+ {title}
+ {ownership && (
+ <>
+ {' · '}
+ {ownership}
+ >
+ )}
+
+ {address && {address}}
+
+
+ {amount && (
+
+ {amount} {asset?.label}
+
+ )}
+
+)
+
+const getBalanceChanges = ({
+ inputs,
+ outputs,
+ ownAddresses,
+ tokenMap,
+ coinTicker,
+}) => {
+ const changes = new Map()
+
+ const apply = (source, address, sign) => {
+ if (!getOwnership(address, ownAddresses)) return
+ const asset = getAsset(source, tokenMap, coinTicker)
+ const amount = getAmount(source)
+ if (!asset || !amount) return
+
+ const current = changes.get(asset.key) || {
+ key: asset.key,
+ label: asset.label,
+ total: new Amount(0),
+ }
+ current.total = current.total.plus(new Amount(amount).times(sign))
+ changes.set(asset.key, current)
+ }
+
+ inputs.forEach((input) => {
+ if (input.input?.input_type !== 'UTXO') return
+ apply(input.utxo, input.utxo?.destination, -1)
+ })
+
+ outputs.forEach((output) => apply(output, output.destination, 1))
+
+ return [...changes.values()].filter(({ total }) => !total.isZero())
+}
+
+const TransactionBreakdown = ({
+ JSONRepresentation,
+ ownAddresses = {},
+ tokenMap = {},
+ coinTicker = 'ML',
+}) => {
+ const inputs = JSONRepresentation?.inputs || []
+ const outputs = JSONRepresentation?.outputs || []
+
+ const balanceChanges = getBalanceChanges({
+ inputs,
+ outputs,
+ ownAddresses,
+ tokenMap,
+ coinTicker,
+ })
+
+ return (
+
+
+ Balance change
+ {balanceChanges.length > 0 ? (
+ <>
+
+ {balanceChanges.map(({ key, label, total }) => (
+ -
+ {total.isNegative() ? '−' : '+'}
+ {formatTotal(total.abs(), key === COIN_KEY)} {label}
+
+ ))}
+
+
+ The network fee is part of this amount
+
+ >
+ ) : (
+
+ This transaction does not move funds held by this wallet.
+
+ )}
+
+
+
+ Inputs ({inputs.length})
+
+ {inputs.map((input, index) => {
+ const isUtxo = input.input?.input_type === 'UTXO'
+ const source = isUtxo ? input.utxo : input.input
+ const address = isUtxo
+ ? input.utxo?.destination
+ : input.input?.destination
+ const title = isUtxo
+ ? input.utxo?.type || 'UTXO'
+ : input.input?.command ||
+ input.input?.account_type ||
+ input.input?.input_type
+
+ return (
+
+ )
+ })}
+
+
+
+
+ Outputs ({outputs.length})
+
+ {outputs.map((output, index) => (
+
+ ))}
+
+
+
+ )
+}
+
+export default TransactionBreakdown
diff --git a/src/components/containers/SignTransaction/TransactionBreakdown/TransactionBreakdown.module.css b/src/components/containers/SignTransaction/TransactionBreakdown/TransactionBreakdown.module.css
new file mode 100644
index 00000000..bf5ef061
--- /dev/null
+++ b/src/components/containers/SignTransaction/TransactionBreakdown/TransactionBreakdown.module.css
@@ -0,0 +1,166 @@
+.breakdown {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-xl);
+ margin-top: var(--space-lg);
+}
+
+.hero {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-3xs);
+ padding: var(--space-lg);
+ border: 1px solid rgba(var(--color-light-green), 0.35);
+ border-radius: 14px;
+ background: rgba(var(--color-light-green), 0.06);
+}
+
+.heroLabel {
+ font-size: var(--font-size-sm);
+ color: rgba(var(--color-black), 0.55);
+}
+
+.heroList {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-3xs);
+}
+
+.heroList li {
+ font-size: var(--font-size-4xl);
+ font-weight: 700;
+ letter-spacing: -0.02em;
+ font-variant-numeric: tabular-nums;
+}
+
+.positive {
+ color: rgb(var(--color-stats-green));
+}
+
+.negative {
+ color: rgb(var(--color-red));
+}
+
+.heroNote,
+.heroEmpty {
+ margin: 0;
+ font-size: var(--font-size-sm);
+ color: rgba(var(--color-black), 0.55);
+}
+
+.group {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-2xs);
+}
+
+.groupHead {
+ display: flex;
+ align-items: center;
+ gap: var(--space-xs);
+ margin: 0;
+ font-size: var(--font-size-xs);
+ font-weight: 600;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ color: rgba(var(--color-black), 0.45);
+}
+
+.groupHead::after {
+ content: '';
+ flex: 1;
+ height: 1px;
+ background: rgba(var(--color-black), 0.08);
+}
+
+.rows {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.row {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: var(--space-3xs) var(--space-sm);
+ padding: var(--space-sm) 0;
+ border-bottom: 1px solid rgba(var(--color-black), 0.06);
+}
+
+.row:last-child {
+ border-bottom: none;
+}
+
+.rowMain {
+ display: flex;
+ flex: 1 1 60%;
+ flex-direction: column;
+ gap: 2px;
+ min-width: 0;
+}
+
+.rowTitle {
+ font-size: var(--font-size-md);
+ font-weight: 600;
+}
+
+.rowOwn {
+ font-weight: 500;
+ color: rgb(var(--color-stats-green));
+}
+
+.rowAddress {
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
+ font-size: var(--font-size-xs);
+ color: rgba(var(--color-black), 0.5);
+ word-break: break-all;
+}
+
+.rowAmount {
+ flex: 0 0 auto;
+ margin-left: auto;
+ font-size: var(--font-size-md);
+ font-weight: 600;
+ font-variant-numeric: tabular-nums;
+ white-space: nowrap;
+}
+
+.fields {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ margin: var(--space-3xs) 0 0;
+}
+
+.field {
+ display: flex;
+ gap: var(--space-2xs);
+ font-size: var(--font-size-xs);
+}
+
+.fieldKey {
+ flex-shrink: 0;
+ color: rgba(var(--color-black), 0.45);
+}
+
+.fieldValue {
+ margin: 0;
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
+ word-break: break-all;
+}
+
+.nested {
+ display: flex;
+ flex-direction: column;
+ gap: 1px;
+}
+
+.nestedLine {
+ display: flex;
+ gap: var(--space-2xs);
+}
diff --git a/src/components/containers/SignTransaction/TransactionBreakdown/TransactionBreakdown.test.js b/src/components/containers/SignTransaction/TransactionBreakdown/TransactionBreakdown.test.js
new file mode 100644
index 00000000..a42edc3f
--- /dev/null
+++ b/src/components/containers/SignTransaction/TransactionBreakdown/TransactionBreakdown.test.js
@@ -0,0 +1,120 @@
+import { render, screen } from '@testing-library/react'
+
+import { MOCKS } from '../../../../pages/SignExternalTransaction/mocks'
+import TransactionBreakdown from './TransactionBreakdown'
+
+const jsonOf = (mock) => mock.request.data.txData.JSONRepresentation
+
+const renderBreakdown = (mock, ownAddresses = {}) =>
+ render(
+
,
+ )
+
+describe('TransactionBreakdown', () => {
+ it('lists every input and output of the transaction', () => {
+ const json = jsonOf(MOCKS.transfer)
+ renderBreakdown(MOCKS.transfer)
+
+ expect(
+ screen.getByText(`Inputs (${json.inputs.length})`),
+ ).toBeInTheDocument()
+ expect(
+ screen.getByText(`Outputs (${json.outputs.length})`),
+ ).toBeInTheDocument()
+ expect(screen.getAllByText('Transfer')).toHaveLength(
+ json.inputs.length + json.outputs.length,
+ )
+ })
+
+ it('shows the amount that leaves the wallet, fee included', () => {
+ // 17071.486043 spent, 17059.486043 returned as change, 10 sent, 2 fee
+ renderBreakdown(MOCKS.transfer, {
+ receiving: ['tmt1qxrwc3gy2lgf4kvqwwfa388vn3cavgrqyyrgswe6'],
+ change: [],
+ })
+
+ expect(screen.getByTestId('balance-change')).toHaveTextContent(
+ '\u221212 TML',
+ )
+ })
+
+ it('marks the addresses that belong to the wallet', () => {
+ renderBreakdown(MOCKS.transfer, {
+ receiving: ['tmt1qxrwc3gy2lgf4kvqwwfa388vn3cavgrqyyrgswe6'],
+ change: [],
+ })
+
+ expect(screen.getAllByText('Your address')).toHaveLength(2)
+ })
+
+ it('counts what a delegation withdrawal brings back', () => {
+ renderBreakdown(MOCKS.delegationWithdraw, {
+ receiving: ['tmt1q9l0g4kd3s6x5rmesaznegz06pw9hxu6qvqu3pa7'],
+ change: [],
+ })
+
+ expect(screen.getByTestId('balance-change')).toHaveTextContent('+8 TML')
+ expect(screen.getByText('DelegationBalance')).toBeInTheDocument()
+ })
+
+ it('keeps coin amounts at eleven decimals and out of exponent notation', () => {
+ const own = 'tmt1qown'
+ render(
+
,
+ )
+
+ expect(screen.getByTestId('balance-change')).toHaveTextContent(
+ '\u22120.00000000001 TML',
+ )
+ })
+
+ it('spells out a lock instead of dumping its json', () => {
+ renderBreakdown(MOCKS.delegationWithdraw)
+
+ expect(screen.getByText('for 7200 blocks')).toBeInTheDocument()
+ expect(screen.queryByText(/ForBlockCount/)).not.toBeInTheDocument()
+ })
+
+ it('opens nested fields into readable lines', () => {
+ renderBreakdown(MOCKS.issueNft)
+
+ expect(screen.getAllByText('name').length).toBeGreaterThan(0)
+ })
+
+ it('says so when no funds of this wallet move', () => {
+ renderBreakdown(MOCKS.transfer)
+
+ expect(
+ screen.getByText(
+ 'This transaction does not move funds held by this wallet.',
+ ),
+ ).toBeInTheDocument()
+ })
+})
diff --git a/src/components/containers/SignTransaction/TransactionPreviewErrorBoundary/TransactionPreviewErrorBoundary.js b/src/components/containers/SignTransaction/TransactionPreviewErrorBoundary/TransactionPreviewErrorBoundary.js
new file mode 100644
index 00000000..d5c5db61
--- /dev/null
+++ b/src/components/containers/SignTransaction/TransactionPreviewErrorBoundary/TransactionPreviewErrorBoundary.js
@@ -0,0 +1,56 @@
+import React from 'react'
+
+// Error Boundary Component for TransactionPreview
+class TransactionPreviewErrorBoundary extends React.Component {
+ constructor(props) {
+ super(props)
+ this.state = { hasError: false, error: null }
+ }
+
+ static getDerivedStateFromError(error) {
+ return { hasError: true, error }
+ }
+
+ componentDidCatch(error, errorInfo) {
+ console.error('TransactionPreview error:', error, errorInfo)
+ }
+
+ render() {
+ if (this.state.hasError) {
+ return (
+
+
+
+
Transaction Preview
+
+
+
+
Unable to display transaction details
+
+ An error occurred while parsing the transaction data. Please
+ try again or contact support.
+
+
+ {this.props.basicInfo && (
+ <>
+
+
Request from
+
{this.props.basicInfo.origin || 'Unknown'}
+
+
+
Request id
+
{this.props.basicInfo.requestId || 'Unknown'}
+
+ >
+ )}
+
+
+
+ )
+ }
+
+ return this.props.children
+ }
+}
+
+export default TransactionPreviewErrorBoundary
diff --git a/src/components/containers/SignTransaction/UnrecognizedOperation/UnrecognizedOperation.js b/src/components/containers/SignTransaction/UnrecognizedOperation/UnrecognizedOperation.js
new file mode 100644
index 00000000..b05be140
--- /dev/null
+++ b/src/components/containers/SignTransaction/UnrecognizedOperation/UnrecognizedOperation.js
@@ -0,0 +1,16 @@
+import styles from './UnrecognizedOperation.module.css'
+
+const UnrecognizedOperation = () => (
+
+
This operation is not recognized
+
+ The wallet cannot name what this transaction does. Read the inputs and
+ outputs below, and sign it only if you know what you are approving.
+
+
+)
+
+export default UnrecognizedOperation
diff --git a/src/components/containers/SignTransaction/UnrecognizedOperation/UnrecognizedOperation.module.css b/src/components/containers/SignTransaction/UnrecognizedOperation/UnrecognizedOperation.module.css
new file mode 100644
index 00000000..8829b588
--- /dev/null
+++ b/src/components/containers/SignTransaction/UnrecognizedOperation/UnrecognizedOperation.module.css
@@ -0,0 +1,19 @@
+.notice {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-3xs);
+ padding: var(--space-sm);
+ border: 1px solid rgba(var(--color-red), 0.4);
+ border-radius: 8px;
+ background: rgba(var(--color-red), 0.06);
+}
+
+.title {
+ margin: 0;
+ font-size: var(--font-size-md);
+}
+
+.text {
+ margin: 0;
+ font-size: var(--font-size-sm);
+}
diff --git a/src/components/containers/Wallet/Orders/OrderDetails/OrderDetails.js b/src/components/containers/Wallet/Orders/OrderDetails/OrderDetails.js
index 3451a268..4b3d3733 100644
--- a/src/components/containers/Wallet/Orders/OrderDetails/OrderDetails.js
+++ b/src/components/containers/Wallet/Orders/OrderDetails/OrderDetails.js
@@ -6,6 +6,7 @@ import { CryptoFiatField } from '@ComposedComponents'
import { ReactComponent as IconArrowTopRight } from '@Assets/images/icon-swap.svg'
import { ReactComponent as ArrowIcon } from '@Assets/images/icon-arrow-down.svg'
import { ML } from '@Helpers'
+import { useFillOrder } from '@Hooks'
import { MintlayerContext } from '@Contexts'
@@ -89,8 +90,9 @@ const SwapInfoContent = ({ order, from }) => {
}
const OrderDetails = ({ order }) => {
- const { client, unusedAddresses, balance, tokenBalances } =
+ const { unusedAddresses, balance, tokenBalances } =
useContext(MintlayerContext)
+ const fillOrder = useFillOrder()
const [txErrorMessage, setTxErrorMessage] = useState(null)
const [loading, setLoading] = useState(false)
const [amount, setAmount] = useState('')
@@ -135,7 +137,7 @@ const OrderDetails = ({ order }) => {
try {
setLoading(true)
if (order) {
- await client.fillOrder({
+ await fillOrder({
order_id: order.order_id,
amount,
destination: unusedAddresses.receive,
diff --git a/src/components/containers/Wallet/Orders/OrderDetails/OrderDetails.test.js b/src/components/containers/Wallet/Orders/OrderDetails/OrderDetails.test.js
index 2bde1986..0c365650 100644
--- a/src/components/containers/Wallet/Orders/OrderDetails/OrderDetails.test.js
+++ b/src/components/containers/Wallet/Orders/OrderDetails/OrderDetails.test.js
@@ -10,6 +10,13 @@ import {
} from '@Contexts'
import { ML } from '@Helpers'
+const mockFillOrder = jest.fn()
+
+jest.mock('@Hooks', () => ({
+ ...jest.requireActual('@Hooks'),
+ useFillOrder: () => mockFillOrder,
+}))
+
describe('OrderDetailsItem', () => {
it('renders with title and content', () => {
const title = 'Test Title'
@@ -296,9 +303,6 @@ const mockCoinOrder = {
}
const mockMintlayerContext = {
- client: {
- fillOrder: jest.fn(),
- },
unusedAddresses: {
receive: 'testnet_addr1',
},
@@ -389,7 +393,7 @@ describe('OrderDetails', () => {
fireEvent.click(swapButton)
await waitFor(() => {
- expect(mockMintlayerContext.client.fillOrder).toHaveBeenCalledWith({
+ expect(mockFillOrder).toHaveBeenCalledWith({
order_id: 'order123456789',
amount: '50',
destination: 'testnet_addr1',
diff --git a/src/components/containers/Wallet/TransactionButton.js b/src/components/containers/Wallet/TransactionButton.js
index 3449439a..2a7fbce2 100644
--- a/src/components/containers/Wallet/TransactionButton.js
+++ b/src/components/containers/Wallet/TransactionButton.js
@@ -2,7 +2,7 @@ import { ReactComponent as ArrowIcon } from '@Assets/images/icon-arrow-down.svg'
import { ReactComponent as DelegationIcon } from '@Assets/images/icon-delegation.svg'
import { ReactComponent as SignIcon } from '@Assets/images/icon-sign.svg'
import { ReactComponent as NftIcon } from '@Assets/images/icon-nft.svg'
-import { ReactComponent as SwapIcon } from '@Assets/images/icon-arrow-swap.svg'
+import { ReactComponent as SwapIcon } from '@Assets/images/icon-loop.svg'
import { ReactComponent as AddressesIcon } from '@Assets/images/icon-inbox.svg'
import { Button } from '@BasicComponents'
diff --git a/src/components/containers/index.js b/src/components/containers/index.js
index 41c73bb8..b87a8ccb 100644
--- a/src/components/containers/index.js
+++ b/src/components/containers/index.js
@@ -33,6 +33,7 @@ import SettingsTestnet from './Settings/SettingsTestnet/SettingsTestnet.tsx'
import SettingsAbout from './Settings/SettingsAbout/SettingsAbout.tsx'
import SettingsBackup from './Settings/SettingsBackup/SettingsBackup'
import SettingsSection from './Settings/SettingsSection/SettingsSection.tsx'
+import SettingsConnections from './Settings/SettingsConnections/SettingsConnections.tsx'
import SignMessage from './Message/SignMessage/SignMessage'
import VerifyMessage from './Message/VerifyMessage/VerifyMessage'
@@ -75,6 +76,7 @@ const Settings = {
SettingsDelete,
SettingsBackup,
SettingsSection,
+ SettingsConnections,
}
const RestoreAccount = {
diff --git a/src/contexts/MintlayerProvider/MintlayerProvider.js b/src/contexts/MintlayerProvider/MintlayerProvider.js
index 11033e57..ae8e80ce 100644
--- a/src/contexts/MintlayerProvider/MintlayerProvider.js
+++ b/src/contexts/MintlayerProvider/MintlayerProvider.js
@@ -87,6 +87,7 @@ const MintlayerProvider = ({ value: propValue, children }) => {
const coinTicker =
networkType === AppInfo.NETWORK_TYPES.TESTNET ? 'TML' : 'ML'
const swapPairsCurrency = orderPair.split('_')
+ const minAskBalance = Number(amount) || 0
const ordersPairInfo = await Mintlayer.getOrdersListByPair(orderPair)
if (!ordersPairInfo || ordersPairInfo.length === 0) {
console.log('No orders found for this pair')
@@ -101,7 +102,7 @@ const MintlayerProvider = ({ value: propValue, children }) => {
swapPairsCurrency[0] === coinTicker) ||
(order.ask_currency.token_id &&
order.ask_currency.token_id === swapPairsCurrency[0])) &&
- Number(order.ask_balance.decimal) >= Number(amount)
+ Number(order.ask_balance.decimal) >= minAskBalance
) {
acc.push({
...order,
diff --git a/src/hooks/UseFillOrder/useFillOrder.js b/src/hooks/UseFillOrder/useFillOrder.js
new file mode 100644
index 00000000..33482895
--- /dev/null
+++ b/src/hooks/UseFillOrder/useFillOrder.js
@@ -0,0 +1,43 @@
+import { useCallback, useContext } from 'react'
+
+import { MintlayerContext } from '@Contexts'
+import { Mintlayer } from '@APIs'
+
+const useFillOrder = () => {
+ const { client, utxos } = useContext(MintlayerContext)
+
+ const fillOrder = useCallback(
+ async ({ order_id, amount, destination }) => {
+ const order_details = JSON.parse(await Mintlayer.getOrderById(order_id))
+
+ const [ask_token_details, give_token_details] = await Promise.all([
+ order_details.ask_currency.type === 'Coin'
+ ? null
+ : Mintlayer.getTokenById(order_details.ask_currency.token_id),
+ order_details.give_currency.type === 'Coin'
+ ? null
+ : Mintlayer.getTokenById(order_details.give_currency.token_id),
+ ])
+
+ const transaction = await client.buildTransaction({
+ type: 'FillOrder',
+ params: {
+ order_id,
+ amount,
+ destination,
+ order_details,
+ ask_token_details,
+ give_token_details,
+ },
+ ...(utxos.length ? { opts: { withUTXO: utxos } } : {}),
+ })
+
+ return client.signTransaction(transaction)
+ },
+ [client, utxos],
+ )
+
+ return fillOrder
+}
+
+export default useFillOrder
diff --git a/src/hooks/index.js b/src/hooks/index.js
index 43f14763..6304ed8c 100644
--- a/src/hooks/index.js
+++ b/src/hooks/index.js
@@ -5,6 +5,7 @@ import useMlWalletInfo from './UseWalletInfo/useMlWalletInfo'
import useExchangeRates from './UseExchangeRates/useExchangeRates'
import useOneDayAgoExchangeRates from './UseOneDayAgoExchangeRates/useOneDayAgoExchangeRates'
import useMediaQuery from './useMediaQuery/useMediaQuery'
+import useFillOrder from './UseFillOrder/useFillOrder'
export {
useStyleClasses,
@@ -14,4 +15,5 @@ export {
useExchangeRates,
useOneDayAgoExchangeRates,
useMediaQuery,
+ useFillOrder,
}
diff --git a/src/index.js b/src/index.js
index 32c14bde..943e9e7a 100644
--- a/src/index.js
+++ b/src/index.js
@@ -62,6 +62,7 @@ import {
import { ML } from '@Cryptos'
import { LocalStorageService } from '@Storage'
+import '@Assets/styles/fonts.css'
import '@Assets/styles/constants.css'
import '@Assets/styles/index.css'
@@ -106,14 +107,19 @@ const App = () => {
useContext(MintlayerContext)
const { networkType } = useContext(SettingsContext)
const [nextAfterUnlock, setNextAfterUnlock] = useState(null)
- const [request, setRequest] = useState(null)
+ const [, setRequest] = useState(null)
const currentMlAddresses = addresses.mlAddresses
const isConnectionAvailable = async (accountUnlocked) => {
try {
const mintlayerResponse = await Mintlayer.getChainTip()
- const exchangeResponse = await ExchangeRates.getRate('ml', 'usd')
+ const exchangeResponse = await ExchangeRates.getRate('ml', 'usd').catch(
+ (error) => {
+ console.error('Exchange rates unavailable:', error)
+ return null
+ },
+ )
return !!mintlayerResponse && !!exchangeResponse
} catch (error) {
if (accountUnlocked) {
@@ -197,38 +203,48 @@ const App = () => {
if (!unlocked) {
setNextAfterUnlock({
route: '/connect',
- state: { action: 'connect', origin, requestId, request },
+ state: {
+ action: 'connect',
+ origin,
+ requestId,
+ request: pendingRequest,
+ },
})
return
}
navigate('/connect', {
- state: { action: 'connect', origin, requestId, request },
+ state: {
+ action: 'connect',
+ origin,
+ requestId,
+ request: pendingRequest,
+ },
})
}
if (action === 'signTransaction') {
- if (request.data.chain === 'bitcoin') {
+ if (pendingRequest.data.chain === 'bitcoin') {
if (!unlocked) {
setNextAfterUnlock({
route: '/wallet/Bitcoin/sign-transaction',
- state: { action: 'signTransaction', request },
+ state: { action: 'signTransaction', request: pendingRequest },
})
return
}
navigate('/wallet/Bitcoin/sign-transaction', {
- state: { action: 'signTransaction', request },
+ state: { action: 'signTransaction', request: pendingRequest },
})
} else {
if (!unlocked) {
setNextAfterUnlock({
route: '/wallet/Mintlayer/sign-external-transaction',
- state: { action: 'signTransaction', request },
+ state: { action: 'signTransaction', request: pendingRequest },
})
return
}
navigate('/wallet/Mintlayer/sign-external-transaction', {
- state: { action: 'signTransaction', request },
+ state: { action: 'signTransaction', request: pendingRequest },
})
}
}
@@ -237,12 +253,12 @@ const App = () => {
if (!unlocked) {
setNextAfterUnlock({
route: '/wallet/Mintlayer/sign-challenge',
- state: { action: 'signChallenge', request },
+ state: { action: 'signChallenge', request: pendingRequest },
})
return
}
navigate('/wallet/Mintlayer/sign-challenge', {
- state: { action: 'signChallenge', request },
+ state: { action: 'signChallenge', request: pendingRequest },
})
}
@@ -252,8 +268,8 @@ const App = () => {
route: '/wallet/Mintlayer/staking/create-delegation',
state: {
action: 'createDelegate',
- pool_id: request.data.pool_id,
- referral_code: request.data.referral_code || '',
+ pool_id: pendingRequest.data.pool_id,
+ referral_code: pendingRequest.data.referral_code || '',
},
})
storage.local.remove('pendingRequest', () => {
@@ -270,8 +286,8 @@ const App = () => {
navigate('/wallet/Mintlayer/staking/create-delegation', {
state: {
action: 'createDelegate',
- pool_id: request.data.pool_id,
- referral_code: request.data.referral_code || '',
+ pool_id: pendingRequest.data.pool_id,
+ referral_code: pendingRequest.data.referral_code || '',
},
})
storage.local.remove('pendingRequest', () => {
diff --git a/src/pages/ConfirmBtcTransaction/ConfirmBtcTransaction.module.css b/src/pages/ConfirmBtcTransaction/ConfirmBtcTransaction.module.css
index 092514f3..1704da69 100644
--- a/src/pages/ConfirmBtcTransaction/ConfirmBtcTransaction.module.css
+++ b/src/pages/ConfirmBtcTransaction/ConfirmBtcTransaction.module.css
@@ -1,4 +1,6 @@
.signTransaction {
+ display: flex;
+ flex-direction: column;
background-color: #ffffff;
margin: 0 auto;
font-family: 'Arial', sans-serif;
@@ -27,7 +29,7 @@
margin-bottom: 20px;
display: flex;
flex-direction: column;
- height: 70%;
+ flex-grow: 1;
@media screen and (min-width: 901px) {
height: 80%;
@@ -46,6 +48,7 @@
justify-content: center;
gap: 12px;
bottom: 0;
+ flex-shrink: 0;
}
.modalTitle {
diff --git a/src/pages/ConnectionPage/BitcoinDataNotice.module.css b/src/pages/ConnectionPage/BitcoinDataNotice.module.css
new file mode 100644
index 00000000..74d7762a
--- /dev/null
+++ b/src/pages/ConnectionPage/BitcoinDataNotice.module.css
@@ -0,0 +1,69 @@
+.section {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-md);
+ width: 100%;
+}
+
+.toggleRow {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--space-md);
+}
+
+.toggleText {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ min-width: 0;
+}
+
+.toggleTitle {
+ font-size: var(--font-size-md);
+ font-weight: 600;
+ color: rgb(var(--color-black));
+}
+
+.toggleDescription {
+ font-size: var(--font-size-sm);
+ color: rgba(var(--color-black), 0.55);
+}
+
+.infoBlock {
+ display: flex;
+ align-items: flex-start;
+ gap: var(--space-sm);
+ padding: var(--space-md);
+ border-radius: 12px;
+ border: 1px solid rgba(var(--color-main-green), 0.3);
+ background: rgba(var(--color-main-green), 0.08);
+}
+
+.infoIcon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 20px;
+ height: 20px;
+ flex-shrink: 0;
+ margin-top: 1px;
+ border-radius: 50%;
+ background: rgb(var(--color-main-green));
+ color: rgb(var(--color-white));
+ font-size: var(--font-size-xs);
+ font-weight: bold;
+ font-style: italic;
+}
+
+.infoText {
+ margin: 0;
+ font-size: var(--font-size-sm);
+ line-height: 1.45;
+ color: rgb(var(--color-dark-gray));
+}
+
+.infoText strong {
+ color: rgb(var(--color-main-green));
+ font-weight: 600;
+}
diff --git a/src/pages/ConnectionPage/BitcoinDataNotice.tsx b/src/pages/ConnectionPage/BitcoinDataNotice.tsx
new file mode 100644
index 00000000..8744e482
--- /dev/null
+++ b/src/pages/ConnectionPage/BitcoinDataNotice.tsx
@@ -0,0 +1,43 @@
+import { Toggle } from '@BasicComponents'
+
+import styles from './BitcoinDataNotice.module.css'
+
+interface BitcoinDataNoticeProps {
+ provideBitcoinData: boolean
+ onToggle: (value: boolean) => void
+}
+
+const BitcoinDataNotice = ({
+ provideBitcoinData,
+ onToggle,
+}: BitcoinDataNoticeProps) => {
+ return (
+
+
+
+ Provide Bitcoin data
+
+ Addresses and public keys
+
+
+
+
+
+
+
i
+
+ Note: This option is mandatory when connecting to
+ HTLC Atomic Swaps dApps. It provides both Bitcoin addresses and public
+ keys required for cross-chain transactions.
+
+
+
+ )
+}
+
+export default BitcoinDataNotice
diff --git a/src/pages/ConnectionPage/ConnectionPage.css b/src/pages/ConnectionPage/ConnectionPage.css
deleted file mode 100644
index d041986d..00000000
--- a/src/pages/ConnectionPage/ConnectionPage.css
+++ /dev/null
@@ -1,165 +0,0 @@
-.connect-page__form {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 1rem;
- width: 100%;
- height: 100%;
- margin: 1rem;
- padding: 0 2rem;
- /* border: 1px solid #ddd; */
- border-radius: 10px;
- /* background-color: #fff; */
- /* box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); */
-}
-
-.connect-page__title {
- display: flex;
- flex-direction: column;
- gap: 1rem;
- align-items: center;
-}
-
-.connect-page__title h2 {
- font-size: 1.25rem;
- font-weight: bold;
-}
-
-.connect-page__content {
- display: flex;
- width: 90%;
- flex-direction: column;
- align-items: center;
- gap: 2rem;
-}
-
-.connect-page__description {
- font-size: 0.95rem;
- margin-bottom: 1rem;
-}
-
-.connect-page__host {
- font-weight: 600;
- color: rgb(var(--color-black));
-}
-
-.connect-page__permissions {
- position: relative;
- text-align: left;
- padding-left: 7rem;
- list-style: disc !important;
- max-width: 400px;
- margin: 0 auto;
- margin-bottom: 20px;
-}
-
-.connect-page__permissions li {
- margin-bottom: 0.5rem;
- font-size: 0.9rem;
- list-style: inside;
-}
-
-.connect-page__remember {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- justify-content: center;
- font-size: 0.85rem;
- margin-bottom: 1.5rem;
-}
-
-.connect-page__checkbox {
- transform: scale(1.1);
-}
-
-.connect-page__actions {
- display: flex;
- justify-content: space-around;
- width: 80%;
- gap: 1rem;
-}
-
-.rejectButton {
- background-color: #eee;
- color: #333;
-}
-
-.connect-page__button--reject:hover {
- background-color: #ddd;
-}
-
-.connect-page__button--connect {
- background-color: #3cb371;
- color: #fff;
-}
-
-.connect-page__button--connect:hover {
- background-color: #35a164;
-}
-
-.connectButton {
- width: 50%;
-}
-
-.connect-page__icon {
- position: absolute;
- width: 90px;
- height: auto;
- left: 4px;
- top: 50%;
- transform: translate(0, -50%);
- stroke: rgb(var(--color-green));
-}
-
-.connect-page__bitcoin-section {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 1rem;
- width: 100%;
-}
-
-.connect-page__bitcoin-toggle {
- display: flex;
- align-items: center;
- gap: 1rem;
-}
-
-.connect-page__info-block {
- display: flex;
- align-items: flex-start;
- gap: 0.75rem;
- padding: 1rem;
- background-color: rgba(var(--color-main-green), 0.1);
- border: 1px solid rgba(var(--color-main-green), 0.3);
- border-radius: 8px;
- width: 100%;
-}
-
-.connect-page__info-icon {
- width: 20px;
- height: 20px;
- flex-shrink: 0;
- margin-top: 2px;
- background-color: rgb(var(--color-main-green));
- color: white;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 12px;
- font-weight: bold;
- font-style: italic;
-}
-
-.connect-page__info-text {
- font-size: 0.85rem;
- line-height: 1.4;
- margin: 0;
- color: rgb(var(--color-dark-gray));
-}
-
-.connect-page__info-text strong {
- color: rgb(var(--color-main-green));
- font-weight: 600;
-}
diff --git a/src/pages/ConnectionPage/ConnectionPage.js b/src/pages/ConnectionPage/ConnectionPage.js
index 358c2aed..06b57c95 100644
--- a/src/pages/ConnectionPage/ConnectionPage.js
+++ b/src/pages/ConnectionPage/ConnectionPage.js
@@ -1,10 +1,15 @@
/* eslint-disable no-undef */
-import './ConnectionPage.css'
import { useLocation } from 'react-router'
import { useContext, useState } from 'react'
import { AccountContext } from '@Contexts'
-import { Button, Toggle, PageWrapper } from '@BasicComponents'
+import { Button, PageWrapper, SiteBadge } from '@BasicComponents'
import { ReactComponent as IconShield } from '@Assets/images/icon-shield.svg'
+import { ReactComponent as IconEye } from '@Assets/images/icon-eye.svg'
+import { ReactComponent as IconSign } from '@Assets/images/icon-sign.svg'
+import { ReactComponent as IconLoop } from '@Assets/images/icon-loop.svg'
+import PermissionItem from './PermissionItem'
+import BitcoinDataNotice from './BitcoinDataNotice'
+import styles from './ConnectionPage.module.css'
const toHexString = (obj) => {
return Object.values(obj)
@@ -39,8 +44,9 @@ export const ConnectionPage = () => {
const permissions = state?.request?.permissions || []
const requireBTC = permissions.includes('bitcoin')
+ const isUnknownOrigin = origin === website
- const connectButtonExtraStyles = ['connectButton']
+ const connectButtonExtraStyles = [styles.actionButton]
const handleConnect = () => {
const remember = document.querySelector('.connect-page__checkbox')?.checked
@@ -78,11 +84,10 @@ export const ConnectionPage = () => {
),
publicKeys: {
receiving: addresses?.btcAddresses?.btcReceivingAddresses.map(
- (addr) =>
- Buffer.from(Object.values(addr)[0].pubkey).toString('hex'),
+ (addr) => toHexString(Object.values(addr)[0].pubkey),
),
change: addresses?.btcAddresses?.btcChangeAddresses.map((addr) =>
- Buffer.from(Object.values(addr)[0].pubkey).toString('hex'),
+ toHexString(Object.values(addr)[0].pubkey),
),
},
},
@@ -162,80 +167,89 @@ export const ConnectionPage = () => {
return (