Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,21 @@ import { consumeRemoteApi, exposeApi, RemoteApiPropertyType } from '@cardano-sdk
import { DappDataService } from '@lib/scripts/types';
import { DAPP_CHANNELS } from '@src/utils/constants';
import { runtime } from 'webextension-polyfill';
import { useRedirection } from '@hooks';
import { assetsBurnedInspector, assetsMintedInspector, createTxInspector } from '@cardano-sdk/core';
import { useFetchCoinPrice, useRedirection } from '@hooks';
import {
assetsBurnedInspector,
assetsMintedInspector,
createTxInspector,
AssetsMintedInspection,
MintedAsset
} from '@cardano-sdk/core';
import { Skeleton } from 'antd';
import { dAppRoutePaths } from '@routes';
import { UserPromptService } from '@lib/scripts/background/services';
import { of } from 'rxjs';
import { CardanoTxOut } from '@src/types';
import { getAssetsInformation, TokenInfo } from '@src/utils/get-assets-information';
import * as HardwareLedger from '../../../../../../node_modules/@cardano-sdk/hardware-ledger/dist/cjs';
import { useAnalyticsContext } from '@providers';
import { useCurrencyStore, useAnalyticsContext } from '@providers';
import { TX_CREATION_TYPE_KEY, TxCreationType } from '@providers/AnalyticsProvider/analyticsTracker';
import { txSubmitted$ } from '@providers/AnalyticsProvider/onChain';

Expand All @@ -39,6 +44,45 @@ const dappDataApi = consumeRemoteApi<Pick<DappDataService, 'getSignTxData'>>(
{ logger: console, runtime }
);

const convertMetadataArrayToObj = (arr: unknown[]): Record<string, unknown> => {
const result: Record<string, unknown> = {};
for (const item of arr) {
if (typeof item === 'object' && !Array.isArray(item) && item !== null) {
Object.assign(result, item);
}
}
return result;
};

// eslint-disable-next-line complexity, sonarjs/cognitive-complexity
const getAssetNameFromMintMetadata = (asset: MintedAsset, metadata: Wallet.Cardano.TxMetadata): string | undefined => {
if (!asset || !metadata) return;
const decodedAssetName = Buffer.from(asset.assetName, 'hex').toString();

// Tries to find the asset name in the tx metadata under label 721 or 20
for (const [key, value] of metadata.entries()) {
// eslint-disable-next-line no-magic-numbers
if (key !== BigInt(721) && key !== BigInt(20)) return;
const cip25Metadata = Wallet.cardanoMetadatumToObj(value);
if (!Array.isArray(cip25Metadata)) return;

// cip25Metadata should be an array containing all policies for the minted assets in the tx
const policyLevelMetadata = convertMetadataArrayToObj(cip25Metadata)[asset.policyId];
if (!Array.isArray(policyLevelMetadata)) return;

// policyLevelMetadata should be an array of objects with the minted assets names as key
// e.g. "policyId" = [{ "AssetName1": { ...metadataAsset1 } }, { "AssetName2": { ...metadataAsset2 } }];
const assetProperties = convertMetadataArrayToObj(policyLevelMetadata)?.[decodedAssetName];
if (!Array.isArray(assetProperties)) return;

// assetProperties[decodedAssetName] should be an array of objects with the properties as keys
// e.g. [{ "name": "Asset Name" }, { "description": "An asset" }, ...]
const assetMetadataName = convertMetadataArrayToObj(assetProperties)?.name;
// eslint-disable-next-line consistent-return
return typeof assetMetadataName === 'string' ? assetMetadataName : undefined;
}
};

// eslint-disable-next-line sonarjs/cognitive-complexity
export const ConfirmTransaction = withAddressBookContext((): React.ReactElement => {
const {
Expand All @@ -49,9 +93,12 @@ export const ConfirmTransaction = withAddressBookContext((): React.ReactElement
walletInfo,
inMemoryWallet,
getKeyAgentType,
blockchainProvider: { assetProvider }
blockchainProvider: { assetProvider },
walletUI: { cardanoCoin }
} = useWalletStore();
const { fiatCurrency } = useCurrencyStore();
const { list: addressList } = useAddressBookContext();
const { priceResult } = useFetchCoinPrice();
const analytics = useAnalyticsContext();

const [tx, setTx] = useState<Wallet.Cardano.Tx>();
Expand All @@ -67,20 +114,23 @@ export const ConfirmTransaction = withAddressBookContext((): React.ReactElement
const [assetsInfo, setAssetsInfo] = useState<TokenInfo | null>();
const [dappInfo, setDappInfo] = useState<Wallet.DappInfo>();

const getTransactionAssetsId = (outputs: CardanoTxOut[]) => {
const assetIds: Wallet.Cardano.AssetId[] = [];
const assetMaps = outputs.map((output) => output.value.assets);
// All assets' ids in the transaction body. Used to fetch their info from cardano services
const assetIds = useMemo(() => {
const uniqueAssetIds = new Set<Wallet.Cardano.AssetId>();
// Merge all assets (TokenMaps) from the tx outputs and mint
const assetMaps = tx?.body?.outputs?.map((output) => output.value.assets) ?? [];
if (tx?.body?.mint?.size > 0) assetMaps.push(tx.body.mint);

// Extract all unique asset ids from the array of TokenMaps
for (const asset of assetMaps) {
if (asset) {
for (const id of asset.keys()) {
!assetIds.includes(id) && assetIds.push(id);
!uniqueAssetIds.has(id) && uniqueAssetIds.add(id);
}
}
}
return assetIds;
};

const assetIds = useMemo(() => tx?.body?.outputs && getTransactionAssetsId(tx.body.outputs), [tx?.body?.outputs]);
return [...uniqueAssetIds.values()];
}, [tx]);

useEffect(() => {
if (assetIds?.length > 0) {
Expand Down Expand Up @@ -154,16 +204,38 @@ export const ConfirmTransaction = withAddressBookContext((): React.ReactElement
});
}, []);

const createMintedList = useCallback(
(mintedAssets: AssetsMintedInspection) => {
if (!assetsInfo) return [];
return mintedAssets.map((asset) => {
const assetId = Wallet.Cardano.AssetId.fromParts(asset.policyId, asset.assetName);
const assetInfo = assets.get(assetId) || assetsInfo?.get(assetId);
// If it's a new asset or the name is being updated we should be getting it from the tx metadata
const metadataName = getAssetNameFromMintMetadata(asset, tx?.auxiliaryData?.blob);
return {
name: assetInfo?.name.toString() || asset.fingerprint || assetId,
ticker:
metadataName ??
assetInfo?.nftMetadata?.name ??
assetInfo?.tokenMetadata?.ticker ??
assetInfo?.tokenMetadata?.name ??
asset.fingerprint.toString(),
amount: Wallet.util.calculateAssetBalance(asset.quantity, assetInfo)
};
});
},
[assets, assetsInfo, tx]
);

const createAssetList = useCallback(
(txAssets: Wallet.Cardano.TokenMap) => {
if (!assetsInfo) return [];
const assetList: Wallet.Cip30SignTxAssetItem[] = [];
// eslint-disable-next-line unicorn/no-array-for-each
txAssets.forEach(async (value, key) => {
const walletAsset = assets.get(key) || assetsInfo?.get(key);
assetList.push({
name: walletAsset.name.toString() || key.toString(),
ticker: walletAsset.tokenMetadata?.ticker || walletAsset.nftMetadata?.name,
name: walletAsset?.name.toString() || key.toString(),
ticker: walletAsset?.tokenMetadata?.ticker || walletAsset?.nftMetadata?.name,
amount: Wallet.util.calculateAssetBalance(value, walletAsset)
});
});
Expand All @@ -185,17 +257,9 @@ export const ConfirmTransaction = withAddressBookContext((): React.ReactElement
});

const { minted, burned } = inspector(tx as Wallet.Cardano.HydratedTx);
const isMintTransaction = minted.length > 0;
const isBurnTransaction = burned.length > 0;
const isMintTransaction = minted.length > 0 || burned.length > 0;

let txType: 'Send' | 'Mint' | 'Burn';
if (isMintTransaction) {
txType = 'Mint';
} else if (isBurnTransaction) {
txType = 'Burn';
} else {
txType = 'Send';
}
const txType = isMintTransaction ? 'Mint' : 'Send';

const externalOutputs = tx.body.outputs.filter((output) => {
if (txType === 'Send') {
Expand Down Expand Up @@ -223,17 +287,11 @@ export const ConfirmTransaction = withAddressBookContext((): React.ReactElement
return {
fee: Wallet.util.lovelacesToAdaString(tx.body.fee.toString()),
outputs: txSummaryOutputs,
type: txType
type: txType,
mintedAssets: createMintedList(minted),
burnedAssets: createMintedList(burned)
};
}, [tx, walletInfo.addresses, createAssetList, addressToNameMap]);

const translations = {
transaction: t('core.dappTransaction.transaction'),
amount: t('core.dappTransaction.amount'),
recipient: t('core.dappTransaction.recipient'),
fee: t('core.dappTransaction.fee'),
adaFollowingNumericValue: t('general.adaFollowingNumericValue')
};
}, [tx, walletInfo.addresses, createAssetList, createMintedList, addressToNameMap]);

const onConfirm = () => {
analytics.sendEventToPostHog(PostHogAction.SendTransactionSummaryConfirmClick, {
Expand All @@ -256,7 +314,9 @@ export const ConfirmTransaction = withAddressBookContext((): React.ReactElement
transaction={txSummary}
dappInfo={dappInfo}
errorMessage={errorMessage}
translations={translations}
fiatCurrencyCode={fiatCurrency?.code}
fiatCurrencyPrice={priceResult?.cardano?.price}
coinSymbol={cardanoCoin.symbol}
/>
) : (
<Skeleton loading />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable import/imports-first */
import * as CurrencyProvider from '@providers/currency';

const mockGetKeyAgentType = jest.fn();
const mockUseWalletStore = jest.fn();
const error = 'error in getSignTxData';
const mockConsumeRemoteApi = jest.fn().mockReturnValue({
getSignTxData: async () => await Promise.reject(error)
});
const mockCreateTxInspector = jest.fn().mockReturnValue(() => ({ minted: [] as any, burned: [] as any }));
const mockUseCurrencyStore = jest.fn().mockReturnValue({ fiatCurrency: { code: 'usd', symbol: '$' } });
import * as React from 'react';
import { cleanup, render, waitFor } from '@testing-library/react';
import { ConfirmTransaction } from '../ConfirmTransaction';
Expand All @@ -31,6 +34,8 @@ import { postHogClientMocks } from '@src/utils/mocks/test-helpers';

const assetInfo$ = new BehaviorSubject(new Map());
const available$ = new BehaviorSubject([]);
const tokenPrices$ = new BehaviorSubject({});
const adaPrices$ = new BehaviorSubject({});

const assetProvider = {
getAsset: () => ({}),
Expand All @@ -50,6 +55,11 @@ jest.mock('@src/stores', () => ({
useWalletStore: mockUseWalletStore
}));

jest.mock('@providers/currency', (): typeof CurrencyProvider => ({
...jest.requireActual<typeof CurrencyProvider>('@providers/currency'),
useCurrencyStore: mockUseCurrencyStore
}));

jest.mock('@cardano-sdk/web-extension', () => {
const original = jest.requireActual('@cardano-sdk/web-extension');
return {
Expand All @@ -74,7 +84,8 @@ const testIds = {

const backgroundService = {
getBackgroundStorage: jest.fn(),
setBackgroundStorage: jest.fn()
setBackgroundStorage: jest.fn(),
coinPrices: { tokenPrices$, adaPrices$ }
} as unknown as BackgroundServiceAPIProviderProps['value'];

const getWrapper =
Expand Down
12 changes: 12 additions & 0 deletions apps/browser-extension-wallet/src/lib/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,18 @@
"noMatchPassword": "Oops! The passwords don't match.",
"secondLevelPasswordStrengthFeedback": "Getting there! Add some symbols and numbers to make it stronger.",
"firstLevelPasswordStrengthFeedback": "Weak password. Add some numbers and characters to make it stronger."
},
"dappTransaction": {
"asset": "Asset",
"burn": "Burn",
"fee": "Transaction Fee",
"insufficientFunds": "You do not have enough funds to complete the transaction",
"mint": "Mint",
"quantity": "Quantity",
"recipient": "Recipient",
"send": "Send",
"sending": "Sending",
"transaction": "Transaction"
}
},
"tab.main.title": "Tab extension",
Expand Down
4 changes: 3 additions & 1 deletion packages/cardano/src/wallet/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ export type Cip30SignTxSummary = {
recipient: string;
assets?: Cip30SignTxAssetItem[];
}[];
type: 'Send' | 'Mint' | 'Burn';
type: 'Send' | 'Mint';
mintedAssets?: Cip30SignTxAssetItem[];
burnedAssets?: Cip30SignTxAssetItem[];
};

export type Cip30SignTxAssetItem = {
Expand Down
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"dependencies": {
"@ant-design/icons": "^4.7.0",
"@lace/common": "0.1.0",
"@lace/ui": "^0.1.0",
"antd": "^4.24.10",
"axios": "0.21.4",
"axios-cache-adapter": "2.7.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,27 +149,6 @@ $border-bottom: 1px solid var(--light-mode-light-grey-plus, var(--dark-mode-mid-
}
}

.txFeeContainer {
display: flex;
align-items: center;
justify-content: center;
gap: size_unit(1);

.txfee {
color: var(--text-color-primary);
font-size: var(--body, 16px);
font-weight: 600;
line-height: size_unit(3);
}

.infoIcon {
width: size_unit(2);
height: size_unit(2);
margin-bottom: -#{size_unit(0.5)};
color: var(--text-color-secondary);
}
}

.metadataLabel {
display: flex;
flex: 0 0 50%;
Expand Down Expand Up @@ -266,7 +245,7 @@ $border-bottom: 1px solid var(--light-mode-light-grey-plus, var(--dark-mode-mid-
margin-bottom: size_unit(2);

.poolsTitle {
@include text-body-semi-bold
@include text-body-semi-bold;
}

.poolsList {
Expand All @@ -280,7 +259,7 @@ $border-bottom: 1px solid var(--light-mode-light-grey-plus, var(--dark-mode-mid-
.poolHeading {
display: flex;
justify-content: flex-end;
gap: size_unit(1)
gap: size_unit(1);
}

.poolRewardAmount {
Expand Down
Loading